refactor: replace TypeORM with knex.js

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-03-14 23:33:29 +01:00
parent 74d50daa0b
commit c9faf81e27
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
242 changed files with 4601 additions and 6871 deletions

View file

@ -6,8 +6,9 @@
import { AliasCreateDto, AliasUpdateDto } from '@hedgedoc/commons';
import request from 'supertest';
import { AliasCreateDto } from '../../src/alias/alias-create.dto';
import { AliasUpdateDto } from '../../src/alias/alias-update.dto';
import { User } from '../../src/database/user.entity';
import { Note } from '../../src/notes/note.entity';
import {
password1,
password2,
@ -55,7 +56,7 @@ describe('Alias', () => {
describe('POST /alias', () => {
const testAlias = 'aliasTest';
const newAliasDto: AliasCreateDto = {
noteIdOrAlias: testAlias,
alias: testAlias,
newAlias: '',
};
let publicId = '';
@ -87,7 +88,7 @@ describe('Alias', () => {
primaryAlias: false,
noteId: publicId,
});
expect(note.body.metadata.primaryAddress).toEqual(testAlias);
expect(note.body.metadata.primaryAlias).toEqual(testAlias);
expect(note.body.metadata.id).toEqual(publicId);
});
@ -158,7 +159,7 @@ describe('Alias', () => {
primaryAlias: true,
noteId: publicId,
});
expect(note.body.metadata.primaryAddress).toEqual(newAlias);
expect(note.body.metadata.primaryAlias).toEqual(newAlias);
expect(note.body.metadata.id).toEqual(publicId);
});

View file

@ -53,9 +53,10 @@ describe('Auth', () => {
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(201);
const newUser = await testSetup.userService.getUserByUsername(username, [
UserRelationEnum.IDENTITIES,
]);
const newUser = await testSetup.userService.getUserDtoByUsername(
username,
[UserRelationEnum.IDENTITIES],
);
expect(newUser.displayName).toEqual(displayName);
await expect(newUser.identities).resolves.toHaveLength(1);
await expect(
@ -115,7 +116,7 @@ describe('Auth', () => {
.expect(400);
expect(response.text).toContain('PasswordTooWeakError');
await expect(() =>
testSetup.userService.getUserByUsername(username, [
testSetup.userService.getUserDtoByUsername(username, [
UserRelationEnum.IDENTITIES,
]),
).rejects.toThrow(NotInDBError);

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { GuestAccess, LoginDto } from '@hedgedoc/commons';
import { LoginDto, PermissionLevel } from '@hedgedoc/commons';
import request from 'supertest';
import { createDefaultMockNoteConfig } from '../../src/config/mock/note.config.mock';
@ -66,7 +66,7 @@ describe('Groups', () => {
describe('API requires authentication', () => {
beforeAll(() => {
noteConfigMock.guestAccess = GuestAccess.DENY;
noteConfigMock.guestAccess = PermissionLevel.DENY;
});
test('get group', async () => {
const response = await request(testSetup.app.getHttpServer()).get(

View file

@ -1,226 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { LocalService } from '../../src/auth/local/local.service';
import { User } from '../../src/database/user.entity';
import { HistoryEntryImportDto } from '../../src/history/history-entry-import.dto';
import { HistoryEntry } from '../../src/history/history-entry.entity';
import { HistoryService } from '../../src/history/history.service';
import { Note } from '../../src/notes/note.entity';
import { NotesService } from '../../src/notes/notes.service';
import { UsersService } from '../../src/users/users.service';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('History', () => {
let testSetup: TestSetup;
let historyService: HistoryService;
let localIdentityService: LocalService;
let user: User;
let note: Note;
let note2: Note;
let forbiddenNoteId: string;
let content: string;
let agent: request.SuperAgentTest;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().build();
forbiddenNoteId =
testSetup.configService.get('noteConfig').forbiddenNoteIds[0];
const moduleRef = testSetup.moduleRef;
const username = 'hardcoded';
const password = 'AHardcodedStrongP@ssword123';
await testSetup.app.init();
content = 'This is a test note.';
historyService = moduleRef.get(HistoryService);
const userService = moduleRef.get(UsersService);
localIdentityService = moduleRef.get(LocalService);
user = await userService.createUser(username, 'Testy', null, null);
await localIdentityService.createLocalIdentity(user, password);
const notesService = moduleRef.get(NotesService);
note = await notesService.createNote(content, user, 'note');
note2 = await notesService.createNote(content, user, 'note2');
agent = request.agent(testSetup.app.getHttpServer());
await agent
.post('/api/private/auth/local/login')
.send({ username: username, password: password })
.expect(201);
});
afterAll(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
it('GET /me/history', async () => {
const emptyResponse = await agent
.get('/api/private/me/history')
.expect('Content-Type', /json/)
.expect(200);
expect(emptyResponse.body.length).toEqual(0);
const entry = await testSetup.historyService.updateHistoryEntryTimestamp(
note,
user,
);
const entryDto = await testSetup.historyService.toHistoryEntryDto(entry);
const response = await agent
.get('/api/private/me/history')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.length).toEqual(1);
expect(response.body[0].identifier).toEqual(entryDto.identifier);
expect(response.body[0].title).toEqual(entryDto.title);
expect(response.body[0].tags).toEqual(entryDto.tags);
expect(response.body[0].pinStatus).toEqual(entryDto.pinStatus);
expect(response.body[0].lastVisitedAt).toEqual(
entryDto.lastVisitedAt.toISOString(),
);
});
describe('POST /me/history', () => {
it('works', async () => {
expect(
await testSetup.historyService.getEntriesByUser(user),
).toHaveLength(1);
const pinStatus = true;
const lastVisited = new Date('2020-12-01 12:23:34');
const postEntryDto = new HistoryEntryImportDto();
postEntryDto.note = (await note2.aliases).filter(
(alias) => alias.primary,
)[0].name;
postEntryDto.pinStatus = pinStatus;
postEntryDto.lastVisitedAt = lastVisited;
await agent
.post('/api/private/me/history')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ history: [postEntryDto] }))
.expect(201);
const userEntries = await testSetup.historyService.getEntriesByUser(user);
expect(userEntries.length).toEqual(1);
expect((await (await userEntries[0].note).aliases)[0].name).toEqual(
(await note2.aliases)[0].name,
);
expect((await (await userEntries[0].note).aliases)[0].primary).toEqual(
(await note2.aliases)[0].primary,
);
expect((await (await userEntries[0].note).aliases)[0].id).toEqual(
(await note2.aliases)[0].id,
);
expect((await userEntries[0].user).username).toEqual(user.username);
expect(userEntries[0].pinStatus).toEqual(pinStatus);
expect(userEntries[0].updatedAt).toEqual(lastVisited);
});
describe('fails', () => {
let pinStatus: boolean;
let lastVisited: Date;
let postEntryDto: HistoryEntryImportDto;
let prevEntry: HistoryEntry;
beforeAll(async () => {
const previousHistory =
await testSetup.historyService.getEntriesByUser(user);
expect(previousHistory).toHaveLength(1);
prevEntry = previousHistory[0];
pinStatus = !previousHistory[0].pinStatus;
lastVisited = new Date('2020-12-01 23:34:45');
postEntryDto = new HistoryEntryImportDto();
postEntryDto.note = (await note2.aliases).filter(
(alias) => alias.primary,
)[0].name;
postEntryDto.pinStatus = pinStatus;
postEntryDto.lastVisitedAt = lastVisited;
});
it('with forbiddenId', async () => {
const brokenEntryDto = new HistoryEntryImportDto();
brokenEntryDto.note = forbiddenNoteId;
brokenEntryDto.pinStatus = pinStatus;
brokenEntryDto.lastVisitedAt = lastVisited;
await agent
.post('/api/private/me/history')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ history: [brokenEntryDto] }))
.expect(400);
});
it('with non-existing note', async () => {
const brokenEntryDto = new HistoryEntryImportDto();
brokenEntryDto.note = 'i_dont_exist';
brokenEntryDto.pinStatus = pinStatus;
brokenEntryDto.lastVisitedAt = lastVisited;
await agent
.post('/api/private/me/history')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ history: [brokenEntryDto] }))
.expect(404);
});
afterEach(async () => {
const historyEntries =
await testSetup.historyService.getEntriesByUser(user);
expect(historyEntries).toHaveLength(1);
expect(await (await historyEntries[0].note).aliases).toEqual(
await (
await prevEntry.note
).aliases,
);
expect((await historyEntries[0].user).username).toEqual(
(await prevEntry.user).username,
);
expect(historyEntries[0].pinStatus).toEqual(prevEntry.pinStatus);
expect(historyEntries[0].updatedAt).toEqual(prevEntry.updatedAt);
});
});
});
it('DELETE /me/history', async () => {
expect(
(await testSetup.historyService.getEntriesByUser(user)).length,
).toEqual(1);
await agent.delete('/api/private/me/history').expect(204);
expect(
(await testSetup.historyService.getEntriesByUser(user)).length,
).toEqual(0);
});
it('PUT /me/history/:note', async () => {
const entry = await testSetup.historyService.updateHistoryEntryTimestamp(
note2,
user,
);
expect(entry.pinStatus).toBeFalsy();
const alias = (await (await entry.note).aliases).filter(
(alias) => alias.primary,
)[0].name;
await agent
.put(`/api/private/me/history/${alias || 'null'}`)
.send({ pinStatus: true })
.expect(200);
const userEntries = await testSetup.historyService.getEntriesByUser(user);
expect(userEntries.length).toEqual(1);
expect(userEntries[0].pinStatus).toBeTruthy();
await testSetup.historyService.deleteHistoryEntry(note2, user);
});
it('DELETE /me/history/:note', async () => {
const entry = await historyService.updateHistoryEntryTimestamp(note2, user);
const alias = (await (await entry.note).aliases).filter(
(alias) => alias.primary,
)[0].name;
const entry2 = await historyService.updateHistoryEntryTimestamp(note, user);
const entryDto = await historyService.toHistoryEntryDto(entry2);
await agent
.delete(`/api/private/me/history/${alias || 'null'}`)
.expect(204);
const userEntries = await historyService.getEntriesByUser(user);
expect(userEntries.length).toEqual(1);
const userEntryDto = await historyService.toHistoryEntryDto(userEntries[0]);
expect(userEntryDto.identifier).toEqual(entryDto.identifier);
expect(userEntryDto.title).toEqual(entryDto.title);
expect(userEntryDto.tags).toEqual(entryDto.tags);
expect(userEntryDto.pinStatus).toEqual(entryDto.pinStatus);
expect(userEntryDto.lastVisitedAt).toEqual(entryDto.lastVisitedAt);
});
});

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { LoginUserInfoDto, ProviderType } from '@hedgedoc/commons';
import { AuthProviderType, LoginUserInfoDto } from '@hedgedoc/commons';
import { promises as fs } from 'fs';
import request from 'supertest';
@ -58,7 +58,7 @@ describe('Me', () => {
it('GET /me', async () => {
const userInfo = testSetup.userService.toLoginUserInfoDto(
user,
ProviderType.LOCAL,
AuthProviderType.LOCAL,
);
const response = await agent
.get('/api/private/me')
@ -127,7 +127,8 @@ describe('Me', () => {
expect(imageIds).toContain(response.body[1].uuid);
expect(imageIds).toContain(response.body[2].uuid);
expect(imageIds).toContain(response.body[3].uuid);
const mediaUploads = await testSetup.mediaService.listUploadsByUser(user);
const mediaUploads =
await testSetup.mediaService.getMediaUploadUuidsByUserId(user);
for (const upload of mediaUploads) {
await testSetup.mediaService.deleteFile(upload);
}
@ -143,7 +144,8 @@ describe('Me', () => {
displayName: newDisplayName,
})
.expect(200);
const dbUser = await testSetup.userService.getUserByUsername('hardcoded');
const dbUser =
await testSetup.userService.getUserDtoByUsername('hardcoded');
expect(dbUser.displayName).toEqual(newDisplayName);
});
@ -155,17 +157,19 @@ describe('Me', () => {
user,
note1,
);
const dbUser = await testSetup.userService.getUserByUsername('hardcoded');
const dbUser =
await testSetup.userService.getUserDtoByUsername('hardcoded');
expect(dbUser).toBeInstanceOf(User);
const mediaUploads = await testSetup.mediaService.listUploadsByUser(dbUser);
const mediaUploads =
await testSetup.mediaService.getMediaUploadUuidsByUserId(dbUser);
expect(mediaUploads).toHaveLength(1);
expect(mediaUploads[0].uuid).toEqual(upload.uuid);
await agent.delete('/api/private/me').expect(204);
await expect(
testSetup.userService.getUserByUsername('hardcoded'),
testSetup.userService.getUserDtoByUsername('hardcoded'),
).rejects.toThrow(NotInDBError);
const mediaUploadsAfter =
await testSetup.mediaService.listUploadsByNote(note1);
await testSetup.mediaService.getMediaUploadUuidsByNoteId(note1);
expect(mediaUploadsAfter).toHaveLength(0);
});
});

View file

@ -187,7 +187,7 @@ describe('Media', () => {
// upload a file with the default test user
const testNote = await testSetup.notesService.createNote(
'test content',
await testSetup.userService.getUserByUsername(username2),
await testSetup.userService.getUserDtoByUsername(username2),
'test_delete_media_note',
);
const testImage = await fs.readFile('test/private-api/fixtures/test.png');

View file

@ -81,7 +81,7 @@ describe('Notes', () => {
expect(response.body.metadata?.id).toBeDefined();
expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias(
await testSetup.notesService.getNoteIdByAlias(
response.body.metadata.id,
),
),
@ -108,7 +108,7 @@ describe('Notes', () => {
});
describe('POST /notes/{note}', () => {
it('works with a non-existing alias', async () => {
it('works with a non-existing aliases', async () => {
const response = await agent
.post('/api/private/notes/test2')
.set('Content-Type', 'text/markdown')
@ -118,14 +118,14 @@ describe('Notes', () => {
expect(response.body.metadata?.id).toBeDefined();
return expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias(
await testSetup.notesService.getNoteIdByAlias(
response.body.metadata?.id,
),
),
).toEqual(content);
});
it('fails with a forbidden alias', async () => {
it('fails with a forbidden aliases', async () => {
await agent
.post(`/api/private/notes/${forbiddenNoteId}`)
.set('Content-Type', 'text/markdown')
@ -134,7 +134,7 @@ describe('Notes', () => {
.expect(400);
});
it('fails with a existing alias', async () => {
it('fails with a existing aliases', async () => {
await agent
.post('/api/private/notes/test2')
.set('Content-Type', 'text/markdown')
@ -156,7 +156,7 @@ describe('Notes', () => {
.expect(413);
});
it('cannot create an alias equal to a note publicId', async () => {
it('cannot create an aliases equal to a note publicId', async () => {
await agent
.post(`/api/private/notes/${testSetup.anonymousNotes[0].publicId}`)
.set('Content-Type', 'text/markdown')
@ -168,7 +168,7 @@ describe('Notes', () => {
describe('DELETE /notes/{note}', () => {
describe('works', () => {
it('with an existing alias and keepMedia false', async () => {
it('with an existing aliases and keepMedia false', async () => {
const noteId = 'test3';
const note = await testSetup.notesService.createNote(
content,
@ -189,16 +189,16 @@ describe('Notes', () => {
})
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias(noteId),
testSetup.notesService.getNoteIdByAlias(noteId),
).rejects.toEqual(
new NotInDBError(`Note with id/alias '${noteId}' not found.`),
);
expect(
await testSetup.mediaService.listUploadsByUser(user1),
await testSetup.mediaService.getMediaUploadUuidsByUserId(user1),
).toHaveLength(0);
await fs.rmdir(uploadPath);
});
it('with an existing alias and keepMedia true', async () => {
it('with an existing aliases and keepMedia true', async () => {
const noteId = 'test3a';
const note = await testSetup.notesService.createNote(
content,
@ -219,22 +219,22 @@ describe('Notes', () => {
})
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias(noteId),
testSetup.notesService.getNoteIdByAlias(noteId),
).rejects.toEqual(
new NotInDBError(`Note with id/alias '${noteId}' not found.`),
);
expect(
await testSetup.mediaService.listUploadsByUser(user1),
await testSetup.mediaService.getMediaUploadUuidsByUserId(user1),
).toHaveLength(1);
// delete the file afterwards
await fs.unlink(join(uploadPath, upload.uuid + '.png'));
await fs.rmdir(uploadPath);
});
});
it('fails with a forbidden alias', async () => {
it('fails with a forbidden aliases', async () => {
await agent.delete(`/api/private/notes/${forbiddenNoteId}`).expect(400);
});
it('fails with a non-existing alias', async () => {
it('fails with a non-existing aliases', async () => {
await agent.delete('/api/private/notes/i_dont_exist').expect(404);
});
});
@ -249,7 +249,7 @@ describe('Notes', () => {
.expect(200);
expect(typeof metadata.body.id).toEqual('string');
expect(metadata.body.aliases[0].name).toEqual(noteAlias);
expect(metadata.body.primaryAddress).toEqual(noteAlias);
expect(metadata.body.primaryAlias).toEqual(noteAlias);
expect(metadata.body.title).toEqual('');
expect(metadata.body.description).toEqual('');
expect(typeof metadata.body.createdAt).toEqual('string');
@ -259,19 +259,19 @@ describe('Notes', () => {
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
expect(metadata.body.tags).toEqual([]);
expect(typeof metadata.body.updatedAt).toEqual('string');
expect(typeof metadata.body.updateUsername).toEqual('string');
expect(typeof metadata.body.lastUpdatedBy).toEqual('string');
expect(typeof metadata.body.viewCount).toEqual('number');
expect(metadata.body.editedBy).toEqual([]);
});
it('fails with a forbidden alias', async () => {
it('fails with a forbidden aliases', async () => {
await agent
.get(`/api/private/notes/${forbiddenNoteId}/metadata`)
.expect('Content-Type', /json/)
.expect(400);
});
it('fails with non-existing alias', async () => {
it('fails with non-existing aliases', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist/metadata')
@ -305,7 +305,7 @@ describe('Notes', () => {
});
describe('GET /notes/{note}/revisions', () => {
it('works with existing alias', async () => {
it('works with existing aliases', async () => {
await testSetup.notesService.createNote(content, user1, 'test4');
// create a second note to check for a regression, where typeorm always returned
// all revisions in the database
@ -317,13 +317,13 @@ describe('Notes', () => {
expect(response.body).toHaveLength(1);
});
it('fails with a forbidden alias', async () => {
it('fails with a forbidden aliases', async () => {
await agent
.get(`/api/private/notes/${forbiddenNoteId}/revisions`)
.expect(400);
});
it('fails with non-existing alias', async () => {
it('fails with non-existing aliases', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist/revisions')
@ -333,7 +333,7 @@ describe('Notes', () => {
});
describe('DELETE /notes/{note}/revisions', () => {
it('works with an existing alias', async () => {
it('works with an existing aliases', async () => {
const noteId = 'test8';
const note = await testSetup.notesService.createNote(
content,
@ -356,12 +356,12 @@ describe('Notes', () => {
.expect(200);
expect(responseAfterDeleting.body).toHaveLength(1);
});
it('fails with a forbidden alias', async () => {
it('fails with a forbidden aliases', async () => {
await agent
.delete(`/api/private/notes/${forbiddenNoteId}/revisions`)
.expect(400);
});
it('fails with non-existing alias', async () => {
it('fails with non-existing aliases', async () => {
// check if a missing note correctly returns 404
await agent
.delete('/api/private/notes/i_dont_exist/revisions')
@ -371,7 +371,7 @@ describe('Notes', () => {
});
describe('GET /notes/{note}/revisions/{revision-id}', () => {
it('works with an existing alias', async () => {
it('works with an existing aliases', async () => {
const note = await testSetup.notesService.createNote(
content,
user1,
@ -384,12 +384,12 @@ describe('Notes', () => {
.expect(200);
expect(response.body.content).toEqual(content);
});
it('fails with a forbidden alias', async () => {
it('fails with a forbidden aliases', async () => {
await agent
.get(`/api/private/notes/${forbiddenNoteId}/revisions/1`)
.expect(400);
});
it('fails with non-existing alias', async () => {
it('fails with non-existing aliases', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist/revisions/1')
@ -459,7 +459,7 @@ describe('Notes', () => {
alias,
);
// Redact default read permissions
const note = await testSetup.notesService.getNoteByIdOrAlias(alias);
const note = await testSetup.notesService.getNoteIdByAlias(alias);
const everyone = await testSetup.groupService.getEveryoneGroup();
const loggedin = await testSetup.groupService.getLoggedInGroup();
await testSetup.permissionsService.removeGroupPermission(note, everyone);
@ -510,7 +510,7 @@ describe('Notes', () => {
it("doesn't do anything if the user is the owner", async () => {
const note =
await testSetup.notesService.getNoteByIdOrAlias(user1NoteAlias);
await testSetup.notesService.getNoteIdByAlias(user1NoteAlias);
await testSetup.permissionsService.removeUserPermission(note, user2);
const response = await agent
@ -557,7 +557,7 @@ describe('Notes', () => {
it('works', async () => {
const note =
await testSetup.notesService.getNoteByIdOrAlias(user1NoteAlias);
await testSetup.notesService.getNoteIdByAlias(user1NoteAlias);
await testSetup.permissionsService.setUserPermission(
note,
user2,
@ -630,7 +630,7 @@ describe('Notes', () => {
it('works', async () => {
const note =
await testSetup.notesService.getNoteByIdOrAlias(user1NoteAlias);
await testSetup.notesService.getNoteIdByAlias(user1NoteAlias);
await testSetup.permissionsService.setGroupPermission(
note,
group1,