Merge pull request #1049 from hedgedoc/publicApi/noteMedia

This commit is contained in:
David Mehren 2021-03-21 19:25:33 +01:00 committed by GitHub
commit 6fab7583f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 143 additions and 1 deletions

View file

@ -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>(NotesController);

View file

@ -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<MediaUploadDto[]> {
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;
}
}
}

View file

@ -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([]);
});
});
});
});

View file

@ -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<MediaUpload[]> {
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':

View file

@ -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();
});