mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	test(server): auth e2e (#3492)
* test(server): auth controller e2e test * test(server): user e2e test * refactor(server): album e2e * fix: linting
This commit is contained in:
		| @@ -1,225 +0,0 @@ | ||||
| import { | ||||
|   AlbumResponseDto, | ||||
|   AuthService, | ||||
|   AuthUserDto, | ||||
|   CreateAlbumDto, | ||||
|   SharedLinkCreateDto, | ||||
|   SharedLinkResponseDto, | ||||
|   UserService, | ||||
| } from '@app/domain'; | ||||
| import { AppModule } from '@app/immich/app.module'; | ||||
| import { SharedLinkType } from '@app/infra/entities'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { DataSource } from 'typeorm'; | ||||
| import { authCustom, clearDb, getAuthUser } from '../test/test-utils'; | ||||
|  | ||||
| async function _createAlbum(app: INestApplication, data: CreateAlbumDto) { | ||||
|   const res = await request(app.getHttpServer()).post('/album').send(data); | ||||
|   expect(res.status).toEqual(201); | ||||
|   return res.body as AlbumResponseDto; | ||||
| } | ||||
|  | ||||
| async function _createAlbumSharedLink(app: INestApplication, data: Omit<SharedLinkCreateDto, 'type'>) { | ||||
|   const res = await request(app.getHttpServer()) | ||||
|     .post('/shared-link') | ||||
|     .send({ ...data, type: SharedLinkType.ALBUM }); | ||||
|   expect(res.status).toEqual(201); | ||||
|   return res.body as SharedLinkResponseDto; | ||||
| } | ||||
|  | ||||
| describe('Album', () => { | ||||
|   let app: INestApplication; | ||||
|   let database: DataSource; | ||||
|  | ||||
|   describe('without auth', () => { | ||||
|     beforeAll(async () => { | ||||
|       const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); | ||||
|  | ||||
|       app = moduleFixture.createNestApplication(); | ||||
|       database = app.get(DataSource); | ||||
|       await app.init(); | ||||
|     }); | ||||
|  | ||||
|     afterAll(async () => { | ||||
|       await clearDb(database); | ||||
|       await app.close(); | ||||
|     }); | ||||
|  | ||||
|     it('prevents fetching albums if not auth', async () => { | ||||
|       const { status } = await request(app.getHttpServer()).get('/album'); | ||||
|       expect(status).toEqual(401); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('with auth', () => { | ||||
|     let userService: UserService; | ||||
|     let authService: AuthService; | ||||
|     let authUser: AuthUserDto; | ||||
|  | ||||
|     beforeAll(async () => { | ||||
|       const builder = Test.createTestingModule({ imports: [AppModule] }); | ||||
|       authUser = getAuthUser(); | ||||
|       const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); | ||||
|  | ||||
|       app = moduleFixture.createNestApplication(); | ||||
|       userService = app.get(UserService); | ||||
|       authService = app.get(AuthService); | ||||
|       database = app.get(DataSource); | ||||
|       await app.init(); | ||||
|     }); | ||||
|  | ||||
|     afterAll(async () => { | ||||
|       await app.close(); | ||||
|     }); | ||||
|  | ||||
|     describe('with empty DB', () => { | ||||
|       it('rejects invalid shared param', async () => { | ||||
|         const { status } = await request(app.getHttpServer()).get('/album?shared=invalid'); | ||||
|         expect(status).toEqual(400); | ||||
|       }); | ||||
|  | ||||
|       it('rejects invalid assetId param', async () => { | ||||
|         const { status } = await request(app.getHttpServer()).get('/album?assetId=invalid'); | ||||
|         expect(status).toEqual(400); | ||||
|       }); | ||||
|  | ||||
|       // TODO - Until someone figure out how to passed in a logged in user to the request. | ||||
|       //   it('creates an album', async () => { | ||||
|       //     const data: CreateAlbumDto = { | ||||
|       //       albumName: 'first albbum', | ||||
|       //     }; | ||||
|       //     const body = await _createAlbum(app, data); | ||||
|       //     expect(body).toEqual( | ||||
|       //       expect.objectContaining({ | ||||
|       //         ownerId: authUser.id, | ||||
|       //         albumName: data.albumName, | ||||
|       //       }), | ||||
|       //     ); | ||||
|       //   }); | ||||
|     }); | ||||
|  | ||||
|     describe('with albums in DB', () => { | ||||
|       const userOneSharedUser = 'userOneSharedUser'; | ||||
|       const userOneSharedLink = 'userOneSharedLink'; | ||||
|       const userOneNotShared = 'userOneNotShared'; | ||||
|       const userTwoSharedUser = 'userTwoSharedUser'; | ||||
|       const userTwoSharedLink = 'userTwoSharedLink'; | ||||
|       const userTwoNotShared = 'userTwoNotShared'; | ||||
|       let userOne: AuthUserDto; | ||||
|       let userTwo: AuthUserDto; | ||||
|  | ||||
|       beforeAll(async () => { | ||||
|         // setup users | ||||
|         const adminSignUpDto = await authService.adminSignUp({ | ||||
|           email: 'one@test.com', | ||||
|           password: '1234', | ||||
|           firstName: 'one', | ||||
|           lastName: 'test', | ||||
|         }); | ||||
|         userOne = { ...adminSignUpDto, isAdmin: true }; // TODO: find out why adminSignUp doesn't have isAdmin (maybe can just return UserResponseDto) | ||||
|  | ||||
|         userTwo = await userService.createUser({ | ||||
|           email: 'two@test.com', | ||||
|           password: '1234', | ||||
|           firstName: 'two', | ||||
|           lastName: 'test', | ||||
|         }); | ||||
|  | ||||
|         // add user one albums | ||||
|         authUser = userOne; | ||||
|         const userOneAlbums = await Promise.all([ | ||||
|           _createAlbum(app, { albumName: userOneSharedUser, sharedWithUserIds: [userTwo.id] }), | ||||
|           _createAlbum(app, { albumName: userOneSharedLink }), | ||||
|           _createAlbum(app, { albumName: userOneNotShared }), | ||||
|         ]); | ||||
|  | ||||
|         // add shared link to userOneSharedLink album | ||||
|         await _createAlbumSharedLink(app, { albumId: userOneAlbums[1].id }); | ||||
|  | ||||
|         // add user two albums | ||||
|         authUser = userTwo; | ||||
|         const userTwoAlbums = await Promise.all([ | ||||
|           _createAlbum(app, { albumName: userTwoSharedUser, sharedWithUserIds: [userOne.id] }), | ||||
|           _createAlbum(app, { albumName: userTwoSharedLink }), | ||||
|           _createAlbum(app, { albumName: userTwoNotShared }), | ||||
|         ]); | ||||
|  | ||||
|         // add shared link to userTwoSharedLink album | ||||
|         await _createAlbumSharedLink(app, { albumId: userTwoAlbums[1].id }); | ||||
|  | ||||
|         // set user one as authed for next requests | ||||
|         authUser = userOne; | ||||
|       }); | ||||
|  | ||||
|       afterAll(async () => { | ||||
|         await clearDb(database); | ||||
|       }); | ||||
|  | ||||
|       it('returns the album collection including owned and shared', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get('/album'); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(3); | ||||
|         expect(body).toEqual( | ||||
|           expect.arrayContaining([ | ||||
|             expect.objectContaining({ ownerId: userOne.id, albumName: userOneSharedUser, shared: true }), | ||||
|             expect.objectContaining({ ownerId: userOne.id, albumName: userOneSharedLink, shared: true }), | ||||
|             expect.objectContaining({ ownerId: userOne.id, albumName: userOneNotShared, shared: false }), | ||||
|           ]), | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('returns the album collection filtered by shared', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get('/album?shared=true'); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(3); | ||||
|         expect(body).toEqual( | ||||
|           expect.arrayContaining([ | ||||
|             expect.objectContaining({ ownerId: userOne.id, albumName: userOneSharedUser, shared: true }), | ||||
|             expect.objectContaining({ ownerId: userOne.id, albumName: userOneSharedLink, shared: true }), | ||||
|             expect.objectContaining({ ownerId: userTwo.id, albumName: userTwoSharedUser, shared: true }), | ||||
|           ]), | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('returns the album collection filtered by NOT shared', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get('/album?shared=false'); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(1); | ||||
|         expect(body).toEqual( | ||||
|           expect.arrayContaining([ | ||||
|             expect.objectContaining({ ownerId: userOne.id, albumName: userOneNotShared, shared: false }), | ||||
|           ]), | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       // TODO: Add asset to album and test if it returns correctly. | ||||
|       it('returns the album collection filtered by assetId', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get( | ||||
|           '/album?assetId=ecb120db-45a2-4a65-9293-51476f0d8790', | ||||
|         ); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(0); | ||||
|       }); | ||||
|  | ||||
|       // TODO: Add asset to album and test if it returns correctly. | ||||
|       it('returns the album collection filtered by assetId and ignores shared=true', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get( | ||||
|           '/album?shared=true&assetId=ecb120db-45a2-4a65-9293-51476f0d8790', | ||||
|         ); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(0); | ||||
|       }); | ||||
|  | ||||
|       // TODO: Add asset to album and test if it returns correctly. | ||||
|       it('returns the album collection filtered by assetId and ignores shared=false', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get( | ||||
|           '/album?shared=false&assetId=ecb120db-45a2-4a65-9293-51476f0d8790', | ||||
|         ); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(0); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,206 +0,0 @@ | ||||
| import { AuthService, AuthUserDto, CreateUserDto, UserResponseDto, UserService } from '@app/domain'; | ||||
| import { AppModule } from '@app/immich/app.module'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { DataSource } from 'typeorm'; | ||||
| import { authCustom, clearDb } from '../test/test-utils'; | ||||
|  | ||||
| function _createUser(userService: UserService, data: CreateUserDto) { | ||||
|   return userService.createUser(data); | ||||
| } | ||||
|  | ||||
| describe('User', () => { | ||||
|   let app: INestApplication; | ||||
|   let database: DataSource; | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await clearDb(database); | ||||
|     await app.close(); | ||||
|   }); | ||||
|  | ||||
|   describe('without auth', () => { | ||||
|     beforeAll(async () => { | ||||
|       const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); | ||||
|  | ||||
|       app = moduleFixture.createNestApplication(); | ||||
|       database = app.get(DataSource); | ||||
|       await app.init(); | ||||
|     }); | ||||
|  | ||||
|     afterAll(async () => { | ||||
|       await app.close(); | ||||
|     }); | ||||
|  | ||||
|     it('prevents fetching users if not auth', async () => { | ||||
|       const { status } = await request(app.getHttpServer()).get('/user'); | ||||
|       expect(status).toEqual(401); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('with admin auth', () => { | ||||
|     let userService: UserService; | ||||
|     let authService: AuthService; | ||||
|     let authUser: AuthUserDto; | ||||
|     let userOne: UserResponseDto; | ||||
|  | ||||
|     beforeAll(async () => { | ||||
|       const builder = Test.createTestingModule({ imports: [AppModule] }); | ||||
|       const moduleFixture: TestingModule = await authCustom(builder, () => authUser).compile(); | ||||
|  | ||||
|       app = moduleFixture.createNestApplication(); | ||||
|       userService = app.get(UserService); | ||||
|       authService = app.get(AuthService); | ||||
|       database = app.get(DataSource); | ||||
|       await app.init(); | ||||
|     }); | ||||
|  | ||||
|     describe('with users in DB', () => { | ||||
|       const authUserEmail = 'auth-user@test.com'; | ||||
|       const userOneEmail = 'one@test.com'; | ||||
|       const userTwoEmail = 'two@test.com'; | ||||
|  | ||||
|       beforeAll(async () => { | ||||
|         // first user must be admin | ||||
|         const adminSignupResponseDto = await authService.adminSignUp({ | ||||
|           firstName: 'auth-user', | ||||
|           lastName: 'test', | ||||
|           email: authUserEmail, | ||||
|           password: '1234', | ||||
|         }); | ||||
|         authUser = { ...adminSignupResponseDto, isAdmin: true }; // TODO: find out why adminSignUp doesn't have isAdmin (maybe can just return UserResponseDto) | ||||
|  | ||||
|         [userOne] = await Promise.all([ | ||||
|           _createUser(userService, { | ||||
|             firstName: 'one', | ||||
|             lastName: 'test', | ||||
|             email: userOneEmail, | ||||
|             password: '1234', | ||||
|           }), | ||||
|           _createUser(userService, { | ||||
|             firstName: 'two', | ||||
|             lastName: 'test', | ||||
|             email: userTwoEmail, | ||||
|             password: '1234', | ||||
|           }), | ||||
|         ]); | ||||
|       }); | ||||
|  | ||||
|       it('fetches the user collection including the auth user', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()).get('/user?isAll=false'); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toHaveLength(3); | ||||
|         expect(body).toEqual( | ||||
|           expect.arrayContaining([ | ||||
|             { | ||||
|               email: userOneEmail, | ||||
|               firstName: 'one', | ||||
|               lastName: 'test', | ||||
|               id: expect.anything(), | ||||
|               createdAt: expect.anything(), | ||||
|               isAdmin: false, | ||||
|               shouldChangePassword: true, | ||||
|               profileImagePath: '', | ||||
|               deletedAt: null, | ||||
|               updatedAt: expect.anything(), | ||||
|               oauthId: '', | ||||
|               storageLabel: null, | ||||
|               externalPath: null, | ||||
|             }, | ||||
|             { | ||||
|               email: userTwoEmail, | ||||
|               firstName: 'two', | ||||
|               lastName: 'test', | ||||
|               id: expect.anything(), | ||||
|               createdAt: expect.anything(), | ||||
|               isAdmin: false, | ||||
|               shouldChangePassword: true, | ||||
|               profileImagePath: '', | ||||
|               deletedAt: null, | ||||
|               updatedAt: expect.anything(), | ||||
|               oauthId: '', | ||||
|               storageLabel: null, | ||||
|               externalPath: null, | ||||
|             }, | ||||
|             { | ||||
|               email: authUserEmail, | ||||
|               firstName: 'auth-user', | ||||
|               lastName: 'test', | ||||
|               id: expect.anything(), | ||||
|               createdAt: expect.anything(), | ||||
|               isAdmin: true, | ||||
|               shouldChangePassword: true, | ||||
|               profileImagePath: '', | ||||
|               deletedAt: null, | ||||
|               updatedAt: expect.anything(), | ||||
|               oauthId: '', | ||||
|               storageLabel: 'admin', | ||||
|               externalPath: null, | ||||
|             }, | ||||
|           ]), | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('disallows admin user from creating a second admin account', async () => { | ||||
|         const { status } = await request(app.getHttpServer()) | ||||
|           .put('/user') | ||||
|           .send({ | ||||
|             ...userOne, | ||||
|             isAdmin: true, | ||||
|           }); | ||||
|         expect(status).toEqual(400); | ||||
|       }); | ||||
|  | ||||
|       it('ignores updates to createdAt, updatedAt and deletedAt', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()) | ||||
|           .put('/user') | ||||
|           .send({ | ||||
|             ...userOne, | ||||
|             createdAt: '2023-01-01T00:00:00.000Z', | ||||
|             updatedAt: '2023-01-01T00:00:00.000Z', | ||||
|             deletedAt: '2023-01-01T00:00:00.000Z', | ||||
|           }); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toStrictEqual({ | ||||
|           ...userOne, | ||||
|           createdAt: new Date(userOne.createdAt).toISOString(), | ||||
|           updatedAt: expect.anything(), | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       it('ignores updates to profileImagePath', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()) | ||||
|           .put('/user') | ||||
|           .send({ | ||||
|             ...userOne, | ||||
|             profileImagePath: 'invalid.jpg', | ||||
|           }); | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toStrictEqual({ | ||||
|           ...userOne, | ||||
|           createdAt: new Date(userOne.createdAt).toISOString(), | ||||
|           updatedAt: expect.anything(), | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       it('allows to update first and last name', async () => { | ||||
|         const { status, body } = await request(app.getHttpServer()) | ||||
|           .put('/user') | ||||
|           .send({ | ||||
|             ...userOne, | ||||
|             firstName: 'newFirstName', | ||||
|             lastName: 'newLastName', | ||||
|           }); | ||||
|  | ||||
|         expect(status).toEqual(200); | ||||
|         expect(body).toMatchObject({ | ||||
|           ...userOne, | ||||
|           createdAt: new Date(userOne.createdAt).toISOString(), | ||||
|           updatedAt: expect.anything(), | ||||
|           firstName: 'newFirstName', | ||||
|           lastName: 'newLastName', | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -17,7 +17,7 @@ | ||||
|     "nest": "nest", | ||||
|     "start:dev": "nest start --watch --", | ||||
|     "start:debug": "nest start --debug 0.0.0.0:9230 --watch --", | ||||
|     "lint": "eslint \"src/**/*.ts\" \"e2e/**/*.ts\" --max-warnings 0", | ||||
|     "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0", | ||||
|     "lint:fix": "npm run lint -- --fix", | ||||
|     "check": "tsc --noEmit", | ||||
|     "check:code": "npm run format && npm run lint && npm run check", | ||||
| @@ -26,7 +26,7 @@ | ||||
|     "test:watch": "jest --watch", | ||||
|     "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 jest-e2e.json --runInBand", | ||||
|     "test:e2e": "jest --config test/e2e/jest-e2e.json --runInBand", | ||||
|     "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", | ||||
|     "typeorm:migrations:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:create", | ||||
|     "typeorm:migrations:generate": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d ./src/infra/database.config.ts", | ||||
|   | ||||
| @@ -101,13 +101,13 @@ describe('AuthService', () => { | ||||
|  | ||||
|     it('should check the user exists', async () => { | ||||
|       userMock.getByEmail.mockResolvedValue(null); | ||||
|       await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(BadRequestException); | ||||
|       await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException); | ||||
|       expect(userMock.getByEmail).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
|  | ||||
|     it('should check the user has a password', async () => { | ||||
|       userMock.getByEmail.mockResolvedValue({} as UserEntity); | ||||
|       await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(BadRequestException); | ||||
|       await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException); | ||||
|       expect(userMock.getByEmail).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,5 @@ | ||||
| import { SystemConfig, UserEntity } from '@app/infra/entities'; | ||||
| import { | ||||
|   BadRequestException, | ||||
|   Inject, | ||||
|   Injectable, | ||||
|   InternalServerErrorException, | ||||
|   Logger, | ||||
|   UnauthorizedException, | ||||
| } from '@nestjs/common'; | ||||
| import { BadRequestException, Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; | ||||
| import cookieParser from 'cookie'; | ||||
| import { IncomingHttpHeaders } from 'http'; | ||||
| import { DateTime } from 'luxon'; | ||||
| @@ -90,7 +83,7 @@ export class AuthService { | ||||
|  | ||||
|     if (!user) { | ||||
|       this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`); | ||||
|       throw new BadRequestException('Incorrect email or password'); | ||||
|       throw new UnauthorizedException('Incorrect email or password'); | ||||
|     } | ||||
|  | ||||
|     return this.createLoginResponse(user, AuthType.PASSWORD, details); | ||||
| @@ -129,21 +122,16 @@ export class AuthService { | ||||
|       throw new BadRequestException('The server already has an admin'); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const admin = await this.userCore.createUser({ | ||||
|         isAdmin: true, | ||||
|         email: dto.email, | ||||
|         firstName: dto.firstName, | ||||
|         lastName: dto.lastName, | ||||
|         password: dto.password, | ||||
|         storageLabel: 'admin', | ||||
|       }); | ||||
|     const admin = await this.userCore.createUser({ | ||||
|       isAdmin: true, | ||||
|       email: dto.email, | ||||
|       firstName: dto.firstName, | ||||
|       lastName: dto.lastName, | ||||
|       password: dto.password, | ||||
|       storageLabel: 'admin', | ||||
|     }); | ||||
|  | ||||
|       return mapAdminSignupResponse(admin); | ||||
|     } catch (error) { | ||||
|       this.logger.error(`Unable to register admin user: ${error}`, (error as Error).stack); | ||||
|       throw new InternalServerErrorException('Failed to register new admin user'); | ||||
|     } | ||||
|     return mapAdminSignupResponse(admin); | ||||
|   } | ||||
|  | ||||
|   async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto | null> { | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { | ||||
|   AdminSignupResponseDto, | ||||
|   AuthDeviceResponseDto, | ||||
|   AuthService, | ||||
|   AuthType, | ||||
|   AuthUserDto, | ||||
|   ChangePasswordDto, | ||||
|   IMMICH_ACCESS_COOKIE, | ||||
| @@ -15,7 +14,7 @@ import { | ||||
|   UserResponseDto, | ||||
|   ValidateAccessTokenResponseDto, | ||||
| } from '@app/domain'; | ||||
| import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common'; | ||||
| import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; | ||||
| import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger'; | ||||
| import { Request, Response } from 'express'; | ||||
| import { Authenticated, AuthUser, GetLoginDetails, PublicRoute } from '../app.guard'; | ||||
| @@ -54,36 +53,39 @@ export class AuthController { | ||||
|   } | ||||
|  | ||||
|   @Delete('devices') | ||||
|   @HttpCode(HttpStatus.NO_CONTENT) | ||||
|   logoutAuthDevices(@AuthUser() authUser: AuthUserDto): Promise<void> { | ||||
|     return this.service.logoutDevices(authUser); | ||||
|   } | ||||
|  | ||||
|   @Delete('devices/:id') | ||||
|   @HttpCode(HttpStatus.NO_CONTENT) | ||||
|   logoutAuthDevice(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { | ||||
|     return this.service.logoutDevice(authUser, id); | ||||
|   } | ||||
|  | ||||
|   @Post('validateToken') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   validateAccessToken(): ValidateAccessTokenResponseDto { | ||||
|     return { authStatus: true }; | ||||
|   } | ||||
|  | ||||
|   @Post('change-password') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> { | ||||
|     return this.service.changePassword(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @Post('logout') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   logout( | ||||
|     @Req() req: Request, | ||||
|     @Res({ passthrough: true }) res: Response, | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|   ): Promise<LogoutResponseDto> { | ||||
|     const authType: AuthType = req.cookies[IMMICH_AUTH_TYPE_COOKIE]; | ||||
|  | ||||
|     res.clearCookie(IMMICH_ACCESS_COOKIE); | ||||
|     res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); | ||||
|  | ||||
|     return this.service.logout(authUser, authType); | ||||
|     return this.service.logout(authUser, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -77,6 +77,7 @@ export class UserController { | ||||
|     return this.service.restoreUser(authUser, userId); | ||||
|   } | ||||
|  | ||||
|   // TODO: replace with @Put(':id') | ||||
|   @Put() | ||||
|   updateUser(@AuthUser() authUser: AuthUserDto, @Body() updateUserDto: UpdateUserDto): Promise<UserResponseDto> { | ||||
|     return this.service.updateUser(authUser, updateUserDto); | ||||
|   | ||||
							
								
								
									
										2
									
								
								server/src/immich/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								server/src/immich/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export * from './app.module'; | ||||
| export * from './controllers'; | ||||
							
								
								
									
										205
									
								
								server/test/e2e/album.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								server/test/e2e/album.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| import { LoginResponseDto } from '@app/domain'; | ||||
| import { AlbumController, AppModule } from '@app/immich'; | ||||
| import { SharedLinkType } from '@app/infra/entities'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { errorStub } from '../fixtures'; | ||||
| import { api, db } from '../test-utils'; | ||||
|  | ||||
| const user1SharedUser = 'user1SharedUser'; | ||||
| const user1SharedLink = 'user1SharedLink'; | ||||
| const user1NotShared = 'user1NotShared'; | ||||
| const user2SharedUser = 'user2SharedUser'; | ||||
| const user2SharedLink = 'user2SharedLink'; | ||||
| const user2NotShared = 'user2NotShared'; | ||||
|  | ||||
| describe(`${AlbumController.name} (e2e)`, () => { | ||||
|   let app: INestApplication; | ||||
|   let server: any; | ||||
|   let user1: LoginResponseDto; | ||||
|   let user2: LoginResponseDto; | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     const moduleFixture: TestingModule = await Test.createTestingModule({ | ||||
|       imports: [AppModule], | ||||
|     }).compile(); | ||||
|  | ||||
|     app = await moduleFixture.createNestApplication().init(); | ||||
|     server = app.getHttpServer(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await db.reset(); | ||||
|     await api.adminSignUp(server); | ||||
|     const admin = await api.adminLogin(server); | ||||
|  | ||||
|     await api.userApi.create(server, admin.accessToken, { | ||||
|       email: 'user1@immich.app', | ||||
|       password: 'Password123', | ||||
|       firstName: 'User 1', | ||||
|       lastName: 'Test', | ||||
|     }); | ||||
|     user1 = await api.login(server, { email: 'user1@immich.app', password: 'Password123' }); | ||||
|  | ||||
|     await api.userApi.create(server, admin.accessToken, { | ||||
|       email: 'user2@immich.app', | ||||
|       password: 'Password123', | ||||
|       firstName: 'User 2', | ||||
|       lastName: 'Test', | ||||
|     }); | ||||
|     user2 = await api.login(server, { email: 'user2@immich.app', password: 'Password123' }); | ||||
|  | ||||
|     const user1Albums = await Promise.all([ | ||||
|       api.albumApi.create(server, user1.accessToken, { | ||||
|         albumName: user1SharedUser, | ||||
|         sharedWithUserIds: [user2.userId], | ||||
|       }), | ||||
|       api.albumApi.create(server, user1.accessToken, { albumName: user1SharedLink }), | ||||
|       api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared }), | ||||
|     ]); | ||||
|  | ||||
|     // add shared link to user1SharedLink album | ||||
|     await api.sharedLinkApi.create(server, user1.accessToken, { | ||||
|       type: SharedLinkType.ALBUM, | ||||
|       albumId: user1Albums[1].id, | ||||
|     }); | ||||
|  | ||||
|     const user2Albums = await Promise.all([ | ||||
|       api.albumApi.create(server, user2.accessToken, { | ||||
|         albumName: user2SharedUser, | ||||
|         sharedWithUserIds: [user1.userId], | ||||
|       }), | ||||
|       api.albumApi.create(server, user2.accessToken, { albumName: user2SharedLink }), | ||||
|       api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }), | ||||
|     ]); | ||||
|  | ||||
|     // add shared link to user2SharedLink album | ||||
|     await api.sharedLinkApi.create(server, user2.accessToken, { | ||||
|       type: SharedLinkType.ALBUM, | ||||
|       albumId: user2Albums[1].id, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await db.disconnect(); | ||||
|     await app.close(); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /album', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).get('/album'); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should reject an invalid shared param', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?shared=invalid') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(400); | ||||
|       expect(body).toEqual(errorStub.badRequest); | ||||
|     }); | ||||
|  | ||||
|     it('should reject an invalid assetId param', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?assetId=invalid') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(400); | ||||
|       expect(body).toEqual(errorStub.badRequest); | ||||
|     }); | ||||
|  | ||||
|     it('should return the album collection including owned and shared', async () => { | ||||
|       const { status, body } = await request(server).get('/album').set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(3); | ||||
|       expect(body).toEqual( | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }), | ||||
|           expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }), | ||||
|           expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }), | ||||
|         ]), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should return the album collection filtered by shared', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?shared=true') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(3); | ||||
|       expect(body).toEqual( | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }), | ||||
|           expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }), | ||||
|           expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedUser, shared: true }), | ||||
|         ]), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should return the album collection filtered by NOT shared', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?shared=false') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(1); | ||||
|       expect(body).toEqual( | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }), | ||||
|         ]), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     // TODO: Add asset to album and test if it returns correctly. | ||||
|     it('should return the album collection filtered by assetId', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?assetId=ecb120db-45a2-4a65-9293-51476f0d8790') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(0); | ||||
|     }); | ||||
|  | ||||
|     // TODO: Add asset to album and test if it returns correctly. | ||||
|     it('should return the album collection filtered by assetId and ignores shared=true', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?shared=true&assetId=ecb120db-45a2-4a65-9293-51476f0d8790') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(0); | ||||
|     }); | ||||
|  | ||||
|     // TODO: Add asset to album and test if it returns correctly. | ||||
|     it('should return the album collection filtered by assetId and ignores shared=false', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get('/album?shared=false&assetId=ecb120db-45a2-4a65-9293-51476f0d8790') | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(0); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /album', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).post('/album').send({ albumName: 'New album' }); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should create an album', async () => { | ||||
|       const body = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' }); | ||||
|       expect(body).toEqual({ | ||||
|         id: expect.any(String), | ||||
|         createdAt: expect.any(String), | ||||
|         updatedAt: expect.any(String), | ||||
|         ownerId: user1.userId, | ||||
|         albumName: 'New album', | ||||
|         albumThumbnailAssetId: null, | ||||
|         shared: false, | ||||
|         sharedUsers: [], | ||||
|         assets: [], | ||||
|         assetCount: 0, | ||||
|         owner: expect.objectContaining({ email: user1.userEmail }), | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										226
									
								
								server/test/e2e/auth.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								server/test/e2e/auth.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | ||||
| import { AppModule, AuthController } from '@app/immich'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { deviceStub, errorStub, loginResponseStub, signupResponseStub, signupStub, uuidStub } from '../fixtures'; | ||||
| import { api, db } from '../test-utils'; | ||||
|  | ||||
| const firstName = 'Immich'; | ||||
| const lastName = 'Admin'; | ||||
| const password = 'Password123'; | ||||
| const email = 'admin@immich.app'; | ||||
|  | ||||
| describe(`${AuthController.name} (e2e)`, () => { | ||||
|   let app: INestApplication; | ||||
|   let server: any; | ||||
|   let accessToken: string; | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     const moduleFixture: TestingModule = await Test.createTestingModule({ | ||||
|       imports: [AppModule], | ||||
|     }).compile(); | ||||
|  | ||||
|     app = await moduleFixture.createNestApplication().init(); | ||||
|     server = app.getHttpServer(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await db.reset(); | ||||
|     await api.adminSignUp(server); | ||||
|     const response = await api.adminLogin(server); | ||||
|     accessToken = response.accessToken; | ||||
|   }); | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await db.disconnect(); | ||||
|     await app.close(); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /auth/admin-sign-up', () => { | ||||
|     beforeEach(async () => { | ||||
|       await db.reset(); | ||||
|     }); | ||||
|  | ||||
|     const invalid = [ | ||||
|       { should: 'require an email address', data: { firstName, lastName, password } }, | ||||
|       { should: 'require a password', data: { firstName, lastName, email } }, | ||||
|       { should: 'require a first name ', data: { lastName, email, password } }, | ||||
|       { should: 'require a last name ', data: { firstName, email, password } }, | ||||
|       { should: 'require a valid email', data: { firstName, lastName, email: 'immich', password } }, | ||||
|     ]; | ||||
|  | ||||
|     for (const { should, data } of invalid) { | ||||
|       it(`should ${should}`, async () => { | ||||
|         const { status, body } = await request(server).post('/auth/admin-sign-up').send(data); | ||||
|         expect(status).toEqual(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     it(`should sign up the admin`, async () => { | ||||
|       await api.adminSignUp(server); | ||||
|     }); | ||||
|  | ||||
|     it('should sign up the admin with a local domain', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post('/auth/admin-sign-up') | ||||
|         .send({ ...signupStub, email: 'admin@local' }); | ||||
|       expect(status).toEqual(201); | ||||
|       expect(body).toEqual({ ...signupResponseStub, email: 'admin@local' }); | ||||
|     }); | ||||
|  | ||||
|     it('should transform email to lower case', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post('/auth/admin-sign-up') | ||||
|         .send({ ...signupStub, email: 'aDmIn@IMMICH.app' }); | ||||
|       expect(status).toEqual(201); | ||||
|       expect(body).toEqual(signupResponseStub); | ||||
|     }); | ||||
|  | ||||
|     it('should not allow a second admin to sign up', async () => { | ||||
|       await api.adminSignUp(server); | ||||
|  | ||||
|       const { status, body } = await request(server).post('/auth/admin-sign-up').send(signupStub); | ||||
|  | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorStub.alreadyHasAdmin); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe(`POST /auth/login`, () => { | ||||
|     it('should reject an incorrect password', async () => { | ||||
|       const { status, body } = await request(server).post('/auth/login').send({ email, password: 'incorrect' }); | ||||
|       expect(body).toEqual(errorStub.incorrectLogin); | ||||
|       expect(status).toBe(401); | ||||
|     }); | ||||
|  | ||||
|     it('should accept a correct password', async () => { | ||||
|       const { status, body, headers } = await request(server).post('/auth/login').send({ email, password }); | ||||
|       expect(status).toBe(201); | ||||
|       expect(body).toEqual(loginResponseStub.admin.response); | ||||
|  | ||||
|       const token = body.accessToken; | ||||
|       expect(token).toBeDefined(); | ||||
|  | ||||
|       const cookies = headers['set-cookie']; | ||||
|       expect(cookies).toHaveLength(2); | ||||
|       expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`); | ||||
|       expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /auth/devices', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).get('/auth/devices'); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should get a list of authorized devices', async () => { | ||||
|       const { status, body } = await request(server).get('/auth/devices').set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual([deviceStub.current]); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('DELETE /auth/devices/:id', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).delete(`/auth/devices`); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should logout all devices (except the current one)', async () => { | ||||
|       for (let i = 0; i < 5; i++) { | ||||
|         await api.adminLogin(server); | ||||
|       } | ||||
|  | ||||
|       await expect(api.getAuthDevices(server, accessToken)).resolves.toHaveLength(6); | ||||
|  | ||||
|       const { status } = await request(server).delete(`/auth/devices`).set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(204); | ||||
|  | ||||
|       await api.validateToken(server, accessToken); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('DELETE /auth/devices/:id', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).delete(`/auth/devices/${uuidStub.notFound}`); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should logout a device', async () => { | ||||
|       const [device] = await api.getAuthDevices(server, accessToken); | ||||
|       const { status } = await request(server) | ||||
|         .delete(`/auth/devices/${device.id}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(204); | ||||
|  | ||||
|       const response = await request(server).post('/auth/validateToken').set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(response.body).toEqual(errorStub.invalidToken); | ||||
|       expect(response.status).toBe(401); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /auth/validateToken', () => { | ||||
|     it('should reject an invalid token', async () => { | ||||
|       const { status, body } = await request(server).post(`/auth/validateToken`).set('Authorization', 'Bearer 123'); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.invalidToken); | ||||
|     }); | ||||
|  | ||||
|     it('should accept a valid token', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/auth/validateToken`) | ||||
|         .send({}) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ authStatus: true }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /auth/change-password', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/auth/change-password`) | ||||
|         .send({ password: 'Password123', newPassword: 'Password1234' }); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should require the current password', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/auth/change-password`) | ||||
|         .send({ password: 'wrong-password', newPassword: 'Password1234' }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorStub.wrongPassword); | ||||
|     }); | ||||
|  | ||||
|     it('should change the password', async () => { | ||||
|       const { status } = await request(server) | ||||
|         .post(`/auth/change-password`) | ||||
|         .send({ password: 'Password123', newPassword: 'Password1234' }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|  | ||||
|       await api.login(server, { email: 'admin@immich.app', password: 'Password1234' }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /auth/logout', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).post(`/auth/logout`); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should logout the user', async () => { | ||||
|       const { status, body } = await request(server).post(`/auth/logout`).set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0' }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,13 +1,16 @@ | ||||
| { | ||||
|   "moduleFileExtensions": ["js", "json", "ts"], | ||||
|   "modulePaths": ["<rootDir>"], | ||||
|   "rootDir": ".", | ||||
|   "globalSetup": "<rootDir>/e2e/setup.ts", | ||||
|   "rootDir": "../..", | ||||
|   "globalSetup": "<rootDir>/test/e2e/setup.ts", | ||||
|   "testEnvironment": "node", | ||||
|   "testRegex": ".e2e-spec.ts$", | ||||
|   "testTimeout": 15000, | ||||
|   "transform": { | ||||
|     "^.+\\.(t|j)s$": "ts-jest" | ||||
|   }, | ||||
|   "collectCoverageFrom": ["<rootDir>/src/**/*.(t|j)s", "!<rootDir>/src/infra/**/*"], | ||||
|   "coverageDirectory": "./coverage", | ||||
|   "moduleNameMapper": { | ||||
|     "^@test(|/.*)$": "<rootDir>/test/$1", | ||||
|     "^@app/immich(|/.*)$": "<rootDir>/src/immich/$1", | ||||
| @@ -9,15 +9,12 @@ export default async () => { | ||||
|     .withDatabase('immich') | ||||
|     .withUsername('postgres') | ||||
|     .withPassword('postgres') | ||||
|     .withReuse() | ||||
|     .start(); | ||||
| 
 | ||||
|   process.env.DB_PORT = String(pg.getMappedPort(5432)); | ||||
|   process.env.DB_HOSTNAME = pg.getHost(); | ||||
|   process.env.DB_USERNAME = pg.getUsername(); | ||||
|   process.env.DB_PASSWORD = pg.getPassword(); | ||||
|   process.env.DB_DATABASE_NAME = pg.getDatabase(); | ||||
|   process.env.DB_URL = pg.getConnectionUri(); | ||||
| 
 | ||||
|   const redis = await new GenericContainer('redis').withExposedPorts(6379).start(); | ||||
|   const redis = await new GenericContainer('redis').withExposedPorts(6379).withReuse().start(); | ||||
| 
 | ||||
|   process.env.REDIS_PORT = String(redis.getMappedPort(6379)); | ||||
|   process.env.REDIS_HOSTNAME = redis.getHost(); | ||||
							
								
								
									
										238
									
								
								server/test/e2e/user.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								server/test/e2e/user.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,238 @@ | ||||
| import { LoginResponseDto } from '@app/domain'; | ||||
| import { AppModule, UserController } from '@app/immich'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { errorStub } from '../fixtures'; | ||||
| import { api, db } from '../test-utils'; | ||||
|  | ||||
| describe(`${UserController.name}`, () => { | ||||
|   let app: INestApplication; | ||||
|   let server: any; | ||||
|   let loginResponse: LoginResponseDto; | ||||
|   let accessToken: string; | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     const moduleFixture: TestingModule = await Test.createTestingModule({ | ||||
|       imports: [AppModule], | ||||
|     }).compile(); | ||||
|  | ||||
|     app = await moduleFixture.createNestApplication().init(); | ||||
|     server = app.getHttpServer(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await db.reset(); | ||||
|     await api.adminSignUp(server); | ||||
|     loginResponse = await api.adminLogin(server); | ||||
|     accessToken = loginResponse.accessToken; | ||||
|   }); | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await db.disconnect(); | ||||
|     await app.close(); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /user', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).get('/user'); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should start with the admin', async () => { | ||||
|       const { status, body } = await request(server).get('/user').set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toEqual(200); | ||||
|       expect(body).toHaveLength(1); | ||||
|       expect(body[0]).toMatchObject({ email: 'admin@immich.app' }); | ||||
|     }); | ||||
|  | ||||
|     it('should hide deleted users', async () => { | ||||
|       const user1 = await api.userApi.create(server, accessToken, { | ||||
|         email: `user1@immich.app`, | ||||
|         password: 'Password123', | ||||
|         firstName: `User 1`, | ||||
|         lastName: 'Test', | ||||
|       }); | ||||
|  | ||||
|       await api.userApi.delete(server, accessToken, user1.id); | ||||
|  | ||||
|       const { status, body } = await request(server) | ||||
|         .get(`/user`) | ||||
|         .query({ isAll: true }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(1); | ||||
|       expect(body[0]).toMatchObject({ email: 'admin@immich.app' }); | ||||
|     }); | ||||
|  | ||||
|     it('should include deleted users', async () => { | ||||
|       const user1 = await api.userApi.create(server, accessToken, { | ||||
|         email: `user1@immich.app`, | ||||
|         password: 'Password123', | ||||
|         firstName: `User 1`, | ||||
|         lastName: 'Test', | ||||
|       }); | ||||
|  | ||||
|       await api.userApi.delete(server, accessToken, user1.id); | ||||
|  | ||||
|       const { status, body } = await request(server) | ||||
|         .get(`/user`) | ||||
|         .query({ isAll: false }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toHaveLength(2); | ||||
|       expect(body[0]).toMatchObject({ id: user1.id, email: 'user1@immich.app', deletedAt: expect.any(String) }); | ||||
|       expect(body[1]).toMatchObject({ id: loginResponse.userId, email: 'admin@immich.app' }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /user/info/:id', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status } = await request(server).get(`/user/info/${loginResponse.userId}`); | ||||
|       expect(status).toEqual(401); | ||||
|     }); | ||||
|  | ||||
|     it('should get the user info', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get(`/user/info/${loginResponse.userId}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ id: loginResponse.userId, email: 'admin@immich.app' }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /user/me', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).get(`/user/me`); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should get my info', async () => { | ||||
|       const { status, body } = await request(server).get(`/user/me`).set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ id: loginResponse.userId, email: 'admin@immich.app' }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /user', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/user`) | ||||
|         .send({ email: 'user1@immich.app', password: 'Password123', firstName: 'Immich', lastName: 'User' }); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should ignore `isAdmin`', async () => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post(`/user`) | ||||
|         .send({ | ||||
|           isAdmin: true, | ||||
|           email: 'user1@immich.app', | ||||
|           password: 'Password123', | ||||
|           firstName: 'Immich', | ||||
|           lastName: 'User', | ||||
|         }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(body).toMatchObject({ | ||||
|         email: 'user1@immich.app', | ||||
|         isAdmin: false, | ||||
|         shouldChangePassword: true, | ||||
|       }); | ||||
|       expect(status).toBe(201); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('PUT /user', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).put(`/user`); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should not allow a non-admin to become an admin', async () => { | ||||
|       const user = await api.userApi.create(server, accessToken, { | ||||
|         email: 'user1@immich.app', | ||||
|         password: 'Password123', | ||||
|         firstName: 'Immich', | ||||
|         lastName: 'User', | ||||
|       }); | ||||
|  | ||||
|       const { status, body } = await request(server) | ||||
|         .put(`/user`) | ||||
|         .send({ isAdmin: true, id: user.id }) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorStub.alreadyHasAdmin); | ||||
|     }); | ||||
|  | ||||
|     it('ignores updates to profileImagePath', async () => { | ||||
|       const user = await api.userApi.update(server, accessToken, { | ||||
|         id: loginResponse.userId, | ||||
|         profileImagePath: 'invalid.jpg', | ||||
|       } as any); | ||||
|  | ||||
|       expect(user).toMatchObject({ id: loginResponse.userId, profileImagePath: '' }); | ||||
|     }); | ||||
|  | ||||
|     it('should ignore updates to createdAt, updatedAt and deletedAt', async () => { | ||||
|       const before = await api.userApi.get(server, accessToken, loginResponse.userId); | ||||
|       const after = await api.userApi.update(server, accessToken, { | ||||
|         id: loginResponse.userId, | ||||
|         createdAt: '2023-01-01T00:00:00.000Z', | ||||
|         updatedAt: '2023-01-01T00:00:00.000Z', | ||||
|         deletedAt: '2023-01-01T00:00:00.000Z', | ||||
|       } as any); | ||||
|  | ||||
|       expect(after).toStrictEqual(before); | ||||
|     }); | ||||
|  | ||||
|     it('should update first and last name', async () => { | ||||
|       const before = await api.userApi.get(server, accessToken, loginResponse.userId); | ||||
|       const after = await api.userApi.update(server, accessToken, { | ||||
|         id: before.id, | ||||
|         firstName: 'First Name', | ||||
|         lastName: 'Last Name', | ||||
|       }); | ||||
|  | ||||
|       expect(after).toMatchObject({ | ||||
|         ...before, | ||||
|         updatedAt: expect.anything(), | ||||
|         firstName: 'First Name', | ||||
|         lastName: 'Last Name', | ||||
|       }); | ||||
|       expect(before.updatedAt).not.toEqual(after.updatedAt); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /user/count', () => { | ||||
|     it('should not require authentication', async () => { | ||||
|       const { status, body } = await request(server).get(`/user/count`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ userCount: 1 }); | ||||
|     }); | ||||
|  | ||||
|     it('should start with just the admin', async () => { | ||||
|       const { status, body } = await request(server).get(`/user/count`).set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ userCount: 1 }); | ||||
|     }); | ||||
|  | ||||
|     it('should return the total user count', async () => { | ||||
|       for (let i = 0; i < 5; i++) { | ||||
|         await api.userApi.create(server, accessToken, { | ||||
|           email: `user${i + 1}@immich.app`, | ||||
|           password: 'Password123', | ||||
|           firstName: `User ${i + 1}`, | ||||
|           lastName: 'Test', | ||||
|         }); | ||||
|       } | ||||
|       const { status, body } = await request(server).get(`/user/count`).set('Authorization', `Bearer ${accessToken}`); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ userCount: 6 }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										34
									
								
								server/test/fixtures/auth.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								server/test/fixtures/auth.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,27 @@ | ||||
| import { AuthUserDto } from '@app/domain'; | ||||
|  | ||||
| export const signupStub = { | ||||
|   firstName: 'Immich', | ||||
|   lastName: 'Admin', | ||||
|   email: 'admin@immich.app', | ||||
|   password: 'Password123', | ||||
| }; | ||||
|  | ||||
| export const signupResponseStub = { | ||||
|   id: expect.any(String), | ||||
|   email: 'admin@immich.app', | ||||
|   firstName: 'Immich', | ||||
|   lastName: 'Admin', | ||||
|   createdAt: expect.any(String), | ||||
| }; | ||||
|  | ||||
| export const loginStub = { | ||||
|   admin: { | ||||
|     email: 'admin@immich.app', | ||||
|     password: 'Password123', | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const authStub = { | ||||
|   admin: Object.freeze<AuthUserDto>({ | ||||
|     id: 'admin_id', | ||||
| @@ -76,6 +98,18 @@ export const authStub = { | ||||
| }; | ||||
|  | ||||
| export const loginResponseStub = { | ||||
|   admin: { | ||||
|     response: { | ||||
|       accessToken: expect.any(String), | ||||
|       firstName: 'Immich', | ||||
|       isAdmin: true, | ||||
|       lastName: 'Admin', | ||||
|       profileImagePath: '', | ||||
|       shouldChangePassword: true, | ||||
|       userEmail: 'admin@immich.app', | ||||
|       userId: expect.any(String), | ||||
|     }, | ||||
|   }, | ||||
|   user1oauth: { | ||||
|     response: { | ||||
|       accessToken: 'cmFuZG9tLWJ5dGVz', | ||||
|   | ||||
							
								
								
									
										10
									
								
								server/test/fixtures/device.stub.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/test/fixtures/device.stub.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| export const deviceStub = { | ||||
|   current: { | ||||
|     id: expect.any(String), | ||||
|     createdAt: expect.any(String), | ||||
|     updatedAt: expect.any(String), | ||||
|     current: true, | ||||
|     deviceOS: '', | ||||
|     deviceType: '', | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										32
									
								
								server/test/fixtures/error.stub.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								server/test/fixtures/error.stub.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| export const errorStub = { | ||||
|   unauthorized: { | ||||
|     error: 'Unauthorized', | ||||
|     statusCode: 401, | ||||
|     message: 'Authentication required', | ||||
|   }, | ||||
|   wrongPassword: { | ||||
|     error: 'Bad Request', | ||||
|     statusCode: 400, | ||||
|     message: 'Wrong password', | ||||
|   }, | ||||
|   invalidToken: { | ||||
|     error: 'Unauthorized', | ||||
|     statusCode: 401, | ||||
|     message: 'Invalid user token', | ||||
|   }, | ||||
|   badRequest: { | ||||
|     error: 'Bad Request', | ||||
|     statusCode: 400, | ||||
|     message: expect.any(Array), | ||||
|   }, | ||||
|   incorrectLogin: { | ||||
|     error: 'Unauthorized', | ||||
|     statusCode: 401, | ||||
|     message: 'Incorrect email or password', | ||||
|   }, | ||||
|   alreadyHasAdmin: { | ||||
|     error: 'Bad Request', | ||||
|     statusCode: 400, | ||||
|     message: 'The server already has an admin', | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										3
									
								
								server/test/fixtures/index.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								server/test/fixtures/index.ts
									
									
									
									
										vendored
									
									
								
							| @@ -2,6 +2,8 @@ export * from './album.stub'; | ||||
| export * from './api-key.stub'; | ||||
| export * from './asset.stub'; | ||||
| export * from './auth.stub'; | ||||
| export * from './device.stub'; | ||||
| export * from './error.stub'; | ||||
| export * from './face.stub'; | ||||
| export * from './file.stub'; | ||||
| export * from './media.stub'; | ||||
| @@ -13,3 +15,4 @@ export * from './system-config.stub'; | ||||
| export * from './tag.stub'; | ||||
| export * from './user-token.stub'; | ||||
| export * from './user.stub'; | ||||
| export * from './uuid.stub'; | ||||
|   | ||||
							
								
								
									
										4
									
								
								server/test/fixtures/uuid.stub.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/test/fixtures/uuid.stub.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export const uuidStub = { | ||||
|   invalid: 'invalid-uuid', | ||||
|   notFound: '00000000-0000-0000-0000-000000000000', | ||||
| }; | ||||
| @@ -1,18 +1,43 @@ | ||||
| import { AuthUserDto } from '@app/domain'; | ||||
| import { AppGuard } from '@app/immich/app.guard'; | ||||
| import { CanActivate, ExecutionContext } from '@nestjs/common'; | ||||
| import { TestingModuleBuilder } from '@nestjs/testing'; | ||||
| import { DataSource } from 'typeorm'; | ||||
| import { | ||||
|   AdminSignupResponseDto, | ||||
|   AlbumResponseDto, | ||||
|   AuthDeviceResponseDto, | ||||
|   AuthUserDto, | ||||
|   CreateAlbumDto, | ||||
|   CreateUserDto, | ||||
|   LoginCredentialDto, | ||||
|   LoginResponseDto, | ||||
|   SharedLinkCreateDto, | ||||
|   SharedLinkResponseDto, | ||||
|   UpdateUserDto, | ||||
|   UserResponseDto, | ||||
| } from '@app/domain'; | ||||
| import { dataSource } from '@app/infra'; | ||||
| import request from 'supertest'; | ||||
| import { loginResponseStub, loginStub, signupResponseStub, signupStub } from './fixtures'; | ||||
|  | ||||
| type CustomAuthCallback = () => AuthUserDto; | ||||
| export const db = { | ||||
|   reset: async () => { | ||||
|     if (!dataSource.isInitialized) { | ||||
|       await dataSource.initialize(); | ||||
|     } | ||||
|  | ||||
| export async function clearDb(db: DataSource) { | ||||
|   const entities = db.entityMetadatas; | ||||
|   for (const entity of entities) { | ||||
|     const repository = db.getRepository(entity.name); | ||||
|     await repository.query(`TRUNCATE ${entity.tableName} RESTART IDENTITY CASCADE;`); | ||||
|   } | ||||
| } | ||||
|     await dataSource.transaction(async (em) => { | ||||
|       for (const entity of dataSource.entityMetadatas) { | ||||
|         if (entity.tableName === 'users') { | ||||
|           continue; | ||||
|         } | ||||
|         await em.query(`DELETE FROM ${entity.tableName} CASCADE;`); | ||||
|       } | ||||
|       await em.query(`DELETE FROM "users" CASCADE;`); | ||||
|     }); | ||||
|   }, | ||||
|   disconnect: async () => { | ||||
|     if (dataSource.isInitialized) { | ||||
|       await dataSource.destroy(); | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function getAuthUser(): AuthUserDto { | ||||
|   return { | ||||
| @@ -22,17 +47,109 @@ export function getAuthUser(): AuthUserDto { | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export function auth(builder: TestingModuleBuilder): TestingModuleBuilder { | ||||
|   return authCustom(builder, getAuthUser); | ||||
| } | ||||
| export const api = { | ||||
|   adminSignUp: async (server: any) => { | ||||
|     const { status, body } = await request(server).post('/auth/admin-sign-up').send(signupStub); | ||||
|  | ||||
| export function authCustom(builder: TestingModuleBuilder, callback: CustomAuthCallback): TestingModuleBuilder { | ||||
|   const canActivate: CanActivate = { | ||||
|     canActivate: (context: ExecutionContext) => { | ||||
|       const req = context.switchToHttp().getRequest(); | ||||
|       req.user = callback(); | ||||
|       return true; | ||||
|     expect(status).toBe(201); | ||||
|     expect(body).toEqual(signupResponseStub); | ||||
|  | ||||
|     return body as AdminSignupResponseDto; | ||||
|   }, | ||||
|   adminLogin: async (server: any) => { | ||||
|     const { status, body } = await request(server).post('/auth/login').send(loginStub.admin); | ||||
|  | ||||
|     expect(body).toEqual(loginResponseStub.admin.response); | ||||
|     expect(body).toMatchObject({ accessToken: expect.any(String) }); | ||||
|     expect(status).toBe(201); | ||||
|  | ||||
|     return body as LoginResponseDto; | ||||
|   }, | ||||
|   login: async (server: any, dto: LoginCredentialDto) => { | ||||
|     const { status, body } = await request(server).post('/auth/login').send(dto); | ||||
|  | ||||
|     expect(status).toEqual(201); | ||||
|     expect(body).toMatchObject({ accessToken: expect.any(String) }); | ||||
|  | ||||
|     return body as LoginResponseDto; | ||||
|   }, | ||||
|   getAuthDevices: async (server: any, accessToken: string) => { | ||||
|     const { status, body } = await request(server).get('/auth/devices').set('Authorization', `Bearer ${accessToken}`); | ||||
|  | ||||
|     expect(body).toEqual(expect.any(Array)); | ||||
|     expect(status).toBe(200); | ||||
|  | ||||
|     return body as AuthDeviceResponseDto[]; | ||||
|   }, | ||||
|   validateToken: async (server: any, accessToken: string) => { | ||||
|     const response = await request(server).post('/auth/validateToken').set('Authorization', `Bearer ${accessToken}`); | ||||
|     expect(response.body).toEqual({ authStatus: true }); | ||||
|     expect(response.status).toBe(200); | ||||
|   }, | ||||
|   albumApi: { | ||||
|     create: async (server: any, accessToken: string, dto: CreateAlbumDto) => { | ||||
|       const res = await request(server).post('/album').set('Authorization', `Bearer ${accessToken}`).send(dto); | ||||
|       expect(res.status).toEqual(201); | ||||
|       return res.body as AlbumResponseDto; | ||||
|     }, | ||||
|   }; | ||||
|   return builder.overrideProvider(AppGuard).useValue(canActivate); | ||||
| } | ||||
|   }, | ||||
|   sharedLinkApi: { | ||||
|     create: async (server: any, accessToken: string, dto: SharedLinkCreateDto) => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post('/shared-link') | ||||
|         .set('Authorization', `Bearer ${accessToken}`) | ||||
|         .send(dto); | ||||
|       expect(status).toBe(201); | ||||
|       return body as SharedLinkResponseDto; | ||||
|     }, | ||||
|   }, | ||||
|   userApi: { | ||||
|     create: async (server: any, accessToken: string, dto: CreateUserDto) => { | ||||
|       const { status, body } = await request(server) | ||||
|         .post('/user') | ||||
|         .set('Authorization', `Bearer ${accessToken}`) | ||||
|         .send(dto); | ||||
|  | ||||
|       expect(status).toBe(201); | ||||
|       expect(body).toMatchObject({ | ||||
|         id: expect.any(String), | ||||
|         createdAt: expect.any(String), | ||||
|         updatedAt: expect.any(String), | ||||
|         email: dto.email, | ||||
|       }); | ||||
|  | ||||
|       return body as UserResponseDto; | ||||
|     }, | ||||
|     get: async (server: any, accessToken: string, id: string) => { | ||||
|       const { status, body } = await request(server) | ||||
|         .get(`/user/info/${id}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ id }); | ||||
|  | ||||
|       return body as UserResponseDto; | ||||
|     }, | ||||
|     update: async (server: any, accessToken: string, dto: UpdateUserDto) => { | ||||
|       const { status, body } = await request(server) | ||||
|         .put('/user') | ||||
|         .set('Authorization', `Bearer ${accessToken}`) | ||||
|         .send(dto); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ id: dto.id }); | ||||
|  | ||||
|       return body as UserResponseDto; | ||||
|     }, | ||||
|     delete: async (server: any, accessToken: string, id: string) => { | ||||
|       const { status, body } = await request(server) | ||||
|         .delete(`/user/${id}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ id, deletedAt: expect.any(String) }); | ||||
|  | ||||
|       return body as UserResponseDto; | ||||
|     }, | ||||
|   }, | ||||
| } as const; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user