mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	refactor: migrate some e2e to medium (#17640)
This commit is contained in:
		
							parent
							
								
									f50e5d006c
								
							
						
					
					
						commit
						8cefa0b84b
					
				@ -5,7 +5,7 @@ import { app, utils } from 'src/utils';
 | 
				
			|||||||
import request from 'supertest';
 | 
					import request from 'supertest';
 | 
				
			||||||
import { beforeEach, describe, expect, it } from 'vitest';
 | 
					import { beforeEach, describe, expect, it } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { name, email, password } = signupDto.admin;
 | 
					const { email, password } = signupDto.admin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(`/auth/admin-sign-up`, () => {
 | 
					describe(`/auth/admin-sign-up`, () => {
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
@ -13,33 +13,6 @@ describe(`/auth/admin-sign-up`, () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('POST /auth/admin-sign-up', () => {
 | 
					  describe('POST /auth/admin-sign-up', () => {
 | 
				
			||||||
    const invalid = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        should: 'require an email address',
 | 
					 | 
				
			||||||
        data: { name, password },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        should: 'require a password',
 | 
					 | 
				
			||||||
        data: { name, email },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        should: 'require a name',
 | 
					 | 
				
			||||||
        data: { email, password },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        should: 'require a valid email',
 | 
					 | 
				
			||||||
        data: { name, email: 'immich', password },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const { should, data } of invalid) {
 | 
					 | 
				
			||||||
      it(`should ${should}`, async () => {
 | 
					 | 
				
			||||||
        const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
 | 
					 | 
				
			||||||
        expect(status).toEqual(400);
 | 
					 | 
				
			||||||
        expect(body).toEqual(errorDto.badRequest());
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it(`should sign up the admin`, async () => {
 | 
					    it(`should sign up the admin`, async () => {
 | 
				
			||||||
      const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
 | 
					      const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
 | 
				
			||||||
      expect(status).toBe(201);
 | 
					      expect(status).toBe(201);
 | 
				
			||||||
@ -57,14 +30,6 @@ describe(`/auth/admin-sign-up`, () => {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should transform email to lower case', async () => {
 | 
					 | 
				
			||||||
      const { status, body } = await request(app)
 | 
					 | 
				
			||||||
        .post('/auth/admin-sign-up')
 | 
					 | 
				
			||||||
        .send({ ...signupDto.admin, email: 'aDmIn@IMMICH.cloud' });
 | 
					 | 
				
			||||||
      expect(status).toEqual(201);
 | 
					 | 
				
			||||||
      expect(body).toEqual(signupResponseDto.admin);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should not allow a second admin to sign up', async () => {
 | 
					    it('should not allow a second admin to sign up', async () => {
 | 
				
			||||||
      await signUpAdmin({ signUpDto: signupDto.admin });
 | 
					      await signUpAdmin({ signUpDto: signupDto.admin });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -31,33 +31,7 @@ describe('/users', () => {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('GET /users', () => {
 | 
					 | 
				
			||||||
    it('should require authentication', async () => {
 | 
					 | 
				
			||||||
      const { status, body } = await request(app).get('/users');
 | 
					 | 
				
			||||||
      expect(status).toBe(401);
 | 
					 | 
				
			||||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should get users', async () => {
 | 
					 | 
				
			||||||
      const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
					 | 
				
			||||||
      expect(status).toEqual(200);
 | 
					 | 
				
			||||||
      expect(body).toHaveLength(2);
 | 
					 | 
				
			||||||
      expect(body).toEqual(
 | 
					 | 
				
			||||||
        expect.arrayContaining([
 | 
					 | 
				
			||||||
          expect.objectContaining({ email: 'admin@immich.cloud' }),
 | 
					 | 
				
			||||||
          expect.objectContaining({ email: 'user2@immich.cloud' }),
 | 
					 | 
				
			||||||
        ]),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('GET /users/me', () => {
 | 
					  describe('GET /users/me', () => {
 | 
				
			||||||
    it('should require authentication', async () => {
 | 
					 | 
				
			||||||
      const { status, body } = await request(app).get(`/users/me`);
 | 
					 | 
				
			||||||
      expect(status).toBe(401);
 | 
					 | 
				
			||||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should not work for shared links', async () => {
 | 
					    it('should not work for shared links', async () => {
 | 
				
			||||||
      const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
 | 
					      const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
 | 
				
			||||||
      const sharedLink = await utils.createSharedLink(admin.accessToken, {
 | 
					      const sharedLink = await utils.createSharedLink(admin.accessToken, {
 | 
				
			||||||
@ -99,24 +73,6 @@ describe('/users', () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('PUT /users/me', () => {
 | 
					  describe('PUT /users/me', () => {
 | 
				
			||||||
    it('should require authentication', async () => {
 | 
					 | 
				
			||||||
      const { status, body } = await request(app).put(`/users/me`);
 | 
					 | 
				
			||||||
      expect(status).toBe(401);
 | 
					 | 
				
			||||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const key of ['email', 'name']) {
 | 
					 | 
				
			||||||
      it(`should not allow null ${key}`, async () => {
 | 
					 | 
				
			||||||
        const dto = { [key]: null };
 | 
					 | 
				
			||||||
        const { status, body } = await request(app)
 | 
					 | 
				
			||||||
          .put(`/users/me`)
 | 
					 | 
				
			||||||
          .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
					 | 
				
			||||||
          .send(dto);
 | 
					 | 
				
			||||||
        expect(status).toBe(400);
 | 
					 | 
				
			||||||
        expect(body).toEqual(errorDto.badRequest());
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should update first and last name', async () => {
 | 
					    it('should update first and last name', async () => {
 | 
				
			||||||
      const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
 | 
					      const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -269,11 +225,6 @@ describe('/users', () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('GET /users/:id', () => {
 | 
					  describe('GET /users/:id', () => {
 | 
				
			||||||
    it('should require authentication', async () => {
 | 
					 | 
				
			||||||
      const { status } = await request(app).get(`/users/${admin.userId}`);
 | 
					 | 
				
			||||||
      expect(status).toEqual(401);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should get the user', async () => {
 | 
					    it('should get the user', async () => {
 | 
				
			||||||
      const { status, body } = await request(app)
 | 
					      const { status, body } = await request(app)
 | 
				
			||||||
        .get(`/users/${admin.userId}`)
 | 
					        .get(`/users/${admin.userId}`)
 | 
				
			||||||
@ -292,12 +243,6 @@ describe('/users', () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('GET /server/license', () => {
 | 
					  describe('GET /server/license', () => {
 | 
				
			||||||
    it('should require authentication', async () => {
 | 
					 | 
				
			||||||
      const { status, body } = await request(app).get('/users/me/license');
 | 
					 | 
				
			||||||
      expect(status).toBe(401);
 | 
					 | 
				
			||||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should return the user license', async () => {
 | 
					    it('should return the user license', async () => {
 | 
				
			||||||
      await request(app)
 | 
					      await request(app)
 | 
				
			||||||
        .put('/users/me/license')
 | 
					        .put('/users/me/license')
 | 
				
			||||||
@ -315,11 +260,6 @@ describe('/users', () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('PUT /users/me/license', () => {
 | 
					  describe('PUT /users/me/license', () => {
 | 
				
			||||||
    it('should require authentication', async () => {
 | 
					 | 
				
			||||||
      const { status } = await request(app).put(`/users/me/license`);
 | 
					 | 
				
			||||||
      expect(status).toEqual(401);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should set the user license', async () => {
 | 
					    it('should set the user license', async () => {
 | 
				
			||||||
      const { status, body } = await request(app)
 | 
					      const { status, body } = await request(app)
 | 
				
			||||||
        .put(`/users/me/license`)
 | 
					        .put(`/users/me/license`)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										119
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										119
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -114,6 +114,7 @@
 | 
				
			|||||||
        "rimraf": "^6.0.0",
 | 
					        "rimraf": "^6.0.0",
 | 
				
			||||||
        "source-map-support": "^0.5.21",
 | 
					        "source-map-support": "^0.5.21",
 | 
				
			||||||
        "sql-formatter": "^15.0.0",
 | 
					        "sql-formatter": "^15.0.0",
 | 
				
			||||||
 | 
					        "supertest": "^7.1.0",
 | 
				
			||||||
        "testcontainers": "^10.18.0",
 | 
					        "testcontainers": "^10.18.0",
 | 
				
			||||||
        "tsconfig-paths": "^4.2.0",
 | 
					        "tsconfig-paths": "^4.2.0",
 | 
				
			||||||
        "typescript": "^5.3.3",
 | 
					        "typescript": "^5.3.3",
 | 
				
			||||||
@ -7060,6 +7061,13 @@
 | 
				
			|||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/asap": {
 | 
				
			||||||
 | 
					      "version": "2.0.6",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/asn1": {
 | 
					    "node_modules/asn1": {
 | 
				
			||||||
      "version": "0.2.6",
 | 
					      "version": "0.2.6",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
 | 
				
			||||||
@ -7999,6 +8007,16 @@
 | 
				
			|||||||
        "node": ">= 6"
 | 
					        "node": ">= 6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/component-emitter": {
 | 
				
			||||||
 | 
					      "version": "1.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/compress-commons": {
 | 
					    "node_modules/compress-commons": {
 | 
				
			||||||
      "version": "6.0.2",
 | 
					      "version": "6.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
 | 
				
			||||||
@ -8129,6 +8147,13 @@
 | 
				
			|||||||
      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
 | 
					      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cookiejar": {
 | 
				
			||||||
 | 
					      "version": "2.1.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/core-js-compat": {
 | 
					    "node_modules/core-js-compat": {
 | 
				
			||||||
      "version": "3.41.0",
 | 
					      "version": "3.41.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
 | 
				
			||||||
@ -8446,6 +8471,17 @@
 | 
				
			|||||||
        "node": ">=8"
 | 
					        "node": ">=8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/dezalgo": {
 | 
				
			||||||
 | 
					      "version": "1.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "ISC",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "asap": "^2.0.0",
 | 
				
			||||||
 | 
					        "wrappy": "1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/diacritics": {
 | 
					    "node_modules/diacritics": {
 | 
				
			||||||
      "version": "1.3.0",
 | 
					      "version": "1.3.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
 | 
				
			||||||
@ -9787,6 +9823,21 @@
 | 
				
			|||||||
        "node": ">= 0.6"
 | 
					        "node": ">= 0.6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/formidable": {
 | 
				
			||||||
 | 
					      "version": "3.5.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "dezalgo": "^1.0.4",
 | 
				
			||||||
 | 
					        "hexoid": "^2.0.0",
 | 
				
			||||||
 | 
					        "once": "^1.4.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://ko-fi.com/tunnckoCore/commissions"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/forwarded": {
 | 
					    "node_modules/forwarded": {
 | 
				
			||||||
      "version": "0.2.0",
 | 
					      "version": "0.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
 | 
				
			||||||
@ -10325,6 +10376,16 @@
 | 
				
			|||||||
        "he": "bin/he"
 | 
					        "he": "bin/he"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/hexoid": {
 | 
				
			||||||
 | 
					      "version": "2.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/hosted-git-info": {
 | 
					    "node_modules/hosted-git-info": {
 | 
				
			||||||
      "version": "7.0.2",
 | 
					      "version": "7.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
 | 
				
			||||||
@ -11511,6 +11572,16 @@
 | 
				
			|||||||
        "node": ">= 8"
 | 
					        "node": ">= 8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/methods": {
 | 
				
			||||||
 | 
					      "version": "1.1.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 0.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/micromatch": {
 | 
					    "node_modules/micromatch": {
 | 
				
			||||||
      "version": "4.0.8",
 | 
					      "version": "4.0.8",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
 | 
				
			||||||
@ -11536,6 +11607,19 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/jonschlinkert"
 | 
					        "url": "https://github.com/sponsors/jonschlinkert"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/mime": {
 | 
				
			||||||
 | 
					      "version": "2.6.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "mime": "cli.js"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=4.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/mime-db": {
 | 
					    "node_modules/mime-db": {
 | 
				
			||||||
      "version": "1.54.0",
 | 
					      "version": "1.54.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
 | 
				
			||||||
@ -15158,6 +15242,41 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/isaacs"
 | 
					        "url": "https://github.com/sponsors/isaacs"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/superagent": {
 | 
				
			||||||
 | 
					      "version": "9.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "component-emitter": "^1.3.0",
 | 
				
			||||||
 | 
					        "cookiejar": "^2.1.4",
 | 
				
			||||||
 | 
					        "debug": "^4.3.4",
 | 
				
			||||||
 | 
					        "fast-safe-stringify": "^2.1.1",
 | 
				
			||||||
 | 
					        "form-data": "^4.0.0",
 | 
				
			||||||
 | 
					        "formidable": "^3.5.1",
 | 
				
			||||||
 | 
					        "methods": "^1.1.2",
 | 
				
			||||||
 | 
					        "mime": "2.6.0",
 | 
				
			||||||
 | 
					        "qs": "^6.11.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=14.18.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/supertest": {
 | 
				
			||||||
 | 
					      "version": "7.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==",
 | 
				
			||||||
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "methods": "^1.1.2",
 | 
				
			||||||
 | 
					        "superagent": "^9.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=14.18.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/supports-color": {
 | 
					    "node_modules/supports-color": {
 | 
				
			||||||
      "version": "7.2.0",
 | 
					      "version": "7.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -140,6 +140,7 @@
 | 
				
			|||||||
    "rimraf": "^6.0.0",
 | 
					    "rimraf": "^6.0.0",
 | 
				
			||||||
    "source-map-support": "^0.5.21",
 | 
					    "source-map-support": "^0.5.21",
 | 
				
			||||||
    "sql-formatter": "^15.0.0",
 | 
					    "sql-formatter": "^15.0.0",
 | 
				
			||||||
 | 
					    "supertest": "^7.1.0",
 | 
				
			||||||
    "testcontainers": "^10.18.0",
 | 
					    "testcontainers": "^10.18.0",
 | 
				
			||||||
    "tsconfig-paths": "^4.2.0",
 | 
					    "tsconfig-paths": "^4.2.0",
 | 
				
			||||||
    "typescript": "^5.3.3",
 | 
					    "typescript": "^5.3.3",
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,7 @@ import { getKyselyConfig } from 'src/utils/database';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const common = [...repositories, ...services, GlobalExceptionFilter];
 | 
					const common = [...repositories, ...services, GlobalExceptionFilter];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const middleware = [
 | 
					export const middleware = [
 | 
				
			||||||
  FileUploadInterceptor,
 | 
					  FileUploadInterceptor,
 | 
				
			||||||
  { provide: APP_FILTER, useClass: GlobalExceptionFilter },
 | 
					  { provide: APP_FILTER, useClass: GlobalExceptionFilter },
 | 
				
			||||||
  { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
 | 
					  { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { Kysely, sql } from 'kysely';
 | 
					import { Kysely, sql } from 'kysely';
 | 
				
			||||||
import { InjectKysely } from 'nestjs-kysely';
 | 
					import { InjectKysely } from 'nestjs-kysely';
 | 
				
			||||||
import { DB } from 'src/db';
 | 
					import { DB } from 'src/db';
 | 
				
			||||||
@ -418,6 +419,7 @@ class TagAccess {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
export class AccessRepository {
 | 
					export class AccessRepository {
 | 
				
			||||||
  activity: ActivityAccess;
 | 
					  activity: ActivityAccess;
 | 
				
			||||||
  album: AlbumAccess;
 | 
					  album: AlbumAccess;
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ import { Mocked } from 'vitest';
 | 
				
			|||||||
const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
 | 
					const sha256 = (value: string) => createHash('sha256').update(value).digest('base64');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
 | 
					// type Repositories = Omit<ServiceOverrides, 'access' | 'telemetry'>;
 | 
				
			||||||
type Repositories = {
 | 
					type RepositoriesTypes = {
 | 
				
			||||||
  activity: ActivityRepository;
 | 
					  activity: ActivityRepository;
 | 
				
			||||||
  album: AlbumRepository;
 | 
					  album: AlbumRepository;
 | 
				
			||||||
  asset: AssetRepository;
 | 
					  asset: AssetRepository;
 | 
				
			||||||
@ -54,22 +54,22 @@ type Repositories = {
 | 
				
			|||||||
  systemMetadata: SystemMetadataRepository;
 | 
					  systemMetadata: SystemMetadataRepository;
 | 
				
			||||||
  versionHistory: VersionHistoryRepository;
 | 
					  versionHistory: VersionHistoryRepository;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
type RepositoryMocks = { [K in keyof Repositories]: Mocked<RepositoryInterface<Repositories[K]>> };
 | 
					type RepositoryMocks = { [K in keyof RepositoriesTypes]: Mocked<RepositoryInterface<RepositoriesTypes[K]>> };
 | 
				
			||||||
type RepositoryOptions = Partial<{ [K in keyof Repositories]: 'mock' | 'real' }>;
 | 
					type RepositoryOptions = Partial<{ [K in keyof RepositoriesTypes]: 'mock' | 'real' }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ContextRepositoryMocks<R extends RepositoryOptions> = {
 | 
					type ContextRepositoryMocks<R extends RepositoryOptions> = {
 | 
				
			||||||
  [K in keyof Repositories as R[K] extends 'mock' ? K : never]: Mocked<RepositoryInterface<Repositories[K]>>;
 | 
					  [K in keyof RepositoriesTypes as R[K] extends 'mock' ? K : never]: Mocked<RepositoryInterface<RepositoriesTypes[K]>>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ContextRepositories<R extends RepositoryOptions> = {
 | 
					type ContextRepositories<R extends RepositoryOptions> = {
 | 
				
			||||||
  [K in keyof Repositories as R[K] extends 'real' ? K : never]: Repositories[K];
 | 
					  [K in keyof RepositoriesTypes as R[K] extends 'real' ? K : never]: RepositoriesTypes[K];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Context<R extends RepositoryOptions, S extends BaseService> = {
 | 
					export type Context<R extends RepositoryOptions, S extends BaseService> = {
 | 
				
			||||||
  sut: S;
 | 
					  sut: S;
 | 
				
			||||||
  mocks: ContextRepositoryMocks<R>;
 | 
					  mocks: ContextRepositoryMocks<R>;
 | 
				
			||||||
  repos: ContextRepositories<R>;
 | 
					  repos: ContextRepositories<R>;
 | 
				
			||||||
  getRepository<T extends keyof Repositories>(key: T): Repositories[T];
 | 
					  getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const newMediumService = <R extends RepositoryOptions, S extends BaseService>(
 | 
					export const newMediumService = <R extends RepositoryOptions, S extends BaseService>(
 | 
				
			||||||
@ -79,7 +79,7 @@ export const newMediumService = <R extends RepositoryOptions, S extends BaseServ
 | 
				
			|||||||
    repos: R;
 | 
					    repos: R;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
): Context<R, S> => {
 | 
					): Context<R, S> => {
 | 
				
			||||||
  const repos: Partial<Repositories> = {};
 | 
					  const repos: Partial<RepositoriesTypes> = {};
 | 
				
			||||||
  const mocks: Partial<RepositoryMocks> = {};
 | 
					  const mocks: Partial<RepositoryMocks> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const loggerMock = getRepositoryMock('logger') as Mocked<LoggingRepository>;
 | 
					  const loggerMock = getRepositoryMock('logger') as Mocked<LoggingRepository>;
 | 
				
			||||||
@ -88,7 +88,7 @@ export const newMediumService = <R extends RepositoryOptions, S extends BaseServ
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  for (const [_key, type] of Object.entries(options.repos)) {
 | 
					  for (const [_key, type] of Object.entries(options.repos)) {
 | 
				
			||||||
    if (type === 'real') {
 | 
					    if (type === 'real') {
 | 
				
			||||||
      const key = _key as keyof Repositories;
 | 
					      const key = _key as keyof RepositoriesTypes;
 | 
				
			||||||
      repos[key] = getRepository(key, options.database) as any;
 | 
					      repos[key] = getRepository(key, options.database) as any;
 | 
				
			||||||
      continue;
 | 
					      continue;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -100,7 +100,7 @@ export const newMediumService = <R extends RepositoryOptions, S extends BaseServ
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const makeRepository = <K extends keyof Repositories>(key: K) => {
 | 
					  const makeRepository = <K extends keyof RepositoriesTypes>(key: K) => {
 | 
				
			||||||
    return repos[key] || getRepository(key, options.database);
 | 
					    return repos[key] || getRepository(key, options.database);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -115,7 +115,7 @@ export const newMediumService = <R extends RepositoryOptions, S extends BaseServ
 | 
				
			|||||||
  } as Context<R, S>;
 | 
					  } as Context<R, S>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getRepository = <K extends keyof Repositories>(key: K, db: Kysely<DB>) => {
 | 
					export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kysely<DB>) => {
 | 
				
			||||||
  switch (key) {
 | 
					  switch (key) {
 | 
				
			||||||
    case 'activity': {
 | 
					    case 'activity': {
 | 
				
			||||||
      return new ActivityRepository(db);
 | 
					      return new ActivityRepository(db);
 | 
				
			||||||
@ -189,10 +189,10 @@ export const getRepository = <K extends keyof Repositories>(key: K, db: Kysely<D
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getRepositoryMock = <K extends keyof Repositories>(key: K) => {
 | 
					const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
 | 
				
			||||||
  switch (key) {
 | 
					  switch (key) {
 | 
				
			||||||
    case 'activity': {
 | 
					    case 'activity': {
 | 
				
			||||||
      return automock(ActivityRepository);
 | 
					      return automock(ActivityRepository) as Mocked<RepositoryInterface<ActivityRepository>>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    case 'album': {
 | 
					    case 'album': {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										121
									
								
								server/test/medium/responses.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								server/test/medium/responses.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					import { expect } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const errorDto = {
 | 
				
			||||||
 | 
					  unauthorized: {
 | 
				
			||||||
 | 
					    error: 'Unauthorized',
 | 
				
			||||||
 | 
					    statusCode: 401,
 | 
				
			||||||
 | 
					    message: 'Authentication required',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  forbidden: {
 | 
				
			||||||
 | 
					    error: 'Forbidden',
 | 
				
			||||||
 | 
					    statusCode: 403,
 | 
				
			||||||
 | 
					    message: expect.any(String),
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  missingPermission: (permission: string) => ({
 | 
				
			||||||
 | 
					    error: 'Forbidden',
 | 
				
			||||||
 | 
					    statusCode: 403,
 | 
				
			||||||
 | 
					    message: `Missing required permission: ${permission}`,
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  wrongPassword: {
 | 
				
			||||||
 | 
					    error: 'Bad Request',
 | 
				
			||||||
 | 
					    statusCode: 400,
 | 
				
			||||||
 | 
					    message: 'Wrong password',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  invalidToken: {
 | 
				
			||||||
 | 
					    error: 'Unauthorized',
 | 
				
			||||||
 | 
					    statusCode: 401,
 | 
				
			||||||
 | 
					    message: 'Invalid user token',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  invalidShareKey: {
 | 
				
			||||||
 | 
					    error: 'Unauthorized',
 | 
				
			||||||
 | 
					    statusCode: 401,
 | 
				
			||||||
 | 
					    message: 'Invalid share key',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  invalidSharePassword: {
 | 
				
			||||||
 | 
					    error: 'Unauthorized',
 | 
				
			||||||
 | 
					    statusCode: 401,
 | 
				
			||||||
 | 
					    message: 'Invalid password',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  badRequest: (message: any = null) => ({
 | 
				
			||||||
 | 
					    error: 'Bad Request',
 | 
				
			||||||
 | 
					    statusCode: 400,
 | 
				
			||||||
 | 
					    message: message ?? expect.anything(),
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  noPermission: {
 | 
				
			||||||
 | 
					    error: 'Bad Request',
 | 
				
			||||||
 | 
					    statusCode: 400,
 | 
				
			||||||
 | 
					    message: expect.stringContaining('Not found or no'),
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  incorrectLogin: {
 | 
				
			||||||
 | 
					    error: 'Unauthorized',
 | 
				
			||||||
 | 
					    statusCode: 401,
 | 
				
			||||||
 | 
					    message: 'Incorrect email or password',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  alreadyHasAdmin: {
 | 
				
			||||||
 | 
					    error: 'Bad Request',
 | 
				
			||||||
 | 
					    statusCode: 400,
 | 
				
			||||||
 | 
					    message: 'The server already has an admin',
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  invalidEmail: {
 | 
				
			||||||
 | 
					    error: 'Bad Request',
 | 
				
			||||||
 | 
					    statusCode: 400,
 | 
				
			||||||
 | 
					    message: ['email must be an email'],
 | 
				
			||||||
 | 
					    correlationId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const signupResponseDto = {
 | 
				
			||||||
 | 
					  admin: {
 | 
				
			||||||
 | 
					    avatarColor: expect.any(String),
 | 
				
			||||||
 | 
					    id: expect.any(String),
 | 
				
			||||||
 | 
					    name: 'Immich Admin',
 | 
				
			||||||
 | 
					    email: 'admin@immich.cloud',
 | 
				
			||||||
 | 
					    storageLabel: 'admin',
 | 
				
			||||||
 | 
					    profileImagePath: '',
 | 
				
			||||||
 | 
					    // why? lol
 | 
				
			||||||
 | 
					    shouldChangePassword: true,
 | 
				
			||||||
 | 
					    isAdmin: true,
 | 
				
			||||||
 | 
					    createdAt: expect.any(String),
 | 
				
			||||||
 | 
					    updatedAt: expect.any(String),
 | 
				
			||||||
 | 
					    deletedAt: null,
 | 
				
			||||||
 | 
					    oauthId: '',
 | 
				
			||||||
 | 
					    quotaUsageInBytes: 0,
 | 
				
			||||||
 | 
					    quotaSizeInBytes: null,
 | 
				
			||||||
 | 
					    status: 'active',
 | 
				
			||||||
 | 
					    license: null,
 | 
				
			||||||
 | 
					    profileChangedAt: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const loginResponseDto = {
 | 
				
			||||||
 | 
					  admin: {
 | 
				
			||||||
 | 
					    accessToken: expect.any(String),
 | 
				
			||||||
 | 
					    name: 'Immich Admin',
 | 
				
			||||||
 | 
					    isAdmin: true,
 | 
				
			||||||
 | 
					    profileImagePath: '',
 | 
				
			||||||
 | 
					    shouldChangePassword: true,
 | 
				
			||||||
 | 
					    userEmail: 'admin@immich.cloud',
 | 
				
			||||||
 | 
					    userId: expect.any(String),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const deviceDto = {
 | 
				
			||||||
 | 
					  current: {
 | 
				
			||||||
 | 
					    id: expect.any(String),
 | 
				
			||||||
 | 
					    createdAt: expect.any(String),
 | 
				
			||||||
 | 
					    updatedAt: expect.any(String),
 | 
				
			||||||
 | 
					    current: true,
 | 
				
			||||||
 | 
					    deviceOS: '',
 | 
				
			||||||
 | 
					    deviceType: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										60
									
								
								server/test/medium/specs/controllers/auth.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								server/test/medium/specs/controllers/auth.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { AuthController } from 'src/controllers/auth.controller';
 | 
				
			||||||
 | 
					import { AuthService } from 'src/services/auth.service';
 | 
				
			||||||
 | 
					import request from 'supertest';
 | 
				
			||||||
 | 
					import { errorDto } from 'test/medium/responses';
 | 
				
			||||||
 | 
					import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe(AuthController.name, () => {
 | 
				
			||||||
 | 
					  let app: TestControllerApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeAll(async () => {
 | 
				
			||||||
 | 
					    app = await createControllerTestApp();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('POST /auth/admin-sign-up', () => {
 | 
				
			||||||
 | 
					    const name = 'admin';
 | 
				
			||||||
 | 
					    const email = 'admin@immich.cloud';
 | 
				
			||||||
 | 
					    const password = 'password';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const invalid = [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        should: 'require an email address',
 | 
				
			||||||
 | 
					        data: { name, password },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        should: 'require a password',
 | 
				
			||||||
 | 
					        data: { name, email },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        should: 'require a name',
 | 
				
			||||||
 | 
					        data: { email, password },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        should: 'require a valid email',
 | 
				
			||||||
 | 
					        data: { name, email: 'immich', password },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const { should, data } of invalid) {
 | 
				
			||||||
 | 
					      it(`should ${should}`, async () => {
 | 
				
			||||||
 | 
					        const { status, body } = await request(app.getHttpServer()).post('/auth/admin-sign-up').send(data);
 | 
				
			||||||
 | 
					        expect(status).toEqual(400);
 | 
				
			||||||
 | 
					        expect(body).toEqual(errorDto.badRequest());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should transform email to lower case', async () => {
 | 
				
			||||||
 | 
					      const { status } = await request(app.getHttpServer())
 | 
				
			||||||
 | 
					        .post('/auth/admin-sign-up')
 | 
				
			||||||
 | 
					        .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
 | 
				
			||||||
 | 
					      expect(status).toEqual(201);
 | 
				
			||||||
 | 
					      expect(app.getMockedService(AuthService).adminSignUp).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        expect.objectContaining({ email: 'admin@immich.cloud' }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterAll(async () => {
 | 
				
			||||||
 | 
					    await app.close();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										100
									
								
								server/test/medium/specs/controllers/user.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								server/test/medium/specs/controllers/user.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import { UserController } from 'src/controllers/user.controller';
 | 
				
			||||||
 | 
					import { AuthService } from 'src/services/auth.service';
 | 
				
			||||||
 | 
					import { UserService } from 'src/services/user.service';
 | 
				
			||||||
 | 
					import request from 'supertest';
 | 
				
			||||||
 | 
					import { errorDto } from 'test/medium/responses';
 | 
				
			||||||
 | 
					import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
 | 
				
			||||||
 | 
					import { factory } from 'test/small.factory';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe(UserController.name, () => {
 | 
				
			||||||
 | 
					  let realApp: TestControllerApp;
 | 
				
			||||||
 | 
					  let mockApp: TestControllerApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeAll(async () => {
 | 
				
			||||||
 | 
					    realApp = await createControllerTestApp({ authType: 'real' });
 | 
				
			||||||
 | 
					    mockApp = await createControllerTestApp({ authType: 'mock' });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('GET /users', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(realApp.getHttpServer()).get('/users');
 | 
				
			||||||
 | 
					      expect(status).toBe(401);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorDto.unauthorized);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should call the service with an auth dto', async () => {
 | 
				
			||||||
 | 
					      const user = factory.user();
 | 
				
			||||||
 | 
					      const authService = mockApp.getMockedService(AuthService);
 | 
				
			||||||
 | 
					      const auth = factory.auth({ user });
 | 
				
			||||||
 | 
					      authService.authenticate.mockResolvedValue(auth);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const userService = mockApp.getMockedService(UserService);
 | 
				
			||||||
 | 
					      const { status } = await request(mockApp.getHttpServer()).get('/users').set('Authorization', `Bearer token`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(status).toBe(200);
 | 
				
			||||||
 | 
					      expect(userService.search).toHaveBeenCalledWith(auth);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('GET /users/me', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(realApp.getHttpServer()).get(`/users/me`);
 | 
				
			||||||
 | 
					      expect(status).toBe(401);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorDto.unauthorized);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('PUT /users/me', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(realApp.getHttpServer()).put(`/users/me`);
 | 
				
			||||||
 | 
					      expect(status).toBe(401);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorDto.unauthorized);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const key of ['email', 'name']) {
 | 
				
			||||||
 | 
					      it(`should not allow null ${key}`, async () => {
 | 
				
			||||||
 | 
					        const dto = { [key]: null };
 | 
				
			||||||
 | 
					        const { status, body } = await request(mockApp.getHttpServer())
 | 
				
			||||||
 | 
					          .put(`/users/me`)
 | 
				
			||||||
 | 
					          .set('Authorization', `Bearer token`)
 | 
				
			||||||
 | 
					          .send(dto);
 | 
				
			||||||
 | 
					        expect(status).toBe(400);
 | 
				
			||||||
 | 
					        expect(body).toEqual(errorDto.badRequest());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('GET /users/:id', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status } = await request(realApp.getHttpServer()).get(`/users/${factory.uuid()}`);
 | 
				
			||||||
 | 
					      expect(status).toEqual(401);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('GET /server/license', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status, body } = await request(realApp.getHttpServer()).get('/users/me/license');
 | 
				
			||||||
 | 
					      expect(status).toBe(401);
 | 
				
			||||||
 | 
					      expect(body).toEqual(errorDto.unauthorized);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('PUT /users/me/license', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`);
 | 
				
			||||||
 | 
					      expect(status).toEqual(401);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('DELETE /users/me/license', () => {
 | 
				
			||||||
 | 
					    it('should require authentication', async () => {
 | 
				
			||||||
 | 
					      const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`);
 | 
				
			||||||
 | 
					      expect(status).toEqual(401);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterAll(async () => {
 | 
				
			||||||
 | 
					    await realApp.close();
 | 
				
			||||||
 | 
					    await mockApp.close();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -60,6 +60,25 @@ describe(UserService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('search', () => {
 | 
				
			||||||
 | 
					    it('should get users', async () => {
 | 
				
			||||||
 | 
					      const { sut, repos } = createSut();
 | 
				
			||||||
 | 
					      const user1 = mediumFactory.userInsert();
 | 
				
			||||||
 | 
					      const user2 = mediumFactory.userInsert();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await Promise.all([repos.user.create(user1), repos.user.create(user2)]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const auth = factory.auth({ user: user1 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.search(auth)).resolves.toEqual(
 | 
				
			||||||
 | 
					        expect.arrayContaining([
 | 
				
			||||||
 | 
					          expect.objectContaining({ email: user1.email }),
 | 
				
			||||||
 | 
					          expect.objectContaining({ email: user2.email }),
 | 
				
			||||||
 | 
					        ]),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('get', () => {
 | 
					  describe('get', () => {
 | 
				
			||||||
    it('should get a user', async () => {
 | 
					    it('should get a user', async () => {
 | 
				
			||||||
      const { sut, repos } = createSut();
 | 
					      const { sut, repos } = createSut();
 | 
				
			||||||
							
								
								
									
										100
									
								
								server/test/medium/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								server/test/medium/utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import { Provider } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { SchedulerRegistry } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					import { Test } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { ClassConstructor } from 'class-transformer';
 | 
				
			||||||
 | 
					import { ClsService } from 'nestjs-cls';
 | 
				
			||||||
 | 
					import { middleware } from 'src/app.module';
 | 
				
			||||||
 | 
					import { controllers } from 'src/controllers';
 | 
				
			||||||
 | 
					import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
 | 
				
			||||||
 | 
					import { LoggingRepository } from 'src/repositories/logging.repository';
 | 
				
			||||||
 | 
					import { services } from 'src/services';
 | 
				
			||||||
 | 
					import { ApiService } from 'src/services/api.service';
 | 
				
			||||||
 | 
					import { AuthService } from 'src/services/auth.service';
 | 
				
			||||||
 | 
					import { BaseService } from 'src/services/base.service';
 | 
				
			||||||
 | 
					import { automock } from 'test/utils';
 | 
				
			||||||
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createControllerTestApp = async (options?: { authType?: 'mock' | 'real' }) => {
 | 
				
			||||||
 | 
					  const { authType = 'mock' } = options || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const configMock = { getEnv: () => ({ noColor: true }) };
 | 
				
			||||||
 | 
					  const clsMock = { getId: vitest.fn().mockReturnValue('cls-id') };
 | 
				
			||||||
 | 
					  const loggerMock = automock(LoggingRepository, { args: [clsMock, configMock], strict: false });
 | 
				
			||||||
 | 
					  loggerMock.setContext.mockReturnValue(void 0);
 | 
				
			||||||
 | 
					  loggerMock.error.mockImplementation((...args: any[]) => {
 | 
				
			||||||
 | 
					    console.log('Logger.error was called with', ...args);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mockBaseService = (service: ClassConstructor<BaseService>) => {
 | 
				
			||||||
 | 
					    return automock(service, { args: [loggerMock], strict: false });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clsServiceMock = clsMock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const FAKE_MOCK = vitest.fn();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const providers: Provider[] = [
 | 
				
			||||||
 | 
					    ...middleware,
 | 
				
			||||||
 | 
					    ...services.map((Service) => {
 | 
				
			||||||
 | 
					      if ((authType === 'real' && Service === AuthService) || Service === ApiService) {
 | 
				
			||||||
 | 
					        return Service;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return { provide: Service, useValue: mockBaseService(Service as ClassConstructor<BaseService>) };
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    GlobalExceptionFilter,
 | 
				
			||||||
 | 
					    { provide: LoggingRepository, useValue: loggerMock },
 | 
				
			||||||
 | 
					    { provide: ClsService, useValue: clsServiceMock },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const moduleRef = await Test.createTestingModule({
 | 
				
			||||||
 | 
					    imports: [],
 | 
				
			||||||
 | 
					    controllers: [...controllers],
 | 
				
			||||||
 | 
					    providers,
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					    .useMocker((token) => {
 | 
				
			||||||
 | 
					      if (token === LoggingRepository) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (token === SchedulerRegistry) {
 | 
				
			||||||
 | 
					        return FAKE_MOCK;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (typeof token === 'function' && token.name.endsWith('Repository')) {
 | 
				
			||||||
 | 
					        return FAKE_MOCK;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (typeof token === 'string' && token === 'KyselyModuleConnectionToken') {
 | 
				
			||||||
 | 
					        return FAKE_MOCK;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const app = moduleRef.createNestApplication();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await app.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getMockedRepository = <T>(token: ClassConstructor<T>) => {
 | 
				
			||||||
 | 
					    return app.get(token) as Mocked<T>;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    getHttpServer: () => app.getHttpServer(),
 | 
				
			||||||
 | 
					    getMockedService: <T>(token: ClassConstructor<T>) => {
 | 
				
			||||||
 | 
					      if (authType === 'real' && token === AuthService) {
 | 
				
			||||||
 | 
					        throw new Error('Auth type is real, cannot get mocked service');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return app.get(token) as Mocked<T>;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getMockedRepository,
 | 
				
			||||||
 | 
					    close: () => app.close(),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type TestControllerApp = {
 | 
				
			||||||
 | 
					  getHttpServer: () => any;
 | 
				
			||||||
 | 
					  getMockedService: <T>(token: ClassConstructor<T>) => Mocked<T>;
 | 
				
			||||||
 | 
					  getMockedRepository: <T>(token: ClassConstructor<T>) => Mocked<T>;
 | 
				
			||||||
 | 
					  close: () => Promise<void>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -310,4 +310,5 @@ export const factory = {
 | 
				
			|||||||
  jobAssets: {
 | 
					  jobAssets: {
 | 
				
			||||||
    sidecarWrite: assetSidecarWriteFactory,
 | 
					    sidecarWrite: assetSidecarWriteFactory,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  uuid: newUuid,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -93,13 +93,17 @@ export const automock = <T>(
 | 
				
			|||||||
      continue;
 | 
					      continue;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const label = `${Dependency.name}.${property}`;
 | 
					    try {
 | 
				
			||||||
    // console.log(`Automocking ${label}`);
 | 
					      const label = `${Dependency.name}.${property}`;
 | 
				
			||||||
 | 
					      // console.log(`Automocking ${label}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const target = instance[property as keyof T];
 | 
					      const target = instance[property as keyof T];
 | 
				
			||||||
    if (typeof target === 'function') {
 | 
					      if (typeof target === 'function') {
 | 
				
			||||||
      mock[property] = mockFn(label, { strict });
 | 
					        mock[property] = mockFn(label, { strict });
 | 
				
			||||||
      continue;
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      // noop
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user