diff --git a/.vscode/launch.json b/.vscode/launch.json index dc6a3be0..92a15d15 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,14 +4,14 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - - { "type": "node", "request": "attach", - "name": "Attach by Process ID", + "name": "Attach NestJS WS", "port": 9229, - "processId": "${command:PickProcess}" - }, + "restart": true, + "stopOnEntry": false, + "protocol": "inspector" + } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4779af0e..52e79a11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vrt-backend", - "version": "0.0.5", + "version": "0.0.8", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1377,6 +1377,12 @@ "@babel/types": "^7.3.0" } }, + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", + "dev": true + }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", @@ -1643,6 +1649,12 @@ } } }, + "@types/uuid-apikey": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/uuid-apikey/-/uuid-apikey-1.4.0.tgz", + "integrity": "sha512-fMUfWcgNH/PFVjkoMJXEFK52vwsifNqDwJ9CmfGTHkKSSbFpsgqa4IKbm/Cgi+dVtOX+6mhIgkEOqsDO1VMnyQ==", + "dev": true + }, "@types/webpack": { "version": "4.41.8", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.8.tgz", diff --git a/package.json b/package.json index 86eed95f..8a216d40 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "docker:build_api": "docker build -t visualregressiontracker/api:$npm_package_version -f ./Dockerfile . && docker push visualregressiontracker/api:$npm_package_version", + "docker:build_api": "npm run test && npm run test:e2e && docker build -t visualregressiontracker/api:$npm_package_version -f ./Dockerfile . && docker push visualregressiontracker/api:$npm_package_version", "docker:build_migration": "docker build -t visualregressiontracker/migration:0.0.2 -f ./prisma/Dockerfile ./prisma" }, "dependencies": { @@ -53,6 +53,7 @@ "@nestjs/schematics": "^7.0.0", "@nestjs/testing": "^7.0.0", "@prisma/cli": "^2.0.0-beta.5", + "@types/bcryptjs": "^2.4.2", "@types/express": "^4.17.3", "@types/jest": "25.1.4", "@types/node": "^13.9.1", @@ -61,6 +62,7 @@ "@types/pixelmatch": "^5.1.0", "@types/pngjs": "^3.4.2", "@types/supertest": "^2.0.8", + "@types/uuid-apikey": "^1.4.0", "@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/parser": "^2.23.0", "eslint": "^6.8.0", diff --git a/prisma/migrations/20200524162125-baseline-history/README.md b/prisma/migrations/20200524162125-baseline-history/README.md new file mode 100644 index 00000000..0f15cf76 --- /dev/null +++ b/prisma/migrations/20200524162125-baseline-history/README.md @@ -0,0 +1,112 @@ +# Migration `20200524162125-baseline-history` + +This migration has been generated by Pavel Strunkin at 5/24/2020, 4:21:25 PM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +CREATE TABLE "public"."Baseline" ( +"baselineName" text NOT NULL ,"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"id" text NOT NULL ,"testRunId" text ,"testVariationId" text NOT NULL ,"updatedAt" timestamp(3) NOT NULL , + PRIMARY KEY ("id")) + +ALTER TABLE "public"."TestRun" ADD COLUMN "baselineName" text , +ADD COLUMN "browser" text , +ADD COLUMN "device" text , +ADD COLUMN "ignoreAreas" text NOT NULL DEFAULT '[]', +ADD COLUMN "name" text NOT NULL DEFAULT '', +ADD COLUMN "os" text , +ADD COLUMN "viewport" text ; + +CREATE UNIQUE INDEX "Baseline_testRunId" ON "public"."Baseline"("testRunId") + +ALTER TABLE "public"."Baseline" ADD FOREIGN KEY ("testVariationId")REFERENCES "public"."TestVariation"("id") ON DELETE CASCADE ON UPDATE CASCADE + +ALTER TABLE "public"."Baseline" ADD FOREIGN KEY ("testRunId")REFERENCES "public"."TestRun"("id") ON DELETE SET NULL ON UPDATE CASCADE +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration 20200503001556-init..20200524162125-baseline-history +--- datamodel.dml ++++ datamodel.dml +@@ -3,9 +3,9 @@ + } + datasource db { + provider = "postgresql" +- url = "***" ++ url = env("DATABASE_URL") + } + model Build { + id String @default(uuid()) @id +@@ -14,12 +14,12 @@ + status String? + testRuns TestRun[] + projectId String + project Project @relation(fields: [projectId], references: [id]) +- // userId String +- // user User @relation(fields: [userId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) ++ user User? @relation(fields: [userId], references: [id]) ++ userId String? + } + model Project { + id String @default(uuid()) @id +@@ -43,26 +43,48 @@ + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) ++ // Test variation data ++ name String @default("") ++ browser String? ++ device String? ++ os String? ++ viewport String? ++ baselineName String? ++ ignoreAreas String @default("[]") ++ // Baseline ++ baseline Baseline? + } + model TestVariation { +- id String @default(uuid()) @id ++ id String @default(uuid()) @id + name String + browser String? + device String? + os String? + viewport String? + baselineName String? +- ignoreAreas String @default("[]") ++ ignoreAreas String @default("[]") + projectId String +- project Project @relation(fields: [projectId], references: [id]) ++ project Project @relation(fields: [projectId], references: [id]) + testRuns TestRun[] +- updatedAt DateTime @updatedAt +- createdAt DateTime @default(now()) ++ baselines Baseline[] ++ updatedAt DateTime @updatedAt ++ createdAt DateTime @default(now()) + } ++model Baseline { ++ id String @default(uuid()) @id ++ baselineName String ++ testVariationId String ++ testVariation TestVariation @relation(fields: [testVariationId], references: [id]) ++ testRunId String? ++ testRun TestRun? @relation(fields: [testRunId], references: [id]) ++ updatedAt DateTime @updatedAt ++ createdAt DateTime @default(now()) ++} ++ + model User { + id String @default(uuid()) @id + email String @unique + password String +``` + + diff --git a/prisma/migrations/20200524162125-baseline-history/schema.prisma b/prisma/migrations/20200524162125-baseline-history/schema.prisma new file mode 100644 index 00000000..9d436c64 --- /dev/null +++ b/prisma/migrations/20200524162125-baseline-history/schema.prisma @@ -0,0 +1,105 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = "***" +} + +model Build { + id String @default(uuid()) @id + number Int? + branchName String? + status String? + testRuns TestRun[] + projectId String + project Project @relation(fields: [projectId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) + userId String? +} + +model Project { + id String @default(uuid()) @id + name String + builds Build[] + testVariations TestVariation[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model TestRun { + id String @default(uuid()) @id + imageName String + diffName String? + diffPercent Float? + diffTollerancePercent Float @default(1.0) + pixelMisMatchCount Int? + status TestStatus + buildId String + build Build @relation(fields: [buildId], references: [id]) + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + // Test variation data + name String @default("") + browser String? + device String? + os String? + viewport String? + baselineName String? + ignoreAreas String @default("[]") + // Baseline + baseline Baseline? +} + +model TestVariation { + id String @default(uuid()) @id + name String + browser String? + device String? + os String? + viewport String? + baselineName String? + ignoreAreas String @default("[]") + projectId String + project Project @relation(fields: [projectId], references: [id]) + testRuns TestRun[] + baselines Baseline[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model Baseline { + id String @default(uuid()) @id + baselineName String + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + testRunId String? + testRun TestRun? @relation(fields: [testRunId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model User { + id String @default(uuid()) @id + email String @unique + password String + firstName String? + lastName String? + apiKey String @unique + isActive Boolean @default(true) + builds Build[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +enum TestStatus { + failed + new + ok + unresolved +} \ No newline at end of file diff --git a/prisma/migrations/20200524162125-baseline-history/steps.json b/prisma/migrations/20200524162125-baseline-history/steps.json new file mode 100644 index 00000000..0f5e22e1 --- /dev/null +++ b/prisma/migrations/20200524162125-baseline-history/steps.json @@ -0,0 +1,381 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "CreateModel", + "model": "Baseline" + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "id", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Baseline", + "field": "id" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Baseline", + "field": "id" + }, + "directive": "default" + }, + "argument": "", + "value": "uuid()" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Baseline", + "field": "id" + }, + "directive": "id" + } + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "baselineName", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "testVariationId", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "testVariation", + "type": "TestVariation", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Baseline", + "field": "testVariation" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Baseline", + "field": "testVariation" + }, + "directive": "relation" + }, + "argument": "fields", + "value": "[testVariationId]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Baseline", + "field": "testVariation" + }, + "directive": "relation" + }, + "argument": "references", + "value": "[id]" + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "testRunId", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "testRun", + "type": "TestRun", + "arity": "Optional" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Baseline", + "field": "testRun" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Baseline", + "field": "testRun" + }, + "directive": "relation" + }, + "argument": "fields", + "value": "[testRunId]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Baseline", + "field": "testRun" + }, + "directive": "relation" + }, + "argument": "references", + "value": "[id]" + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "updatedAt", + "type": "DateTime", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Baseline", + "field": "updatedAt" + }, + "directive": "updatedAt" + } + }, + { + "tag": "CreateField", + "model": "Baseline", + "field": "createdAt", + "type": "DateTime", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Baseline", + "field": "createdAt" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Baseline", + "field": "createdAt" + }, + "directive": "default" + }, + "argument": "", + "value": "now()" + }, + { + "tag": "CreateField", + "model": "Build", + "field": "user", + "type": "User", + "arity": "Optional" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Build", + "field": "user" + }, + "directive": "relation" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Build", + "field": "user" + }, + "directive": "relation" + }, + "argument": "fields", + "value": "[userId]" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Build", + "field": "user" + }, + "directive": "relation" + }, + "argument": "references", + "value": "[id]" + }, + { + "tag": "CreateField", + "model": "Build", + "field": "userId", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "name", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestRun", + "field": "name" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestRun", + "field": "name" + }, + "directive": "default" + }, + "argument": "", + "value": "\"\"" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "browser", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "device", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "os", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "viewport", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "baselineName", + "type": "String", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "ignoreAreas", + "type": "String", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "TestRun", + "field": "ignoreAreas" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "TestRun", + "field": "ignoreAreas" + }, + "directive": "default" + }, + "argument": "", + "value": "\"[]\"" + }, + { + "tag": "CreateField", + "model": "TestRun", + "field": "baseline", + "type": "Baseline", + "arity": "Optional" + }, + { + "tag": "CreateField", + "model": "TestVariation", + "field": "baselines", + "type": "Baseline", + "arity": "List" + } + ] +} \ No newline at end of file diff --git a/prisma/migrations/migrate.lock b/prisma/migrations/migrate.lock index 78f2bfe8..796c62b8 100644 --- a/prisma/migrations/migrate.lock +++ b/prisma/migrations/migrate.lock @@ -3,4 +3,5 @@ # Prisma Migrate lockfile v1 # Read more about conflict resolution here: TODO -20200503001556-init \ No newline at end of file +20200503001556-init +20200524162125-baseline-history \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a12f5e71..bf247ca9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,10 +15,10 @@ model Build { testRuns TestRun[] projectId String project Project @relation(fields: [projectId], references: [id]) - // userId String - // user User @relation(fields: [userId], references: [id]) updatedAt DateTime @updatedAt createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) + userId String? } model Project { @@ -44,22 +44,44 @@ model TestRun { testVariation TestVariation @relation(fields: [testVariationId], references: [id]) updatedAt DateTime @updatedAt createdAt DateTime @default(now()) + // Test variation data + name String @default("") + browser String? + device String? + os String? + viewport String? + baselineName String? + ignoreAreas String @default("[]") + // Baseline + baseline Baseline? } model TestVariation { - id String @default(uuid()) @id + id String @default(uuid()) @id name String browser String? device String? os String? viewport String? baselineName String? - ignoreAreas String @default("[]") + ignoreAreas String @default("[]") projectId String - project Project @relation(fields: [projectId], references: [id]) + project Project @relation(fields: [projectId], references: [id]) testRuns TestRun[] - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) + baselines Baseline[] + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model Baseline { + id String @default(uuid()) @id + baselineName String + testVariationId String + testVariation TestVariation @relation(fields: [testVariationId], references: [id]) + testRunId String? + testRun TestRun? @relation(fields: [testRunId], references: [id]) + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) } model User { diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 5e8fd252..690be3a6 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './jwt.strategy'; import { ApiGuard } from './guards/api.guard'; import { ConfigService } from '@nestjs/config'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; @Module({ imports: [ diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5dd584ae..bb8e0bdb 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -20,8 +20,8 @@ export class AuthService { return uuidAPIKey.create({ noDashes: true }).apiKey; } - async compare(password1: string, password2: string): Promise { - return await compare(password1, password2); + async compare(password: string, hashedPassword: string): Promise { + return await compare(password, hashedPassword); } signToken(user: User): string { diff --git a/src/auth/guards/api.guard.ts b/src/auth/guards/api.guard.ts index 491291ff..929a3129 100644 --- a/src/auth/guards/api.guard.ts +++ b/src/auth/guards/api.guard.ts @@ -5,7 +5,7 @@ import { UnauthorizedException, } from '@nestjs/common'; import { Request } from 'express'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../../prisma/prisma.service'; @Injectable() export class ApiGuard implements CanActivate { diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 4a0a8a68..c0c73973 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { JwtPayload } from './jwt-payload.model'; import { ConfigService } from '@nestjs/config'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { diff --git a/src/builds/builds.controller.spec.ts b/src/builds/builds.controller.spec.ts index 84b281dd..bfa907d6 100644 --- a/src/builds/builds.controller.spec.ts +++ b/src/builds/builds.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BuildsController } from './builds.controller'; +import { BuildsService } from './builds.service'; +import { PrismaService } from '../prisma/prisma.service'; describe('Builds Controller', () => { let controller: BuildsController; @@ -7,6 +9,8 @@ describe('Builds Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [BuildsController], + providers: [{ provide: BuildsService, useValue: {} }, + { provide: PrismaService, useValue: {} }], }).compile(); controller = module.get(BuildsController); diff --git a/src/builds/builds.controller.ts b/src/builds/builds.controller.ts index 19d8297f..e52c63ee 100644 --- a/src/builds/builds.controller.ts +++ b/src/builds/builds.controller.ts @@ -7,25 +7,26 @@ import { Param, ParseUUIDPipe, Delete, + Query, } from '@nestjs/common'; import { BuildsService } from './builds.service'; -import { JwtAuthGuard } from 'src/auth/guards/auth.guard'; -import { ApiBearerAuth, ApiTags, ApiParam, ApiSecurity } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/auth.guard'; +import { ApiBearerAuth, ApiTags, ApiParam, ApiSecurity, ApiQuery } from '@nestjs/swagger'; import { CreateBuildDto } from './dto/build-create.dto'; -import { ApiGuard } from 'src/auth/guards/api.guard'; -import { Build, TestRun } from '@prisma/client'; +import { ApiGuard } from '../auth/guards/api.guard'; +import { Build } from '@prisma/client'; @Controller('builds') @ApiTags('builds') export class BuildsController { constructor(private buildsService: BuildsService) { } - @Get(':id') - @ApiParam({ name: 'id', required: true }) + @Get() + @ApiQuery({ name: 'projectId', required: true }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) - getDetails(@Param('id', new ParseUUIDPipe()) id: string): Promise { - return this.buildsService.findById(id); + get(@Query('projectId', new ParseUUIDPipe()) projectId: string): Promise { + return this.buildsService.findMany(projectId); } @Delete(':id') diff --git a/src/builds/builds.module.ts b/src/builds/builds.module.ts index a3cb83fb..504805bd 100644 --- a/src/builds/builds.module.ts +++ b/src/builds/builds.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { BuildsService } from './builds.service'; import { BuildsController } from './builds.controller'; -import { UsersModule } from 'src/users/users.module'; -import { TestModule } from 'src/test/test.module'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { UsersModule } from '../users/users.module'; +import { PrismaService } from '../prisma/prisma.service'; +import { TestRunsModule } from '../test-runs/test-runs.module'; @Module({ - imports: [UsersModule, TestModule], + imports: [UsersModule, TestRunsModule], providers: [BuildsService, PrismaService], controllers: [BuildsController], exports: [BuildsService], diff --git a/src/builds/builds.service.spec.ts b/src/builds/builds.service.spec.ts index a27e4603..e1ca7b8f 100644 --- a/src/builds/builds.service.spec.ts +++ b/src/builds/builds.service.spec.ts @@ -1,12 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BuildsService } from './builds.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { TestRunsService } from '../test-runs/test-runs.service'; describe('BuildsService', () => { let service: BuildsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [BuildsService], + providers: [BuildsService, + { provide: PrismaService, useValue: {} }, + { provide: TestRunsService, useValue: {} }, + ], }).compile(); service = module.get(BuildsService); diff --git a/src/builds/builds.service.ts b/src/builds/builds.service.ts index 10f13cba..d120cd15 100644 --- a/src/builds/builds.service.ts +++ b/src/builds/builds.service.ts @@ -1,20 +1,16 @@ import { Injectable } from '@nestjs/common'; import { CreateBuildDto } from './dto/build-create.dto'; -import { TestService } from 'src/test/test.service'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { Build, TestRun, TestVariation } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { Build } from '@prisma/client'; +import { TestRunsService } from '../test-runs/test-runs.service'; @Injectable() export class BuildsService { constructor( private prismaService: PrismaService, - private testService: TestService + private testRunsService: TestRunsService ) { } - async findById(id: string): Promise<(TestRun & { testVariation: TestVariation; })[]> { - return this.testService.getTestRunsByBuildId(id); - } - async findMany(projectId: string): Promise { return this.prismaService.build.findMany({ where: { projectId }, @@ -36,11 +32,16 @@ export class BuildsService { } async remove(id: string): Promise { - const testRuns = await this.findById(id); + const build = await this.prismaService.build.findOne({ + where: { id }, + include: { + testRuns: true + } + }); try { await Promise.all( - testRuns.map((testRun) => this.testService.deleteTestRun(testRun.id)) + build.testRuns.map((testRun) => this.testRunsService.delete(testRun.id)) ); } catch (err) { console.log(err); diff --git a/src/projects/projects.controller.spec.ts b/src/projects/projects.controller.spec.ts index be8d7140..ce52c690 100644 --- a/src/projects/projects.controller.spec.ts +++ b/src/projects/projects.controller.spec.ts @@ -1,5 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ProjectsController } from './projects.controller'; +import { ProjectsService } from './projects.service'; describe('Projects Controller', () => { let controller: ProjectsController; @@ -7,6 +8,7 @@ describe('Projects Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ProjectsController], + providers: [{ provide: ProjectsService, useValue: {} }] }).compile(); controller = module.get(ProjectsController); diff --git a/src/projects/projects.controller.ts b/src/projects/projects.controller.ts index c5d53935..338c91af 100644 --- a/src/projects/projects.controller.ts +++ b/src/projects/projects.controller.ts @@ -9,16 +9,15 @@ import { Delete, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOkResponse, ApiParam } from '@nestjs/swagger'; -import { JwtAuthGuard } from 'src/auth/guards/auth.guard'; +import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { ProjectsService } from './projects.service'; import { CreateProjectDto } from './dto/create-project.dto'; -import { Project, Build } from '@prisma/client'; -import { BuildsService } from 'src/builds/builds.service'; +import { Project } from '@prisma/client'; @Controller('projects') @ApiTags('projects') export class ProjectsController { - constructor(private projectsService: ProjectsService, private buildsService: BuildsService) {} + constructor(private projectsService: ProjectsService) {} @Get() @ApiBearerAuth() @@ -27,14 +26,6 @@ export class ProjectsController { return this.projectsService.findAll(); } - @Get(':id') - @ApiParam({ name: 'id', required: true }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - getBuilds(@Param('id', new ParseUUIDPipe()) id: string): Promise { - return this.buildsService.findMany(id); - } - @Post() @ApiBearerAuth() @UseGuards(JwtAuthGuard) diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts index a5e0e08f..c47fe15e 100644 --- a/src/projects/projects.module.ts +++ b/src/projects/projects.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { ProjectsService } from './projects.service'; import { ProjectsController } from './projects.controller'; -import { BuildsModule } from 'src/builds/builds.module'; -import { TestVariationsModule } from 'src/test-variations/test-variations.module'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { BuildsModule } from '../builds/builds.module'; +import { TestVariationsModule } from '../test-variations/test-variations.module'; +import { PrismaService } from '../prisma/prisma.service'; @Module({ imports: [BuildsModule, TestVariationsModule], diff --git a/src/projects/projects.service.spec.ts b/src/projects/projects.service.spec.ts index d3b31018..58a847a8 100644 --- a/src/projects/projects.service.spec.ts +++ b/src/projects/projects.service.spec.ts @@ -1,12 +1,20 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ProjectsService } from './projects.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { TestVariationsService } from '../test-variations/test-variations.service'; +import { BuildsService } from '../builds/builds.service'; describe('ProjectsService', () => { let service: ProjectsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ProjectsService], + providers: [ + ProjectsService, + { provide: PrismaService, useValue: {} }, + { provide: TestVariationsService, useValue: {} }, + { provide: BuildsService, useValue: {} }, + ], }).compile(); service = module.get(ProjectsService); diff --git a/src/projects/projects.service.ts b/src/projects/projects.service.ts index 4fb0985d..575d746a 100644 --- a/src/projects/projects.service.ts +++ b/src/projects/projects.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { CreateProjectDto } from './dto/create-project.dto'; -import { BuildsService } from 'src/builds/builds.service'; -import { TestVariationsService } from 'src/test-variations/test-variations.service'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { BuildsService } from '../builds/builds.service'; +import { TestVariationsService } from '../test-variations/test-variations.service'; +import { PrismaService } from '../prisma/prisma.service'; import { Project } from '@prisma/client'; @Injectable() @@ -51,12 +51,11 @@ export class ProjectsService { }); try { + await Promise.all(project.builds.map(build => this.buildsService.remove(build.id))); await Promise.all( - project.testVariations.map(testVariation => - this.testVariationsService.remove(testVariation.id), + project.testVariations.map(testVariation => this.testVariationsService.remove(testVariation.id) ), ); - await Promise.all(project.builds.map(build => this.buildsService.remove(build.id))); } catch (err) { console.log(err); } diff --git a/src/shared/static/static.service.spec.ts b/src/shared/static/static.service.spec.ts index ad49a5b6..1cdfc951 100644 --- a/src/shared/static/static.service.spec.ts +++ b/src/shared/static/static.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StaticService } from './static.service'; +import { ConfigService } from '@nestjs/config'; describe('StaticService', () => { let service: StaticService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [StaticService], + providers: [ + StaticService, + { + provide: ConfigService, useValue: {} + } + ], }).compile(); service = module.get(StaticService); diff --git a/src/shared/static/static.service.ts b/src/shared/static/static.service.ts index bb340266..8f2954e4 100644 --- a/src/shared/static/static.service.ts +++ b/src/shared/static/static.service.ts @@ -1,15 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { resolve } from 'path'; -import { writeFileSync, readFileSync, unlink, mkdir } from 'fs'; +import path from 'path'; +import { writeFileSync, readFileSync, unlink, mkdirSync, existsSync } from 'fs'; import { PNG, PNGWithMetadata } from 'pngjs'; import { ConfigService } from '@nestjs/config'; @Injectable() export class StaticService { constructor(private configService: ConfigService) { - mkdir(this.configService.get('IMG_UPLOAD_FOLDER'), { recursive: true }, (err) => { - if (err) throw err; - }); } saveImage(imageName: string, imageBuffer: Buffer) { @@ -32,6 +29,18 @@ export class StaticService { } private getImagePath(imageName: string): string { - return resolve(this.configService.get('IMG_UPLOAD_FOLDER'), imageName); + const dir = this.configService.get('IMG_UPLOAD_FOLDER') + this.ensureDirectoryExistence(dir) + return path.resolve(dir, imageName); + } + + private ensureDirectoryExistence(dir: string) { + const filePath = path.resolve(dir) + if (existsSync(filePath)) { + return true; + } else { + mkdirSync(dir, { recursive: true }); + this.ensureDirectoryExistence(dir) + } } } diff --git a/src/test-runs/test-runs.controller.spec.ts b/src/test-runs/test-runs.controller.spec.ts new file mode 100644 index 00000000..99ef17a3 --- /dev/null +++ b/src/test-runs/test-runs.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TestRunsController } from './test-runs.controller'; +import { TestRunsService } from './test-runs.service'; + +describe('TestRuns Controller', () => { + let controller: TestRunsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TestRunsController], + providers: [{ provide: TestRunsService, useValue: {} }] + }).compile(); + + controller = module.get(TestRunsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/test-runs/test-runs.controller.ts b/src/test-runs/test-runs.controller.ts new file mode 100644 index 00000000..29e29903 --- /dev/null +++ b/src/test-runs/test-runs.controller.ts @@ -0,0 +1,55 @@ +import { Controller, Delete, UseGuards, Param, ParseUUIDPipe, Put, Body, Get, Query } from '@nestjs/common'; +import { ApiTags, ApiParam, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/auth.guard'; +import { TestRun } from '@prisma/client'; +import { TestRunsService } from './test-runs.service'; +import { IgnoreAreaDto } from '../test/dto/ignore-area.dto'; + +@ApiTags('test-runs') +@Controller('test-runs') +export class TestRunsController { + constructor(private testRunsService: TestRunsService) { } + + @Get() + @ApiQuery({ name: 'buildId', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + get(@Query('buildId', new ParseUUIDPipe()) buildId: string): Promise { + return this.testRunsService.findMany(buildId); + } + + @Get('approve/:id') + @ApiParam({ name: 'id', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + approveTestRun(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.testRunsService.approve(id); + } + + @Get('reject/:id') + @ApiParam({ name: 'id', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + rejectTestRun(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.testRunsService.reject(id); + } + + @Delete('/:id') + @ApiParam({ name: 'id', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + deleteTestRun(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.testRunsService.delete(id); + } + + @Put('ignoreArea/:testRunId') + @ApiParam({ name: 'testRunId', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + updateIgnoreAreas( + @Param('testRunId', new ParseUUIDPipe()) testRunId: string, + @Body() ignoreAreas: IgnoreAreaDto[], + ): Promise { + return this.testRunsService.updateIgnoreAreas(testRunId, ignoreAreas); + } +} diff --git a/src/test-runs/test-runs.module.ts b/src/test-runs/test-runs.module.ts index 021ac289..453b88b0 100644 --- a/src/test-runs/test-runs.module.ts +++ b/src/test-runs/test-runs.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { TestRunsService } from './test-runs.service'; -import { SharedModule } from 'src/shared/shared.module'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { SharedModule } from '../shared/shared.module'; +import { PrismaService } from '../prisma/prisma.service'; +import { TestRunsController } from './test-runs.controller'; @Module({ imports: [SharedModule], providers: [TestRunsService, PrismaService], - exports: [TestRunsService] + exports: [TestRunsService], + controllers: [TestRunsController] }) export class TestRunsModule {} diff --git a/src/test-runs/test-runs.service.spec.ts b/src/test-runs/test-runs.service.spec.ts index c370426d..34d6d82c 100644 --- a/src/test-runs/test-runs.service.spec.ts +++ b/src/test-runs/test-runs.service.spec.ts @@ -1,12 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestRunsService } from './test-runs.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { StaticService } from '../shared/static/static.service'; describe('TestRunsService', () => { let service: TestRunsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [TestRunsService], + providers: [TestRunsService, + { provide: PrismaService, useValue: {} }, + { provide: StaticService, useValue: {} },], }).compile(); service = module.get(TestRunsService); diff --git a/src/test-runs/test-runs.service.ts b/src/test-runs/test-runs.service.ts index 934b97d1..3236d6fa 100644 --- a/src/test-runs/test-runs.service.ts +++ b/src/test-runs/test-runs.service.ts @@ -1,27 +1,19 @@ import { Injectable } from '@nestjs/common'; import { PNG, PNGWithMetadata } from 'pngjs'; import Pixelmatch from 'pixelmatch'; -import { CreateTestRequestDto } from 'src/test/dto/create-test-request.dto'; -import { IgnoreAreaDto } from 'src/test/dto/ignore-area.dto'; -import { StaticService } from 'src/shared/static/static.service'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { CreateTestRequestDto } from '../test/dto/create-test-request.dto'; +import { IgnoreAreaDto } from '../test/dto/ignore-area.dto'; +import { StaticService } from '../shared/static/static.service'; +import { PrismaService } from '../prisma/prisma.service'; import { TestRun, TestStatus, TestVariation, TestRunCreateInput } from '@prisma/client'; @Injectable() export class TestRunsService { constructor(private prismaService: PrismaService, private staticService: StaticService) { } - async getAll(buildId: string): Promise<(TestRun & { - testVariation: TestVariation; - })[]> { + async findMany(buildId: string): Promise { return this.prismaService.testRun.findMany({ where: { buildId }, - include: { - testVariation: true, - }, - orderBy: { - createdAt: 'asc', - }, }); } @@ -36,27 +28,35 @@ export class TestRunsService { }); } - async approve(id: string): Promise { + async approve(id: string): Promise { const testRun = await this.findOne(id); - // remove old baseline - if (testRun.testVariation.baselineName) { - this.staticService.deleteImage(testRun.testVariation.baselineName); - } - // save new baseline const baseline = this.staticService.getImage(testRun.imageName) const imageName = `${Date.now()}.baseline.png`; this.staticService.saveImage(imageName, PNG.sync.write(baseline)); + // add in baseline history + await this.prismaService.baseline.create({ + data: { + baselineName: imageName, + testRun: { + connect: { + id: testRun.id + } + }, + testVariation: { + connect: { + id: testRun.testVariationId + } + }, + } + }) + return this.prismaService.testRun.update({ where: { id }, - include: { - testVariation: true, - }, data: { + baselineName: imageName, status: TestStatus.ok, testVariation: { update: { @@ -67,14 +67,9 @@ export class TestRunsService { }); } - async reject(id: string): Promise { + async reject(id: string): Promise { return this.prismaService.testRun.update({ where: { id }, - include: { - testVariation: true, - }, data: { status: TestStatus.failed, }, @@ -104,17 +99,24 @@ export class TestRunsService { id: createTestRequestDto.buildId, }, }, + name: testVariation.name, + browser: testVariation.browser, + device: testVariation.device, + os: testVariation.os, + viewport: testVariation.viewport, + baselineName: testVariation.baselineName, + ignoreAreas: testVariation.ignoreAreas, diffTollerancePercent: createTestRequestDto.diffTollerancePercent, status: TestStatus.new, }; // get baseline image let baseline: PNGWithMetadata - if (testVariation.baselineName) { + if (testRun.baselineName) { try { - baseline = this.staticService.getImage(testVariation.baselineName) + baseline = this.staticService.getImage(testRun.baselineName) } catch (ex) { - console.log(`Cannot load baseline image: ${testVariation.baselineName}. ${ex}`) + console.log(`Cannot load baseline image: ${testRun.baselineName}. ${ex}`) } } @@ -128,8 +130,8 @@ export class TestRunsService { // compare const pixelMisMatchCount = Pixelmatch( - this.applyIgnoreAreas(baseline, JSON.parse(testVariation.ignoreAreas)), - this.applyIgnoreAreas(image, JSON.parse(testVariation.ignoreAreas)), + this.applyIgnoreAreas(baseline, JSON.parse(testRun.ignoreAreas)), + this.applyIgnoreAreas(image, JSON.parse(testRun.ignoreAreas)), diff.data, baseline.width, baseline.height, @@ -178,6 +180,16 @@ export class TestRunsService { }); } + async updateIgnoreAreas(id: string, ignoreAreas: IgnoreAreaDto[]): Promise { + return this.prismaService.testRun + .update({ + where: { id }, + data: { + ignoreAreas: JSON.stringify(ignoreAreas), + }, + }); + } + private applyIgnoreAreas(image: PNG, ignoreAreas: IgnoreAreaDto[]): Buffer { ignoreAreas.forEach(area => { for (let y = area.y; y < area.y + area.height; y++) { diff --git a/src/test-variations/test-variations.controller.spec.ts b/src/test-variations/test-variations.controller.spec.ts index 961d6372..74fc4073 100644 --- a/src/test-variations/test-variations.controller.spec.ts +++ b/src/test-variations/test-variations.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestVariationsController } from './test-variations.controller'; +import { TestVariationsService } from './test-variations.service'; +import { PrismaService } from '../prisma/prisma.service'; describe('TestVariations Controller', () => { let controller: TestVariationsController; @@ -7,6 +9,10 @@ describe('TestVariations Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [TestVariationsController], + providers: [ + { provide: TestVariationsService, useValue: {} }, + { provide: PrismaService, useValue: {} }, + ], }).compile(); controller = module.get(TestVariationsController); diff --git a/src/test-variations/test-variations.controller.ts b/src/test-variations/test-variations.controller.ts index 3e68f69a..d23e9622 100644 --- a/src/test-variations/test-variations.controller.ts +++ b/src/test-variations/test-variations.controller.ts @@ -1,9 +1,46 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { Controller, ParseUUIDPipe, Get, UseGuards, Param, Query, Put, Body } from '@nestjs/common'; +import { ApiTags, ApiParam, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; import { TestVariationsService } from './test-variations.service'; +import { TestVariation, Baseline } from '@prisma/client'; +import { JwtAuthGuard } from '../auth/guards/auth.guard'; +import { PrismaService } from '../prisma/prisma.service'; +import { IgnoreAreaDto } from '../test/dto/ignore-area.dto'; @ApiTags('test-variations') @Controller('test-variations') export class TestVariationsController { - constructor(private testVariations: TestVariationsService) {} + constructor(private testVariations: TestVariationsService, private prismaService: PrismaService) { } + + @Get() + @ApiQuery({ name: 'projectId', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + getList( + @Query('projectId', new ParseUUIDPipe()) projectId, + ): Promise { + return this.prismaService.testVariation.findMany({ + where: { projectId } + }); + } + + @Get(':id') + @ApiQuery({ name: 'id', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + getDetails( + @Param('id', new ParseUUIDPipe()) id, + ): Promise { + return this.testVariations.getDetails(id); + } + + @Put('ignoreArea/:variationId') + @ApiParam({ name: 'variationId', required: true }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + updateIgnoreAreas( + @Param('variationId', new ParseUUIDPipe()) variationId: string, + @Body() ignoreAreas: IgnoreAreaDto[], + ): Promise { + return this.testVariations.updateIgnoreAreas(variationId, ignoreAreas); + } } diff --git a/src/test-variations/test-variations.module.ts b/src/test-variations/test-variations.module.ts index 047c7600..337832e8 100644 --- a/src/test-variations/test-variations.module.ts +++ b/src/test-variations/test-variations.module.ts @@ -1,11 +1,9 @@ import { Module } from '@nestjs/common'; import { TestVariationsService } from './test-variations.service'; import { TestVariationsController } from './test-variations.controller'; -import { TestRunsModule } from 'src/test-runs/test-runs.module'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; @Module({ - imports: [TestRunsModule], providers: [TestVariationsService, PrismaService], controllers: [TestVariationsController], exports: [TestVariationsService] diff --git a/src/test-variations/test-variations.service.spec.ts b/src/test-variations/test-variations.service.spec.ts index 60532210..01cc9982 100644 --- a/src/test-variations/test-variations.service.spec.ts +++ b/src/test-variations/test-variations.service.spec.ts @@ -1,18 +1,238 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestVariationsService } from './test-variations.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateTestRequestDto } from '../test/dto/create-test-request.dto'; +import { StaticService } from '../shared/static/static.service'; +import { IgnoreAreaDto } from 'src/test/dto/ignore-area.dto'; +import { TestVariation, Baseline } from '@prisma/client'; + +const initModule = async ({ + imageDeleteMock = jest.fn(), + variationFindOneMock = jest.fn, + variationFindManyMock = jest.fn().mockReturnValue([]), + variationCreateMock = jest.fn(), + variationUpdateMock = jest.fn(), + variationDeleteMock = jest.fn(), + baselineDeleteMock = jest.fn() +}) => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TestVariationsService, + { + provide: StaticService, useValue: { + deleteImage: imageDeleteMock + } + }, + { + provide: PrismaService, useValue: { + testVariation: { + findOne: variationFindOneMock, + findMany: variationFindManyMock, + create: variationCreateMock, + update: variationUpdateMock, + delete: variationDeleteMock, + }, + baseline: { + delete: baselineDeleteMock + } + } + }, + ], + }).compile(); + + return module.get(TestVariationsService); +} + +const dataRequiredFields: CreateTestRequestDto = { + buildId: 'buildId', + projectId: 'projectId', + name: 'Test name', + imageBase64: 'Image' +} + +const dataAllFields: CreateTestRequestDto = { + buildId: 'buildId', + projectId: 'projectId', + name: 'Test name', + imageBase64: 'Image', + os: 'OS', + browser: 'browser', + viewport: 'viewport', + device: 'device', +} describe('TestVariationsService', () => { let service: TestVariationsService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [TestVariationsService], - }).compile(); + describe('getDetails', () => { + it('can find one', async () => { + const id = 'test id' + const variationFindOneMock = jest.fn() + service = await initModule({ variationFindOneMock }) + + await service.getDetails(id) + + expect(variationFindOneMock).toHaveBeenCalledWith({ + where: { id }, + include: { + baselines: { + include: { + testRun: true, + }, + orderBy: { + createdAt: 'desc' + } + }, + } + }) + }) + }) + + describe('findOrCreate', () => { + + it('can find by required fields', async () => { + const data = dataRequiredFields + const variationFindManyMock = jest.fn() + service = await initModule({ variationFindManyMock: variationFindManyMock.mockResolvedValueOnce([data]) }) + + const result = await service.findOrCreate(data) + + expect(variationFindManyMock).toHaveBeenCalledWith({ + where: { + name: data.name, + projectId: data.projectId, + os: null, + browser: null, + viewport: null, + device: null, + }, + }) + expect(result).toBe(data) + }) + + it('can find by all fields', async () => { + const data = dataAllFields + const variationFindManyMock = jest.fn() + service = await initModule({ variationFindManyMock: variationFindManyMock.mockResolvedValueOnce([data]) }) + + const result = await service.findOrCreate(data) + + expect(variationFindManyMock).toHaveBeenCalledWith({ + where: { + name: data.name, + projectId: data.projectId, + os: data.os, + browser: data.browser, + viewport: data.viewport, + device: data.device, + }, + }) + expect(result).toBe(data) + }) + + it('can create if not found', async () => { + const data = dataAllFields + const variationCreateMock = jest.fn() + service = await initModule({ variationCreateMock: variationCreateMock.mockResolvedValueOnce(data) }) + + const result = await service.findOrCreate(data) + + expect(variationCreateMock).toHaveBeenCalledWith({ + data: { + name: data.name, + os: data.os, + browser: data.browser, + viewport: data.viewport, + device: data.device, + project: { + connect: { + id: data.projectId, + } + } + }, + }) + expect(result).toBe(data) + }) + }) + + describe('updateIgnoreAreas', () => { + it('can update', async () => { + const id = 'test id' + const ignoreAreas: IgnoreAreaDto[] = [ + { + x: 1, + y: 2.3, + width: 442.1, + height: 32.0 + } + ] + const variationUpdateMock = jest.fn() + service = await initModule({ variationUpdateMock }) + + await service.updateIgnoreAreas(id, ignoreAreas) + + expect(variationUpdateMock).toBeCalledWith({ + where: { + id + }, + data: { + ignoreAreas: JSON.stringify(ignoreAreas) + } + }) + }) + }) + + describe('remove', () => { + it('can remove', async () => { + const id = 'test id' + const variation: TestVariation & { + baselines: Baseline[]; + } = { + id, + projectId: 'project Id', + name: 'Test name', + baselineName: 'baselineName', + os: 'OS', + browser: 'browser', + viewport: 'viewport', + device: 'device', + ignoreAreas: '[]', + createdAt: new Date(), + updatedAt: new Date(), + baselines: [ + { + id: 'baseline id 1', + baselineName: 'image name 1', + testVariationId: id, + testRunId: 'test run id 1', + createdAt: new Date(), + updatedAt: new Date(), + }, + ] + } + const variationFindOneMock = jest.fn() + const variationDeleteMock = jest.fn() + const imageDeleteMock = jest.fn() + const baselineDeleteMock = jest.fn() + service = await initModule( + { + variationFindOneMock: variationFindOneMock.mockResolvedValueOnce(variation), + variationDeleteMock, + imageDeleteMock, + baselineDeleteMock + }) - service = module.get(TestVariationsService); - }); + await service.remove(id) - it('should be defined', () => { - expect(service).toBeDefined(); - }); + expect(imageDeleteMock).toHaveBeenCalledWith( + variation.baselines[0].baselineName + ) + expect(baselineDeleteMock).toHaveBeenCalledWith({ + where: { id: variation.baselines[0].id } + }) + expect(variationDeleteMock).toHaveBeenCalledWith({ + where: { id: variation.id } + }) + }) + }) }); diff --git a/src/test-variations/test-variations.service.ts b/src/test-variations/test-variations.service.ts index fc6e4c95..0e698f02 100644 --- a/src/test-variations/test-variations.service.ts +++ b/src/test-variations/test-variations.service.ts @@ -1,18 +1,32 @@ import { Injectable } from '@nestjs/common'; -import { CreateTestRequestDto } from 'src/test/dto/create-test-request.dto'; -import { IgnoreAreaDto } from 'src/test/dto/ignore-area.dto'; -import { TestRunsService } from 'src/test-runs/test-runs.service'; -import { StaticService } from 'src/shared/static/static.service'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { TestVariation } from '@prisma/client'; +import { CreateTestRequestDto } from '../test/dto/create-test-request.dto'; +import { IgnoreAreaDto } from '../test/dto/ignore-area.dto'; +import { PrismaService } from '../prisma/prisma.service'; +import { TestVariation, Baseline } from '@prisma/client'; +import { StaticService } from '../shared/static/static.service'; @Injectable() export class TestVariationsService { constructor( - private testRunsService: TestRunsService, - private staticService: StaticService, private prismaService: PrismaService, - ) {} + private staticService: StaticService, + ) { } + + async getDetails(id: string): Promise { + return this.prismaService.testVariation.findOne({ + where: { id }, + include: { + baselines: { + include: { + testRun: true, + }, + orderBy: { + createdAt: 'desc' + } + }, + }, + }); + } async findOrCreate(createTestDto: CreateTestRequestDto): Promise { const data = { @@ -52,20 +66,20 @@ export class TestVariationsService { } async remove(id: string): Promise { - const testVariation = await this.prismaService.testVariation.findOne({ - where: { id }, - include: { - testRuns: true, - }, - }); + const variation = await this.getDetails(id) + // clear history try { await Promise.all( - testVariation.testRuns.map(testRun => this.testRunsService.delete(testRun.id)), - ); - if (testVariation.baselineName) this.staticService.deleteImage(testVariation.baselineName); + variation.baselines.map(baseline => Promise.all([ + this.staticService.deleteImage(baseline.baselineName), + this.prismaService.baseline.delete({ + where: { id: baseline.id } + }) + ])) + ) } catch (err) { - console.log(err); + console.log(err) } return this.prismaService.testVariation.delete({ diff --git a/src/test/dto/create-test-request.dto.ts b/src/test/dto/create-test-request.dto.ts index 76f44f0f..b7ab1c18 100644 --- a/src/test/dto/create-test-request.dto.ts +++ b/src/test/dto/create-test-request.dto.ts @@ -13,22 +13,22 @@ export class CreateTestRequestDto { @ApiProperty() @IsOptional() @IsString() - os: string; + os?: string; @ApiProperty() @IsOptional() @IsString() - browser: string; + browser?: string; @ApiProperty() @IsOptional() @IsString() - viewport: string; + viewport?: string; @ApiProperty() @IsOptional() @IsString() - device: string; + device?: string; @ApiProperty() @IsUUID() @@ -41,5 +41,5 @@ export class CreateTestRequestDto { @ApiProperty() @IsOptional() @IsNumber() - diffTollerancePercent: number; + diffTollerancePercent?: number; } diff --git a/src/test/test.controller.spec.ts b/src/test/test.controller.spec.ts index 11e5e26d..ef9dafa7 100644 --- a/src/test/test.controller.spec.ts +++ b/src/test/test.controller.spec.ts @@ -1,5 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestController } from './test.controller'; +import { TestService } from './test.service'; +import { TestVariationsService } from '../test-variations/test-variations.service'; +import { TestRunsService } from '../test-runs/test-runs.service'; +import { PrismaService } from '../prisma/prisma.service'; describe('Test Controller', () => { let controller: TestController; @@ -7,6 +11,12 @@ describe('Test Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [TestController], + providers: [ + { provide: PrismaService, useValue: {} }, + { provide: TestService, useValue: {} }, + { provide: TestVariationsService, useValue: {} }, + { provide: TestRunsService, useValue: {} }, + ], }).compile(); controller = module.get(TestController); diff --git a/src/test/test.controller.ts b/src/test/test.controller.ts index bdea243e..1ffcf3bc 100644 --- a/src/test/test.controller.ts +++ b/src/test/test.controller.ts @@ -1,21 +1,13 @@ import { Controller, - Get, UseGuards, - Param, - ParseUUIDPipe, Post, Body, - Put, - Delete, } from '@nestjs/common'; -import { ApiTags, ApiParam, ApiOkResponse, ApiBearerAuth, ApiSecurity } from '@nestjs/swagger'; -import { JwtAuthGuard } from 'src/auth/guards/auth.guard'; +import { ApiTags, ApiOkResponse, ApiSecurity } from '@nestjs/swagger'; import { TestService } from './test.service'; import { CreateTestRequestDto } from './dto/create-test-request.dto'; -import { ApiGuard } from 'src/auth/guards/api.guard'; -import { IgnoreAreaDto } from './dto/ignore-area.dto'; -import { TestRun, TestVariation } from '@prisma/client'; +import { ApiGuard } from '../auth/guards/api.guard'; import { TestRunResultDto } from './dto/testRunResult.dto'; @ApiTags('test') @@ -23,45 +15,6 @@ import { TestRunResultDto } from './dto/testRunResult.dto'; export class TestController { constructor(private testService: TestService) { } - @Get(':testRunId') - @ApiParam({ name: 'testRunId', required: true }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - getTestRunDetails( - @Param('testRunId', new ParseUUIDPipe()) testRunId: string, - ): Promise { - return this.testService.getTestRunById(testRunId); - } - - @Get('approve/:testRunId') - @ApiParam({ name: 'testRunId', required: true }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - approveTestRun(@Param('testRunId', new ParseUUIDPipe()) testRunId: string): Promise { - return this.testService.approveTestRun(testRunId); - } - - @Get('reject/:testRunId') - @ApiParam({ name: 'testRunId', required: true }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - rejectTestRun(@Param('testRunId', new ParseUUIDPipe()) testRunId: string): Promise { - return this.testService.rejectTestRun(testRunId); - } - - @Put('ignoreArea/:variationId') - @ApiParam({ name: 'variationId', required: true }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - updateIgnoreAreas( - @Param('variationId', new ParseUUIDPipe()) variationId: string, - @Body() ignoreAreas: IgnoreAreaDto[], - ): Promise { - return this.testService.updateIgnoreAreas(variationId, ignoreAreas); - } - @Post() @ApiSecurity('api_key') @ApiOkResponse({ type: TestRunResultDto }) @@ -69,12 +22,4 @@ export class TestController { postTestRun(@Body() createTestRequestDto: CreateTestRequestDto): Promise { return this.testService.postTestRun(createTestRequestDto); } - - @Delete('/:testRunId') - @ApiParam({ name: 'testRunId', required: true }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - deleteTestRun(@Param('testRunId', new ParseUUIDPipe()) testRunId: string): Promise { - return this.testService.deleteTestRun(testRunId); - } } diff --git a/src/test/test.module.ts b/src/test/test.module.ts index 34f483e6..b80767ba 100644 --- a/src/test/test.module.ts +++ b/src/test/test.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TestService } from './test.service'; import { TestController } from './test.controller'; -import { TestRunsModule } from 'src/test-runs/test-runs.module'; -import { TestVariationsModule } from 'src/test-variations/test-variations.module'; -import { UsersModule } from 'src/users/users.module'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { TestRunsModule } from '../test-runs/test-runs.module'; +import { TestVariationsModule } from '../test-variations/test-variations.module'; +import { UsersModule } from '../users/users.module'; +import { PrismaService } from '../prisma/prisma.service'; @Module({ imports: [TestRunsModule, TestVariationsModule, UsersModule], diff --git a/src/test/test.service.spec.ts b/src/test/test.service.spec.ts index 1aacbe5d..05b7f0f9 100644 --- a/src/test/test.service.spec.ts +++ b/src/test/test.service.spec.ts @@ -1,12 +1,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TestService } from './test.service'; +import { TestRunsService } from '../test-runs/test-runs.service'; +import { TestVariationsService } from '../test-variations/test-variations.service'; describe('TestService', () => { let service: TestService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [TestService], + providers: [TestService, { provide: TestRunsService, useValue: {} }, { provide: TestVariationsService, useValue: {} },], }).compile(); service = module.get(TestService); diff --git a/src/test/test.service.ts b/src/test/test.service.ts index 7f0427a7..1568172c 100644 --- a/src/test/test.service.ts +++ b/src/test/test.service.ts @@ -1,9 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { TestVariationsService } from 'src/test-variations/test-variations.service'; -import { TestRunsService } from 'src/test-runs/test-runs.service'; +import { TestVariationsService } from '../test-variations/test-variations.service'; +import { TestRunsService } from '../test-runs/test-runs.service'; import { CreateTestRequestDto } from './dto/create-test-request.dto'; -import { IgnoreAreaDto } from './dto/ignore-area.dto'; -import { TestRun, TestVariation } from '@prisma/client'; import { TestRunResultDto } from './dto/testRunResult.dto'; @Injectable() @@ -13,19 +11,6 @@ export class TestService { private testRunsService: TestRunsService, ) { } - async getTestRunsByBuildId(buildId: string): Promise<(TestRun & { - testVariation: TestVariation; - })[]> { - - return this.testRunsService.getAll(buildId); - } - - async getTestRunById(testRunId: string): Promise { - return this.testRunsService.findOne(testRunId); - } - async postTestRun(createTestRequestDto: CreateTestRequestDto): Promise { const testVariation = await this.testVariationService.findOrCreate(createTestRequestDto); @@ -33,27 +18,4 @@ export class TestService { return new TestRunResultDto(testRun, testVariation); } - - async deleteTestRun(id: string): Promise { - return this.testRunsService.delete(id); - } - - async approveTestRun(testRunId: string): Promise { - return this.testRunsService.approve(testRunId); - } - - async rejectTestRun(testRunId: string): Promise { - return this.testRunsService.reject(testRunId); - } - - async updateIgnoreAreas( - testRunId: string, - ignoreAreas: IgnoreAreaDto[], - ): Promise { - return this.testVariationService.updateIgnoreAreas(testRunId, ignoreAreas); - } } diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 5bd21bae..dc032175 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -1,5 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; describe('Users Controller', () => { let controller: UsersController; @@ -7,6 +8,7 @@ describe('Users Controller', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], + providers: [{ provide: UsersService, useValue: {} }] }).compile(); controller = module.get(UsersController); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index d11c6e4c..cc2ddc29 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,9 +1,9 @@ -import { Controller, Post, Body, Get, UseGuards, Param, ParseUUIDPipe, Put, Request } from '@nestjs/common'; +import { Controller, Post, Body, Get, UseGuards, Param, ParseUUIDPipe, Put } from '@nestjs/common'; import { UsersService } from './users.service'; import { ApiOkResponse, ApiParam, ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { UserLoginResponseDto } from './dto/user-login-response.dto'; import { CreateUserDto } from './dto/user-create.dto'; -import { JwtAuthGuard } from 'src/auth/guards/auth.guard'; +import { JwtAuthGuard } from '../auth/guards/auth.guard'; import { UserDto } from './dto/user.dto'; import { UpdateUserDto } from './dto/user-update.dto'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; @@ -49,24 +49,14 @@ export class UsersController { return this.usersService.changePassword(user, password) } - @Get(':id') - @ApiParam({ name: 'id', required: true }) - @ApiOkResponse({ type: UserDto }) - @ApiBearerAuth() - @UseGuards(JwtAuthGuard) - get(@Param('id', new ParseUUIDPipe()) id: string): Promise { - return this.usersService.get(id); - } - - @Put(':id') - @ApiParam({ name: 'id', required: true }) + @Put() @ApiOkResponse({ type: UserLoginResponseDto }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) - updated( - @Param('id', new ParseUUIDPipe()) id: string, + update( + @CurrentUser() user: User, @Body() updateUserDto: UpdateUserDto ): Promise { - return this.usersService.update(id, updateUserDto); + return this.usersService.update(user.id, updateUserDto); } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 462844ee..7d54196b 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; import { UsersController } from './users.controller'; -import { AuthModule } from 'src/auth/auth.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ imports: [AuthModule], diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index 65a7ef35..de28b0ba 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; +import { AuthService } from '../auth/auth.service'; +import { PrismaService } from '../prisma/prisma.service'; describe('UsersService', () => { let service: UsersService; @@ -8,6 +10,14 @@ describe('UsersService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, + { + provide: PrismaService, useValue: { + user: { + findMany: jest.fn().mockResolvedValueOnce([]) + } + } + }, + { provide: AuthService, useValue: {} }, ], }).compile(); diff --git a/src/users/users.service.ts b/src/users/users.service.ts index c3366fee..4eb31726 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,11 +1,11 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; import { CreateUserDto } from './dto/user-create.dto'; import { UserLoginResponseDto } from './dto/user-login-response.dto'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../prisma/prisma.service'; import { User } from '@prisma/client'; import { UserDto } from './dto/user.dto'; import { UpdateUserDto } from './dto/user-update.dto'; -import { AuthService } from 'src/auth/auth.service'; +import { AuthService } from '../auth/auth.service'; import { UserLoginRequestDto } from './dto/user-login-request.dto'; @Injectable() @@ -67,8 +67,12 @@ export class UsersService { } } + async findOne(id: string): Promise { + return this.prismaService.user.findOne({ where: { id } }) + } + async get(id: string): Promise { - const user = await this.prismaService.user.findOne({ where: { id } }) + const user = await this.findOne(id) return new UserDto(user) } diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts deleted file mode 100644 index 50cda623..00000000 --- a/test/app.e2e-spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; - -describe('AppController (e2e)', () => { - let app: INestApplication; - - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); diff --git a/test/preconditions.ts b/test/preconditions.ts new file mode 100644 index 00000000..1ff5b8a9 --- /dev/null +++ b/test/preconditions.ts @@ -0,0 +1,29 @@ +import { INestApplication } from '@nestjs/common'; +import { UsersService } from 'src/users/users.service'; +import { UserLoginRequestDto } from 'src/users/dto/user-login-request.dto'; +import uuidAPIKey from 'uuid-apikey'; +import request, { Test, CallbackHandler, Request } from 'supertest'; +import { CreateUserDto } from 'src/users/dto/user-create.dto'; + +export const generateUser = (password: string): { email: string, password: string, firstName: string, lastName: string } => ({ + email: `${uuidAPIKey.create().uuid}@example.com'`, + password, + firstName: 'fName', + lastName: 'lName', +}) + +export const requestWithAuth = (app: INestApplication, method: 'post' | 'get' | 'put' | 'delete', url: string, body = {}, token: string): Test => + request(app.getHttpServer()) + [method](url) + .set('Authorization', 'Bearer ' + token) + .send(body) + +export const haveUserLogged = async (usersService: UsersService) => { + const password = '123456' + const user = await usersService.create(generateUser(password)) + + return usersService.login({ + email: user.email, + password + }) +} \ No newline at end of file diff --git a/test/projects.e2e-spec.ts b/test/projects.e2e-spec.ts new file mode 100644 index 00000000..d420bc22 --- /dev/null +++ b/test/projects.e2e-spec.ts @@ -0,0 +1,93 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { AppModule } from '../src/app.module'; +import { UsersService } from '../src/users/users.service'; +import { requestWithAuth, haveUserLogged } from './preconditions'; +import { ProjectsService } from '../src/projects/projects.service'; +import uuidAPIKey from 'uuid-apikey'; + +const project = { + id: uuidAPIKey.create().uuid, + name: 'Test project' +} + +const projectServiceMock = { + findAll: () => ['test'], + create: () => project, + remove: () => project, +}; + +describe('Projects (e2e)', () => { + let app: INestApplication; + let usersService: UsersService; + let loggedUser + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(ProjectsService) + .useValue(projectServiceMock) + .compile(); + + app = moduleFixture.createNestApplication(); + usersService = moduleFixture.get(UsersService) + + await app.init(); + loggedUser = await haveUserLogged(usersService) + }); + + afterAll(async () => { + await app.close(); + }); + + describe('POST /', () => { + it('200', () => { + return requestWithAuth(app, 'post', '/projects', project, loggedUser.token) + .expect(201) + .expect(res => { + expect(res.body.name).toBe(project.name); + }) + }); + + it('401', () => { + return requestWithAuth(app, 'post', '/projects', project, '') + .expect(401) + }); + }) + + describe('GET /', () => { + it('200', async () => { + const res = await requestWithAuth(app, 'get', '/projects', {}, loggedUser.token) + .expect(200) + + expect(res.body).toEqual(expect.arrayContaining(projectServiceMock.findAll())) + }); + + it('401', async () => { + await requestWithAuth(app, 'get', '/projects', {}, '') + .expect(401) + }); + }) + + + describe('DELETE /', () => { + + it('can delete', async () => { + const res = await requestWithAuth(app, 'delete', `/projects/${project.id}`, {}, loggedUser.token) + .expect(200) + + expect(res.body).toStrictEqual(projectServiceMock.remove()) + }) + + it('not valid UUID', async () => { + await requestWithAuth(app, 'delete', `/projects/123`, {}, loggedUser.token) + .expect(400) + }) + + it('not valid token', async () => { + await requestWithAuth(app, 'delete', `/projects/${project.id}`, {}, 'asd') + .expect(401) + }) + }) +}); diff --git a/test/users.e2e-spec.ts b/test/users.e2e-spec.ts new file mode 100644 index 00000000..9342323a --- /dev/null +++ b/test/users.e2e-spec.ts @@ -0,0 +1,127 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import uuidAPIKey from 'uuid-apikey'; +import { UsersService } from '../src/users/users.service'; +import { CreateUserDto } from '../src/users/dto/user-create.dto'; +import { UserLoginRequestDto } from '../src/users/dto/user-login-request.dto'; +import { compareSync } from 'bcryptjs'; +import { requestWithAuth, generateUser } from './preconditions'; + +describe('Users (e2e)', () => { + let app: INestApplication; + let usersService: UsersService; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + usersService = moduleFixture.get(UsersService) + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('POST /register', () => { + const user: CreateUserDto = { + email: `${uuidAPIKey.create().uuid}@example.com'`, + password: '123456', + firstName: 'fName', + lastName: 'lName', + } + return request(app.getHttpServer()) + .post('/users/register') + .send(user) + .expect(201) + .expect(res => { + expect(res.body.email).toBe(user.email); + expect(res.body.firstName).toBe(user.firstName); + expect(res.body.lastName).toBe(user.lastName); + expect(res.body.apiKey).not.toBeNull(); + }) + }); + + it('POST /login', async () => { + const password = '123456' + const user = await usersService.create(generateUser(password)) + const loginData: UserLoginRequestDto = { + email: user.email, + password + } + + return request(app.getHttpServer()) + .post('/users/login') + .send(loginData) + .expect(201) + .expect(res => { + expect(res.body.id).toBe(user.id); + expect(res.body.email).toBe(user.email); + expect(res.body.firstName).toBe(user.firstName); + expect(res.body.lastName).toBe(user.lastName); + expect(res.body.apiKey).toBe(user.apiKey); + expect(res.body.token).not.toBeNull(); + }) + }); + + it('GET /newApiKey', async () => { + const password = '123456' + const user = await usersService.create(generateUser(password)) + const loggedUser = await usersService.login({ + email: user.email, + password + }) + + const res = await requestWithAuth(app, 'get', '/users/newApiKey', {}, loggedUser.token) + .expect(200) + + const newUser = await usersService.findOne(user.id) + expect(res.text).not.toBe(user.apiKey); + expect(res.text).toBe(newUser.apiKey); + }); + + it('PUT /password', async () => { + const newPassword = 'newPassword' + const password = '123456' + const user = await usersService.create(generateUser(password)) + const loggedUser = await usersService.login({ + email: user.email, + password + }) + + await requestWithAuth(app, 'put', '/users/password', { password: newPassword }, loggedUser.token) + .expect(200) + + const newUser = await usersService.findOne(user.id) + expect(compareSync(newPassword, newUser.password)).toBe(true); + }); + + it('PUT /', async () => { + const password = '123456' + const user = await usersService.create(generateUser(password)) + const editedUser = { + email: `${uuidAPIKey.create().uuid}@example.com'`, + firstName: 'EDITEDfName', + lastName: 'EDITEDlName', + } + + const loggedUser = await usersService.login({ + email: user.email, + password + }) + + const res = await requestWithAuth(app, 'put', '/users', editedUser, loggedUser.token) + .expect(200) + + expect(res.body.id).toBe(user.id); + expect(res.body.email).toBe(editedUser.email); + expect(res.body.firstName).toBe(editedUser.firstName); + expect(res.body.lastName).toBe(editedUser.lastName); + expect(res.body.apiKey).toBe(user.apiKey); + expect(res.body.token).not.toBeNull(); + }); +});