Merge pull request #993 from hedgedoc/publicApi/me

This commit is contained in:
David Mehren 2021-03-14 16:28:49 +01:00 committed by GitHub
commit b67ec817e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 217 additions and 49 deletions

View file

@ -59,6 +59,26 @@ export class MeController {
); );
} }
@UseGuards(TokenAuthGuard)
@Get('history/:note')
async getHistoryEntry(
@Req() req: Request,
@Param('note') note: string,
): Promise<HistoryEntryDto> {
try {
const foundEntry = await this.historyService.getEntryByNoteIdOrAlias(
note,
req.user,
);
return this.historyService.toHistoryEntryDto(foundEntry);
} catch (e) {
if (e instanceof NotInDBError) {
throw new NotFoundException(e.message);
}
throw e;
}
}
@UseGuards(TokenAuthGuard) @UseGuards(TokenAuthGuard)
@Put('history/:note') @Put('history/:note')
async updateHistoryEntry( async updateHistoryEntry(
@ -86,13 +106,13 @@ export class MeController {
@UseGuards(TokenAuthGuard) @UseGuards(TokenAuthGuard)
@Delete('history/:note') @Delete('history/:note')
@HttpCode(204) @HttpCode(204)
deleteHistoryEntry( async deleteHistoryEntry(
@Req() req: Request, @Req() req: Request,
@Param('note') note: string, @Param('note') note: string,
): Promise<void> { ): Promise<void> {
// ToDo: Check if user is allowed to delete note // ToDo: Check if user is allowed to delete note
try { try {
return this.historyService.deleteHistoryEntry(note, req.user); await this.historyService.deleteHistoryEntry(note, req.user);
} catch (e) { } catch (e) {
if (e instanceof NotInDBError) { if (e instanceof NotInDBError) {
throw new NotFoundException(e.message); throw new NotFoundException(e.message);

View file

@ -121,6 +121,32 @@ describe('HistoryService', () => {
}); });
}); });
describe('getEntryByNoteIdOrAlias', () => {
const user = {} as User;
const alias = 'alias';
describe('works', () => {
it('with history entry', async () => {
const note = Note.create(user, alias);
const historyEntry = HistoryEntry.create(user, note);
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry);
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note);
expect(await service.getEntryByNoteIdOrAlias(alias, user)).toEqual(
historyEntry,
);
});
});
describe('fails', () => {
it('with an non-existing note', async () => {
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined);
try {
await service.getEntryByNoteIdOrAlias(alias, {} as User);
} catch (e) {
expect(e).toBeInstanceOf(NotInDBError);
}
});
});
});
describe('createOrUpdateHistoryEntry', () => { describe('createOrUpdateHistoryEntry', () => {
describe('works', () => { describe('works', () => {
it('without an preexisting entry', async () => { it('without an preexisting entry', async () => {
@ -231,10 +257,11 @@ describe('HistoryService', () => {
); );
await service.deleteHistoryEntry(alias, user); await service.deleteHistoryEntry(alias, user);
}); });
});
it('without an entry', async () => { describe('fails', () => {
const user = {} as User; const user = {} as User;
const alias = 'alias'; const alias = 'alias';
it('without an entry', async () => {
const note = Note.create(user, alias); const note = Note.create(user, alias);
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note); jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(note);
@ -244,6 +271,14 @@ describe('HistoryService', () => {
expect(e).toBeInstanceOf(NotInDBError); expect(e).toBeInstanceOf(NotInDBError);
} }
}); });
it('without a note', async () => {
jest.spyOn(noteRepo, 'findOne').mockResolvedValueOnce(undefined);
try {
await service.getEntryByNoteIdOrAlias(alias, {} as User);
} catch (e) {
expect(e).toBeInstanceOf(NotInDBError);
}
});
}); });
}); });

View file

@ -29,6 +29,12 @@ export class HistoryService {
this.logger.setContext(HistoryService.name); this.logger.setContext(HistoryService.name);
} }
/**
* @async
* Get all entries of a user
* @param {User} user - the user the entries should be from
* @return {HistoryEntry[]} an array of history entries of the specified user
*/
async getEntriesByUser(user: User): Promise<HistoryEntry[]> { async getEntriesByUser(user: User): Promise<HistoryEntry[]> {
return await this.historyEntryRepository.find({ return await this.historyEntryRepository.find({
where: { user: user }, where: { user: user },
@ -36,7 +42,15 @@ export class HistoryService {
}); });
} }
private async getEntryByNoteIdOrAlias( /**
* @async
* Get a history entry by the user and note, which is specified via id or alias
* @param {string} noteIdOrAlias - the id or alias specifying the note
* @param {User} user - the user that the note belongs to
* @throws {NotInDBError} the specified note does not exist
* @return {HistoryEntry} the requested history entry
*/
async getEntryByNoteIdOrAlias(
noteIdOrAlias: string, noteIdOrAlias: string,
user: User, user: User,
): Promise<HistoryEntry> { ): Promise<HistoryEntry> {
@ -44,6 +58,13 @@ export class HistoryService {
return await this.getEntryByNote(note, user); return await this.getEntryByNote(note, user);
} }
/**
* @async
* Get a history entry by the user and note
* @param {Note} note - the note that the history entry belongs to
* @param {User} user - the user that the history entry belongs to
* @return {HistoryEntry} the requested history entry
*/
private async getEntryByNote(note: Note, user: User): Promise<HistoryEntry> { private async getEntryByNote(note: Note, user: User): Promise<HistoryEntry> {
return await this.historyEntryRepository.findOne({ return await this.historyEntryRepository.findOne({
where: { where: {
@ -54,6 +75,13 @@ export class HistoryService {
}); });
} }
/**
* @async
* Create or update a history entry by the user and note. If the entry is merely updated the updatedAt date is set to the current date.
* @param {Note} note - the note that the history entry belongs to
* @param {User} user - the user that the history entry belongs to
* @return {HistoryEntry} the requested history entry
*/
async createOrUpdateHistoryEntry( async createOrUpdateHistoryEntry(
note: Note, note: Note,
user: User, user: User,
@ -67,6 +95,14 @@ export class HistoryService {
return await this.historyEntryRepository.save(entry); return await this.historyEntryRepository.save(entry);
} }
/**
* @async
* Update a history entry identified by the user and a note id or alias
* @param {string} noteIdOrAlias - the note that the history entry belongs to
* @param {User} user - the user that the history entry belongs to
* @param {HistoryEntryUpdateDto} updateDto - the change that should be applied to the history entry
* @return {HistoryEntry} the requested history entry
*/
async updateHistoryEntry( async updateHistoryEntry(
noteIdOrAlias: string, noteIdOrAlias: string,
user: User, user: User,
@ -82,6 +118,13 @@ export class HistoryService {
return await this.historyEntryRepository.save(entry); return await this.historyEntryRepository.save(entry);
} }
/**
* @async
* Delete the history entry identified by the user and a note id or alias
* @param {string} noteIdOrAlias - the note that the history entry belongs to
* @param {User} user - the user that the history entry belongs to
* @throws {NotInDBError} the specified history entry does not exist
*/
async deleteHistoryEntry(noteIdOrAlias: string, user: User): Promise<void> { async deleteHistoryEntry(noteIdOrAlias: string, user: User): Promise<void> {
const entry = await this.getEntryByNoteIdOrAlias(noteIdOrAlias, user); const entry = await this.getEntryByNoteIdOrAlias(noteIdOrAlias, user);
if (!entry) { if (!entry) {
@ -93,6 +136,11 @@ export class HistoryService {
return; return;
} }
/**
* Build HistoryEntryDto from a history entry.
* @param {HistoryEntry} entry - the history entry to use
* @return {HistoryEntryDto} the built HistoryEntryDto
*/
toHistoryEntryDto(entry: HistoryEntry): HistoryEntryDto { toHistoryEntryDto(entry: HistoryEntry): HistoryEntryDto {
return { return {
identifier: entry.note.alias ? entry.note.alias : entry.note.id, identifier: entry.note.alias ? entry.note.alias : entry.note.id,

View file

@ -33,6 +33,7 @@ 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 { 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
@ -99,16 +100,55 @@ describe('Notes', () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200); .expect(200);
const history = <HistoryEntryDto[]>response.body; const history = <HistoryEntryDto[]>response.body;
expect(history.length).toEqual(1);
const historyDto = historyService.toHistoryEntryDto(createdHistoryEntry);
for (const historyEntry of history) { for (const historyEntry of history) {
if (historyEntry.identifier === 'testGetHistory') { expect(historyEntry.identifier).toEqual(historyDto.identifier);
expect(historyEntry).toEqual(createdHistoryEntry); expect(historyEntry.title).toEqual(historyDto.title);
} expect(historyEntry.tags).toEqual(historyDto.tags);
expect(historyEntry.pinStatus).toEqual(historyDto.pinStatus);
expect(historyEntry.lastVisited).toEqual(
historyDto.lastVisited.toISOString(),
);
} }
}); });
it(`PUT /me/history/{note}`, async () => { describe(`GET /me/history/{note}`, () => {
it('works with an existing note', async () => {
const noteName = 'testGetNoteHistory2'; const noteName = 'testGetNoteHistory2';
const note = await notesService.createNote('', noteName); const note = await notesService.createNote('', noteName);
const createdHistoryEntry = await historyService.createOrUpdateHistoryEntry(
note,
user,
);
const response = await request(app.getHttpServer())
.get(`/me/history/${noteName}`)
.expect('Content-Type', /json/)
.expect(200);
const historyEntry = <HistoryEntryDto>response.body;
const historyEntryDto = historyService.toHistoryEntryDto(
createdHistoryEntry,
);
expect(historyEntry.identifier).toEqual(historyEntryDto.identifier);
expect(historyEntry.title).toEqual(historyEntryDto.title);
expect(historyEntry.tags).toEqual(historyEntryDto.tags);
expect(historyEntry.pinStatus).toEqual(historyEntryDto.pinStatus);
expect(historyEntry.lastVisited).toEqual(
historyEntryDto.lastVisited.toISOString(),
);
});
it('fails with a non-existing note', async () => {
await request(app.getHttpServer())
.get('/me/history/i_dont_exist')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe(`PUT /me/history/{note}`, () => {
it('works', async () => {
const noteName = 'testGetNoteHistory3';
const note = await notesService.createNote('', noteName);
await historyService.createOrUpdateHistoryEntry(note, user); await historyService.createOrUpdateHistoryEntry(note, user);
const historyEntryUpdateDto = new HistoryEntryUpdateDto(); const historyEntryUpdateDto = new HistoryEntryUpdateDto();
historyEntryUpdateDto.pinStatus = true; historyEntryUpdateDto.pinStatus = true;
@ -127,9 +167,17 @@ describe('Notes', () => {
} }
expect(historyEntry.pinStatus).toEqual(true); expect(historyEntry.pinStatus).toEqual(true);
}); });
it('fails with a non-existing note', async () => {
await request(app.getHttpServer())
.put('/me/history/i_dont_exist')
.expect('Content-Type', /json/)
.expect(404);
});
});
it(`DELETE /me/history/{note}`, async () => { describe(`DELETE /me/history/{note}`, () => {
const noteName = 'testGetNoteHistory3'; it('works', async () => {
const noteName = 'testGetNoteHistory4';
const note = await notesService.createNote('', noteName); const note = await notesService.createNote('', noteName);
await historyService.createOrUpdateHistoryEntry(note, user); await historyService.createOrUpdateHistoryEntry(note, user);
const response = await request(app.getHttpServer()) const response = await request(app.getHttpServer())
@ -145,16 +193,33 @@ describe('Notes', () => {
} }
return expect(historyEntry).toBeNull(); return expect(historyEntry).toBeNull();
}); });
describe('fails', () => {
it('with a non-existing note', async () => {
await request(app.getHttpServer())
.delete('/me/history/i_dont_exist')
.expect(404);
});
it('with a non-existing history entry', async () => {
const noteName = 'testGetNoteHistory5';
await notesService.createNote('', noteName);
await request(app.getHttpServer())
.delete(`/me/history/${noteName}`)
.expect(404);
});
});
});
it.skip(`GET /me/notes/`, async () => { it(`GET /me/notes/`, async () => {
// TODO use function from HistoryService to add an History Entry const noteName = 'testNote';
await notesService.createNote('This is a test note.', 'test7'); await notesService.createNote('', noteName, user);
// usersService.getALLNotesOwnedByUser() TODO Implement function
const response = await request(app.getHttpServer()) const response = await request(app.getHttpServer())
.get('/me/notes/') .get('/me/notes/')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200); .expect(200);
expect(response.body.revisions).toHaveLength(1); const noteMetaDtos = response.body as NoteMetadataDto[];
expect(noteMetaDtos).toHaveLength(1);
expect(noteMetaDtos[0].alias).toEqual(noteName);
expect(noteMetaDtos[0].updateUser.userName).toEqual(user.userName);
}); });
afterAll(async () => { afterAll(async () => {