fix(repository): Move backend code into subdirectory

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-10-02 20:10:32 +02:00 committed by David Mehren
parent 86584e705f
commit bf30cbcf48
272 changed files with 87 additions and 67 deletions

View file

@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getConfigToken } from '@nestjs/config';
import { WsAdapter } from '@nestjs/platform-ws';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { BackendType } from '../src/media/backends/backend-type.enum';
describe('App', () => {
it('should not crash on requests to /', async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(getConfigToken('appConfig'))
.useValue({
domain: 'localhost',
port: 3333,
})
.overrideProvider(getConfigToken('mediaConfig'))
.useValue({
backend: {
use: BackendType.FILESYSTEM,
filesystem: {
uploadPath:
'test_uploads' + Math.floor(Math.random() * 100000).toString(),
},
},
})
.overrideProvider(getConfigToken('databaseConfig'))
.useValue({
database: ':memory:',
type: 'sqlite',
})
.overrideProvider(getConfigToken('authConfig'))
.useValue({
session: {
secret: 'secret',
},
})
.compile();
/**
* TODO: This is not really a regression test, as it does not use the
* real initialization code in main.ts.
* Should be fixed after https://github.com/hedgedoc/hedgedoc/issues/2083
* is done.
*/
const app = moduleRef.createNestApplication();
app.useWebSocketAdapter(new WsAdapter(app));
await app.init();
await request(app.getHttpServer()).get('/').expect(404);
await app.close();
});
});

View file

@ -0,0 +1,261 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { AliasCreateDto } from '../../src/notes/alias-create.dto';
import { AliasUpdateDto } from '../../src/notes/alias-update.dto';
import { User } from '../../src/users/user.entity';
import {
password1,
password2,
TestSetup,
TestSetupBuilder,
username1,
username2,
} from '../test-setup';
describe('Alias', () => {
let testSetup: TestSetup;
let users: User[];
const content = 'This is a test note.';
let forbiddenNoteId: string;
let agent1: request.SuperAgentTest;
let agent2: request.SuperAgentTest;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().withUsers().build();
await testSetup.app.init();
forbiddenNoteId =
testSetup.configService.get('noteConfig').forbiddenNoteIds[0];
users = testSetup.users;
agent1 = request.agent(testSetup.app.getHttpServer());
await agent1
.post('/api/private/auth/local/login')
.send({ username: username1, password: password1 })
.expect(201);
agent2 = request.agent(testSetup.app.getHttpServer());
await agent2
.post('/api/private/auth/local/login')
.send({ username: username2, password: password2 })
.expect(201);
});
afterAll(async () => {
await testSetup.cleanup();
});
describe('POST /alias', () => {
const testAlias = 'aliasTest';
const newAliasDto: AliasCreateDto = {
noteIdOrAlias: testAlias,
newAlias: '',
};
let publicId = '';
beforeAll(async () => {
const note = await testSetup.notesService.createNote(
content,
users[0],
testAlias,
);
publicId = note.publicId;
});
it('create with normal alias', async () => {
const newAlias = 'normalAlias';
newAliasDto.newAlias = newAlias;
const metadata = await agent1
.post(`/api/private/alias`)
.set('Content-Type', 'application/json')
.send(newAliasDto)
.expect(201);
expect(metadata.body.name).toEqual(newAlias);
expect(metadata.body.primaryAlias).toBeFalsy();
expect(metadata.body.noteId).toEqual(publicId);
const note = await agent1
.get(`/api/private/notes/${newAlias}`)
.expect(200);
expect(note.body.metadata.aliases).toContainEqual({
name: 'normalAlias',
primaryAlias: false,
noteId: publicId,
});
expect(note.body.metadata.primaryAddress).toEqual(testAlias);
expect(note.body.metadata.id).toEqual(publicId);
});
describe('does not create an alias', () => {
it('because of a forbidden alias', async () => {
newAliasDto.newAlias = forbiddenNoteId;
await agent1
.post(`/api/private/alias`)
.set('Content-Type', 'application/json')
.send(newAliasDto)
.expect(400)
.then((response) => {
expect(response.body.message).toContain(
'is forbidden by the administrator',
);
});
});
it('because of a alias that is a public id', async () => {
newAliasDto.newAlias = publicId;
await agent1
.post(`/api/private/alias`)
.set('Content-Type', 'application/json')
.send(newAliasDto)
.expect(409);
});
it('because the user is not an owner', async () => {
newAliasDto.newAlias = publicId;
await agent2
.post(`/api/private/alias`)
.set('Content-Type', 'application/json')
.send(newAliasDto)
.expect(401);
});
});
});
describe('PUT /alias/{alias}', () => {
const testAlias = 'aliasTest2';
const newAlias = 'normalAlias2';
const changeAliasDto: AliasUpdateDto = {
primaryAlias: true,
};
let publicId = '';
beforeAll(async () => {
const note = await testSetup.notesService.createNote(
content,
users[0],
testAlias,
);
publicId = note.publicId;
await testSetup.aliasService.addAlias(note, newAlias);
});
it('updates a note with a normal alias', async () => {
const metadata = await agent1
.put(`/api/private/alias/${newAlias}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(200);
expect(metadata.body.name).toEqual(newAlias);
expect(metadata.body.primaryAlias).toBeTruthy();
expect(metadata.body.noteId).toEqual(publicId);
const note = await agent1
.get(`/api/private/notes/${newAlias}`)
.expect(200);
expect(note.body.metadata.aliases).toContainEqual({
name: newAlias,
primaryAlias: true,
noteId: publicId,
});
expect(note.body.metadata.primaryAddress).toEqual(newAlias);
expect(note.body.metadata.id).toEqual(publicId);
});
describe('does not update', () => {
it('a note with unknown alias', async () => {
await agent1
.put(`/api/private/alias/i_dont_exist`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(404);
});
it('a note with a forbidden ID', async () => {
await agent1
.put(`/api/private/alias/${forbiddenNoteId}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(400)
.then((response) => {
expect(response.body.message).toContain(
'is forbidden by the administrator',
);
});
});
it('if the property primaryAlias is false', async () => {
changeAliasDto.primaryAlias = false;
await agent1
.put(`/api/private/alias/${newAlias}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(400);
});
it('if the user is not an owner', async () => {
changeAliasDto.primaryAlias = true;
await agent2
.put(`/api/private/alias/${newAlias}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(401);
});
});
});
describe('DELETE /alias/{alias}', () => {
const testAlias = 'aliasTest3';
const newAlias = 'normalAlias3';
let note;
beforeEach(async () => {
note = await testSetup.notesService.createNote(
content,
users[0],
testAlias,
);
await testSetup.aliasService.addAlias(note, newAlias);
});
afterEach(async () => {
try {
await testSetup.aliasService.removeAlias(note, newAlias);
// Ignore errors on removing alias
// eslint-disable-next-line no-empty
} catch (e) {}
await testSetup.notesService.deleteNote(note);
});
it('deletes a normal alias', async () => {
await agent1.delete(`/api/private/alias/${newAlias}`).expect(204);
await agent1.get(`/api/private/notes/${newAlias}`).expect(404);
});
it('does not delete an unknown alias', async () => {
await agent1.delete(`/api/private/alias/i_dont_exist`).expect(404);
});
it('does not delete an alias of a forbidden note', async () => {
await agent1
.delete(`/api/private/alias/${forbiddenNoteId}`)
.expect(400)
.then((response) => {
expect(response.body.message).toContain(
'is forbidden by the administrator',
);
});
});
it('fails if the user does not own the note', async () => {
await agent2.delete(`/api/private/alias/${newAlias}`).expect(401);
});
it('does not delete an primary alias (if it is not the only one)', async () => {
await agent1.delete(`/api/private/alias/${testAlias}`).expect(400);
await agent1.get(`/api/private/notes/${newAlias}`).expect(200);
});
it('deletes a primary alias (if it is the only one)', async () => {
await agent1.delete(`/api/private/alias/${newAlias}`).expect(204);
await agent1.delete(`/api/private/alias/${testAlias}`).expect(204);
});
});
});

View file

@ -0,0 +1,246 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
*/
import request from 'supertest';
import { LoginDto } from '../../src/identity/local/login.dto';
import { RegisterDto } from '../../src/identity/local/register.dto';
import { UpdatePasswordDto } from '../../src/identity/local/update-password.dto';
import { UserRelationEnum } from '../../src/users/user-relation.enum';
import { checkPassword } from '../../src/utils/password';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Auth', () => {
let testSetup: TestSetup;
let username: string;
let displayName: string;
let password: string;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().build();
await testSetup.app.init();
username = 'hardcoded';
displayName = 'Testy';
password = 'test_password';
});
afterAll(async () => {
// Yes, this is a bad hack, but there is a race somewhere and I have
// no idea how to fix it.
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
await testSetup.cleanup();
});
describe('POST /auth/local', () => {
it('works', async () => {
const registrationDto: RegisterDto = {
displayName: displayName,
password: password,
username: username,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(201);
const newUser = await testSetup.userService.getUserByUsername(username, [
UserRelationEnum.IDENTITIES,
]);
expect(newUser.displayName).toEqual(displayName);
await expect(newUser.identities).resolves.toHaveLength(1);
await expect(
checkPassword(
password,
(await newUser.identities)[0].passwordHash ?? '',
),
).resolves.toBeTruthy();
});
describe('fails', () => {
it('when the user already exits', async () => {
const username2 = 'already_existing';
await testSetup.userService.createUser(username2, displayName);
const registrationDto: RegisterDto = {
displayName: displayName,
password: password,
username: username2,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(409);
});
it('when registration is disabled', async () => {
testSetup.configService.get('authConfig').local.enableRegister = false;
const registrationDto: RegisterDto = {
displayName: displayName,
password: password,
username: username,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(400);
testSetup.configService.get('authConfig').local.enableRegister = true;
});
});
});
describe('PUT /auth/local', () => {
const newPassword = 'new_password';
let cookie = '';
beforeEach(async () => {
const loginDto: LoginDto = {
password: password,
username: username,
};
const response = await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
cookie = response.get('Set-Cookie')[0];
});
it('works', async () => {
// Change password
const changePasswordDto: UpdatePasswordDto = {
currentPassword: password,
newPassword: newPassword,
};
await request(testSetup.app.getHttpServer())
.put('/api/private/auth/local')
.set('Content-Type', 'application/json')
.set('Cookie', cookie)
.send(JSON.stringify(changePasswordDto))
.expect(200);
// Successfully login with new password
const loginDto: LoginDto = {
password: newPassword,
username: username,
};
const response = await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
cookie = response.get('Set-Cookie')[0];
// Reset password
const changePasswordBackDto: UpdatePasswordDto = {
currentPassword: newPassword,
newPassword: password,
};
await request(testSetup.app.getHttpServer())
.put('/api/private/auth/local')
.set('Content-Type', 'application/json')
.set('Cookie', cookie)
.send(JSON.stringify(changePasswordBackDto))
.expect(200);
});
it('fails, when registration is disabled', async () => {
testSetup.configService.get('authConfig').local.enableLogin = false;
// Try to change password
const changePasswordDto: UpdatePasswordDto = {
currentPassword: password,
newPassword: newPassword,
};
await request(testSetup.app.getHttpServer())
.put('/api/private/auth/local')
.set('Content-Type', 'application/json')
.set('Cookie', cookie)
.send(JSON.stringify(changePasswordDto))
.expect(400);
// enable login again
testSetup.configService.get('authConfig').local.enableLogin = true;
// new password doesn't work for login
const loginNewPasswordDto: LoginDto = {
password: newPassword,
username: username,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginNewPasswordDto))
.expect(401);
// old password does work for login
const loginOldPasswordDto: LoginDto = {
password: password,
username: username,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginOldPasswordDto))
.expect(201);
});
it('fails, when old password is wrong', async () => {
// Try to change password
const changePasswordDto: UpdatePasswordDto = {
currentPassword: 'wrong',
newPassword: newPassword,
};
await request(testSetup.app.getHttpServer())
.put('/api/private/auth/local')
.set('Content-Type', 'application/json')
.set('Cookie', cookie)
.send(JSON.stringify(changePasswordDto))
.expect(401);
// old password still does work for login
const loginOldPasswordDto: LoginDto = {
password: password,
username: username,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginOldPasswordDto))
.expect(201);
});
});
describe('POST /auth/local/login', () => {
it('works', async () => {
testSetup.configService.get('authConfig').local.enableLogin = true;
const loginDto: LoginDto = {
password: password,
username: username,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
});
});
describe('DELETE /auth/logout', () => {
it('works', async () => {
testSetup.configService.get('authConfig').local.enableLogin = true;
const loginDto: LoginDto = {
password: password,
username: username,
};
const response = await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
const cookie = response.get('Set-Cookie')[0];
await request(testSetup.app.getHttpServer())
.delete('/api/private/auth/logout')
.set('Cookie', cookie)
.expect(204);
});
});
});

View file

@ -0,0 +1 @@
test-cert

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

Binary file not shown.

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { GuestAccess } from '../../src/config/guest_access.enum';
import { createDefaultMockNoteConfig } from '../../src/config/mock/note.config.mock';
import { NoteConfig } from '../../src/config/note.config';
import { LoginDto } from '../../src/identity/local/login.dto';
import {
password1,
TestSetup,
TestSetupBuilder,
username1,
} from '../test-setup';
describe('Groups', () => {
let testSetup: TestSetup;
let testuser1Session: request.SuperAgentTest;
const noteConfigMock: NoteConfig = createDefaultMockNoteConfig();
beforeEach(async () => {
testSetup = await TestSetupBuilder.create({
noteConfigMock: noteConfigMock,
})
.withUsers()
.build();
await testSetup.app.init();
// create a test group
await testSetup.groupService.createGroup('testgroup1', 'testgroup1', false);
// log in to create a session
const loginDto: LoginDto = {
password: password1,
username: username1,
};
testuser1Session = request.agent(testSetup.app.getHttpServer());
await testuser1Session
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
});
afterEach(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
test('details for existing groups can be retrieved', async () => {
const response = await testuser1Session.get(
'/api/private/groups/testgroup1',
);
expect(response.status).toBe(200);
expect(response.body.name).toBe('testgroup1');
});
test('details for non-existing groups cannot be retrieved', async () => {
const response = await testuser1Session.get(
'/api/private/groups/i_dont_exist',
);
expect(response.status).toBe(404);
});
describe('API requires authentication', () => {
beforeAll(() => {
noteConfigMock.guestAccess = GuestAccess.DENY;
});
test('get group', async () => {
const response = await request(testSetup.app.getHttpServer()).get(
'/api/private/groups/testgroup1',
);
expect(response.status).toBe(401);
});
});
});

View file

@ -0,0 +1,228 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
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 { IdentityService } from '../../src/identity/identity.service';
import { Note } from '../../src/notes/note.entity';
import { NotesService } from '../../src/notes/notes.service';
import { User } from '../../src/users/user.entity';
import { UsersService } from '../../src/users/users.service';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('History', () => {
let testSetup: TestSetup;
let historyService: HistoryService;
let identityService: IdentityService;
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);
identityService = moduleRef.get(IdentityService);
user = await userService.createUser(username, 'Testy');
await identityService.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

@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
import request from 'supertest';
import { NotInDBError } from '../../src/errors/errors';
import { Note } from '../../src/notes/note.entity';
import { UserLoginInfoDto } from '../../src/users/user-info.dto';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Me', () => {
let testSetup: TestSetup;
let uploadPath: string;
let user: User;
let content: string;
let note1: Note;
let alias2: string;
let note2: Note;
let agent: request.SuperAgentTest;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().build();
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
const username = 'hardcoded';
const password = 'AHardcodedStrongP@ssword123';
await testSetup.app.init();
user = await testSetup.userService.createUser(username, 'Testy');
await testSetup.identityService.createLocalIdentity(user, password);
content = 'This is a test note.';
alias2 = 'note2';
note1 = await testSetup.notesService.createNote(content, user);
note2 = await testSetup.notesService.createNote(content, user, alias2);
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.cleanup();
});
it('GET /me', async () => {
const userInfo = testSetup.userService.toUserLoginInfoDto(user, 'local');
const response = await agent
.get('/api/private/me')
.expect('Content-Type', /json/)
.expect(200);
const gotUser = response.body as UserLoginInfoDto;
expect(gotUser).toEqual(userInfo);
});
it('GET /me/media', async () => {
const responseBefore = await agent
.get('/api/private/me/media/')
.expect('Content-Type', /json/)
.expect(200);
expect(responseBefore.body).toHaveLength(0);
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const imageUrls = [];
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note1)).fileUrl,
);
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note1)).fileUrl,
);
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note2)).fileUrl,
);
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note2)).fileUrl,
);
const response = await agent
.get('/api/private/me/media/')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(4);
expect(imageUrls).toContain(response.body[0].url);
expect(imageUrls).toContain(response.body[1].url);
expect(imageUrls).toContain(response.body[2].url);
expect(imageUrls).toContain(response.body[3].url);
const mediaUploads = await testSetup.mediaService.listUploadsByUser(user);
for (const upload of mediaUploads) {
await testSetup.mediaService.deleteFile(upload);
}
await fs.rmdir(uploadPath);
});
it('POST /me/profile', async () => {
const newDisplayName = 'Another name';
expect(user.displayName).not.toEqual(newDisplayName);
await agent
.post('/api/private/me/profile')
.send({
displayName: newDisplayName,
})
.expect(201);
const dbUser = await testSetup.userService.getUserByUsername('hardcoded');
expect(dbUser.displayName).toEqual(newDisplayName);
});
it('DELETE /me', async () => {
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const upload = await testSetup.mediaService.saveFile(
testImage,
user,
note1,
);
const dbUser = await testSetup.userService.getUserByUsername('hardcoded');
expect(dbUser).toBeInstanceOf(User);
const mediaUploads = await testSetup.mediaService.listUploadsByUser(dbUser);
expect(mediaUploads).toHaveLength(1);
expect(mediaUploads[0].fileUrl).toEqual(upload.fileUrl);
await agent.delete('/api/private/me').expect(204);
await expect(
testSetup.userService.getUserByUsername('hardcoded'),
).rejects.toThrow(NotInDBError);
const mediaUploadsAfter = await testSetup.mediaService.listUploadsByNote(
note1,
);
expect(mediaUploadsAfter).toHaveLength(0);
});
});

View file

@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import { User } from 'src/users/user.entity';
import request from 'supertest';
import { ConsoleLoggerService } from '../../src/logger/console-logger.service';
import { TestSetup, TestSetupBuilder } from '../test-setup';
import { ensureDeleted } from '../utils';
describe('Media', () => {
let testSetup: TestSetup;
let uploadPath: string;
let agent: request.SuperAgentTest;
let user: User;
beforeAll(async () => {
const username = 'hardcoded';
const password = 'AHardcodedStrongP@ssword123';
testSetup = await TestSetupBuilder.create().build();
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
testSetup.app.useStaticAssets(uploadPath, {
prefix: '/uploads',
});
await testSetup.app.init();
const logger = await testSetup.app.resolve(ConsoleLoggerService);
logger.log('Switching logger', 'AppBootstrap');
testSetup.app.useLogger(logger);
await testSetup.notesService.createNote(
'test content',
null,
'test_upload_media',
);
user = await testSetup.userService.createUser(username, 'Testy');
await testSetup.identityService.createLocalIdentity(user, password);
agent = request.agent(testSetup.app.getHttpServer());
await agent
.post('/api/private/auth/local/login')
.send({ username: username, password: password })
.expect(201);
});
afterAll(async () => {
// Delete the upload folder
await ensureDeleted(uploadPath);
await testSetup.app.close();
await testSetup.cleanup();
});
describe('POST /media', () => {
it('works', async () => {
const uploadResponse = await agent
.post('/api/private/media')
.attach('file', 'test/private-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(201);
const path: string = uploadResponse.body.url;
const testImage = await fs.readFile('test/private-api/fixtures/test.png');
const downloadResponse = await agent.get(path);
expect(downloadResponse.body).toEqual(testImage);
// Remove /uploads/ from path as we just need the filename.
const fileName = path.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
});
describe('fails:', () => {
beforeEach(async () => {
await ensureDeleted(uploadPath);
});
it('MIME type not supported', async () => {
await agent
.post('/api/private/media')
.attach('file', 'test/private-api/fixtures/test.zip')
.set('HedgeDoc-Note', 'test_upload_media')
.expect(400);
await expect(fs.access(uploadPath)).rejects.toBeDefined();
});
it('note does not exist', async () => {
await agent
.post('/api/private/media')
.attach('file', 'test/private-api/fixtures/test.zip')
.set('HedgeDoc-Note', 'i_dont_exist')
.expect(404);
await expect(fs.access(uploadPath)).rejects.toBeDefined();
});
it('mediaBackend error', async () => {
await fs.mkdir(uploadPath, {
mode: '444',
});
await agent
.post('/api/private/media')
.attach('file', 'test/private-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(500);
});
afterEach(async () => {
await ensureDeleted(uploadPath);
});
});
});
it('DELETE /media/{filename}', async () => {
const testNote = await testSetup.notesService.createNote(
'test content',
null,
'test_delete_media',
);
const testImage = await fs.readFile('test/private-api/fixtures/test.png');
const upload = await testSetup.mediaService.saveFile(
testImage,
user,
testNote,
);
const filename = upload.fileUrl.split('/').pop() || '';
await agent.delete('/api/private/media/' + filename).expect(204);
});
});

View file

@ -0,0 +1,443 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import request from 'supertest';
import { NotInDBError } from '../../src/errors/errors';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Notes', () => {
let testSetup: TestSetup;
let user: User;
let user2: User;
let content: string;
let forbiddenNoteId: string;
let uploadPath: string;
let testImage: Buffer;
let agent: request.SuperAgentTest;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().build();
forbiddenNoteId =
testSetup.configService.get('noteConfig').forbiddenNoteIds[0];
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
await testSetup.app.init();
const username1 = 'hardcoded';
const password1 = 'AHardcodedStrongP@ssword123';
const username2 = 'hardcoded2';
const password2 = 'AHardcodedStrongP@ssword12';
user = await testSetup.userService.createUser(username1, 'Testy');
await testSetup.identityService.createLocalIdentity(user, password1);
user2 = await testSetup.userService.createUser(username2, 'Max Mustermann');
await testSetup.identityService.createLocalIdentity(user2, password2);
content = 'This is a test note.';
testImage = await fs.readFile('test/public-api/fixtures/test.png');
agent = request.agent(testSetup.app.getHttpServer());
await agent
.post('/api/private/auth/local/login')
.send({ username: username1, password: password1 })
.expect(201);
});
afterAll(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
it('POST /notes', async () => {
const response = await agent
.post('/api/private/notes')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body.metadata?.id).toBeDefined();
expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias(
response.body.metadata.id,
),
),
).toEqual(content);
});
describe('GET /notes/{note}', () => {
it('works with an existing note', async () => {
// check if we can succefully get a note that exists
await testSetup.notesService.createNote(content, user, 'test1');
const response = await agent
.get('/api/private/notes/test1')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.content).toEqual(content);
});
it('fails with an non-existing note', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('POST /notes/{note}', () => {
it('works with a non-existing alias', async () => {
const response = await agent
.post('/api/private/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body.metadata?.id).toBeDefined();
return expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias(
response.body.metadata?.id,
),
),
).toEqual(content);
});
it('fails with a forbidden alias', async () => {
await agent
.post(`/api/private/notes/${forbiddenNoteId}`)
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(400);
});
it('fails with a existing alias', async () => {
await agent
.post('/api/private/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(409);
});
it('fails with a content, that is too long', async () => {
const content = 'x'.repeat(
(testSetup.configService.get('noteConfig')
.maxDocumentLength as number) + 1,
);
await agent
.post('/api/private/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(413);
});
});
describe('DELETE /notes/{note}', () => {
describe('works', () => {
it('with an existing alias and keepMedia false', async () => {
const noteId = 'test3';
const note = await testSetup.notesService.createNote(
content,
user,
noteId,
);
await testSetup.mediaService.saveFile(testImage, user, note);
await agent
.delete(`/api/private/notes/${noteId}`)
.set('Content-Type', 'application/json')
.send({
keepMedia: false,
})
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias(noteId),
).rejects.toEqual(
new NotInDBError(`Note with id/alias '${noteId}' not found.`),
);
expect(
await testSetup.mediaService.listUploadsByUser(user),
).toHaveLength(0);
await fs.rmdir(uploadPath);
});
it('with an existing alias and keepMedia true', async () => {
const noteId = 'test3a';
const note = await testSetup.notesService.createNote(
content,
user,
noteId,
);
const upload = await testSetup.mediaService.saveFile(
testImage,
user,
note,
);
await agent
.delete(`/api/private/notes/${noteId}`)
.set('Content-Type', 'application/json')
.send({
keepMedia: true,
})
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias(noteId),
).rejects.toEqual(
new NotInDBError(`Note with id/alias '${noteId}' not found.`),
);
expect(
await testSetup.mediaService.listUploadsByUser(user),
).toHaveLength(1);
// Remove /upload/ from path as we just need the filename.
const fileName = upload.fileUrl.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
await fs.rmdir(uploadPath);
});
});
it('fails with a forbidden alias', async () => {
await agent.delete(`/api/private/notes/${forbiddenNoteId}`).expect(400);
});
it('fails with a non-existing alias', async () => {
await agent.delete('/api/private/notes/i_dont_exist').expect(404);
});
});
describe('GET /notes/{note}/metadata', () => {
it('returns complete metadata object', async () => {
const noteAlias = 'metadata_test_note';
await testSetup.notesService.createNote(content, user, noteAlias);
const metadata = await agent
.get(`/api/private/notes/${noteAlias}/metadata`)
.expect('Content-Type', /json/)
.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.title).toEqual('');
expect(metadata.body.description).toEqual('');
expect(typeof metadata.body.createdAt).toEqual('string');
expect(metadata.body.editedBy).toEqual([]);
expect(metadata.body.permissions.owner).toEqual('hardcoded');
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
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.viewCount).toEqual('number');
expect(metadata.body.editedBy).toEqual([]);
});
it('fails with a forbidden alias', async () => {
await agent
.get(`/api/private/notes/${forbiddenNoteId}/metadata`)
.expect('Content-Type', /json/)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist/metadata')
.expect('Content-Type', /json/)
.expect(404);
});
it('has the correct update/create dates', async () => {
const noteAlias = 'metadata_test_note_date';
// create a note
const note = await testSetup.notesService.createNote(
content,
user,
noteAlias,
);
// save the creation time
const createDate = note.createdAt;
const revisions = await note.revisions;
const updatedDate = revisions[revisions.length - 1].createdAt;
// wait one second
await new Promise((r) => setTimeout(r, 1000));
// update the note
await testSetup.notesService.updateNote(note, 'More test content');
const metadata = await agent
.get(`/api/private/notes/${noteAlias}/metadata`)
.expect('Content-Type', /json/)
.expect(200);
expect(metadata.body.createdAt).toEqual(createDate.toISOString());
expect(metadata.body.updatedAt).not.toEqual(updatedDate.toISOString());
});
});
describe('GET /notes/{note}/revisions', () => {
it('works with existing alias', async () => {
await testSetup.notesService.createNote(content, user, 'test4');
// create a second note to check for a regression, where typeorm always returned
// all revisions in the database
await testSetup.notesService.createNote(content, user, 'test4a');
const response = await agent
.get('/api/private/notes/test4/revisions')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(1);
});
it('fails with a forbidden alias', async () => {
await agent
.get(`/api/private/notes/${forbiddenNoteId}/revisions`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist/revisions')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('DELETE /notes/{note}/revisions', () => {
it('works with an existing alias', async () => {
const noteId = 'test8';
const note = await testSetup.notesService.createNote(
content,
user,
noteId,
);
await testSetup.notesService.updateNote(note, 'update');
const responseBeforeDeleting = await agent
.get('/api/private/notes/test8/revisions')
.expect('Content-Type', /json/)
.expect(200);
expect(responseBeforeDeleting.body).toHaveLength(2);
await agent
.delete(`/api/private/notes/${noteId}/revisions`)
.set('Content-Type', 'application/json')
.expect(204);
const responseAfterDeleting = await agent
.get('/api/private/notes/test8/revisions')
.expect('Content-Type', /json/)
.expect(200);
expect(responseAfterDeleting.body).toHaveLength(1);
});
it('fails with a forbidden alias', async () => {
await agent
.delete(`/api/private/notes/${forbiddenNoteId}/revisions`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await agent
.delete('/api/private/notes/i_dont_exist/revisions')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('GET /notes/{note}/revisions/{revision-id}', () => {
it('works with an existing alias', async () => {
const note = await testSetup.notesService.createNote(
content,
user,
'test5',
);
const revision = await testSetup.revisionsService.getLatestRevision(note);
const response = await agent
.get(`/api/private/notes/test5/revisions/${revision.id}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.content).toEqual(content);
});
it('fails with a forbidden alias', async () => {
await agent
.get(`/api/private/notes/${forbiddenNoteId}/revisions/1`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await agent
.get('/api/private/notes/i_dont_exist/revisions/1')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('GET /notes/{note}/media', () => {
it('works', async () => {
const alias = 'test6';
const extraAlias = 'test7';
const note1 = await testSetup.notesService.createNote(
content,
user,
alias,
);
const note2 = await testSetup.notesService.createNote(
content,
user,
extraAlias,
);
const response = await agent
.get(`/api/private/notes/${alias}/media/`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(0);
const testImage = await fs.readFile('test/private-api/fixtures/test.png');
const upload0 = await testSetup.mediaService.saveFile(
testImage,
user,
note1,
);
const upload1 = await testSetup.mediaService.saveFile(
testImage,
user,
note2,
);
const responseAfter = await agent
.get(`/api/private/notes/${alias}/media/`)
.expect('Content-Type', /json/)
.expect(200);
expect(responseAfter.body).toHaveLength(1);
expect(responseAfter.body[0].url).toEqual(upload0.fileUrl);
expect(responseAfter.body[0].url).not.toEqual(upload1.fileUrl);
for (const upload of [upload0, upload1]) {
const fileName = upload.fileUrl.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
}
await fs.rm(uploadPath, { recursive: true });
});
it('fails, when note does not exist', async () => {
await agent
.get(`/api/private/notes/i_dont_exist/media/`)
.expect('Content-Type', /json/)
.expect(404);
});
it("fails, when user can't read note", async () => {
const alias = 'test11';
await testSetup.notesService.createNote(
'This is a test note.',
user2,
alias,
);
// Redact default read permissions
const note = await testSetup.notesService.getNoteByIdOrAlias(alias);
const everyone = await testSetup.groupService.getEveryoneGroup();
const loggedin = await testSetup.groupService.getLoggedInGroup();
await testSetup.permissionsService.removeGroupPermission(note, everyone);
await testSetup.permissionsService.removeGroupPermission(note, loggedin);
await agent
.get(`/api/private/notes/${alias}/media/`)
.expect('Content-Type', /json/)
.expect(403);
});
});
});

View file

@ -0,0 +1,196 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { LoginDto } from '../../src/identity/local/login.dto';
import { RegisterDto } from '../../src/identity/local/register.dto';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Register and Login', () => {
let testSetup: TestSetup;
const USERNAME = 'testuser';
const DISPLAYNAME = 'A Test User';
const PASSWORD = 'AVerySecurePassword';
beforeEach(async () => {
testSetup = await TestSetupBuilder.create().build();
await testSetup.app.init();
});
afterEach(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
test('a user can successfully create a local account and log in', async () => {
// register a new user
const registrationDto: RegisterDto = {
displayName: DISPLAYNAME,
password: PASSWORD,
username: USERNAME,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(201);
// log in with the new user and create a session
const loginDto: LoginDto = {
password: PASSWORD,
username: USERNAME,
};
const session = request.agent(testSetup.app.getHttpServer());
await session
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
// request user profile
const profile = await session.get('/api/private/me').expect(200);
expect(profile.body.username).toEqual(USERNAME);
expect(profile.body.displayName).toEqual(DISPLAYNAME);
expect(profile.body.authProvider).toEqual('local');
// logout again
await session.delete('/api/private/auth/logout').expect(204);
// not allowed to request profile now
await session.get('/api/private/me').expect(401);
});
test('a username cannot be used twice', async () => {
// register a new user
const registrationDto: RegisterDto = {
displayName: DISPLAYNAME,
password: PASSWORD,
username: USERNAME,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(201);
// try to use the same username again
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(409);
});
test('a user cannot create a local account with a weak password', async () => {
// register a new user
const registrationDto: RegisterDto = {
displayName: DISPLAYNAME,
password: 'test123',
username: USERNAME,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(400);
});
test('a user can create a local account and change the password', async () => {
// register a new user
const registrationDto: RegisterDto = {
displayName: DISPLAYNAME,
password: PASSWORD,
username: USERNAME,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(201);
// log in with the new user and create a session
const loginDto: LoginDto = {
password: PASSWORD,
username: USERNAME,
};
const newPassword = 'ASecureNewPassword';
let session = request.agent(testSetup.app.getHttpServer());
await session
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
// change the password
await session
.put('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(
JSON.stringify({
currentPassword: PASSWORD,
newPassword: newPassword,
}),
)
.expect(200);
// get new session
session = request.agent(testSetup.app.getHttpServer());
// not allowed to request profile now
await session.get('/api/private/me').expect(401);
// login with new password
loginDto.password = newPassword;
await session
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
// allowed to request profile now
await session.get('/api/private/me').expect(200);
});
test('a user can create a local account and cannot change the password to a weak one', async () => {
// register a new user
const registrationDto: RegisterDto = {
displayName: DISPLAYNAME,
password: PASSWORD,
username: USERNAME,
};
await request(testSetup.app.getHttpServer())
.post('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(JSON.stringify(registrationDto))
.expect(201);
// log in with the new user and create a session
const loginDto: LoginDto = {
password: PASSWORD,
username: USERNAME,
};
const newPassword = 'pasword1';
const session = request.agent(testSetup.app.getHttpServer());
await session
.post('/api/private/auth/local/login')
.set('Content-Type', 'application/json')
.send(JSON.stringify(loginDto))
.expect(201);
// change the password
await session
.put('/api/private/auth/local')
.set('Content-Type', 'application/json')
.send(
JSON.stringify({
currentPassword: PASSWORD,
newPassword: newPassword,
}),
)
.expect(400);
});
});

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Tokens', () => {
let testSetup: TestSetup;
let agent: request.SuperAgentTest;
let user: User;
let keyId: string;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().build();
const username = 'hardcoded';
const password = 'AHardcodedStrongP@ssword123';
user = await testSetup.userService.createUser(username, 'Testy');
await testSetup.identityService.createLocalIdentity(user, password);
await testSetup.app.init();
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.cleanup();
});
it(`POST /tokens`, async () => {
const tokenName = 'testToken';
const response = await agent
.post('/api/private/tokens')
.send({
label: tokenName,
validUntil: 0,
})
.expect('Content-Type', /json/)
.expect(201);
keyId = response.body.keyId;
expect(response.body.label).toBe(tokenName);
expect(new Date(response.body.validUntil).getTime()).toBeGreaterThan(
Date.now(),
);
expect(response.body.lastUsedAt).toBe(null);
expect(response.body.secret.length).toBe(98);
});
it(`GET /tokens`, async () => {
const tokenName = 'testToken';
const response = await agent
.get('/api/private/tokens/')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body[0].label).toBe(tokenName);
expect(new Date(response.body[0].validUntil).getTime()).toBeGreaterThan(
Date.now(),
);
expect(response.body[0].lastUsedAt).toBe(null);
expect(response.body[0].secret).not.toBeDefined();
});
it(`DELETE /tokens/:keyid`, async () => {
const response = await agent
.delete('/api/private/tokens/' + keyId)
.expect(204);
expect(response.body).toStrictEqual({});
});
it(`GET /tokens 2`, async () => {
const response = await agent
.get('/api/private/tokens/')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toStrictEqual([]);
});
});

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Users', () => {
let testSetup: TestSetup;
beforeEach(async () => {
testSetup = await TestSetupBuilder.create().withUsers().build();
await testSetup.app.init();
});
afterEach(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
test('details for existing users can be retrieved', async () => {
let response = await request
.agent(testSetup.app.getHttpServer())
.get('/api/private/users/testuser1');
expect(response.status).toBe(200);
expect(response.body.username).toBe('testuser1');
response = await request
.agent(testSetup.app.getHttpServer())
.get('/api/private/users/testuser2');
expect(response.status).toBe(200);
expect(response.body.username).toBe('testuser2');
});
test('details for non-existing users cannot be retrieved', async () => {
const response = await request
.agent(testSetup.app.getHttpServer())
.get('/api/private/users/i_dont_exist');
expect(response.status).toBe(404);
});
});

View file

@ -0,0 +1,253 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import request from 'supertest';
import { AliasUpdateDto } from '../../src/notes/alias-update.dto';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Alias', () => {
let testSetup: TestSetup;
let forbiddenNoteId: string;
let testAlias: string;
let publicId: string;
beforeEach(async () => {
testSetup = await TestSetupBuilder.create().withUsers().withNotes().build();
forbiddenNoteId =
testSetup.configService.get('noteConfig').forbiddenNoteIds[0];
await testSetup.app.init();
testAlias = (await testSetup.ownedNotes[0].aliases)[0].name;
publicId = testSetup.ownedNotes[0].publicId;
});
afterEach(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
describe('POST /alias', () => {
it('create with normal alias', async () => {
const metadata = await request(testSetup.app.getHttpServer())
.post(`/api/v2/alias`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send({
noteIdOrAlias: testAlias,
newAlias: 'normalAlias',
})
.expect(201);
expect(metadata.body.name).toEqual('normalAlias');
expect(metadata.body.primaryAlias).toBeFalsy();
expect(metadata.body.noteId).toEqual(publicId);
const note = await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/normalAlias`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(200);
expect(note.body.metadata.aliases).toContainEqual({
name: 'normalAlias',
primaryAlias: false,
noteId: publicId,
});
expect(note.body.metadata.primaryAddress).toEqual(testAlias);
expect(note.body.metadata.id).toEqual(publicId);
});
describe('does not create an alias', () => {
it('because because it is already used', async () => {
await request(testSetup.app.getHttpServer())
.post(`/api/v2/alias`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send({
noteIdOrAlias: testAlias,
newAlias: 'testAlias1',
})
.expect(409);
});
it('because of a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.post(`/api/v2/alias`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send({
noteIdOrAlias: testAlias,
newAlias: forbiddenNoteId,
})
.expect(400);
});
it('because of a alias that is a public id', async () => {
await request(testSetup.app.getHttpServer())
.post(`/api/v2/alias`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send({
noteIdOrAlias: testAlias,
newAlias: publicId,
})
.expect(409);
});
it('because the user is not an owner', async () => {
await request(testSetup.app.getHttpServer())
.post(`/api/v2/alias`)
.set('Authorization', `Bearer ${testSetup.authTokens[1].secret}`)
.set('Content-Type', 'application/json')
.send({
noteIdOrAlias: testAlias,
newAlias: '',
})
.expect(401);
});
});
});
describe('PUT /alias/{alias}', () => {
const changeAliasDto: AliasUpdateDto = {
primaryAlias: true,
};
it('updates a note with a normal alias', async () => {
const metadata = await request(testSetup.app.getHttpServer())
.put(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(200);
expect(metadata.body.name).toEqual(testAlias);
expect(metadata.body.primaryAlias).toBeTruthy();
expect(metadata.body.noteId).toEqual(publicId);
const note = await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(200);
expect(note.body.metadata.aliases).toContainEqual({
name: testAlias,
primaryAlias: true,
noteId: publicId,
});
expect(note.body.metadata.primaryAddress).toEqual(testAlias);
expect(note.body.metadata.id).toEqual(publicId);
});
describe('does not update', () => {
it('a note with unknown alias', async () => {
await request(testSetup.app.getHttpServer())
.put(`/api/v2/alias/i_dont_exist`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(404);
});
it('a note with a forbidden id', async () => {
await request(testSetup.app.getHttpServer())
.put(`/api/v2/alias/${forbiddenNoteId}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(400);
});
it('if the user is not an owner', async () => {
await request(testSetup.app.getHttpServer())
.put(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[1].secret}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(401);
});
it('if the property primaryAlias is false', async () => {
changeAliasDto.primaryAlias = false;
await request(testSetup.app.getHttpServer())
.put(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.set('Content-Type', 'application/json')
.send(changeAliasDto)
.expect(400);
});
});
});
describe('DELETE /alias/{alias}', () => {
const secondAlias = 'secondAlias';
it('deletes a normal alias', async () => {
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(204);
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(404);
});
it('does not delete an unknown alias', async () => {
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/i_dont_exist`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(404);
});
it('errors on forbidden notes', async () => {
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/${forbiddenNoteId}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(400);
});
it('errors if the user is not the owner', async () => {
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[1].secret}`)
.expect(401);
});
it('does not delete a primary alias (if it is not the only one)', async () => {
// add another alias
await testSetup.aliasService.addAlias(
testSetup.ownedNotes[0],
secondAlias,
);
// try to delete the primary alias
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(400);
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${secondAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(200);
});
it('deletes a primary alias (if it is the only one)', async () => {
// add another alias
await testSetup.aliasService.addAlias(
testSetup.ownedNotes[0],
secondAlias,
);
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/${secondAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(204);
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/alias/${testAlias}`)
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
.expect(204);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

Binary file not shown.

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,236 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import request from 'supertest';
import { HistoryEntryUpdateDto } from '../../src/history/history-entry-update.dto';
import { HistoryEntryDto } from '../../src/history/history-entry.dto';
import { NoteMetadataDto } from '../../src/notes/note-metadata.dto';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
// TODO Tests have to be reworked using UserService functions
describe('Me', () => {
let testSetup: TestSetup;
let uploadPath: string;
let user: User;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().withMockAuth().build();
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
user = await testSetup.userService.createUser('hardcoded', 'Testy');
await testSetup.app.init();
});
afterAll(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
it(`GET /me`, async () => {
const userInfo = testSetup.userService.toFullUserDto(user);
const response = await request(testSetup.app.getHttpServer())
.get('/api/v2/me')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toEqual(userInfo);
});
it(`GET /me/history`, async () => {
const noteName = 'testGetNoteHistory1';
const note = await testSetup.notesService.createNote('', null, noteName);
const createdHistoryEntry =
await testSetup.historyService.updateHistoryEntryTimestamp(note, user);
const response = await request(testSetup.app.getHttpServer())
.get('/api/v2/me/history')
.expect('Content-Type', /json/)
.expect(200);
const history: HistoryEntryDto[] = response.body;
expect(history.length).toEqual(1);
const historyDto = await testSetup.historyService.toHistoryEntryDto(
createdHistoryEntry,
);
for (const historyEntry of history) {
expect(historyEntry.identifier).toEqual(historyDto.identifier);
expect(historyEntry.title).toEqual(historyDto.title);
expect(historyEntry.tags).toEqual(historyDto.tags);
expect(historyEntry.pinStatus).toEqual(historyDto.pinStatus);
expect(historyEntry.lastVisitedAt).toEqual(
historyDto.lastVisitedAt.toISOString(),
);
}
});
describe(`GET /me/history/{note}`, () => {
it('works with an existing note', async () => {
const noteName = 'testGetNoteHistory2';
const note = await testSetup.notesService.createNote('', null, noteName);
const createdHistoryEntry =
await testSetup.historyService.updateHistoryEntryTimestamp(note, user);
const response = await request(testSetup.app.getHttpServer())
.get(`/api/v2/me/history/${noteName}`)
.expect('Content-Type', /json/)
.expect(200);
const historyEntry: HistoryEntryDto = response.body;
const historyEntryDto = await testSetup.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.lastVisitedAt).toEqual(
historyEntryDto.lastVisitedAt.toISOString(),
);
});
it('fails with a non-existing note', async () => {
await request(testSetup.app.getHttpServer())
.get('/api/v2/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 testSetup.notesService.createNote('', null, noteName);
await testSetup.historyService.updateHistoryEntryTimestamp(note, user);
const historyEntryUpdateDto = new HistoryEntryUpdateDto();
historyEntryUpdateDto.pinStatus = true;
const response = await request(testSetup.app.getHttpServer())
.put('/api/v2/me/history/' + noteName)
.send(historyEntryUpdateDto)
.expect(200);
const history = await testSetup.historyService.getEntriesByUser(user);
const historyEntry: HistoryEntryDto = response.body;
expect(historyEntry.pinStatus).toEqual(true);
let theEntry: HistoryEntryDto;
for (const entry of history) {
if (
(await (await entry.note).aliases).find(
(element) => element.name === noteName,
)
) {
theEntry = await testSetup.historyService.toHistoryEntryDto(entry);
}
}
expect(theEntry.pinStatus).toEqual(true);
});
it('fails with a non-existing note', async () => {
await request(testSetup.app.getHttpServer())
.put('/api/v2/me/history/i_dont_exist')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe(`DELETE /me/history/{note}`, () => {
it('works', async () => {
const noteName = 'testGetNoteHistory4';
const note = await testSetup.notesService.createNote('', null, noteName);
await testSetup.historyService.updateHistoryEntryTimestamp(note, user);
const response = await request(testSetup.app.getHttpServer())
.delete(`/api/v2/me/history/${noteName}`)
.expect(204);
expect(response.body).toEqual({});
const history = await testSetup.historyService.getEntriesByUser(user);
for (const entry of history) {
if (
(await (await entry.note).aliases).find(
(element) => element.name === noteName,
)
) {
throw new Error('Deleted history entry still in history');
}
}
});
describe('fails', () => {
it('with a non-existing note', async () => {
await request(testSetup.app.getHttpServer())
.delete('/api/v2/me/history/i_dont_exist')
.expect(404);
});
it('with a non-existing history entry', async () => {
const noteName = 'testGetNoteHistory5';
await testSetup.notesService.createNote('', null, noteName);
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/me/history/${noteName}`)
.expect(404);
});
});
});
it(`GET /me/notes/`, async () => {
const noteName = 'testNote';
await testSetup.notesService.createNote('', user, noteName);
const response = await request(testSetup.app.getHttpServer())
.get('/api/v2/me/notes/')
.expect('Content-Type', /json/)
.expect(200);
const noteMetaDtos = response.body as NoteMetadataDto[];
expect(noteMetaDtos).toHaveLength(1);
expect(noteMetaDtos[0].primaryAddress).toEqual(noteName);
expect(noteMetaDtos[0].updateUsername).toEqual(user.username);
});
it('GET /me/media', async () => {
const note1 = await testSetup.notesService.createNote(
'This is a test note.',
await testSetup.userService.getUserByUsername('hardcoded'),
'test8',
);
const note2 = await testSetup.notesService.createNote(
'This is a test note.',
await testSetup.userService.getUserByUsername('hardcoded'),
'test9',
);
const httpServer = testSetup.app.getHttpServer();
const response1 = await request(httpServer)
.get('/api/v2/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 imageUrls = [];
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note1)).fileUrl,
);
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note1)).fileUrl,
);
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note2)).fileUrl,
);
imageUrls.push(
(await testSetup.mediaService.saveFile(testImage, user, note2)).fileUrl,
);
const response = await request(httpServer)
.get('/api/v2/me/media/')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(4);
expect(imageUrls).toContain(response.body[0].url);
expect(imageUrls).toContain(response.body[1].url);
expect(imageUrls).toContain(response.body[2].url);
expect(imageUrls).toContain(response.body[3].url);
for (const fileUrl of imageUrls) {
const fileName = fileUrl.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
}
await fs.rm(uploadPath, { recursive: true });
});
});

View file

@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import request from 'supertest';
import { ConsoleLoggerService } from '../../src/logger/console-logger.service';
import { Note } from '../../src/notes/note.entity';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
import { ensureDeleted } from '../utils';
describe('Media', () => {
let testSetup: TestSetup;
let uploadPath: string;
let testNote: Note;
let user: User;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().withMockAuth().build();
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
testSetup.app.useStaticAssets(uploadPath, {
prefix: '/uploads',
});
await testSetup.app.init();
const logger = await testSetup.app.resolve(ConsoleLoggerService);
logger.log('Switching logger', 'AppBootstrap');
testSetup.app.useLogger(logger);
user = await testSetup.userService.createUser('hardcoded', 'Testy');
testNote = await testSetup.notesService.createNote(
'test content',
null,
'test_upload_media',
);
});
afterAll(async () => {
// Delete the upload folder
await ensureDeleted(uploadPath);
await testSetup.app.close();
await testSetup.cleanup();
});
describe('POST /media', () => {
it('works', async () => {
const uploadResponse = await request(testSetup.app.getHttpServer())
.post('/api/v2/media')
.attach('file', 'test/public-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(201);
const path: string = uploadResponse.body.url;
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const downloadResponse = await request(testSetup.app.getHttpServer()).get(
path,
);
expect(downloadResponse.body).toEqual(testImage);
// Remove /uploads/ from path as we just need the filename.
const fileName = path.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
});
describe('fails:', () => {
beforeEach(async () => {
await ensureDeleted(uploadPath);
});
it('MIME type not supported', async () => {
await request(testSetup.app.getHttpServer())
.post('/api/v2/media')
.attach('file', 'test/public-api/fixtures/test.zip')
.set('HedgeDoc-Note', 'test_upload_media')
.expect(400);
await expect(fs.access(uploadPath)).rejects.toBeDefined();
});
it('note does not exist', async () => {
await request(testSetup.app.getHttpServer())
.post('/api/v2/media')
.attach('file', 'test/public-api/fixtures/test.zip')
.set('HedgeDoc-Note', 'i_dont_exist')
.expect(404);
await expect(fs.access(uploadPath)).rejects.toBeDefined();
});
it('mediaBackend error', async () => {
await fs.mkdir(uploadPath, {
mode: '444',
});
await request(testSetup.app.getHttpServer())
.post('/api/v2/media')
.attach('file', 'test/public-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(500);
});
afterEach(async () => {
await ensureDeleted(uploadPath);
});
});
});
it('DELETE /media/{filename}', async () => {
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const upload = await testSetup.mediaService.saveFile(
testImage,
user,
testNote,
);
const filename = upload.fileUrl.split('/').pop() || '';
await request(testSetup.app.getHttpServer())
.delete('/api/v2/media/' + filename)
.expect(204);
});
});

View file

@ -0,0 +1,492 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import request from 'supertest';
import { NotInDBError } from '../../src/errors/errors';
import { NotePermissionsUpdateDto } from '../../src/notes/note-permissions.dto';
import { User } from '../../src/users/user.entity';
import { TestSetup, TestSetupBuilder } from '../test-setup';
describe('Notes', () => {
let testSetup: TestSetup;
let user: User;
let user2: User;
let content: string;
let forbiddenNoteId: string;
let uploadPath: string;
let testImage: Buffer;
beforeAll(async () => {
testSetup = await TestSetupBuilder.create().withMockAuth().build();
forbiddenNoteId =
testSetup.configService.get('noteConfig').forbiddenNoteIds[0];
uploadPath =
testSetup.configService.get('mediaConfig').backend.filesystem.uploadPath;
await testSetup.app.init();
user = await testSetup.userService.createUser('hardcoded', 'Testy');
user2 = await testSetup.userService.createUser(
'hardcoded2',
'Max Mustermann',
);
content = 'This is a test note.';
testImage = await fs.readFile('test/public-api/fixtures/test.png');
});
afterAll(async () => {
await testSetup.app.close();
await testSetup.cleanup();
});
it('POST /notes', async () => {
const response = await request(testSetup.app.getHttpServer())
.post('/api/v2/notes')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body.metadata?.id).toBeDefined();
expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias(
response.body.metadata.id,
),
),
).toEqual(content);
});
describe('GET /notes/{note}', () => {
it('works with an existing note', async () => {
// check if we can succefully get a note that exists
await testSetup.notesService.createNote(content, user, 'test1');
const response = await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/test1')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.content).toEqual(content);
});
it('fails with an non-existing note', async () => {
// check if a missing note correctly returns 404
await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/i_dont_exist')
.expect('Content-Type', /json/)
.expect(404);
});
it('fails with a forbidden note id', async () => {
// check if a forbidden note correctly returns 400
await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/forbiddenNoteId')
.expect('Content-Type', /json/)
.expect(400);
});
});
describe('POST /notes/{note}', () => {
it('works with a non-existing alias', async () => {
const response = await request(testSetup.app.getHttpServer())
.post('/api/v2/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body.metadata?.id).toBeDefined();
return expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias(
response.body.metadata?.id,
),
),
).toEqual(content);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.post(`/api/v2/notes/${forbiddenNoteId}`)
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(400);
});
it('fails with a existing alias', async () => {
await request(testSetup.app.getHttpServer())
.post('/api/v2/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(409);
});
it('fails with a content, that is too long', async () => {
const content = 'x'.repeat(
(testSetup.configService.get('noteConfig')
.maxDocumentLength as number) + 1,
);
await request(testSetup.app.getHttpServer())
.post('/api/v2/notes/test2')
.set('Content-Type', 'text/markdown')
.send(content)
.expect('Content-Type', /json/)
.expect(413);
});
});
describe('DELETE /notes/{note}', () => {
describe('works', () => {
it('with an existing alias and keepMedia false', async () => {
const noteId = 'test3';
const note = await testSetup.notesService.createNote(
content,
user,
noteId,
);
await testSetup.mediaService.saveFile(testImage, user, note);
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/notes/${noteId}`)
.set('Content-Type', 'application/json')
.send({
keepMedia: false,
})
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias(noteId),
).rejects.toEqual(
new NotInDBError(`Note with id/alias '${noteId}' not found.`),
);
expect(
await testSetup.mediaService.listUploadsByUser(user),
).toHaveLength(0);
});
it('with an existing alias and keepMedia true', async () => {
const noteId = 'test3a';
const note = await testSetup.notesService.createNote(
content,
user,
noteId,
);
const upload = await testSetup.mediaService.saveFile(
testImage,
user,
note,
);
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/notes/${noteId}`)
.set('Content-Type', 'application/json')
.send({
keepMedia: true,
})
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias(noteId),
).rejects.toEqual(
new NotInDBError(`Note with id/alias '${noteId}' not found.`),
);
expect(
await testSetup.mediaService.listUploadsByUser(user),
).toHaveLength(1);
// Remove /upload/ from path as we just need the filename.
const fileName = upload.fileUrl.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
});
});
it('works with an existing alias with permissions', async () => {
const note = await testSetup.notesService.createNote(
content,
user,
'test3',
);
const updateNotePermission = new NotePermissionsUpdateDto();
updateNotePermission.sharedToUsers = [
{
username: user.username,
canEdit: true,
},
];
updateNotePermission.sharedToGroups = [];
await testSetup.permissionsService.updateNotePermissions(
note,
updateNotePermission,
);
const updatedNote = await testSetup.notesService.getNoteByIdOrAlias(
(await note.aliases).filter((alias) => alias.primary)[0].name,
);
expect(await updatedNote.userPermissions).toHaveLength(1);
expect((await updatedNote.userPermissions)[0].canEdit).toEqual(
updateNotePermission.sharedToUsers[0].canEdit,
);
expect(
(await (await updatedNote.userPermissions)[0].user).username,
).toEqual(user.username);
expect(await updatedNote.groupPermissions).toHaveLength(0);
await request(testSetup.app.getHttpServer())
.delete('/api/v2/notes/test3')
.send({ keepMedia: false })
.expect(204);
await expect(
testSetup.notesService.getNoteByIdOrAlias('test3'),
).rejects.toEqual(
new NotInDBError("Note with id/alias 'test3' not found."),
);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.delete(`/api/v2/notes/${forbiddenNoteId}`)
.expect(400);
});
it('fails with a non-existing alias', async () => {
await request(testSetup.app.getHttpServer())
.delete('/api/v2/notes/i_dont_exist')
.expect(404);
});
});
describe('PUT /notes/{note}', () => {
const changedContent = 'New note text';
it('works with existing alias', async () => {
await testSetup.notesService.createNote(content, user, 'test4');
const response = await request(testSetup.app.getHttpServer())
.put('/api/v2/notes/test4')
.set('Content-Type', 'text/markdown')
.send(changedContent)
.expect(200);
expect(
await testSetup.notesService.getNoteContent(
await testSetup.notesService.getNoteByIdOrAlias('test4'),
),
).toEqual(changedContent);
expect(response.body.content).toEqual(changedContent);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.put(`/api/v2/notes/${forbiddenNoteId}`)
.set('Content-Type', 'text/markdown')
.send(changedContent)
.expect(400);
});
it('fails with a non-existing alias', async () => {
await request(testSetup.app.getHttpServer())
.put('/api/v2/notes/i_dont_exist')
.set('Content-Type', 'text/markdown')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('GET /notes/{note}/metadata', () => {
it('returns complete metadata object', async () => {
await testSetup.notesService.createNote(content, user, 'test5');
const metadata = await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/test5/metadata')
.expect(200);
expect(typeof metadata.body.id).toEqual('string');
expect(metadata.body.aliases[0].name).toEqual('test5');
expect(metadata.body.primaryAddress).toEqual('test5');
expect(metadata.body.title).toEqual('');
expect(metadata.body.description).toEqual('');
expect(typeof metadata.body.createdAt).toEqual('string');
expect(metadata.body.editedBy).toEqual([]);
expect(metadata.body.permissions.owner).toEqual('hardcoded');
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
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.viewCount).toEqual('number');
expect(metadata.body.editedBy).toEqual([]);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${forbiddenNoteId}/metadata`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/i_dont_exist/metadata')
.expect('Content-Type', /json/)
.expect(404);
});
it('has the correct update/create dates', async () => {
// create a note
const note = await testSetup.notesService.createNote(
content,
user,
'test5a',
);
// save the creation time
const createDate = note.createdAt;
const revisions = await note.revisions;
const updatedDate = revisions[revisions.length - 1].createdAt;
// wait one second
await new Promise((r) => setTimeout(r, 1000));
// update the note
await testSetup.notesService.updateNote(note, 'More test content');
const metadata = await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/test5a/metadata')
.expect(200);
expect(metadata.body.createdAt).toEqual(createDate.toISOString());
expect(metadata.body.updatedAt).not.toEqual(updatedDate.toISOString());
});
});
describe('GET /notes/{note}/revisions', () => {
it('works with existing alias', async () => {
await testSetup.notesService.createNote(content, user, 'test6');
const response = await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/test6/revisions')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(1);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${forbiddenNoteId}/revisions`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/i_dont_exist/revisions')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('GET /notes/{note}/revisions/{revision-id}', () => {
it('works with an existing alias', async () => {
const note = await testSetup.notesService.createNote(
content,
user,
'test7',
);
const revision = await testSetup.revisionsService.getLatestRevision(note);
const response = await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/test7/revisions/${revision.id}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.content).toEqual(content);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${forbiddenNoteId}/revisions/1`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/i_dont_exist/revisions/1')
.expect('Content-Type', /json/)
.expect(404);
});
});
describe('GET /notes/{note}/content', () => {
it('works with an existing alias', async () => {
await testSetup.notesService.createNote(content, user, 'test8');
const response = await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/test8/content')
.expect(200);
expect(response.text).toEqual(content);
});
it('fails with a forbidden alias', async () => {
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${forbiddenNoteId}/content`)
.expect(400);
});
it('fails with non-existing alias', async () => {
// check if a missing note correctly returns 404
await request(testSetup.app.getHttpServer())
.get('/api/v2/notes/i_dont_exist/content')
.expect(404);
});
});
describe('GET /notes/{note}/media', () => {
it('works', async () => {
const alias = 'test9';
const extraAlias = 'test10';
const note1 = await testSetup.notesService.createNote(
content,
user,
alias,
);
const note2 = await testSetup.notesService.createNote(
content,
user,
extraAlias,
);
const httpServer = testSetup.app.getHttpServer();
const response = await request(httpServer)
.get(`/api/v2/notes/${alias}/media/`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(0);
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const upload0 = await testSetup.mediaService.saveFile(
testImage,
user,
note1,
);
const upload1 = await testSetup.mediaService.saveFile(
testImage,
user,
note2,
);
const responseAfter = await request(httpServer)
.get(`/api/v2/notes/${alias}/media/`)
.expect('Content-Type', /json/)
.expect(200);
expect(responseAfter.body).toHaveLength(1);
expect(responseAfter.body[0].url).toEqual(upload0.fileUrl);
expect(responseAfter.body[0].url).not.toEqual(upload1.fileUrl);
for (const upload of [upload0, upload1]) {
const fileName = upload.fileUrl.replace('/uploads/', '');
// delete the file afterwards
await fs.unlink(join(uploadPath, fileName));
}
await fs.rm(uploadPath, { recursive: true });
});
it('fails, when note does not exist', async () => {
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/i_dont_exist/media/`)
.expect('Content-Type', /json/)
.expect(404);
});
it("fails, when user can't read note", async () => {
const alias = 'test11';
await testSetup.notesService.createNote(
'This is a test note.',
user2,
alias,
);
// Redact default read permissions
const note = await testSetup.notesService.getNoteByIdOrAlias(alias);
const everyone = await testSetup.groupService.getEveryoneGroup();
const loggedin = await testSetup.groupService.getLoggedInGroup();
await testSetup.permissionsService.removeGroupPermission(note, everyone);
await testSetup.permissionsService.removeGroupPermission(note, loggedin);
await request(testSetup.app.getHttpServer())
.get(`/api/v2/notes/${alias}/media/`)
.expect('Content-Type', /json/)
.expect(403);
});
});
});

470
backend/test/test-setup.ts Normal file
View file

@ -0,0 +1,470 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { RouterModule, Routes } from 'nest-router';
import { Connection, createConnection } from 'typeorm';
import { PrivateApiModule } from '../src/api/private/private-api.module';
import { PublicApiModule } from '../src/api/public/public-api.module';
import { setupApp } from '../src/app-init';
import { AuthTokenWithSecretDto } from '../src/auth/auth-token.dto';
import { AuthModule } from '../src/auth/auth.module';
import { AuthService } from '../src/auth/auth.service';
import { MockAuthGuard } from '../src/auth/mock-auth.guard';
import { TokenAuthGuard } from '../src/auth/token.strategy';
import { AuthorsModule } from '../src/authors/authors.module';
import { AppConfig } from '../src/config/app.config';
import { AuthConfig } from '../src/config/auth.config';
import { CustomizationConfig } from '../src/config/customization.config';
import { DatabaseConfig } from '../src/config/database.config';
import { ExternalServicesConfig } from '../src/config/external-services.config';
import { MediaConfig } from '../src/config/media.config';
import {
createDefaultMockAppConfig,
registerAppConfig,
} from '../src/config/mock/app.config.mock';
import {
createDefaultMockAuthConfig,
registerAuthConfig,
} from '../src/config/mock/auth.config.mock';
import {
createDefaultMockCustomizationConfig,
registerCustomizationConfig,
} from '../src/config/mock/customization.config.mock';
import {
createDefaultMockDatabaseConfig,
registerDatabaseConfig,
} from '../src/config/mock/database.config.mock';
import {
createDefaultMockExternalServicesConfig,
registerExternalServiceConfig,
} from '../src/config/mock/external-services.config.mock';
import {
createDefaultMockMediaConfig,
registerMediaConfig,
} from '../src/config/mock/media.config.mock';
import {
createDefaultMockNoteConfig,
registerNoteConfig,
} from '../src/config/mock/note.config.mock';
import { NoteConfig } from '../src/config/note.config';
import { ErrorExceptionMapping } from '../src/errors/error-mapping';
import { eventModuleConfig } from '../src/events';
import { FrontendConfigModule } from '../src/frontend-config/frontend-config.module';
import { GroupsModule } from '../src/groups/groups.module';
import { GroupsService } from '../src/groups/groups.service';
import { HistoryModule } from '../src/history/history.module';
import { HistoryService } from '../src/history/history.service';
import { IdentityModule } from '../src/identity/identity.module';
import { IdentityService } from '../src/identity/identity.service';
import { ConsoleLoggerService } from '../src/logger/console-logger.service';
import { LoggerModule } from '../src/logger/logger.module';
import { MediaModule } from '../src/media/media.module';
import { MediaService } from '../src/media/media.service';
import { MonitoringModule } from '../src/monitoring/monitoring.module';
import { AliasService } from '../src/notes/alias.service';
import { Note } from '../src/notes/note.entity';
import { NotesModule } from '../src/notes/notes.module';
import { NotesService } from '../src/notes/notes.service';
import { PermissionsModule } from '../src/permissions/permissions.module';
import { PermissionsService } from '../src/permissions/permissions.service';
import { RevisionsModule } from '../src/revisions/revisions.module';
import { RevisionsService } from '../src/revisions/revisions.service';
import { SessionModule } from '../src/session/session.module';
import { SessionService } from '../src/session/session.service';
import { User } from '../src/users/user.entity';
import { UsersModule } from '../src/users/users.module';
import { UsersService } from '../src/users/users.service';
interface CreateTestSetupParameters {
appConfigMock?: AppConfig;
authConfigMock?: AuthConfig;
customizationConfigMock?: CustomizationConfig;
databaseConfigMock?: DatabaseConfig;
externalServicesConfigMock?: ExternalServicesConfig;
mediaConfigMock?: MediaConfig;
noteConfigMock?: NoteConfig;
}
export class TestSetup {
moduleRef: TestingModule;
app: NestExpressApplication;
userService: UsersService;
groupService: GroupsService;
configService: ConfigService;
identityService: IdentityService;
notesService: NotesService;
mediaService: MediaService;
historyService: HistoryService;
aliasService: AliasService;
authService: AuthService;
sessionService: SessionService;
revisionsService: RevisionsService;
users: User[] = [];
authTokens: AuthTokenWithSecretDto[] = [];
anonymousNotes: Note[] = [];
ownedNotes: Note[] = [];
permissionsService: PermissionsService;
/**
* Cleans up remnants from a test run from the database
*/
public async cleanup() {
const appConnection = this.app.get<Connection>(Connection);
const connectionOptions = appConnection.options;
if (!connectionOptions.database) {
throw new Error('Database name not set in connection options');
}
if (connectionOptions.type === 'sqlite') {
// Bail out early, as SQLite runs from memory anyway
await this.app.close();
return;
}
if (appConnection.isConnected) {
await appConnection.close();
}
switch (connectionOptions.type) {
case 'postgres':
case 'mariadb': {
const connection = await createConnection({
type: connectionOptions.type,
username: 'hedgedoc',
password: 'hedgedoc',
});
await connection.query(`DROP DATABASE ${connectionOptions.database}`);
await connection.close();
}
}
await this.app.close();
}
}
/**
* Builder class for TestSetup
* Should be instantiated with the create() method
* The useable TestSetup is genereated using build()
*/
export class TestSetupBuilder {
// list of functions that should be executed before or after builing the TestingModule
private setupPreCompile: (() => Promise<void>)[] = [];
private setupPostCompile: (() => Promise<void>)[] = [];
private testingModuleBuilder: TestingModuleBuilder;
private testSetup = new TestSetup();
private testId: string;
/**
* Prepares a test database
* @param dbName The name of the database to use
* @private
*/
private static async setupTestDB(dbName: string) {
const dbType = process.env.HEDGEDOC_TEST_DB_TYPE;
if (!dbType || dbType === 'sqlite') {
return;
}
if (!['postgres', 'mariadb'].includes(dbType)) {
throw new Error('Unknown database type in HEDGEDOC_TEST_DB_TYPE');
}
const connection = await createConnection({
type: dbType as 'postgres' | 'mariadb',
username: dbType === 'mariadb' ? 'root' : 'hedgedoc',
password: 'hedgedoc',
});
await connection.query(`CREATE DATABASE ${dbName}`);
if (dbType === 'mariadb') {
await connection.query(
`GRANT ALL PRIVILEGES ON ${dbName}.* TO 'hedgedoc'@'%'`,
);
}
await connection.close();
}
private static getTestDBConf(dbName: string): TypeOrmModuleOptions {
switch (process.env.HEDGEDOC_TEST_DB_TYPE || 'sqlite') {
case 'sqlite':
return {
type: 'sqlite',
database: ':memory:',
autoLoadEntities: true,
synchronize: true,
dropSchema: true,
};
case 'postgres':
case 'mariadb':
return {
type: process.env.HEDGEDOC_TEST_DB_TYPE as 'postgres' | 'mariadb',
database: dbName,
username: 'hedgedoc',
password: 'hedgedoc',
autoLoadEntities: true,
synchronize: true,
dropSchema: true,
};
default:
throw new Error('Unknown database type in HEDGEDOC_TEST_DB_TYPE');
}
}
/**
* Creates a new instance of TestSetupBuilder
*/
public static create(mocks?: CreateTestSetupParameters): TestSetupBuilder {
const testSetupBuilder = new TestSetupBuilder();
testSetupBuilder.testId =
'hedgedoc_test_' + Math.random().toString(36).substring(2, 15);
const routes: Routes = [
{
path: '/api/v2',
module: PublicApiModule,
},
{
path: '/api/private',
module: PrivateApiModule,
},
];
testSetupBuilder.testingModuleBuilder = Test.createTestingModule({
imports: [
RouterModule.forRoutes(routes),
TypeOrmModule.forRoot(
TestSetupBuilder.getTestDBConf(testSetupBuilder.testId),
),
ConfigModule.forRoot({
isGlobal: true,
load: [
registerAppConfig(
mocks?.appConfigMock ?? createDefaultMockAppConfig(),
),
registerAuthConfig(
mocks?.authConfigMock ?? createDefaultMockAuthConfig(),
),
registerCustomizationConfig(
mocks?.customizationConfigMock ??
createDefaultMockCustomizationConfig(),
),
registerDatabaseConfig(
mocks?.databaseConfigMock ?? createDefaultMockDatabaseConfig(),
),
registerExternalServiceConfig(
mocks?.externalServicesConfigMock ??
createDefaultMockExternalServicesConfig(),
),
registerMediaConfig(
mocks?.mediaConfigMock ?? createDefaultMockMediaConfig(),
),
registerNoteConfig(
mocks?.noteConfigMock ?? createDefaultMockNoteConfig(),
),
],
}),
NotesModule,
UsersModule,
RevisionsModule,
AuthorsModule,
PublicApiModule,
PrivateApiModule,
HistoryModule,
MonitoringModule,
PermissionsModule,
GroupsModule,
LoggerModule,
MediaModule,
AuthModule,
FrontendConfigModule,
IdentityModule,
SessionModule,
EventEmitterModule.forRoot(eventModuleConfig),
],
providers: [
{
provide: 'APP_FILTER',
useClass: ErrorExceptionMapping,
},
],
});
return testSetupBuilder;
}
/**
* Builds the final TestSetup from the configured builder
*/
public async build(): Promise<TestSetup> {
await TestSetupBuilder.setupTestDB(this.testId);
for (const setupFunction of this.setupPreCompile) {
await setupFunction();
}
this.testSetup.moduleRef = await this.testingModuleBuilder.compile();
this.testSetup.userService =
this.testSetup.moduleRef.get<UsersService>(UsersService);
this.testSetup.groupService =
this.testSetup.moduleRef.get<GroupsService>(GroupsService);
this.testSetup.configService =
this.testSetup.moduleRef.get<ConfigService>(ConfigService);
this.testSetup.identityService =
this.testSetup.moduleRef.get<IdentityService>(IdentityService);
this.testSetup.notesService =
this.testSetup.moduleRef.get<NotesService>(NotesService);
this.testSetup.mediaService =
this.testSetup.moduleRef.get<MediaService>(MediaService);
this.testSetup.historyService =
this.testSetup.moduleRef.get<HistoryService>(HistoryService);
this.testSetup.aliasService =
this.testSetup.moduleRef.get<AliasService>(AliasService);
this.testSetup.authService =
this.testSetup.moduleRef.get<AuthService>(AuthService);
this.testSetup.permissionsService =
this.testSetup.moduleRef.get<PermissionsService>(PermissionsService);
this.testSetup.sessionService =
this.testSetup.moduleRef.get<SessionService>(SessionService);
this.testSetup.revisionsService =
this.testSetup.moduleRef.get<RevisionsService>(RevisionsService);
this.testSetup.app = this.testSetup.moduleRef.createNestApplication();
await setupApp(
this.testSetup.app,
this.testSetup.configService.get<AppConfig>('appConfig'),
this.testSetup.configService.get<AuthConfig>('authConfig'),
this.testSetup.configService.get<MediaConfig>('mediaConfig'),
await this.testSetup.app.resolve(ConsoleLoggerService),
);
for (const setupFunction of this.setupPostCompile) {
await setupFunction();
}
return this.testSetup;
}
/**
* Enable mock authentication for the public API
*/
public withMockAuth() {
this.setupPreCompile.push(async () => {
this.testingModuleBuilder
.overrideGuard(TokenAuthGuard)
.useClass(MockAuthGuard);
return await Promise.resolve();
});
return this;
}
/**
* Generate a few users, identities and auth tokens for testing
*/
public withUsers() {
this.setupPostCompile.push(async () => {
// Create users
this.testSetup.users.push(
await this.testSetup.userService.createUser(username1, 'Test User 1'),
);
this.testSetup.users.push(
await this.testSetup.userService.createUser(username2, 'Test User 2'),
);
this.testSetup.users.push(
await this.testSetup.userService.createUser(username3, 'Test User 3'),
);
// Create identities for login
await this.testSetup.identityService.createLocalIdentity(
this.testSetup.users[0],
password1,
);
await this.testSetup.identityService.createLocalIdentity(
this.testSetup.users[1],
password2,
);
await this.testSetup.identityService.createLocalIdentity(
this.testSetup.users[2],
password3,
);
// create auth tokens
this.testSetup.authTokens = await Promise.all(
this.testSetup.users.map(async (user) => {
return await this.testSetup.authService.addToken(
user,
'test',
new Date().getTime() + 60 * 60 * 1000,
);
}),
);
// create notes with owner
this.testSetup.ownedNotes.push(
await this.testSetup.notesService.createNote(
'Test Note 1',
this.testSetup.users[0],
'testAlias1',
),
);
this.testSetup.ownedNotes.push(
await this.testSetup.notesService.createNote(
'Test Note 2',
this.testSetup.users[1],
'testAlias2',
),
);
this.testSetup.ownedNotes.push(
await this.testSetup.notesService.createNote(
'Test Note 3',
this.testSetup.users[2],
'testAlias3',
),
);
});
return this;
}
/**
* Generate a few anonymousNotes for testing
*/
public withNotes(): TestSetupBuilder {
this.setupPostCompile.push(async () => {
this.testSetup.anonymousNotes.push(
await this.testSetup.notesService.createNote(
'Anonymous Note 1',
null,
'anonAlias1',
),
);
this.testSetup.anonymousNotes.push(
await this.testSetup.notesService.createNote(
'Anonymous Note 2',
null,
'anonAlias2',
),
);
this.testSetup.anonymousNotes.push(
await this.testSetup.notesService.createNote(
'Anonymous Note 3',
null,
'anonAlias3',
),
);
});
return this;
}
}
export const username1 = 'testuser1';
export const password1 = 'AStrongP@sswordForUser1';
export const username2 = 'testuser2';
export const password2 = 'AStrongP@sswordForUser2';
export const username3 = 'testuser3';
export const password3 = 'AStrongP@sswordForUser3';

View file

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"strict": false
}
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

22
backend/test/utils.ts Normal file
View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { promises as fs } from 'fs';
/**
* Ensures the directory at `path` is deleted.
* If `path` does not exist, nothing happens.
*/
export async function ensureDeleted(path: string): Promise<void> {
try {
await fs.rm(path, { recursive: true });
} catch (e) {
if (e.code && e.code == 'ENOENT') {
// ignore error, path is already deleted
return;
}
throw e;
}
}