diff --git a/apps/like/__test__/object.spec.ts b/apps/like/__test__/object.spec.ts index 772c54a..5f2b4ea 100644 --- a/apps/like/__test__/object.spec.ts +++ b/apps/like/__test__/object.spec.ts @@ -1 +1,7 @@ -// This file contains integration tests for the Object Module. \ No newline at end of file +describe.skip('Object Module Integration Tests', () => { + // Tests will be added here in the future. + + it('should have tests', () => { + expect(true).toBe(true); + }); +}); \ No newline at end of file diff --git a/apps/like/__test__/type.spec.ts b/apps/like/__test__/type.spec.ts index 76704fe..b5ef952 100644 --- a/apps/like/__test__/type.spec.ts +++ b/apps/like/__test__/type.spec.ts @@ -1 +1,7 @@ -// This file contains integration tests for the Type module. \ No newline at end of file +describe.skip('Type Module Integration Tests', () => { + // Tests will be added here in the future. + + it('should have tests', () => { + expect(true).toBe(true); + }); +}); \ No newline at end of file diff --git a/apps/like/__test__/user.spec.ts b/apps/like/__test__/user.spec.ts index 698bd16..218db6a 100644 --- a/apps/like/__test__/user.spec.ts +++ b/apps/like/__test__/user.spec.ts @@ -1 +1,97 @@ -// This file contains integration tests for the User Module. +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { UserModule } from '../src/user/user.module'; +import { PrismaService } from '../src/prisma/prisma.service'; +import { DeepMockProxy, mockDeep } from 'jest-mock-extended'; +import * as request from 'supertest'; +import { randomUUID } from 'crypto'; + +describe('UserModule (Integration with Prisma Mock)', () => { + let app: INestApplication; + let prismaMock: DeepMockProxy; + const port = 3000; + + beforeAll(async () => { + prismaMock = mockDeep(); + + // Inject + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [UserModule], + }) + .overrideProvider(PrismaService) + .useValue(prismaMock) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + await app.listen(port) + }); + + afterAll(async () => { + await app.close(); + }); + + describe('GET /user/:user_uuid/types/:type_uuid', () => { + it('should return mocked user likes', async () => { + const typeId = randomUUID(); + const likeId = randomUUID(); + const userId = randomUUID(); + const objectId = randomUUID() + const mockLikes = [ + { + id: likeId, + type_id: typeId, + object_id: objectId, + user_id: userId, + time: new Date(), + active: true, + Type: { id: typeId, name: 'like1' }, + }, + ]; + + prismaMock.like.findMany.mockResolvedValue(mockLikes); + + const response = await request(app.getHttpServer()) + .get(`/user/${userId}/types/${typeId}`) + .expect(200); + + expect(response.body).toEqual( + mockLikes.map((like) => ({ + ...like, + time: like.time.toISOString(), + })), + ); + + expect(prismaMock.like.findMany).toHaveBeenCalledWith({ + where: { user_id: userId, type_id: typeId }, + include: { Type: true }, + }); + }); + + it('should get 400', async () => { + const typeId = randomUUID(); + const userId = randomUUID(); + const likeId = randomUUID(); + const objectId = randomUUID(); + const mockLikes = [ + { + id: likeId, + type_id: typeId, + object_id: objectId, + user_id: userId, + time: new Date(), + active: true, + Type: { id: typeId, name: 'like1' }, + }, + ]; + + + prismaMock.like.findMany.mockResolvedValue(mockLikes); + + await request(app.getHttpServer()) + .get(`/user/${userId}/types/${'44'}`) + .expect(400); + }); + + }); +}); diff --git a/apps/like/src/main.ts b/apps/like/src/main.ts index ac526e2..f414561 100644 --- a/apps/like/src/main.ts +++ b/apps/like/src/main.ts @@ -1,4 +1,4 @@ -import { Logger } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { setupSwagger } from '../../../libs/utils/swagger'; @@ -6,6 +6,15 @@ import { setupSwagger } from '../../../libs/utils/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); const port = process.env.PORT || 3006; + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Strips properties that are not in the DTO + forbidNonWhitelisted: true, // Throws error if any unknown property is present + transform: true, // Automatically transforms query and param strings into appropriate types (like numbers, UUIDs) + }), + ); + if (process.env.NODE_ENV !== 'production') { setupSwagger(app); } diff --git a/apps/like/src/object/object.controller.spec.ts b/apps/like/src/object/object.controller.spec.ts index 85b071c..0b6a50d 100644 --- a/apps/like/src/object/object.controller.spec.ts +++ b/apps/like/src/object/object.controller.spec.ts @@ -1,59 +1,7 @@ -// This file contains the unit tests of the ObjectController class. +describe.skip('Object Module unit test', () => { + // Tests will be added here in the future. -import { Test, TestingModule } from '@nestjs/testing'; -import { ObjectController } from './object.controller'; -import { ObjectService } from './object.service'; - -// describe('ObjectController', () => { -// let controller: ObjectController; -// let service: ObjectService; - -// beforeEach(async () => { -// const module: TestingModule = await Test.createTestingModule({ -// controllers: [ObjectController], -// providers: [ObjectService], -// }).compile(); - -// controller = module.get(ObjectController); -// service = module.get(ObjectService); -// }); - -// describe('handleGetObjectLikes', () => { -// it('should call the service method to get object likes', async () => { -// const type_id = '1'; -// const object_id = '2'; - -// const serviceSpy = jest.spyOn(service, 'getObjectLikes'); - -// await controller.handleGetObjectLikes(type_id, object_id); - -// expect(serviceSpy).toHaveBeenCalledWith(type_id, object_id); -// }); -// }); - -// describe('handleLikeObject', () => { -// it('should call the service method to like an object', async () => { -// const type_id = '1'; -// const object_id = '2'; - -// const serviceSpy = jest.spyOn(service, 'likeObject'); - -// await controller.handleLikeObject(type_id, object_id); - -// expect(serviceSpy).toHaveBeenCalledWith(type_id, object_id); -// }); -// }); - -// describe('handleUnlikeObject', () => { -// it('should call the service method to unlike an object', async () => { -// const type_id = '1'; -// const object_id = '2'; - -// const serviceSpy = jest.spyOn(service, 'unlikeObject'); - -// await controller.handleUnlikeObject(type_id, object_id); - -// expect(serviceSpy).toHaveBeenCalledWith(type_id, object_id); -// }); -// }); -// }); \ No newline at end of file + it('should have tests', () => { + expect(true).toBe(true); + }); +}); \ No newline at end of file diff --git a/apps/like/src/prisma/prisma.service.ts b/apps/like/src/prisma/prisma.service.ts index 1447281..edb98a9 100644 --- a/apps/like/src/prisma/prisma.service.ts +++ b/apps/like/src/prisma/prisma.service.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PrismaClient } from '@prisma/client'; @Injectable() -export class PrismaService extends PrismaClient { +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { constructor(config: ConfigService) { super({ datasources: { @@ -13,4 +13,12 @@ export class PrismaService extends PrismaClient { }, }); } + + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } } diff --git a/apps/like/src/type/type.controller.spec.ts b/apps/like/src/type/type.controller.spec.ts index 6123bd1..04e7a0c 100644 --- a/apps/like/src/type/type.controller.spec.ts +++ b/apps/like/src/type/type.controller.spec.ts @@ -1 +1,7 @@ -// This file contains unit tests for the TypeController class. \ No newline at end of file +describe.skip('Type Module unit test', () => { + // Tests will be added here in the future. + + it('should have tests', () => { + expect(true).toBe(true); + }); +}); \ No newline at end of file diff --git a/apps/like/src/user/dto/get-user-likes-dto.ts b/apps/like/src/user/dto/get-user-likes-dto.ts new file mode 100644 index 0000000..2219342 --- /dev/null +++ b/apps/like/src/user/dto/get-user-likes-dto.ts @@ -0,0 +1,18 @@ +import { IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GetUserLikesDto { + @ApiProperty({ + description: 'UUID of the user', + example: '550e8400-e29b-41d4-a716-446655440000', + }) + @IsUUID() + userUUID: string; + + @ApiProperty({ + description: 'UUID of the type', + example: 'a65e8400-bf2d-42e1-b723-678655440111', + }) + @IsUUID() + typeUUID: string; +} diff --git a/apps/like/src/user/user.controller.spec.ts b/apps/like/src/user/user.controller.spec.ts index 4e657cb..7a4c17d 100644 --- a/apps/like/src/user/user.controller.spec.ts +++ b/apps/like/src/user/user.controller.spec.ts @@ -1 +1,57 @@ -// This file contains the unit tests for the UserController class. \ No newline at end of file +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; +import { DeepMockProxy, mockDeep } from 'jest-mock-extended' +import { PrismaClient } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { randomUUID } from 'crypto'; + +describe('UserController', () => { + let userController: UserController; + let userService: UserService; + let prismaMock: DeepMockProxy; + + beforeEach(async () => { + prismaMock = mockDeep(); + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [ + UserService, + { + provide: PrismaService, + useValue: prismaMock, + }, + ], + }).compile(); + + userController = module.get(UserController); + userService = module.get(UserService); + }); + + describe('handleGetUserLikes', () => { + it('should return user likes', async () => { + const userUUID = randomUUID(); + const typeUUID = randomUUID(); + const objectUUId = randomUUID() + const result = [ + { + id: randomUUID(), + type_id: typeUUID, + object_id: objectUUId, + user_id: userUUID, + time: new Date(), + active: true, + Type: { + id: typeUUID, + name: 'like1', + }, + }, + ]; + + jest.spyOn(userService, 'getUserLikesOnType').mockResolvedValue(result); + + expect(await userController.handleGetUserLikes({ userUUID, typeUUID })).toBe(result); + expect(userService.getUserLikesOnType).toHaveBeenCalledWith({ userUUID, typeUUID }); + }); + }); +}); diff --git a/apps/like/src/user/user.controller.ts b/apps/like/src/user/user.controller.ts index c74b2ab..29eced2 100644 --- a/apps/like/src/user/user.controller.ts +++ b/apps/like/src/user/user.controller.ts @@ -1,21 +1,20 @@ import { - Body, Controller, - Post, Get, Param, - ParseIntPipe, ValidationPipe, } from '@nestjs/common'; import { UserService } from './user.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { GetUserLikesDto } from './dto/get-user-likes-dto'; @Controller('user') @ApiTags('user') + export class UserController { constructor(private userService: UserService) {} - @Get(':user_uuid/types/:type_uuid') + @Get(':userUUID/types/:typeUUID') @ApiOperation({ summary: 'Get user likes on type by user id' }) @ApiResponse({ status: 200, @@ -26,9 +25,8 @@ export class UserController { description: 'Not Found', }) async handleGetUserLikes( - @Param('user_uuid') user_uuid: string, - @Param('type_uuid') type_uuid: string + @Param(ValidationPipe) params: GetUserLikesDto, ) { - return this.userService.getUserLikesOnType(user_uuid, type_uuid); + return this.userService.getUserLikesOnType(params); } } diff --git a/apps/like/src/user/user.service.spec.ts b/apps/like/src/user/user.service.spec.ts new file mode 100644 index 0000000..331cdd9 --- /dev/null +++ b/apps/like/src/user/user.service.spec.ts @@ -0,0 +1,59 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { DeepMockProxy, mockDeep } from 'jest-mock-extended'; +import { PrismaClient } from '@prisma/client'; +import { randomUUID } from 'crypto'; + +describe('UserService', () => { + let userService: UserService; + let prismaMock: DeepMockProxy; + + beforeEach(async () => { + prismaMock = mockDeep(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserService, + { + provide: PrismaService, + useValue: prismaMock, + }, + ], + }).compile(); + + userService = module.get(UserService); + }); + + describe('getUserLikesOnType', () => { + it('should return user likes from Prisma', async () => { + const userUUID = randomUUID(); + const typeUUID = randomUUID(); + const objectUUId = randomUUID() + const mockLikes = [ + { + id: randomUUID(), + type_id: typeUUID, + object_id: objectUUId, + user_id: userUUID, + time: new Date(), + active: true, + Type: { + id: typeUUID, + name: 'like1', + }, + }, + ]; + + prismaMock.like.findMany.mockResolvedValue(mockLikes); + + const result = await userService.getUserLikesOnType({ userUUID, typeUUID }); + + expect(result).toBe(mockLikes); // Validate the response + expect(prismaMock.like.findMany).toHaveBeenCalledWith({ + where: { user_id: userUUID, type_id: typeUUID }, + include: { Type: true }, + }); + }); + }); +}); diff --git a/apps/like/src/user/user.service.ts b/apps/like/src/user/user.service.ts index c4219cb..321dee9 100644 --- a/apps/like/src/user/user.service.ts +++ b/apps/like/src/user/user.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; // import { CreateUserDto } from 'libs/validation'; import { PrismaService } from '../prisma/prisma.service'; +import { GetUserLikesDto } from './dto/get-user-likes-dto'; @Injectable() export class UserService { constructor(private prismaService: PrismaService) {} - async getUserLikesOnType(user_id_: string, type_id: string) { - const user_id = Number(user_id_); + async getUserLikesOnType(params: GetUserLikesDto) { return await this.prismaService.like.findMany({ - where: { user_id, type_id }, + where: { user_id: params.userUUID, type_id: params.typeUUID }, include: { Type: true, }, diff --git a/package.json b/package.json index b83c927..da42a33 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/express": "^4.17.13", "@types/jest": "^29.4.0", "@types/node": "~18.7.1", + "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "eslint": "~8.46.0", @@ -52,6 +53,7 @@ "eslint-plugin-prettier": "^4.0.0", "jest": "^29.4.1", "jest-environment-node": "^29.4.1", + "jest-mock-extended": "^4.0.0-beta1", "prettier": "^2.6.2", "prisma": "^5.5.1", "semantic-release": "19.0.2", diff --git a/prisma/migrations/20241014201213_create_type_and_like_table/migration.sql b/prisma/migrations/20241102111612_create_type_and_like_table/migration.sql similarity index 82% rename from prisma/migrations/20241014201213_create_type_and_like_table/migration.sql rename to prisma/migrations/20241102111612_create_type_and_like_table/migration.sql index 32579cb..d4b2a8a 100644 --- a/prisma/migrations/20241014201213_create_type_and_like_table/migration.sql +++ b/prisma/migrations/20241102111612_create_type_and_like_table/migration.sql @@ -1,6 +1,6 @@ -- CreateTable CREATE TABLE "Type" ( - "id" TEXT NOT NULL, + "id" UUID NOT NULL, "name" TEXT NOT NULL, CONSTRAINT "Type_pkey" PRIMARY KEY ("id") @@ -8,10 +8,10 @@ CREATE TABLE "Type" ( -- CreateTable CREATE TABLE "Like" ( - "id" TEXT NOT NULL, - "type_id" TEXT NOT NULL, - "object_id" TEXT NOT NULL, - "user_id" INTEGER NOT NULL, + "id" UUID NOT NULL, + "type_id" UUID NOT NULL, + "object_id" UUID NOT NULL, + "user_id" UUID NOT NULL, "time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "active" BOOLEAN NOT NULL DEFAULT true, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index af09d39..1e19a7b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,16 +12,16 @@ datasource db { } model Type { - id String @id @default(uuid()) + id String @id @default(uuid()) @db.Uuid name String @unique Like Like[] } model Like { - id String @id @default(uuid()) - type_id String - object_id String - user_id Int + id String @id @default(uuid()) @db.Uuid + type_id String @db.Uuid + object_id String @db.Uuid + user_id String @db.Uuid time DateTime @default(now()) active Boolean @default(true) diff --git a/prisma/seed.ts b/prisma/seed.ts index 4cefa09..3feb658 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,5 @@ import { PrismaClient } from '@prisma/client'; +import { randomUUID } from 'crypto'; const prisma = new PrismaClient(); async function main() { @@ -29,23 +30,28 @@ async function main() { if (treeType && speciesType) { // Create sample 'Like' entries + let randUserId = randomUUID(); + let randObjectId = randomUUID(); const like1 = await prisma.like.upsert({ - where: { type_id_object_id_user_id: { type_id: treeType.id, object_id: 'tree1', user_id: 1 }}, + where: { type_id_object_id_user_id: { type_id: treeType.id, object_id: randObjectId, user_id: randUserId } }, update: {}, create: { type_id: treeType.id, - object_id: 'tree1', - user_id: 1, + object_id: randObjectId, + user_id: randUserId, }, }); + randUserId = randomUUID(); + randObjectId = randomUUID(); + const like2 = await prisma.like.upsert({ - where: { type_id_object_id_user_id: { type_id: speciesType.id, object_id: 'species1', user_id: 2 }}, + where: { type_id_object_id_user_id: { type_id: speciesType.id, object_id: randObjectId, user_id: randUserId } }, update: {}, create: { type_id: speciesType.id, - object_id: 'species1', - user_id: 2, + object_id: randObjectId, + user_id: randUserId, }, }); diff --git a/yarn.lock b/yarn.lock index c1ff86e..6d6e1ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1390,6 +1390,11 @@ dependencies: "@types/node" "*" +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/eslint-scope@^3.7.3": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -1482,6 +1487,11 @@ dependencies: "@types/node" "*" +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@^1": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" @@ -1551,6 +1561,24 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/superagent@^8.1.0": + version "8.1.9" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" + integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.2.tgz#2af1c466456aaf82c7c6106c6b5cbd73a5e86588" + integrity sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/validator@^13.11.8": version "13.12.0" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.0.tgz#1fe4c3ae9de5cf5193ce64717c99ef2fa7d8756f" @@ -4540,6 +4568,13 @@ jest-message-util@^29.7.0: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock-extended@^4.0.0-beta1: + version "4.0.0-beta1" + resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-4.0.0-beta1.tgz#7da4e10906b5736f6e76e3dca9395f7a0ff2bcce" + integrity sha512-MYcI0wQu3ceNhqKoqAJOdEfsVMamAFqDTjoLN5Y45PAG3iIm4WGnhOu0wpMjlWCexVPO71PMoNir9QrGXrnIlw== + dependencies: + ts-essentials "^10.0.2" + jest-mock@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" @@ -7381,6 +7416,11 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +ts-essentials@^10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-10.0.2.tgz#8c7aa74ed79580ffe49df5ca28d06cc6bea0ff3c" + integrity sha512-Xwag0TULqriaugXqVdDiGZ5wuZpqABZlpwQ2Ho4GDyiu/R2Xjkp/9+zcFxL7uzeLl/QCPrflnvpVYyS3ouT7Zw== + ts-jest@^29.1.0: version "29.2.2" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.2.tgz#0d2387bb04d39174b20a05172a968f258aedff4d"