From 4a946a41b2625fc5699ae9becbf3accae15f89b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Garc=C3=ADa=20Laverde?= Date: Thu, 10 Aug 2023 21:40:17 -0500 Subject: [PATCH] done user and movie integration test --- package-lock.json | 7 ++ package.json | 3 +- src/factories/MovieFactory.ts | 30 +++++ src/factories/ReviewFactory.ts | 38 ++++++ .../movie/__test__/movieService.int.test.ts | 119 ++++++++++++++++++ src/services/movie/movieService.ts | 1 - src/services/movie/movieTypes.ts | 11 +- src/services/review/reviewTypes.ts | 23 ++-- .../user/__test__/userService.int.test.ts | 67 +++++++++- src/services/user/userMappers.ts | 1 - src/services/user/userTypes.ts | 11 +- 11 files changed, 277 insertions(+), 34 deletions(-) create mode 100644 src/factories/MovieFactory.ts create mode 100644 src/factories/ReviewFactory.ts create mode 100644 src/services/movie/__test__/movieService.int.test.ts diff --git a/package-lock.json b/package-lock.json index e2b38ca..df57a47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "devDependencies": { "@types/express-serve-static-core": "^4.17.30", "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.197", "@types/luxon": "^3.3.1", "@types/supertest": "^2.0.12", "apidoc": "^1.0.3", @@ -1826,6 +1827,12 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.197", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", + "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==", + "dev": true + }, "node_modules/@types/luxon": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz", diff --git a/package.json b/package.json index c07c306..deeacf7 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "@types/express": "^4.17.11", "@types/jsonwebtoken": "^8.5.1", "@types/node": "14.18.2", + "@types/request": "^2.48.5", "@types/superagent": "4.1.10", "@types/supertest": "^2.0.11", - "@types/request": "^2.48.5", "async": "^3.2.4", "axios": "^0.26.1", "bcrypt": "^5.1.0", @@ -45,6 +45,7 @@ "devDependencies": { "@types/express-serve-static-core": "^4.17.30", "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.197", "@types/luxon": "^3.3.1", "@types/supertest": "^2.0.12", "apidoc": "^1.0.3", diff --git a/src/factories/MovieFactory.ts b/src/factories/MovieFactory.ts new file mode 100644 index 0000000..053daa8 --- /dev/null +++ b/src/factories/MovieFactory.ts @@ -0,0 +1,30 @@ +import * as Chance from 'chance'; +import { EntityManager } from 'typeorm'; +import { Movie } from '../entities/Movie'; + +export class MovieFactory { + private static chance = new Chance(); + + /** + * Creates a test movie + * @param {Partial} movie + * @param {EntityManager} manager + * @returns {Promise} + */ + public static async createMovie(movie: Partial, manager?: EntityManager): Promise { + let newMovie = new Movie({ + tmdbId: movie.tmdbId ?? this.chance.integer({ min: 1, max: 1000000 }), + title: movie.title ?? this.chance.name(), + overview: movie.overview ?? this.chance.string(), + posterPath: movie.posterPath ?? `https://image.tmdb.org/t/p/w500/${this.chance.string({ length: 10 })}.jpg`, + releaseDate: + movie.releaseDate ?? + new Date(this.chance.date({ year: this.chance.integer({ min: 1998, max: 2023 }) })), + }); + + Object.assign(newMovie, movie); + if (manager) newMovie = await manager.save(Movie, newMovie); + + return newMovie; + } +} diff --git a/src/factories/ReviewFactory.ts b/src/factories/ReviewFactory.ts new file mode 100644 index 0000000..79413d0 --- /dev/null +++ b/src/factories/ReviewFactory.ts @@ -0,0 +1,38 @@ +import * as Chance from 'chance'; +import { EntityManager } from 'typeorm'; +import { Movie } from '../entities/Movie'; +import { Review } from '../entities/Review'; +import { User } from '../entities/User'; + +export class ReviewFactory { + private static chance = new Chance(); + + /** + * Creates a test review + * @param {Partial} review + * @param {EntityManager} manager + * @param {User} user + * @param {Movie} movie + * @param {EntityManager} manager + * @returns {Promise} + */ + public static async createReview( + review: Partial, + user: User, + movie: Movie, + manager?: EntityManager, + ): Promise { + let newReview = new Review({ + rating: review.rating ?? this.chance.integer({ min: 1, max: 10 }), + comment: review.comment ?? this.chance.string(), + movieTMDBId: movie.tmdbId, + username: user.username, + userId: user.id, + }); + + Object.assign(newReview, review); + if (manager) newReview = await manager.save(Review, newReview); + + return newReview; + } +} diff --git a/src/services/movie/__test__/movieService.int.test.ts b/src/services/movie/__test__/movieService.int.test.ts new file mode 100644 index 0000000..704e519 --- /dev/null +++ b/src/services/movie/__test__/movieService.int.test.ts @@ -0,0 +1,119 @@ +import * as Chance from 'chance'; +import * as _ from 'lodash'; +import { DateTime } from 'luxon'; +import * as request from 'supertest'; +import { EntityManager } from 'typeorm'; +import App from '../../../app'; +import { Movie } from '../../../entities/Movie'; +import { User, UserRoles } from '../../../entities/User'; +import { MovieFactory } from '../../../factories/MovieFactory'; +import { ReviewFactory } from '../../../factories/ReviewFactory'; +import { UserFactory } from '../../../factories/UserFactory'; +import * as Encryption from '../../../utils/encryption'; +import { ReviewResponse } from '../../review/reviewTypes'; +import { MovieController } from '../movieController'; + +const app = new App([new MovieController()]); +const chance = new Chance(); +const server = app.getServer(); +let manager: EntityManager; + +beforeAll(async () => { + await app.listen(); + await app.databaseConnection.resetConnections(); + manager = app.getDatabaseManager(); +}); + +beforeEach(async () => { + await app.databaseConnection.resetConnections(); +}); + +afterAll(async () => { + await app.databaseConnection.resetConnections(); + await app.databaseConnection.closeConnection(); +}); + +describe('When sending a request', () => { + let user: User; + beforeEach(async () => { + user = await UserFactory.createUser( + { + email: 'ivangarl@yopmail.com', + role: UserRoles.USER, + password: await Encryption.getHashedPassword('very-secret-password'), + }, + manager, + ); + + jest.spyOn(Encryption, 'decodeToken').mockReturnValue({ + userId: user.id, + role: UserRoles.USER, + email: user.email, + exp: DateTime.local().plus({ hours: 1 }).toJSDate(), + }); + }); + describe('GET /movies, then', () => { + test('it should return a list of movies', async () => { + const movies = await Promise.all( + _.range(100).map(async () => { + return await MovieFactory.createMovie({}, manager); + }), + ); + + const response = await request(server).get('/movies'); + + expect(response.error).toBeFalsy(); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty('movies'); + expect(response.body.movies).toHaveLength(10); + expect(response.body.pages).toBe(10); + response.body.movies.forEach((movie: Movie) => { + const dbMovie = movies.find((m) => m.id === movie.id); + expect(dbMovie).toBeDefined(); + }); + }); + }); + describe('GET /movies/:tmdbId/reviews, then', () => { + test('it should return all reviews from a movie', async () => { + const movie = await MovieFactory.createMovie( + { + tmdbId: 888, + title: 'Batman: The Dark Knight', + overview: + 'The Dark Knight of Gotham City begins his war on crime with his first major enemy being the clownishly homicidal Joker.', + releaseDate: DateTime.fromISO('2008-07-16').toJSDate(), + }, + manager, + ); + + const reviews = await Promise.all( + _.range(50).map(async () => { + const user = await UserFactory.createUser( + { + email: chance.email(), + role: UserRoles.USER, + password: await Encryption.getHashedPassword('very-secret-password'), + }, + manager, + ); + + return await ReviewFactory.createReview({}, user, movie, manager); + }), + ); + + const response = await request(server).get(`/movies/${movie.tmdbId}/reviews`); + + expect(response.error).toBeFalsy(); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty('reviews'); + expect(response.body.reviews).toHaveLength(10); + expect(response.body.pages).toBe(5); + response.body.reviews.forEach((review: ReviewResponse) => { + const dbReview = reviews.find((r) => r.id === review.id); + expect(dbReview).toBeDefined(); + }); + }); + }); +}); diff --git a/src/services/movie/movieService.ts b/src/services/movie/movieService.ts index 0ad3665..e419b09 100644 --- a/src/services/movie/movieService.ts +++ b/src/services/movie/movieService.ts @@ -190,7 +190,6 @@ export default class MovieService { res.status(200).send({ movies: movies.currentPage, - pageCount: movies.currentPage.length, pages: movies.pagesCount, }); }, diff --git a/src/services/movie/movieTypes.ts b/src/services/movie/movieTypes.ts index eaab73f..e2115c8 100644 --- a/src/services/movie/movieTypes.ts +++ b/src/services/movie/movieTypes.ts @@ -1,3 +1,5 @@ +import { ReviewResponse } from '../review/reviewTypes'; + /** * Response for the GET /movies/{tmdbId}/reviews endpoint */ @@ -8,14 +10,7 @@ export interface MovieReviewsResponse { overview: string; posterPath: string; releaseDate: Date; - reviews: { - id: string; - comment: string; - rating: number; - username: string; - createdAt: Date; - updatedAt: Date; - }[]; + reviews: ReviewResponse[]; pageCount: number; pages: number; } diff --git a/src/services/review/reviewTypes.ts b/src/services/review/reviewTypes.ts index 8430953..9ff0feb 100644 --- a/src/services/review/reviewTypes.ts +++ b/src/services/review/reviewTypes.ts @@ -1,15 +1,20 @@ +/** + * Review object sent in the response body + */ +export interface ReviewResponse { + id: string; + tmdbId?: number; + username?: string; + rating: number; + comment: string; + createdAt: Date; + updatedAt: Date; +} + /** * Response for the POST /reviews endpoint */ export interface SubmitReviewResponse { message: string; - review: { - id: string; - tmdbId: number; - username: string; - rating: number; - comment: string; - createdAt: Date; - updatedAt: Date; - }; + review: ReviewResponse; } diff --git a/src/services/user/__test__/userService.int.test.ts b/src/services/user/__test__/userService.int.test.ts index f7efd82..fe13b0c 100644 --- a/src/services/user/__test__/userService.int.test.ts +++ b/src/services/user/__test__/userService.int.test.ts @@ -1,13 +1,18 @@ +import * as _ from 'lodash'; import { DateTime } from 'luxon'; import * as request from 'supertest'; import { EntityManager } from 'typeorm'; import App from '../../../app'; -import { UserController } from '../userController'; import { User, UserRoles } from '../../../entities/User'; -import * as Encryption from '../../../utils/encryption'; +import { MovieFactory } from '../../../factories/MovieFactory'; +import { ReviewFactory } from '../../../factories/ReviewFactory'; import { UserFactory } from '../../../factories/UserFactory'; +import * as Encryption from '../../../utils/encryption'; +import { ReviewResponse } from '../../review/reviewTypes'; +import { UserController } from '../userController'; const app = new App([new UserController()]); +const server = app.getServer(); let manager: EntityManager; beforeAll(async () => { @@ -27,7 +32,7 @@ afterAll(async () => { describe('When sending a POST request to /users/register, then', () => { test('it should create a new user', async () => { - const response = await request(app.getServer()).post('/users/register').send({ + const response = await request(server).post('/users/register').send({ username: 'ivangarl', email: 'ivangarl@yopmail.com', password: 'very-secret-password', @@ -66,16 +71,68 @@ describe('When sending a POST request to /users/login, then', () => { jest.spyOn(Encryption, 'decodeToken').mockReturnValue({ userId: user.id, role: UserRoles.USER, - email: 'ivangarl@yopmail.com', + email: user.email, exp: DateTime.local().plus({ hours: 1 }).toJSDate(), }); - const response = await request(app.getServer()).post('/users/login').send({ + const response = await request(server).post('/users/login').send({ email: user.email, password, }); expect(response.error).toBeFalsy(); expect(response.status).toBe(200); + + expect(response.body).toHaveProperty('token'); + expect(response.body).toHaveProperty('email'); + }); +}); + +describe('When sending a GET request to /users/:username/reviews, then', () => { + test('it should return all reviews from a user', async () => { + const password = 'very-secret-password'; + const user = await UserFactory.createUser( + { + email: 'ivangarl@yopmail.com', + role: UserRoles.USER, + password: await Encryption.getHashedPassword(password), + }, + manager, + ); + + // create movies and reviews + const reviews = await Promise.all( + _.range(50).map(async () => { + const movie = await MovieFactory.createMovie({}, manager); + return await ReviewFactory.createReview({}, user, movie, manager); + }), + ); + + jest.spyOn(Encryption, 'decodeToken').mockReturnValue({ + userId: user.id, + role: UserRoles.USER, + email: user.email, + exp: DateTime.local().plus({ hours: 1 }).toJSDate(), + }); + + const response = await request(server).get(`/users/${user.username}/reviews`); + + expect(response.error).toBeFalsy(); + expect(response.status).toBe(200); + + expect(response.body.reviews).toHaveLength(10); + expect(response.body.id).toBe(user.id); + expect(response.body.username).toBe(user.username); + expect(response.body.email).toBe(user.email); + expect(response.body.role).toBe(user.role); + expect(response.body.pages).toBe(5); + response.body.reviews.forEach((review: ReviewResponse) => { + const dbReview = reviews.find((r) => r.id === review.id); + expect(dbReview).toBeDefined(); + expect(review.id).toBe(dbReview.id); + expect(review.rating).toBe(dbReview.rating); + expect(review.comment).toBe(dbReview.comment); + expect(review.tmdbId).toBe(dbReview.movieTmdbId); + }); }); }); diff --git a/src/services/user/userMappers.ts b/src/services/user/userMappers.ts index 421d711..cb31057 100644 --- a/src/services/user/userMappers.ts +++ b/src/services/user/userMappers.ts @@ -24,7 +24,6 @@ export const mapUserReviews = (user: User, reviews: Review[], pages: number): Us updatedAt: review.updatedAt, })) : [], - pageCount: reviews.length, pages, }; }; \ No newline at end of file diff --git a/src/services/user/userTypes.ts b/src/services/user/userTypes.ts index 51da55f..4126d4e 100644 --- a/src/services/user/userTypes.ts +++ b/src/services/user/userTypes.ts @@ -1,4 +1,5 @@ import { UserRoles } from 'entities/User'; +import { ReviewResponse } from '../review/reviewTypes'; /** * Payload for the User register request @@ -35,14 +36,6 @@ export interface UserReviewsResponse { username: string; email: string; role: UserRoles; - reviews: { - id: string; - comment: string; - rating: number; - tmdbId: number; - createdAt: Date; - updatedAt: Date; - }[]; - pageCount: number; + reviews: ReviewResponse[]; pages: number; }