From 3ef2fce067a615438b0c8a0250597627b2243d1a Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 19 Mar 2021 16:47:52 +0100 Subject: [PATCH 1/3] MediaService: Add listUploadsByNote method Signed-off-by: Philip Molares --- src/media/media.service.spec.ts | 36 +++++++++++++++++++++++++++++++++ src/media/media.service.ts | 18 +++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts index a71464e81..bab440fdc 100644 --- a/src/media/media.service.spec.ts +++ b/src/media/media.service.spec.ts @@ -264,4 +264,40 @@ describe('MediaService', () => { }); }); }); + + describe('listUploadsByNote', () => { + describe('works', () => { + it('with one upload to note', async () => { + const mockMediaUploadEntry = { + id: 'testMediaUpload', + backendData: 'testBackendData', + note: { + id: '123', + } as Note, + } as MediaUpload; + jest + .spyOn(mediaRepo, 'find') + .mockResolvedValueOnce([mockMediaUploadEntry]); + const mediaList = await service.listUploadsByNote({ + id: '123', + } as Note); + expect(mediaList).toEqual([mockMediaUploadEntry]); + }); + + it('without uploads to note', async () => { + jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce([]); + const mediaList = await service.listUploadsByNote({ + id: '123', + } as Note); + expect(mediaList).toEqual([]); + }); + it('with error (undefined as return value of find)', async () => { + jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce(undefined); + const mediaList = await service.listUploadsByNote({ + id: '123', + } as Note); + expect(mediaList).toEqual([]); + }); + }); + }); }); diff --git a/src/media/media.service.ts b/src/media/media.service.ts index 90a72049b..8a71f2678 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -24,6 +24,7 @@ import { AzureBackend } from './backends/azure-backend'; import { ImgurBackend } from './backends/imgur-backend'; import { User } from '../users/user.entity'; import { MediaUploadDto } from './media-upload.dto'; +import { Note } from '../notes/note.entity'; @Injectable() export class MediaService { @@ -175,6 +176,23 @@ export class MediaService { return mediaUploads; } + /** + * @async + * List all uploads by a specific note + * @param {Note} note - the specific user + * @return {MediaUpload[]} arary of media uploads owned by the user + */ + async listUploadsByNote(note: Note): Promise { + const mediaUploads = await this.mediaUploadRepository.find({ + where: { note: note }, + relations: ['user', 'note'], + }); + if (mediaUploads === undefined) { + return []; + } + return mediaUploads; + } + private chooseBackendType(): BackendType { switch (this.mediaConfig.backend.use) { case 'filesystem': From 37fa75fc912357cae90737f16c9e69f0ea2d1ee4 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 19 Mar 2021 16:53:04 +0100 Subject: [PATCH 2/3] PublicApi: Add GET /api/v2/notes/{note}/media Signed-off-by: Philip Molares --- src/api/public/notes/notes.controller.spec.ts | 8 ++++- src/api/public/notes/notes.controller.ts | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/api/public/notes/notes.controller.spec.ts b/src/api/public/notes/notes.controller.spec.ts index 117b4fd9f..dead1c5ed 100644 --- a/src/api/public/notes/notes.controller.spec.ts +++ b/src/api/public/notes/notes.controller.spec.ts @@ -27,7 +27,10 @@ import { NoteUserPermission } from '../../../permissions/note-user-permission.en import { Group } from '../../../groups/group.entity'; import { GroupsModule } from '../../../groups/groups.module'; import { ConfigModule } from '@nestjs/config'; +import { MediaModule } from '../../../media/media.module'; +import { MediaUpload } from '../../../media/media-upload.entity'; import appConfigMock from '../../../config/app.config.mock'; +import mediaConfigMock from '../../../config/media.config.mock'; describe('Notes Controller', () => { let controller: NotesController; @@ -53,9 +56,10 @@ describe('Notes Controller', () => { LoggerModule, PermissionsModule, HistoryModule, + MediaModule, ConfigModule.forRoot({ isGlobal: true, - load: [appConfigMock], + load: [appConfigMock, mediaConfigMock], }), ], }) @@ -85,6 +89,8 @@ describe('Notes Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(Group)) .useValue({}) + .overrideProvider(getRepositoryToken(MediaUpload)) + .useValue({}) .compile(); controller = module.get(NotesController); diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 2185284d0..cf0a905b9 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -60,6 +60,8 @@ import { successfullyDeletedDescription, unauthorizedDescription, } from '../../utils/descriptions'; +import { MediaUploadDto } from '../../../media/media-upload.dto'; +import { MediaService } from '../../../media/media.service'; @ApiTags('notes') @ApiSecurity('token') @@ -71,6 +73,7 @@ export class NotesController { private revisionsService: RevisionsService, private permissionsService: PermissionsService, private historyService: HistoryService, + private mediaService: MediaService, ) { this.logger.setContext(NotesController.name); } @@ -389,4 +392,31 @@ export class NotesController { throw e; } } + + @UseGuards(TokenAuthGuard) + @Get(':noteIdOrAlias/media') + @ApiOkResponse({ + description: 'All media uploads of the note', + isArray: true, + type: MediaUploadDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + async getNotesMedia( + @Req() req: Request, + @Param('noteIdOrAlias') noteIdOrAlias: string, + ): Promise { + try { + const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias); + if (!this.permissionsService.mayRead(req.user, note)) { + throw new UnauthorizedException('Reading note denied!'); + } + const media = await this.mediaService.listUploadsByNote(note); + return media.map((media) => this.mediaService.toMediaUploadDto(media)); + } catch (e) { + if (e instanceof NotInDBError) { + throw new NotFoundException(e.message); + } + throw e; + } + } } From 9b427dc6d18859a5c507ffa214d17d7cfd9be130 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 19 Mar 2021 16:54:24 +0100 Subject: [PATCH 3/3] NotesE2ETest: Add GET /api/v2/notes/{note}/media test Signed-off-by: Philip Molares --- test/public-api/notes.e2e-spec.ts | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index 155ca9c86..2d4b5bab2 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -29,11 +29,15 @@ import { MockAuthGuard } from '../../src/auth/mock-auth.guard'; import { UsersService } from '../../src/users/users.service'; import { User } from '../../src/users/user.entity'; import { UsersModule } from '../../src/users/users.module'; +import { promises as fs } from 'fs'; +import { MediaService } from '../../src/media/media.service'; describe('Notes', () => { let app: INestApplication; let notesService: NotesService; + let mediaService: MediaService; let user: User; + let user2: User; let content: string; let forbiddenNoteId: string; @@ -69,8 +73,10 @@ describe('Notes', () => { app = moduleRef.createNestApplication(); await app.init(); notesService = moduleRef.get(NotesService); + mediaService = moduleRef.get(MediaService); const userService = moduleRef.get(UsersService); user = await userService.createUser('hardcoded', 'Testy'); + user2 = await userService.createUser('hardcoded2', 'Max Mustermann'); content = 'This is a test note.'; }); @@ -322,6 +328,52 @@ describe('Notes', () => { }); }); + describe('GET /notes/{note}/media', () => { + it('works', async () => { + const note = await notesService.createNote(content, 'test9', user); + const extraNote = await notesService.createNote(content, 'test10', user); + const httpServer = app.getHttpServer(); + const response = await request(httpServer) + .get(`/notes/${note.id}/media/`) + .expect('Content-Type', /json/) + .expect(200); + expect(response.body).toHaveLength(0); + + const testImage = await fs.readFile('test/public-api/fixtures/test.png'); + const url0 = await mediaService.saveFile(testImage, 'hardcoded', note.id); + const url1 = await mediaService.saveFile( + testImage, + 'hardcoded', + extraNote.id, + ); + + const responseAfter = await request(httpServer) + .get(`/notes/${note.id}/media/`) + .expect('Content-Type', /json/) + .expect(200); + expect(responseAfter.body).toHaveLength(1); + expect(responseAfter.body[0].url).toEqual(url0); + expect(responseAfter.body[0].url).not.toEqual(url1); + }); + it('fails, when note does not exist', async () => { + await request(app.getHttpServer()) + .get(`/notes/i_dont_exist/media/`) + .expect('Content-Type', /json/) + .expect(404); + }); + it("fails, when user can't read note", async () => { + const note = await notesService.createNote( + 'This is a test note.', + 'test11', + user2, + ); + await request(app.getHttpServer()) + .get(`/notes/${note.id}/media/`) + .expect('Content-Type', /json/) + .expect(401); + }); + }); + afterAll(async () => { await app.close(); });