Add GET /me/media

Returns all media files uploaded by the authenticated user.

Signed-off-by: Yannick Bungers <git@innay.de>
This commit is contained in:
Yannick Bungers 2021-03-14 17:47:16 +01:00
parent b67ec817e6
commit ef352a1313
7 changed files with 174 additions and 10 deletions

View file

@ -900,19 +900,21 @@ components:
MediaUpload: MediaUpload:
type: object type: object
properties: properties:
note:
type: string
description: ID of the note the file was uploaded to
user:
type: string
description: username of the user who uploaded the file
url: url:
type: string type: string
description: URL of the file description: URL of the file
owningNote:
type: string
description: ID of the note the file was uploaded to
createdAt: createdAt:
type: string type: string
format: date-time format: date-time
description: Date when the file was upladed description: Date when the file was upladed
owningUser:
type: string
description: username of the user who uploaded the file
examples: examples:
markdownExample: markdownExample:
value: '# Some header\nSome normal text. **Some bold text**' value: '# Some header\nSome normal text. **Some bold text**'

View file

@ -23,7 +23,10 @@ import { HistoryEntry } from '../../../history/history-entry.entity';
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
import { Group } from '../../../groups/group.entity'; import { Group } from '../../../groups/group.entity';
import { MediaModule } from '../../../media/media.module';
import { MediaUpload } from '../../../media/media-upload.entity';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import mediaConfigMock from '../../../config/media.config.mock';
import appConfigMock from '../../../config/app.config.mock'; import appConfigMock from '../../../config/app.config.mock';
describe('Me Controller', () => { describe('Me Controller', () => {
@ -33,14 +36,19 @@ describe('Me Controller', () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [MeController], controllers: [MeController],
imports: [ imports: [
UsersModule, ConfigModule.forRoot({
HistoryModule, isGlobal: true,
NotesModule, load: [mediaConfigMock],
LoggerModule, }),
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
load: [appConfigMock], load: [appConfigMock],
}), }),
UsersModule,
HistoryModule,
NotesModule,
LoggerModule,
MediaModule,
], ],
}) })
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
@ -67,6 +75,8 @@ describe('Me Controller', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Group)) .overrideProvider(getRepositoryToken(Group))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.compile(); .compile();
controller = module.get<MeController>(MeController); controller = module.get<MeController>(MeController);

View file

@ -28,6 +28,9 @@ import { HistoryEntryDto } from '../../../history/history-entry.dto';
import { UserInfoDto } from '../../../users/user-info.dto'; import { UserInfoDto } from '../../../users/user-info.dto';
import { NotInDBError } from '../../../errors/errors'; import { NotInDBError } from '../../../errors/errors';
import { Request } from 'express'; import { Request } from 'express';
import { MediaService } from '../../../media/media.service';
import { MediaUploadUrlDto } from '../../../media/media-upload-url.dto';
import { MediaUploadDto } from '../../../media/media-upload.dto';
@ApiTags('me') @ApiTags('me')
@ApiSecurity('token') @ApiSecurity('token')
@ -38,6 +41,7 @@ export class MeController {
private usersService: UsersService, private usersService: UsersService,
private historyService: HistoryService, private historyService: HistoryService,
private notesService: NotesService, private notesService: NotesService,
private mediaService: MediaService,
) { ) {
this.logger.setContext(MeController.name); this.logger.setContext(MeController.name);
} }
@ -129,4 +133,11 @@ export class MeController {
(await notes).map((note) => this.notesService.toNoteMetadataDto(note)), (await notes).map((note) => this.notesService.toNoteMetadataDto(note)),
); );
} }
@UseGuards(TokenAuthGuard)
@Get('media')
async getMyMedia(@Req() req: Request): Promise<MediaUploadDto[]> {
const media = await this.mediaService.listUploadsByUser(req.user);
return media.map((media) => this.mediaService.toMediaUploadDto(media));
}
} }

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsDate, IsString } from 'class-validator';
export class MediaUploadDto {
/**
* The link to the media file.
* @example "https://example.com/uploads/testfile123.jpg"
*/
@IsString()
url: string;
/**
* The noteId of the note to which the uploaded file is linked to.
* @example "noteId" TODO how looks a note id?
*/
@IsString()
noteId: string;
/**
* The date when the upload objects was created.
* @example "2020-12-01 12:23:34"
*/
@IsDate()
createdAt: Date;
/**
* The userName of the user which uploaded the media file.
* @example "testuser5"
*/
@IsString()
userName: string;
}

View file

@ -229,4 +229,39 @@ describe('MediaService', () => {
} }
}); });
}); });
describe('listUploadsByUser', () => {
describe('works', () => {
it('with one upload from user', async () => {
const mockMediaUploadEntry = {
id: 'testMediaUpload',
backendData: 'testBackendData',
user: {
userName: 'hardcoded',
} as User,
} as MediaUpload;
jest
.spyOn(mediaRepo, 'find')
.mockResolvedValueOnce([mockMediaUploadEntry]);
expect(
await service.listUploadsByUser({ userName: 'hardcoded' } as User),
).toEqual([mockMediaUploadEntry]);
});
it('without uploads from user', async () => {
jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce([]);
const mediaList = await service.listUploadsByUser({
userName: 'hardcoded',
} as User);
expect(mediaList).toEqual([]);
});
it('with error (undefined as return value of find)', async () => {
jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce(undefined);
const mediaList = await service.listUploadsByUser({
userName: 'hardcoded',
} as User);
expect(mediaList).toEqual([]);
});
});
});
}); });

View file

@ -22,6 +22,8 @@ import { MediaUploadUrlDto } from './media-upload-url.dto';
import { S3Backend } from './backends/s3-backend'; import { S3Backend } from './backends/s3-backend';
import { AzureBackend } from './backends/azure-backend'; import { AzureBackend } from './backends/azure-backend';
import { ImgurBackend } from './backends/imgur-backend'; import { ImgurBackend } from './backends/imgur-backend';
import { User } from '../users/user.entity';
import { MediaUploadDto } from './media-upload.dto';
@Injectable() @Injectable()
export class MediaService { export class MediaService {
@ -157,6 +159,23 @@ export class MediaService {
return mediaUpload; return mediaUpload;
} }
/**
* @async
* List all uploads by a specific user
* @param {User} user - the specific user
* @return {MediaUpload[]} arary of media uploads owned by the user
*/
async listUploadsByUser(user: User): Promise<MediaUpload[]> {
const mediaUploads = await this.mediaUploadRepository.find({
where: { user: user },
relations: ['user', 'note'],
});
if (mediaUploads === undefined) {
return [];
}
return mediaUploads;
}
private chooseBackendType(): BackendType { private chooseBackendType(): BackendType {
switch (this.mediaConfig.backend.use) { switch (this.mediaConfig.backend.use) {
case 'filesystem': case 'filesystem':
@ -183,6 +202,15 @@ export class MediaService {
} }
} }
toMediaUploadDto(mediaUpload: MediaUpload): MediaUploadDto {
return {
url: mediaUpload.fileUrl,
noteId: mediaUpload.note.id,
createdAt: mediaUpload.createdAt,
userName: mediaUpload.user.userName,
};
}
toMediaUploadUrlDto(url: string): MediaUploadUrlDto { toMediaUploadUrlDto(url: string): MediaUploadUrlDto {
return { return {
link: url, link: url,

View file

@ -33,6 +33,9 @@ import { ConfigModule } from '@nestjs/config';
import mediaConfigMock from '../../src/config/media.config.mock'; import mediaConfigMock from '../../src/config/media.config.mock';
import appConfigMock from '../../src/config/app.config.mock'; import appConfigMock from '../../src/config/app.config.mock';
import { User } from '../../src/users/user.entity'; import { User } from '../../src/users/user.entity';
import { MediaService } from '../../src/media/media.service';
import { MediaModule } from '../../src/media/media.module';
import { promises as fs } from 'fs';
import { NoteMetadataDto } from '../../src/notes/note-metadata.dto'; import { NoteMetadataDto } from '../../src/notes/note-metadata.dto';
// TODO Tests have to be reworked using UserService functions // TODO Tests have to be reworked using UserService functions
@ -42,6 +45,7 @@ describe('Notes', () => {
let historyService: HistoryService; let historyService: HistoryService;
let notesService: NotesService; let notesService: NotesService;
let userService: UsersService; let userService: UsersService;
let mediaService: MediaService;
let user: User; let user: User;
beforeAll(async () => { beforeAll(async () => {
@ -66,6 +70,7 @@ describe('Notes', () => {
AuthModule, AuthModule,
UsersModule, UsersModule,
HistoryModule, HistoryModule,
MediaModule,
], ],
}) })
.overrideGuard(TokenAuthGuard) .overrideGuard(TokenAuthGuard)
@ -75,6 +80,7 @@ describe('Notes', () => {
notesService = moduleRef.get(NotesService); notesService = moduleRef.get(NotesService);
historyService = moduleRef.get(HistoryService); historyService = moduleRef.get(HistoryService);
userService = moduleRef.get(UsersService); userService = moduleRef.get(UsersService);
mediaService = moduleRef.get(MediaService);
user = await userService.createUser('hardcoded', 'Testy'); user = await userService.createUser('hardcoded', 'Testy');
await app.init(); await app.init();
}); });
@ -222,6 +228,41 @@ describe('Notes', () => {
expect(noteMetaDtos[0].updateUser.userName).toEqual(user.userName); expect(noteMetaDtos[0].updateUser.userName).toEqual(user.userName);
}); });
it('GET /me/media', async () => {
const note1 = await notesService.createNote(
'This is a test note.',
'test8',
await userService.getUserByUsername('hardcoded'),
);
const note2 = await notesService.createNote(
'This is a test note.',
'test9',
await userService.getUserByUsername('hardcoded'),
);
const httpServer = app.getHttpServer();
const response1 = await request(httpServer)
.get('/me/media/')
.expect('Content-Type', /json/)
.expect(200);
expect(response1.body).toHaveLength(0);
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const url0 = await mediaService.saveFile(testImage, 'hardcoded', note1.id);
const url1 = await mediaService.saveFile(testImage, 'hardcoded', note1.id);
const url2 = await mediaService.saveFile(testImage, 'hardcoded', note2.id);
const url3 = await mediaService.saveFile(testImage, 'hardcoded', note2.id);
const response = await request(httpServer)
.get('/me/media/')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(4);
expect(response.body[0].url).toEqual(url0);
expect(response.body[1].url).toEqual(url1);
expect(response.body[2].url).toEqual(url2);
expect(response.body[3].url).toEqual(url3);
});
afterAll(async () => { afterAll(async () => {
await app.close(); await app.close();
}); });