mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web,server): activity (#4682)
* feat: activity * regenerate api * fix: make asset owner unable to delete comment * fix: merge * fix: tests * feat: use textarea instead of input * fix: do actions only if the album is shared * fix: placeholder opacity * fix(web): improve messages UI * fix(web): improve input message UI * pr feedback * fix: tests * pr feedback * pr feedback * pr feedback * fix permissions * regenerate api * pr feedback * pr feedback * multiple improvements on web * fix: ui colors * WIP * chore: open api * pr feedback * fix: add comment * chore: clean up * pr feedback * refactor: endpoints * chore: open api * fix: filter by type * fix: e2e * feat: e2e remove own comment * fix: web tests * remove console.log * chore: cleanup * fix: ui tweaks * pr feedback * fix web test * fix: unit tests * chore: remove unused code * revert useless changes * fix: grouping messages * fix: remove nullable on updatedAt * fix: text overflow * styling --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							
								
								
									
										376
									
								
								server/test/e2e/activity.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								server/test/e2e/activity.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,376 @@
 | 
			
		||||
import { AlbumResponseDto, LoginResponseDto, ReactionType } from '@app/domain';
 | 
			
		||||
import { ActivityController } from '@app/immich';
 | 
			
		||||
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
 | 
			
		||||
import { api } from '@test/api';
 | 
			
		||||
import { db } from '@test/db';
 | 
			
		||||
import { errorStub, uuidStub } from '@test/fixtures';
 | 
			
		||||
import { testApp } from '@test/test-utils';
 | 
			
		||||
import request from 'supertest';
 | 
			
		||||
 | 
			
		||||
describe(`${ActivityController.name} (e2e)`, () => {
 | 
			
		||||
  let server: any;
 | 
			
		||||
  let admin: LoginResponseDto;
 | 
			
		||||
  let asset: AssetFileUploadResponseDto;
 | 
			
		||||
  let album: AlbumResponseDto;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    [server] = await testApp.create();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterAll(async () => {
 | 
			
		||||
    await testApp.teardown();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await db.reset();
 | 
			
		||||
    await api.authApi.adminSignUp(server);
 | 
			
		||||
    admin = await api.authApi.adminLogin(server);
 | 
			
		||||
    asset = await api.assetApi.upload(server, admin.accessToken, 'example');
 | 
			
		||||
    album = await api.albumApi.create(server, admin.accessToken, { albumName: 'Album 1', assetIds: [asset.id] });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('GET /activity', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(server).get('/activity');
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorStub.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require an albumId', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should reject an invalid albumId', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: uuidStub.invalid })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should reject an invalid assetId', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: uuidStub.notFound, assetId: uuidStub.invalid })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should start off empty', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: album.id })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(body).toEqual([]);
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter by album id', async () => {
 | 
			
		||||
      const album2 = await api.albumApi.create(server, admin.accessToken, {
 | 
			
		||||
        albumName: 'Album 2',
 | 
			
		||||
        assetIds: [asset.id],
 | 
			
		||||
      });
 | 
			
		||||
      const [reaction] = await Promise.all([
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
          albumId: album.id,
 | 
			
		||||
          type: ReactionType.LIKE,
 | 
			
		||||
        }),
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
          albumId: album2.id,
 | 
			
		||||
          type: ReactionType.LIKE,
 | 
			
		||||
        }),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: album.id })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body.length).toBe(1);
 | 
			
		||||
      expect(body[0]).toEqual(reaction);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter by type=comment', async () => {
 | 
			
		||||
      const [reaction] = await Promise.all([
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
          albumId: album.id,
 | 
			
		||||
          type: ReactionType.COMMENT,
 | 
			
		||||
          comment: 'comment',
 | 
			
		||||
        }),
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: album.id, type: 'comment' })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body.length).toBe(1);
 | 
			
		||||
      expect(body[0]).toEqual(reaction);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter by type=like', async () => {
 | 
			
		||||
      const [reaction] = await Promise.all([
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
          albumId: album.id,
 | 
			
		||||
          type: ReactionType.COMMENT,
 | 
			
		||||
          comment: 'comment',
 | 
			
		||||
        }),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: album.id, type: 'like' })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body.length).toBe(1);
 | 
			
		||||
      expect(body[0]).toEqual(reaction);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter by assetId', async () => {
 | 
			
		||||
      const [reaction] = await Promise.all([
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
          albumId: album.id,
 | 
			
		||||
          assetId: asset.id,
 | 
			
		||||
          type: ReactionType.LIKE,
 | 
			
		||||
        }),
 | 
			
		||||
        api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .query({ albumId: album.id, assetId: asset.id })
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body.length).toBe(1);
 | 
			
		||||
      expect(body[0]).toEqual(reaction);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('POST /activity', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(server).post('/activity');
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorStub.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require an albumId', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: uuidStub.invalid });
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require a comment when type is comment', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: uuidStub.notFound, type: 'comment', comment: null });
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest(['comment must be a string', 'comment should not be empty']));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should add a comment to an album', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: album.id, type: 'comment', comment: 'This is my first comment' });
 | 
			
		||||
      expect(status).toEqual(201);
 | 
			
		||||
      expect(body).toEqual({
 | 
			
		||||
        id: expect.any(String),
 | 
			
		||||
        assetId: null,
 | 
			
		||||
        createdAt: expect.any(String),
 | 
			
		||||
        type: 'comment',
 | 
			
		||||
        comment: 'This is my first comment',
 | 
			
		||||
        user: expect.objectContaining({ email: admin.userEmail }),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should add a like to an album', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: album.id, type: 'like' });
 | 
			
		||||
      expect(status).toEqual(201);
 | 
			
		||||
      expect(body).toEqual({
 | 
			
		||||
        id: expect.any(String),
 | 
			
		||||
        assetId: null,
 | 
			
		||||
        createdAt: expect.any(String),
 | 
			
		||||
        type: 'like',
 | 
			
		||||
        comment: null,
 | 
			
		||||
        user: expect.objectContaining({ email: admin.userEmail }),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return a 200 for a duplicate like on the album', async () => {
 | 
			
		||||
      const reaction = await api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        type: ReactionType.LIKE,
 | 
			
		||||
      });
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: album.id, type: 'like' });
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body).toEqual(reaction);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should add a comment to an asset', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: album.id, assetId: asset.id, type: 'comment', comment: 'This is my first comment' });
 | 
			
		||||
      expect(status).toEqual(201);
 | 
			
		||||
      expect(body).toEqual({
 | 
			
		||||
        id: expect.any(String),
 | 
			
		||||
        assetId: asset.id,
 | 
			
		||||
        createdAt: expect.any(String),
 | 
			
		||||
        type: 'comment',
 | 
			
		||||
        comment: 'This is my first comment',
 | 
			
		||||
        user: expect.objectContaining({ email: admin.userEmail }),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should add a like to an asset', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: album.id, assetId: asset.id, type: 'like' });
 | 
			
		||||
      expect(status).toEqual(201);
 | 
			
		||||
      expect(body).toEqual({
 | 
			
		||||
        id: expect.any(String),
 | 
			
		||||
        assetId: asset.id,
 | 
			
		||||
        createdAt: expect.any(String),
 | 
			
		||||
        type: 'like',
 | 
			
		||||
        comment: null,
 | 
			
		||||
        user: expect.objectContaining({ email: admin.userEmail }),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return a 200 for a duplicate like on an asset', async () => {
 | 
			
		||||
      const reaction = await api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        assetId: asset.id,
 | 
			
		||||
        type: ReactionType.LIKE,
 | 
			
		||||
      });
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`)
 | 
			
		||||
        .send({ albumId: album.id, assetId: asset.id, type: 'like' });
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body).toEqual(reaction);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('DELETE /activity/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(server).delete(`/activity/${uuidStub.notFound}`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorStub.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require a valid uuid', async () => {
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .delete(`/activity/${uuidStub.invalid}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should remove a comment from an album', async () => {
 | 
			
		||||
      const reaction = await api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        type: ReactionType.COMMENT,
 | 
			
		||||
        comment: 'This is a test comment',
 | 
			
		||||
      });
 | 
			
		||||
      const { status } = await request(server)
 | 
			
		||||
        .delete(`/activity/${reaction.id}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(204);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should remove a like from an album', async () => {
 | 
			
		||||
      const reaction = await api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        type: ReactionType.LIKE,
 | 
			
		||||
      });
 | 
			
		||||
      const { status } = await request(server)
 | 
			
		||||
        .delete(`/activity/${reaction.id}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(204);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should let the owner remove a comment by another user', async () => {
 | 
			
		||||
      const { id: userId } = await api.userApi.create(server, admin.accessToken, {
 | 
			
		||||
        email: 'user1@immich.app',
 | 
			
		||||
        password: 'Password123',
 | 
			
		||||
        firstName: 'User 1',
 | 
			
		||||
        lastName: 'Test',
 | 
			
		||||
      });
 | 
			
		||||
      await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] });
 | 
			
		||||
      const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' });
 | 
			
		||||
      const reaction = await api.activityApi.create(server, nonOwner.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        type: ReactionType.COMMENT,
 | 
			
		||||
        comment: 'This is a test comment',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const { status } = await request(server)
 | 
			
		||||
        .delete(`/activity/${reaction.id}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(204);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not let a user remove a comment by another user', async () => {
 | 
			
		||||
      const { id: userId } = await api.userApi.create(server, admin.accessToken, {
 | 
			
		||||
        email: 'user1@immich.app',
 | 
			
		||||
        password: 'Password123',
 | 
			
		||||
        firstName: 'User 1',
 | 
			
		||||
        lastName: 'Test',
 | 
			
		||||
      });
 | 
			
		||||
      await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] });
 | 
			
		||||
      const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' });
 | 
			
		||||
      const reaction = await api.activityApi.create(server, admin.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        type: ReactionType.COMMENT,
 | 
			
		||||
        comment: 'This is a test comment',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(server)
 | 
			
		||||
        .delete(`/activity/${reaction.id}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${nonOwner.accessToken}`);
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(errorStub.badRequest('Not found or no activity.delete access'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should let a non-owner remove their own comment', async () => {
 | 
			
		||||
      const { id: userId } = await api.userApi.create(server, admin.accessToken, {
 | 
			
		||||
        email: 'user1@immich.app',
 | 
			
		||||
        password: 'Password123',
 | 
			
		||||
        firstName: 'User 1',
 | 
			
		||||
        lastName: 'Test',
 | 
			
		||||
      });
 | 
			
		||||
      await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] });
 | 
			
		||||
      const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' });
 | 
			
		||||
      const reaction = await api.activityApi.create(server, nonOwner.accessToken, {
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        type: ReactionType.COMMENT,
 | 
			
		||||
        comment: 'This is a test comment',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const { status } = await request(server)
 | 
			
		||||
        .delete(`/activity/${reaction.id}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${nonOwner.accessToken}`);
 | 
			
		||||
      expect(status).toBe(204);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user