Merge pull request #543 from codimd/media-controller-tests

This commit is contained in:
David Mehren 2020-10-24 21:17:29 +02:00 committed by GitHub
commit 23c07dc67d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 128 additions and 39 deletions

View file

@ -25,7 +25,6 @@
"@nestjs/common": "^7.0.0", "@nestjs/common": "^7.0.0",
"@nestjs/core": "^7.0.0", "@nestjs/core": "^7.0.0",
"@nestjs/platform-express": "^7.0.0", "@nestjs/platform-express": "^7.0.0",
"@nestjs/serve-static": "^2.1.3",
"@nestjs/swagger": "^4.5.12", "@nestjs/swagger": "^4.5.12",
"@nestjs/typeorm": "^7.1.0", "@nestjs/typeorm": "^7.1.0",
"class-transformer": "^0.2.3", "class-transformer": "^0.2.3",

View file

@ -19,14 +19,12 @@ import {
import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { MediaService } from '../../../media/media.service'; import { MediaService } from '../../../media/media.service';
import { MulterFile } from '../../../media/multer-file.interface'; import { MulterFile } from '../../../media/multer-file.interface';
import { NotesService } from '../../../notes/notes.service';
@Controller('media') @Controller('media')
export class MediaController { export class MediaController {
constructor( constructor(
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
private mediaService: MediaService, private mediaService: MediaService,
private notesService: NotesService,
) { ) {
this.logger.setContext(MediaController.name); this.logger.setContext(MediaController.name);
} }
@ -44,7 +42,11 @@ export class MediaController {
'uploadImage', 'uploadImage',
); );
try { try {
const url = await this.mediaService.saveFile(file, username, noteId); const url = await this.mediaService.saveFile(
file.buffer,
username,
noteId,
);
return { return {
link: url, link: url,
}; };

View file

@ -1,7 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
import { PublicApiModule } from './api/public/public-api.module'; import { PublicApiModule } from './api/public/public-api.module';
import { AuthorsModule } from './authors/authors.module'; import { AuthorsModule } from './authors/authors.module';
import { GroupsModule } from './groups/groups.module'; import { GroupsModule } from './groups/groups.module';
@ -22,11 +20,6 @@ import { UsersModule } from './users/users.module';
autoLoadEntities: true, autoLoadEntities: true,
synchronize: true, synchronize: true,
}), }),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..'),
// TODO: Get uploads directory from config
renderPath: 'uploads',
}),
NotesModule, NotesModule,
UsersModule, UsersModule,
RevisionsModule, RevisionsModule,

View file

@ -1,11 +1,12 @@
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { NestConsoleLoggerService } from './logger/nest-console-logger.service'; import { NestConsoleLoggerService } from './logger/nest-console-logger.service';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
const logger = await app.resolve(NestConsoleLoggerService); const logger = await app.resolve(NestConsoleLoggerService);
logger.log('Switching logger', 'AppBootstrap'); logger.log('Switching logger', 'AppBootstrap');
app.useLogger(logger); app.useLogger(logger);
@ -24,6 +25,10 @@ async function bootstrap() {
transform: true, transform: true,
}), }),
); );
// TODO: Get uploads directory from config
app.useStaticAssets('uploads', {
prefix: '/uploads',
});
await app.listen(3000); await app.listen(3000);
logger.log('Listening on port 3000', 'AppBootstrap'); logger.log('Listening on port 3000', 'AppBootstrap');
} }

View file

@ -7,33 +7,43 @@ import { BackendData } from '../media-upload.entity';
@Injectable() @Injectable()
export class FilesystemBackend implements MediaBackend { export class FilesystemBackend implements MediaBackend {
// TODO: Get uploads directory from config
uploadDirectory = './uploads';
constructor(private readonly logger: ConsoleLoggerService) { constructor(private readonly logger: ConsoleLoggerService) {
this.logger.setContext(FilesystemBackend.name); this.logger.setContext(FilesystemBackend.name);
} }
private getFilePath(fileName: string): string {
return join(this.uploadDirectory, fileName);
}
private async ensureDirectory() {
try {
await fs.access(this.uploadDirectory);
} catch (e) {
await fs.mkdir(this.uploadDirectory);
}
}
async saveFile( async saveFile(
buffer: Buffer, buffer: Buffer,
fileName: string, fileName: string,
): Promise<[string, BackendData]> { ): Promise<[string, BackendData]> {
const filePath = FilesystemBackend.getFilePath(fileName); const filePath = this.getFilePath(fileName);
this.logger.debug(`Writing file to: ${filePath}`, 'saveFile'); this.logger.debug(`Writing file to: ${filePath}`, 'saveFile');
await this.ensureDirectory();
await fs.writeFile(filePath, buffer, null); await fs.writeFile(filePath, buffer, null);
return ['/' + filePath, null]; return ['/' + filePath, null];
} }
async deleteFile(fileName: string, _: BackendData): Promise<void> { async deleteFile(fileName: string, _: BackendData): Promise<void> {
return fs.unlink(FilesystemBackend.getFilePath(fileName)); return fs.unlink(this.getFilePath(fileName));
} }
getFileURL(fileName: string, _: BackendData): Promise<string> { getFileURL(fileName: string, _: BackendData): Promise<string> {
const filePath = FilesystemBackend.getFilePath(fileName); const filePath = this.getFilePath(fileName);
// TODO: Add server address to url // TODO: Add server address to url
return Promise.resolve('/' + filePath); return Promise.resolve('/' + filePath);
} }
private static getFilePath(fileName: string): string {
// TODO: Get uploads directory from config
const uploadDirectory = './uploads';
return join(uploadDirectory, fileName);
}
} }

View file

@ -10,7 +10,6 @@ import { UsersService } from '../users/users.service';
import { BackendType } from './backends/backend-type.enum'; import { BackendType } from './backends/backend-type.enum';
import { FilesystemBackend } from './backends/filesystem-backend'; import { FilesystemBackend } from './backends/filesystem-backend';
import { MediaUpload } from './media-upload.entity'; import { MediaUpload } from './media-upload.entity';
import { MulterFile } from './multer-file.interface';
@Injectable() @Injectable()
export class MediaService { export class MediaService {
@ -44,14 +43,14 @@ export class MediaService {
return allowedTypes.includes(mimeType); return allowedTypes.includes(mimeType);
} }
public async saveFile(file: MulterFile, username: string, noteId: string) { public async saveFile(fileBuffer: Buffer, username: string, noteId: string) {
this.logger.debug( this.logger.debug(
`Saving '${file.originalname}' for note '${noteId}' and user '${username}'`, `Saving file for note '${noteId}' and user '${username}'`,
'saveFile', 'saveFile',
); );
const note = await this.notesService.getNoteByIdOrAlias(noteId); const note = await this.notesService.getNoteByIdOrAlias(noteId);
const user = await this.usersService.getUserByUsername(username); const user = await this.usersService.getUserByUsername(username);
const fileTypeResult = await FileType.fromBuffer(file.buffer); const fileTypeResult = await FileType.fromBuffer(fileBuffer);
if (!fileTypeResult) { if (!fileTypeResult) {
throw new ClientError('Could not detect file type.'); throw new ClientError('Could not detect file type.');
} }
@ -68,7 +67,7 @@ export class MediaService {
this.logger.debug(`Generated filename: '${mediaUpload.id}'`, 'saveFile'); this.logger.debug(`Generated filename: '${mediaUpload.id}'`, 'saveFile');
const backend = this.moduleRef.get(FilesystemBackend); const backend = this.moduleRef.get(FilesystemBackend);
const [url, backendData] = await backend.saveFile( const [url, backendData] = await backend.saveFile(
file.buffer, fileBuffer,
mediaUpload.id, mediaUpload.id,
); );
mediaUpload.backendData = backendData; mediaUpload.backendData = backendData;

View file

@ -154,10 +154,15 @@ export class NotesService {
], ],
}); });
if (note === undefined) { if (note === undefined) {
this.logger.debug(
`Could not find note '${noteIdOrAlias}'`,
'getNoteByIdOrAlias',
);
throw new NotInDBError( throw new NotInDBError(
`Note with id/alias '${noteIdOrAlias}' not found.`, `Note with id/alias '${noteIdOrAlias}' not found.`,
); );
} }
this.logger.debug(`Found note '${noteIdOrAlias}'`, 'getNoteByIdOrAlias');
return note; return note;
} }

View file

@ -1,4 +1,9 @@
import { Entity, PrimaryGeneratedColumn } from 'typeorm'; import {
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Column, OneToMany } from 'typeorm/index'; import { Column, OneToMany } from 'typeorm/index';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { AuthToken } from './auth-token.entity'; import { AuthToken } from './auth-token.entity';
@ -15,10 +20,10 @@ export class User {
@Column() @Column()
displayName: string; displayName: string;
@Column() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@Column() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
@Column({ @Column({

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,79 @@
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import * as request from 'supertest';
import { PublicApiModule } from '../../src/api/public/public-api.module';
import { GroupsModule } from '../../src/groups/groups.module';
import { LoggerModule } from '../../src/logger/logger.module';
import { NestConsoleLoggerService } from '../../src/logger/nest-console-logger.service';
import { MediaModule } from '../../src/media/media.module';
import { MediaService } from '../../src/media/media.service';
import { NotesModule } from '../../src/notes/notes.module';
import { NotesService } from '../../src/notes/notes.service';
import { PermissionsModule } from '../../src/permissions/permissions.module';
import { UsersService } from '../../src/users/users.service';
describe('Notes', () => {
let app: NestExpressApplication;
let mediaService: MediaService;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
PublicApiModule,
MediaModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: './hedgedoc-e2e-media.sqlite',
autoLoadEntities: true,
dropSchema: true,
synchronize: true,
}),
NotesModule,
PermissionsModule,
GroupsModule,
LoggerModule,
],
}).compile();
app = moduleRef.createNestApplication<NestExpressApplication>();
app.useStaticAssets('uploads', {
prefix: '/uploads',
});
await app.init();
const logger = await app.resolve(NestConsoleLoggerService);
logger.log('Switching logger', 'AppBootstrap');
app.useLogger(logger);
const notesService: NotesService = moduleRef.get('NotesService');
await notesService.createNote('test content', 'test_upload_media');
const usersService: UsersService = moduleRef.get('UsersService');
await usersService.createUser('hardcoded', 'Hard Coded');
mediaService = moduleRef.get('MediaService');
});
it('POST /media', async () => {
const uploadResponse = await request(app.getHttpServer())
.post('/media')
.attach('file', 'test/public-api/fixtures/test.png')
.set('HedgeDoc-Note', 'test_upload_media')
.expect('Content-Type', /json/)
.expect(201);
const path = uploadResponse.body.link;
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const downloadResponse = await request(app.getHttpServer()).get(path);
expect(downloadResponse.body).toEqual(testImage);
});
it('DELETE /media/{filename}', async () => {
const testImage = await fs.readFile('test/public-api/fixtures/test.png');
const url = await mediaService.saveFile(
testImage,
'hardcoded',
'test_upload_media',
);
const filename = url.split('/').pop();
await request(app.getHttpServer())
.delete('/media/' + filename)
.expect(200);
});
});

View file

@ -23,9 +23,10 @@ describe('Notes', () => {
GroupsModule, GroupsModule,
TypeOrmModule.forRoot({ TypeOrmModule.forRoot({
type: 'sqlite', type: 'sqlite',
database: './hedgedoc-e2e.sqlite', database: './hedgedoc-e2e-notes.sqlite',
autoLoadEntities: true, autoLoadEntities: true,
synchronize: true, synchronize: true,
dropSchema: true,
}), }),
LoggerModule, LoggerModule,
], ],
@ -34,8 +35,6 @@ describe('Notes', () => {
app = moduleRef.createNestApplication(); app = moduleRef.createNestApplication();
await app.init(); await app.init();
notesService = moduleRef.get(NotesService); notesService = moduleRef.get(NotesService);
const noteRepository = moduleRef.get('NoteRepository');
noteRepository.clear();
}); });
it(`POST /notes`, async () => { it(`POST /notes`, async () => {

View file

@ -614,13 +614,6 @@
"@angular-devkit/schematics" "9.1.7" "@angular-devkit/schematics" "9.1.7"
fs-extra "9.0.0" fs-extra "9.0.0"
"@nestjs/serve-static@^2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@nestjs/serve-static/-/serve-static-2.1.3.tgz#bdcb6d3463d193153b334212facc24a9767046e9"
integrity sha512-9xyysggaOdfbABWqhty+hAkauDWv/Q8YKHm4OMXdQbQei5tquFuTjiSx8IFDOZeSOKlA9fjBq/2MXCJRSo23SQ==
dependencies:
path-to-regexp "0.1.7"
"@nestjs/swagger@^4.5.12": "@nestjs/swagger@^4.5.12":
version "4.5.12" version "4.5.12"
resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.5.12.tgz#e8aa65fbb0033007ece1d494b002f47ff472c20b" resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.5.12.tgz#e8aa65fbb0033007ece1d494b002f47ff472c20b"