Merge pull request #992 from hedgedoc/privateApi/me

This commit is contained in:
Yannick Bungers 2021-04-19 23:09:19 +02:00 committed by GitHub
commit 2c75de747f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 542 additions and 71 deletions

View file

@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Test, TestingModule } from '@nestjs/testing';
import { MeController } from './me.controller';
import { UsersModule } from '../../../users/users.module';
import { LoggerModule } from '../../../logger/logger.module';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from '../../../users/user.entity';
import { Identity } from '../../../users/identity.entity';
import { MediaModule } from '../../../media/media.module';
import { AuthorColor } from '../../../notes/author-color.entity';
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
import { Authorship } from '../../../revisions/authorship.entity';
import { ConfigModule } from '@nestjs/config';
import appConfigMock from '../../../config/mock/app.config.mock';
import authConfigMock from '../../../config/mock/auth.config.mock';
import mediaConfigMock from '../../../config/mock/media.config.mock';
import customizationConfigMock from '../../../config/mock/customization.config.mock';
import externalServicesConfigMock from '../../../config/mock/external-services.config.mock';
import { MediaUpload } from '../../../media/media-upload.entity';
import { Note } from '../../../notes/note.entity';
import { Tag } from '../../../notes/tag.entity';
import { Revision } from '../../../revisions/revision.entity';
import { Group } from '../../../groups/group.entity';
describe('MeController', () => {
let controller: MeController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MeController],
imports: [
UsersModule,
LoggerModule,
MediaModule,
ConfigModule.forRoot({
isGlobal: true,
load: [
appConfigMock,
authConfigMock,
mediaConfigMock,
customizationConfigMock,
externalServicesConfigMock,
],
}),
],
})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.overrideProvider(getRepositoryToken(Identity))
.useValue({})
.overrideProvider(getRepositoryToken(Note))
.useValue({})
.overrideProvider(getRepositoryToken(Tag))
.useValue({})
.overrideProvider(getRepositoryToken(Revision))
.useValue({})
.overrideProvider(getRepositoryToken(Group))
.useValue({})
.overrideProvider(getRepositoryToken(AuthorColor))
.useValue({})
.overrideProvider(getRepositoryToken(NoteGroupPermission))
.useValue({})
.overrideProvider(getRepositoryToken(NoteUserPermission))
.useValue({})
.overrideProvider(getRepositoryToken(Authorship))
.useValue({})
.overrideProvider(getRepositoryToken(MediaUpload))
.useValue({})
.compile();
controller = module.get<MeController>(MeController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Body, Controller, Delete, Get, HttpCode, Post } from '@nestjs/common';
import { UserInfoDto } from '../../../users/user-info.dto';
import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { UsersService } from '../../../users/users.service';
import { MediaService } from '../../../media/media.service';
import { MediaUploadDto } from '../../../media/media-upload.dto';
@Controller('me')
export class MeController {
constructor(
private readonly logger: ConsoleLoggerService,
private userService: UsersService,
private mediaService: MediaService,
) {
this.logger.setContext(MeController.name);
}
@Get()
async getMe(): Promise<UserInfoDto> {
// ToDo: use actual user here
const user = await this.userService.getUserByUsername('hardcoded');
return this.userService.toUserDto(user);
}
@Get('media')
async getMyMedia(): Promise<MediaUploadDto[]> {
// ToDo: use actual user here
const user = await this.userService.getUserByUsername('hardcoded');
const media = await this.mediaService.listUploadsByUser(user);
return media.map((media) => this.mediaService.toMediaUploadDto(media));
}
@Delete()
@HttpCode(204)
async deleteUser(): Promise<void> {
// ToDo: use actual user here
const user = await this.userService.getUserByUsername('hardcoded');
const mediaUploads = await this.mediaService.listUploadsByUser(user);
for (const mediaUpload of mediaUploads) {
await this.mediaService.deleteFile(mediaUpload);
}
this.logger.debug(`Deleted all media uploads of ${user.userName}`);
await this.userService.deleteUser(user);
this.logger.debug(`Deleted ${user.userName}`);
}
@Post('profile')
@HttpCode(200)
async updateDisplayName(@Body('name') newDisplayName: string): Promise<void> {
// ToDo: use actual user here
const user = await this.userService.getUserByUsername('hardcoded');
await this.userService.changeDisplayName(user, newDisplayName);
}
}

View file

@ -9,6 +9,7 @@ import { TokensController } from './tokens/tokens.controller';
import { LoggerModule } from '../../logger/logger.module';
import { UsersModule } from '../../users/users.module';
import { AuthModule } from '../../auth/auth.module';
import { MeController } from './me/me.controller';
import { ConfigController } from './config/config.controller';
import { FrontendConfigModule } from '../../frontend-config/frontend-config.module';
import { HistoryController } from './me/history/history.controller';
@ -27,8 +28,8 @@ import { RevisionsModule } from '../../revisions/revisions.module';
AuthModule,
FrontendConfigModule,
HistoryModule,
NotesModule,
PermissionsModule,
NotesModule,
MediaModule,
RevisionsModule,
],
@ -37,6 +38,7 @@ import { RevisionsModule } from '../../revisions/revisions.module';
ConfigController,
MediaController,
HistoryController,
MeController,
NotesController,
],
})

View file

@ -132,7 +132,23 @@ export class MediaController {
): Promise<void> {
const username = req.user.userName;
try {
await this.mediaService.deleteFile(filename, username);
this.logger.debug(
`Deleting '${filename}' for user '${username}'`,
'deleteFile',
);
const mediaUpload = await this.mediaService.findUploadByFilename(
filename,
);
if (mediaUpload.user.userName !== username) {
this.logger.warn(
`${username} tried to delete '${filename}', but is not the owner`,
'deleteFile',
);
throw new PermissionError(
`File '${filename}' is not owned by '${username}'`,
);
}
await this.mediaService.deleteFile(mediaUpload);
} catch (e) {
if (e instanceof PermissionError) {
throw new UnauthorizedException(e.message);

View file

@ -21,7 +21,9 @@ export class AuthToken {
@Column({ unique: true })
keyId: string;
@ManyToOne((_) => User, (user) => user.authTokens)
@ManyToOne((_) => User, (user) => user.authTokens, {
onDelete: 'CASCADE', // This deletes the AuthToken, when the associated User is deleted
})
user: User;
@Column()

View file

@ -36,7 +36,6 @@ export class Group {
@ManyToMany((_) => User, (user) => user.groups, {
eager: true,
cascade: true,
})
@JoinTable()
members: User[];

View file

@ -23,10 +23,14 @@ export class MediaUpload {
@PrimaryColumn()
id: string;
@ManyToOne((_) => Note, { nullable: false })
@ManyToOne((_) => Note, (note) => note.mediaUploads, {
nullable: false,
})
note: Note;
@ManyToOne((_) => User, { nullable: false })
@ManyToOne((_) => User, (user) => user.mediaUploads, {
nullable: false,
})
user: User;
@Column({

View file

@ -24,7 +24,7 @@ import { BackendData, MediaUpload } from './media-upload.entity';
import { MediaService } from './media.service';
import { Repository } from 'typeorm';
import { promises as fs } from 'fs';
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
import { ClientError, NotInDBError } from '../errors/errors';
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Group } from '../groups/group.entity';
@ -145,7 +145,6 @@ describe('MediaService', () => {
describe('deleteFile', () => {
it('works', async () => {
const testFileName = 'testFilename';
const mockMediaUploadEntry = {
id: 'testMediaUpload',
backendData: 'testBackendData',
@ -153,12 +152,9 @@ describe('MediaService', () => {
userName: 'hardcoded',
} as User,
} as MediaUpload;
jest
.spyOn(mediaRepo, 'findOne')
.mockResolvedValueOnce(mockMediaUploadEntry);
jest.spyOn(service.mediaBackend, 'deleteFile').mockImplementationOnce(
async (fileName: string, backendData: BackendData): Promise<void> => {
expect(fileName).toEqual(testFileName);
expect(fileName).toEqual(mockMediaUploadEntry.id);
expect(backendData).toEqual(mockMediaUploadEntry.backendData);
},
);
@ -168,24 +164,7 @@ describe('MediaService', () => {
expect(entry).toEqual(mockMediaUploadEntry);
return entry;
});
await service.deleteFile(testFileName, 'hardcoded');
});
it('fails: the mediaUpload is not owned by user', async () => {
const testFileName = 'testFilename';
const mockMediaUploadEntry = {
id: 'testMediaUpload',
backendData: 'testBackendData',
user: {
userName: 'not-hardcoded',
} as User,
} as MediaUpload;
jest
.spyOn(mediaRepo, 'findOne')
.mockResolvedValueOnce(mockMediaUploadEntry);
await expect(
service.deleteFile(testFileName, 'hardcoded'),
).rejects.toThrow(PermissionError);
await service.deleteFile(mockMediaUploadEntry);
});
});
describe('findUploadByFilename', () => {

View file

@ -10,7 +10,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import * as FileType from 'file-type';
import { Repository } from 'typeorm';
import mediaConfiguration, { MediaConfig } from '../config/media.config';
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
import { ClientError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { NotesService } from '../notes/notes.service';
import { UsersService } from '../users/users.service';
@ -113,30 +113,12 @@ export class MediaService {
/**
* @async
* Try to delete the file specified by the filename with the user specified by the username.
* @param {string} filename - the name of the file to delete.
* @param {string} username - the username of the user who uploaded this file
* @return {string} the url of the saved file
* @throws {PermissionError} the user is not permitted to delete this file.
* @throws {NotInDBError} - the file entry specified is not in the database
* Try to delete the specified file.
* @param {MediaUpload} mediaUpload - the name of the file to delete.
* @throws {MediaBackendError} - there was an error deleting the file
*/
async deleteFile(filename: string, username: string): Promise<void> {
this.logger.debug(
`Deleting '${filename}' for user '${username}'`,
'deleteFile',
);
const mediaUpload = await this.findUploadByFilename(filename);
if (mediaUpload.user.userName !== username) {
this.logger.warn(
`${username} tried to delete '${filename}', but is not the owner`,
'deleteFile',
);
throw new PermissionError(
`File '${filename}' is not owned by '${username}'`,
);
}
await this.mediaBackend.deleteFile(filename, mediaUpload.backendData);
async deleteFile(mediaUpload: MediaUpload): Promise<void> {
await this.mediaBackend.deleteFile(mediaUpload.id, mediaUpload.backendData);
await this.mediaUploadRepository.remove(mediaUpload);
}

View file

@ -21,6 +21,7 @@ import { User } from '../users/user.entity';
import { AuthorColor } from './author-color.entity';
import { Tag } from './tag.entity';
import { HistoryEntry } from '../history/history-entry.entity';
import { MediaUpload } from '../media/media-upload.entity';
@Entity()
export class Note {
@ -53,7 +54,9 @@ export class Note {
default: 0,
})
viewCount: number;
@ManyToOne((_) => User, (user) => user.ownedNotes, { onDelete: 'CASCADE' })
@ManyToOne((_) => User, (user) => user.ownedNotes, {
onDelete: 'CASCADE', // This deletes the Note, when the associated User is deleted
})
owner: User;
@OneToMany((_) => Revision, (revision) => revision.note, { cascade: true })
revisions: Promise<Revision[]>;
@ -61,6 +64,8 @@ export class Note {
authorColors: AuthorColor[];
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
historyEntries: HistoryEntry[];
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.note)
mediaUploads: MediaUpload[];
@Column({
nullable: true,

View file

@ -19,7 +19,9 @@ export class Identity {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne((_) => User, (user) => user.identities)
@ManyToOne((_) => User, (user) => user.identities, {
onDelete: 'CASCADE', // This deletes the Identity, when the associated User is deleted
})
user: User;
@Column()

View file

@ -17,13 +17,16 @@ import { AuthToken } from '../auth/auth-token.entity';
import { Identity } from './identity.entity';
import { Group } from '../groups/group.entity';
import { HistoryEntry } from '../history/history-entry.entity';
import { MediaUpload } from '../media/media-upload.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
@Column({
unique: true,
})
userName: string;
@Column()
@ -60,6 +63,9 @@ export class User {
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
historyEntries: HistoryEntry[];
@OneToMany((_) => MediaUpload, (mediaUpload) => mediaUpload.user)
mediaUploads: MediaUpload[];
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}

View file

@ -9,11 +9,14 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { Repository } from 'typeorm';
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConfigModule } from '@nestjs/config';
import appConfigMock from '../config/mock/app.config.mock';
describe('UsersService', () => {
let service: UsersService;
let userRepo: Repository<User>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -21,7 +24,7 @@ describe('UsersService', () => {
UsersService,
{
provide: getRepositoryToken(User),
useValue: {},
useClass: Repository,
},
],
imports: [
@ -31,15 +34,124 @@ describe('UsersService', () => {
}),
LoggerModule,
],
})
.overrideProvider(getRepositoryToken(User))
.useValue({})
.compile();
}).compile();
service = module.get<UsersService>(UsersService);
userRepo = module.get<Repository<User>>(getRepositoryToken(User));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('createUser', () => {
const username = 'hardcoded';
const displayname = 'Testy';
beforeEach(() => {
jest
.spyOn(userRepo, 'save')
.mockImplementationOnce(async (user: User): Promise<User> => user);
});
it('works', async () => {
const user = await service.createUser(username, displayname);
expect(user.userName).toEqual(username);
expect(user.displayName).toEqual(displayname);
});
it('fails if username is already taken', async () => {
jest.spyOn(userRepo, 'save').mockImplementationOnce(() => {
throw new Error();
});
// create first user with username
await service.createUser(username, displayname);
// attempt to create second user with username
await expect(service.createUser(username, displayname)).rejects.toThrow(
AlreadyInDBError,
);
});
});
describe('deleteUser', () => {
it('works', async () => {
const username = 'hardcoded';
const displayname = 'Testy';
const newUser = User.create(username, displayname) as User;
jest.spyOn(userRepo, 'remove').mockImplementationOnce(
// eslint-disable-next-line @typescript-eslint/require-await
async (user: User): Promise<User> => {
expect(user).toEqual(newUser);
return user;
},
);
await service.deleteUser(newUser);
});
});
describe('changedDisplayName', () => {
it('works', async () => {
const username = 'hardcoded';
const displayname = 'Testy';
const user = User.create(username, displayname) as User;
const newDisplayName = 'Testy2';
jest.spyOn(userRepo, 'save').mockImplementationOnce(
// eslint-disable-next-line @typescript-eslint/require-await
async (user: User): Promise<User> => {
expect(user.displayName).toEqual(newDisplayName);
return user;
},
);
await service.changeDisplayName(user, newDisplayName);
});
});
describe('getUserByUsername', () => {
const username = 'hardcoded';
const displayname = 'Testy';
const user = User.create(username, displayname) as User;
it('works', async () => {
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(user);
const getUser = await service.getUserByUsername(username);
expect(getUser.userName).toEqual(username);
expect(getUser.displayName).toEqual(displayname);
});
it('fails when user does not exits', async () => {
jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce(undefined);
await expect(service.getUserByUsername(username)).rejects.toThrow(
NotInDBError,
);
});
});
describe('getPhotoUrl', () => {
const username = 'hardcoded';
const displayname = 'Testy';
const user = User.create(username, displayname) as User;
it('works if a user has a photoUrl', () => {
const photo = 'testPhotoUrl';
user.photo = photo;
const photoUrl = service.getPhotoUrl(user);
expect(photoUrl).toEqual(photo);
});
it('works if a user no photoUrl', () => {
user.photo = undefined;
const photoUrl = service.getPhotoUrl(user);
expect(photoUrl).toEqual('');
});
});
describe('toUserDto', () => {
const username = 'hardcoded';
const displayname = 'Testy';
const user = User.create(username, displayname) as User;
it('works if a user is provided', () => {
const userDto = service.toUserDto(user);
expect(userDto.userName).toEqual(username);
expect(userDto.displayName).toEqual(displayname);
expect(userDto.photo).toEqual('');
expect(userDto.email).toEqual('');
});
it('fails if no user is provided', () => {
expect(service.toUserDto(null)).toBeNull();
expect(service.toUserDto(undefined)).toBeNull();
});
});
});

View file

@ -7,7 +7,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotInDBError } from '../errors/errors';
import { AlreadyInDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { UserInfoDto } from './user-info.dto';
import { User } from './user.entity';
@ -21,23 +21,69 @@ export class UsersService {
this.logger.setContext(UsersService.name);
}
createUser(userName: string, displayName: string): Promise<User> {
/**
* @async
* Create a new user with a given userName and displayName
* @param userName - the userName the new user shall have
* @param displayName - the display the new user shall have
* @return {User} the user
* @throws {AlreadyInDBError} the userName is already taken.
*/
async createUser(userName: string, displayName: string): Promise<User> {
const user = User.create(userName, displayName);
return this.userRepository.save(user);
try {
return await this.userRepository.save(user);
} catch {
this.logger.debug(
`A user with the username '${userName}' already exists.`,
'createUser',
);
throw new AlreadyInDBError(
`A user with the username '${userName}' already exists.`,
);
}
}
async deleteUser(userName: string): Promise<void> {
// TODO: Handle owned notes and edits
const user = await this.userRepository.findOne({
where: { userName: userName },
});
await this.userRepository.delete(user);
/**
* @async
* Delete the user with the specified userName
* @param {User} user - the username of the user to be delete
* @throws {NotInDBError} the userName has no user associated with it.
*/
async deleteUser(user: User): Promise<void> {
await this.userRepository.remove(user);
this.logger.debug(
`Successfully deleted user with username ${user.userName}`,
'deleteUser',
);
}
/**
* @async
* Change the displayName of the specified user
* @param {User} user - the user to be changed
* @param displayName - the new displayName
*/
async changeDisplayName(user: User, displayName: string): Promise<void> {
user.displayName = displayName;
await this.userRepository.save(user);
}
/**
* @async
* Get the user specified by the username
* @param {string} userName the username by which the user is specified
* @param {boolean} [withTokens=false] if the returned user object should contain authTokens
* @return {User} the specified user
*/
async getUserByUsername(userName: string, withTokens = false): Promise<User> {
const relations: string[] = [];
if (withTokens) {
relations.push('authTokens');
}
const user = await this.userRepository.findOne({
where: { userName: userName },
relations: withTokens ? ['authTokens'] : null,
relations: relations,
});
if (user === undefined) {
throw new NotInDBError(`User with username '${userName}' not found`);
@ -45,6 +91,11 @@ export class UsersService {
return user;
}
/**
* Extract the photoUrl of the user or in case no photo url is present generate a deterministic user photo
* @param {User} user - the specified User
* @return the url of the photo
*/
getPhotoUrl(user: User): string {
if (user.photo) {
return user.photo;
@ -54,6 +105,11 @@ export class UsersService {
}
}
/**
* Build UserInfoDto from a user.
* @param {User=} user - the user to use
* @return {(UserInfoDto|null)} the built UserInfoDto
*/
toUserDto(user: User | null | undefined): UserInfoDto | null {
if (!user) {
this.logger.warn(`Recieved ${String(user)} argument!`, 'toUserDto');

View file

@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: 2021 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 { INestApplication } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as request from 'supertest';
import appConfigMock from '../../src/config/mock/app.config.mock';
import authConfigMock from '../../src/config/mock/auth.config.mock';
import mediaConfigMock from '../../src/config/mock/media.config.mock';
import customizationConfigMock from '../../src/config/mock/customization.config.mock';
import externalServicesConfigMock from '../../src/config/mock/external-services.config.mock';
import { GroupsModule } from '../../src/groups/groups.module';
import { LoggerModule } from '../../src/logger/logger.module';
import { NotesModule } from '../../src/notes/notes.module';
import { PermissionsModule } from '../../src/permissions/permissions.module';
import { AuthModule } from '../../src/auth/auth.module';
import { UsersService } from '../../src/users/users.service';
import { User } from '../../src/users/user.entity';
import { UsersModule } from '../../src/users/users.module';
import { PrivateApiModule } from '../../src/api/private/private-api.module';
import { UserInfoDto } from '../../src/users/user-info.dto';
import { MediaModule } from '../../src/media/media.module';
import { HistoryModule } from '../../src/history/history.module';
import { NotInDBError } from '../../src/errors/errors';
import { promises as fs } from 'fs';
import { Note } from '../../src/notes/note.entity';
import { NotesService } from '../../src/notes/notes.service';
import { MediaService } from '../../src/media/media.service';
describe('Me', () => {
let app: INestApplication;
let userService: UsersService;
let mediaService: MediaService;
let uploadPath: string;
let user: User;
let content: string;
let note1: Note;
let note2: Note;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
appConfigMock,
authConfigMock,
mediaConfigMock,
customizationConfigMock,
externalServicesConfigMock,
],
}),
PrivateApiModule,
NotesModule,
PermissionsModule,
GroupsModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: './hedgedoc-e2e-private-me.sqlite',
autoLoadEntities: true,
synchronize: true,
dropSchema: true,
}),
LoggerModule,
AuthModule,
UsersModule,
MediaModule,
HistoryModule,
],
}).compile();
const config = moduleRef.get<ConfigService>(ConfigService);
uploadPath = config.get('mediaConfig').backend.filesystem.uploadPath;
app = moduleRef.createNestApplication();
await app.init();
//historyService = moduleRef.get();
userService = moduleRef.get(UsersService);
mediaService = moduleRef.get(MediaService);
user = await userService.createUser('hardcoded', 'Testy');
const notesService = moduleRef.get(NotesService);
content = 'This is a test note.';
note1 = await notesService.createNote(content, null, user);
note2 = await notesService.createNote(content, 'note2', user);
});
it('GET /me', async () => {
const userInfo = userService.toUserDto(user);
const response = await request(app.getHttpServer())
.get('/me')
.expect('Content-Type', /json/)
.expect(200);
const gotUser = response.body as UserInfoDto;
expect(gotUser).toEqual(userInfo);
});
it('GET /me/media', async () => {
const httpServer = app.getHttpServer();
const responseBefore = await request(httpServer)
.get('/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 url0 = await mediaService.saveFile(testImage, 'hardcoded', note1.id);
const url1 = await mediaService.saveFile(testImage, 'hardcoded', note1.id);
const url2 = await mediaService.saveFile(testImage, 'hardcoded', note2.id);
const url3 = await mediaService.saveFile(testImage, 'hardcoded', note2.id);
const response = await request(httpServer)
.get('/me/media/')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(4);
expect(response.body[0].url).toEqual(url0);
expect(response.body[1].url).toEqual(url1);
expect(response.body[2].url).toEqual(url2);
expect(response.body[3].url).toEqual(url3);
const mediaUploads = await mediaService.listUploadsByUser(user);
for (const upload of mediaUploads) {
await mediaService.deleteFile(upload);
}
await fs.rmdir(uploadPath);
});
it('POST /me/profile', async () => {
const newDisplayName = 'Another name';
expect(user.displayName).not.toEqual(newDisplayName);
await request(app.getHttpServer())
.post('/me/profile')
.send({
name: newDisplayName,
})
.expect(200);
const dbUser = await userService.getUserByUsername('hardcoded');
expect(dbUser.displayName).toEqual(newDisplayName);
});
it('DELETE /me', async () => {
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const url0 = await mediaService.saveFile(testImage, 'hardcoded', note1.id);
const dbUser = await userService.getUserByUsername('hardcoded');
expect(dbUser).toBeInstanceOf(User);
const mediaUploads = await mediaService.listUploadsByUser(dbUser);
expect(mediaUploads).toHaveLength(1);
expect(mediaUploads[0].fileUrl).toEqual(url0);
await request(app.getHttpServer()).delete('/me').expect(204);
await expect(userService.getUserByUsername('hardcoded')).rejects.toThrow(
NotInDBError,
);
const mediaUploadsAfter = await mediaService.listUploadsByNote(note1);
expect(mediaUploadsAfter).toHaveLength(0);
});
});