Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions backend/src/controllers/ratings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { PrismaClient } from '@prisma/client';
import type { AuthenticatedRequest } from '../middleware/auth.ts';
import type { Response } from 'express';

const prisma = new PrismaClient();

export const createRating = async (req: AuthenticatedRequest, res: Response) => {
const timestamp = new Date().toISOString();
try {
if (!req.user) {
return res.status(401).json({ message: 'User not authenticated', timestamp, endpoint: '/api/ratings' });
}
const stars = parseInt(req.body.stars, 10);
if (stars < 0 || stars > 5) {
return res.status(400).json({ message: 'Stars must be between 0 and 5', timestamp, endpoint: '/api/ratings' });
}
const newRatingData = {
userId: req.user.id,
movieId: req.body.movieId,
stars,
comment: req.body.comment?.trim(),
tags: req.body.tags || [],
date: new Date(),
votes: 0,
};
const newRating = await prisma.rating.create({ data: newRatingData });
return res.status(201).json({
message: 'Rating created successfully',
rating: newRating,
timestamp,
endpoint: '/api/ratings',
});
} catch (error) {
console.error(`[${timestamp}] createRating error:`, error);
return res.status(500).json({
message: 'Internal server error while creating rating',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp,
endpoint: '/api/ratings',
});
}
};

export const getRatings = async (req: AuthenticatedRequest, res: Response) => {
const timestamp = new Date().toISOString();
try {
if (!req.user) {
return res.status(401).json({ message: 'User not authenticated', timestamp, endpoint: '/api/ratings' });
}
const ratings = await prisma.rating.findMany({
where: { userId: req.user.id },
orderBy: { date: 'desc' },
});
return res.status(200).json({
message: 'Ratings retrieved successfully',
ratings,
timestamp,
endpoint: '/api/ratings',
});
} catch (error) {
console.error(`[${timestamp}] getRatings error:`, error);
return res.status(500).json({
message: 'Internal server error while retrieving ratings',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp,
endpoint: '/api/ratings',
});
}
};

export const getRatingById = async (req: AuthenticatedRequest, res: Response) => {
const timestamp = new Date().toISOString();
try {
if (!req.user) {
return res.status(401).json({ message: 'User not authenticated', timestamp, endpoint: '/api/ratings' });
}
const rating = await prisma.rating.findUnique({
where: { id: req.params.id },
});
if (!rating || rating.userId !== req.user.id) {
return res.status(404).json({ message: 'Rating not found', timestamp, endpoint: '/api/ratings' });
}
return res.status(200).json({
message: 'Rating retrieved successfully',
rating,
timestamp,
endpoint: `/api/ratings/${req.params.id}`,
});
} catch (error) {
console.error(`[${timestamp}] getRatingById error:`, error);
return res.status(500).json({
message: 'Internal server error while retrieving rating',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp,
endpoint: `/api/ratings/${req.params.id}`,
});
}
};

export const updateRating = async (req: AuthenticatedRequest, res: Response) => {
const timestamp = new Date().toISOString();
try {
if (!req.user) {
return res.status(401).json({ message: 'User not authenticated', timestamp, endpoint: '/api/ratings' });
}
const existing = await prisma.rating.findUnique({ where: { id: req.params.id } });
if (!existing || existing.userId !== req.user.id) {
return res.status(404).json({ message: 'Rating not found', timestamp, endpoint: '/api/ratings' });
}
const stars = parseInt(req.body.stars, 10);
if (isNaN(stars) || stars < 0 || stars > 5) {
return res.status(400).json({ message: 'Stars must be between 0 and 5', timestamp, endpoint: '/api/ratings' });
}
const updated = await prisma.rating.update({
where: { id: req.params.id },
data: {
stars,
comment: req.body.comment?.trim(),
tags: req.body.tags || [],
},
});
return res.status(200).json({
message: 'Rating updated successfully',
rating: updated,
timestamp,
endpoint: `/api/ratings/${req.params.id}`,
});
} catch (error) {
console.error(`[${timestamp}] updateRating error:`, error);
return res.status(500).json({
message: 'Internal server error while updating rating',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp,
endpoint: `/api/ratings/${req.params.id}`,
});
}
};

export const deleteRating = async (req: AuthenticatedRequest, res: Response) => {
const timestamp = new Date().toISOString();
try {
if (!req.user) {
return res.status(401).json({ message: 'User not authenticated', timestamp, endpoint: '/api/ratings' });
}
const existing = await prisma.rating.findUnique({ where: { id: req.params.id } });
if (!existing || existing.userId !== req.user.id) {
return res.status(404).json({ message: 'Rating not found', timestamp, endpoint: '/api/ratings' });
}
await prisma.rating.delete({ where: { id: req.params.id } });
return res.status(200).json({
message: 'Rating deleted',
timestamp,
endpoint: `/api/ratings/${req.params.id}`,
});
} catch (error) {
console.error(`[${timestamp}] deleteRating error:`, error);
return res.status(500).json({
message: 'Internal server error while deleting rating',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp,
endpoint: `/api/ratings/${req.params.id}`,
});
}
};
8 changes: 8 additions & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { authenticateUser } from '../middleware/auth';
import { protect } from "../controllers/protected";
import { getLocalEvent, createLocalEvent, updateLocalEvent, deleteLocalEvent } from "../controllers/local-events"
import { followUser, unfollowUser, getFollowers, getFollowing } from "../controllers/userFollows";
import { createRating, getRatings, getRatingById, deleteRating, updateRating } from "../controllers/ratings";


const router = Router();
Expand Down Expand Up @@ -45,6 +46,13 @@ router.get("/movies/cinecircle/:movieId", getMovieById);
router.put("/movies/cinecircle/:movieId", updateMovie);
router.delete("/movies/:movieId", deleteMovie);

// Ratings routes
router.post('/api/ratings', createRating);
router.get('/api/ratings', getRatings);
router.get('/api/ratings/:id', getRatingById);
router.put('/api/ratings/:id', updateRating);
router.delete('/api/ratings/:id', deleteRating);

// Local events routes
router.get("/api/local-event/:id", getLocalEvent);
router.post("/api/local-event", createLocalEvent);
Expand Down
196 changes: 196 additions & 0 deletions backend/src/tests/api/rating.api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import request from "supertest";
import express, { NextFunction } from "express";
import { createApp } from "../../app";
import { prisma } from "../../services/db";
import jwt from "jsonwebtoken";
import { HTTP_STATUS } from "../helpers/constants";
import { AuthenticatedRequest } from "../../middleware/auth";

jest.mock('../../middleware/auth', () => ({
authenticateUser: (req: AuthenticatedRequest, res: any, next: NextFunction) => {
// Check for Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'User not authenticated' });
}

req.user = {
id: '123e4567-e89b-12d3-a456-426614174000',
email: 'testuser@example.com',
role: 'USER',
};
next();
},
}));

describe("Ratings API Tests", () => {
let app: express.Express;
const TEST_USER_ID = "123e4567-e89b-12d3-a456-426614174000";
const TEST_MOVIE_ID = "movie-1";

const generateToken = () => {
return jwt.sign(
{ id: TEST_USER_ID, email: "testuser@example.com", role: "USER" },
process.env.JWT_SECRET || "test-secret",
{ expiresIn: "1h" }
);
};

const authHeader = () => ({
Authorization: `Bearer ${generateToken()}`,
});

beforeAll(async () => {
app = createApp();
await prisma.rating.deleteMany({ where: { userId: TEST_USER_ID } });
});

afterAll(async () => {
await prisma.rating.deleteMany({ where: { userId: TEST_USER_ID } });
await prisma.$disconnect();
});

describe("POST /api/ratings", () => {
it("should create a rating for authenticated user", async () => {
const payload = {
movieId: TEST_MOVIE_ID,
stars: 5,
comment: "Great movie!",
tags: ["action", "thriller"],
votes: 0,
};

const res = await request(app)
.post("/api/ratings")
.send(payload)
.set(authHeader())
.expect(HTTP_STATUS.CREATED)
.expect("Content-Type", /json/);

expect(res.body).toHaveProperty("message", "Rating created successfully");
expect(res.body.rating).toMatchObject({
userId: TEST_USER_ID,
movieId: TEST_MOVIE_ID,
stars: 5,
comment: "Great movie!",
tags: ["action", "thriller"],
votes: 0,
});
});

it("should return 401 if user is not authenticated", async () => {
const payload = {
movieId: TEST_MOVIE_ID,
stars: 4,
comment: "Nice!",
votes: 0,
};

const res = await request(app)
.post("/api/ratings")
.send(payload)
.expect(HTTP_STATUS.UNAUTHORIZED);

expect(res.body).toHaveProperty("message", "User not authenticated");
});

it("should return 400 for invalid stars value", async () => {
const payload = {
movieId: TEST_MOVIE_ID,
stars: 10,
comment: "Invalid stars",
votes: 0,
};

const res = await request(app)
.post("/api/ratings")
.send(payload)
.set(authHeader())
.expect(HTTP_STATUS.BAD_REQUEST);

expect(res.body).toHaveProperty("message", "Stars must be between 0 and 5");
});
});

describe("GET /api/ratings", () => {
beforeAll(async () => {
await prisma.rating.createMany({
data: [
{
id: 'rating-1',
userId: TEST_USER_ID,
movieId: "movie-2",
stars: 4,
date: new Date(),
votes: 0,
},
{
id: 'rating-2',
userId: TEST_USER_ID,
movieId: "movie-3",
stars: 3,
date: new Date(),
votes: 0,
},
],
});
});

afterAll(async () => {
await prisma.rating.deleteMany({ where: { userId: TEST_USER_ID } });
});

it("should return ratings for authenticated user", async () => {
const res = await request(app)
.get("/api/ratings")
.set(authHeader())
.expect(HTTP_STATUS.OK);

expect(res.body).toHaveProperty("message");
expect(res.body.ratings).toBeInstanceOf(Array);
expect(res.body.ratings.length).toBeGreaterThan(0);
res.body.ratings.forEach((rating: any) => {
expect(rating.userId).toBe(TEST_USER_ID);
});
});
});

describe("DELETE /api/ratings/:id", () => {
let ratingId: string;

beforeAll(async () => {
const rating = await prisma.rating.create({
data: {
userId: TEST_USER_ID,
movieId: "movie-4",
stars: 2,
comment: "Not my favorite",
date: new Date(),
votes: 0,
},
});
ratingId = rating.id;
});

it("should delete a rating for authenticated user", async () => {
const res = await request(app)
.delete(`/api/ratings/${ratingId}`)
.set(authHeader())
.expect(HTTP_STATUS.OK);

expect(res.body).toHaveProperty("message", "Rating deleted");

const deleted = await prisma.rating.findUnique({ where: { id: ratingId } });
expect(deleted).toBeNull();
});

it("should return 404 for non-existent rating", async () => {
const res = await request(app)
.delete(`/api/ratings/non-existent-id`)
.set(authHeader())
.expect(HTTP_STATUS.NOT_FOUND);

expect(res.body).toHaveProperty("message", "Rating not found");
});
});
});
8 changes: 7 additions & 1 deletion backend/src/tests/api/users.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { HTTP_STATUS } from "../helpers/constants";
import { AuthenticatedRequest } from "../../middleware/auth";

jest.mock('../../middleware/auth', () => ({
authenticateUser: (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
authenticateUser: (req: AuthenticatedRequest, res: any, next: NextFunction) => {
// Check for Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'User not authenticated' });
}

req.user = {
id: '123e4567-e89b-12d3-a456-426614174000',
email: 'testuser@example.com',
Expand Down