Browse Source

Update tests

Properly test responses.
master
TheoryOfNekomata 1 year ago
parent
commit
d5bf5e9eb8
5 changed files with 209 additions and 77 deletions
  1. +114
    -0
      src/controllers/ClipController.ts
  2. +2
    -61
      src/routes.ts
  3. +1
    -1
      src/server.ts
  4. +92
    -0
      test/index.test.ts
  5. +0
    -15
      test/index.test.tsx

+ 114
- 0
src/controllers/ClipController.ts View File

@@ -0,0 +1,114 @@
import {
createVideoClipper,
VideoType,
CreateVideoClipperParams,
} from '@modal/webvideo-clip-core';
import { constants } from 'http2';
import { RouteHandlerMethod } from 'fastify';

export type ClipArgs = {
url?: unknown,
start?: string | number,
end?: string | number,
}

const DURATION_STRING_REGEXP = /^\d\d:[0-5]\d:[0-5]\d(\.\d+)?$/;

const validateRequestBody = (body: ClipArgs) => {
const messages = [] as string[];
const { url, start, end } = body;
if (typeof url !== 'string') {
messages.push('URL is required.');
}

const typeofStart = typeof start;
if (typeofStart !== 'undefined') {
if (!['string', 'number'].includes(typeofStart)) {
messages.push('Invalid end value.');
} else if (typeofStart === 'string' && !DURATION_STRING_REGEXP.test(start as string)) {
messages.push('Invalid start value.');
}
}

const typeofEnd = typeof end;
if (typeofEnd !== 'undefined') {
if (!['string', 'number'].includes(typeofEnd)) {
messages.push('Invalid end value.');
} else if (typeofEnd === 'string' && !DURATION_STRING_REGEXP.test(end as string)) {
messages.push('Invalid end value.');
}
}

return messages;
};

const getVideoType = (url: string) => {
if (url.startsWith('https://www.youtube.com')) {
return VideoType.YOUTUBE;
}

return null;
};

export const clip: RouteHandlerMethod = async (request, reply) => {
const validationMessages = validateRequestBody(request.body as ClipArgs);
if (validationMessages.length > 0) {
reply
.status(constants.HTTP_STATUS_BAD_REQUEST)
.send({
errors: validationMessages,
});
return;
}
const videoType = getVideoType((request.body as ClipArgs).url as string);
if (videoType === null) {
reply
.status(constants.HTTP_STATUS_UNPROCESSABLE_ENTITY)
.send({
message: 'Unsupported URL.',
});
}

const { url, start, end } = request.body as ClipArgs;
const videoClipperArgs = {
type: videoType,
url,
start,
end,
downloaderExecutablePath: process.env.YOUTUBE_DOWNLOADER_EXECUTABLE_PATH,
} as CreateVideoClipperParams;
const clipper = createVideoClipper(videoClipperArgs);
clipper.on('process', (arg: Record<string, unknown>) => {
request.server.log.info(`${arg.type as string}:${arg.phase as string}`);
if (typeof arg.command === 'string') {
request.server.log.debug(`> ${arg.command}`);
}
});

let clipResult: Record<string, unknown>;
clipper.on('success', (result: Record<string, unknown>) => {
clipResult = result;
});

let theError: Error;
clipper.on('error', (error: Error) => {
theError = error;
});

clipper.on('end', () => {
if (theError) {
reply
.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR)
.send({
message: theError.message,
});
return;
}

reply
.header('Content-Type', clipResult.type as string)
.send(clipResult.output as Buffer);
});

clipper.process();
};

+ 2
- 61
src/routes.ts View File

@@ -1,67 +1,8 @@
import {
createVideoClipper,
VideoType,
CreateVideoClipperParams,
} from '@modal/webvideo-clip-core';
import { constants } from 'http2';
import * as ClipController from './controllers/ClipController';
import SERVER from './server';

SERVER.route({
method: 'POST',
url: '/clip',
handler: async (request, reply) => {
const {
url,
start,
end,
} = request.body as Record<string, unknown>;

const { postprocess = false } = request.query as Record<string, unknown>;

let videoType: string = '';

if (url.startsWith('https://www.youtube.com')) {
videoType = VideoType.YOUTUBE;
}

const videoClipperArgs = {
type: videoType,
url,
start,
end,
downloaderExecutablePath: process.env.YOUTUBE_DOWNLOADER_EXECUTABLE_PATH,
} as CreateVideoClipperParams;
if (postprocess) {
videoClipperArgs.postprocessorExecutablePath = process.env.POSTPROCESSOR_EXECUTABLE_PATH;
}

const clipper = createVideoClipper(videoClipperArgs);

let clipResult: Record<string, unknown>;
clipper.on('success', (result: Record<string, unknown>) => {
clipResult = result;
});

let theError: Error;
clipper.on('error', (error: Error) => {
theError = error;
});

clipper.on('end', () => {
if (theError) {
reply
.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR)
.send({
message: theError.message,
});
return;
}

reply
.header('Content-Type', clipResult.type as string)
.send(clipResult.output as Buffer);
});

clipper.process();
},
handler: ClipController.clip,
});

+ 1
- 1
src/server.ts View File

@@ -1,7 +1,7 @@
import fastify from 'fastify';

const SERVER = fastify({
logger: true,
logger: process.env.NODE_ENV !== 'test',
});

export default SERVER;

+ 92
- 0
test/index.test.ts View File

@@ -0,0 +1,92 @@
import {
describe, it, expect, vi, Mock, beforeAll, afterEach,
} from 'vitest';
import { EventEmitter } from 'events';
import { createVideoClipper, VideoClipEventEmitter } from '@modal/webvideo-clip-core';
import SERVER from '../src/server';
import '../src/routes';
import { constants } from 'http2';

class MockEventEmitter extends EventEmitter {
process = vi.fn();
}

vi.mock('@modal/webvideo-clip-core');

describe('ClipController.clip: POST /clip', () => {
let mockEventEmitter: VideoClipEventEmitter;
beforeAll(() => {
mockEventEmitter = new MockEventEmitter();
(createVideoClipper as Mock).mockReturnValue(mockEventEmitter);
});

afterEach(() => {
(mockEventEmitter.process as Mock).mockReset();
});

it('returns the clip', async () => {
const dummyOutput = 'string content';
(mockEventEmitter.process as Mock).mockImplementationOnce(
function mockProcess(this: VideoClipEventEmitter) {
this.emit('success', {
type: 'video/webm',
output: Buffer.from(dummyOutput),
});
this.emit('end');
},
);

const response = await SERVER
.inject()
.post('/clip')
.body({
url: 'https://www.youtube.com/watch?v=BaW_jenozKc',
start: '00:00:00',
end: '00:00:05',
});

expect(response.statusCode).toBe(constants.HTTP_STATUS_OK);
expect(response.headers['content-type']).toBe('video/webm');
expect(response.headers['content-length']).toBe(dummyOutput.length.toString());
});

it('returns an error when the clip function throws', async () => {
(mockEventEmitter.process as Mock).mockImplementationOnce(
function mockProcess(this: VideoClipEventEmitter) {
this.emit('error', new Error());
this.emit('end');
},
);

const response = await SERVER
.inject()
.post('/clip')
.body({
url: 'https://www.youtube.com/watch?v=BaW_jenozKc',
start: '00:00:00',
end: '00:00:05',
});

expect(response.statusCode).toBe(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
});

it('returns an error when the URL could not be found', async () => {
const response = await SERVER
.inject()
.post('/clip')
.body({});

expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST);
});

it('returns an error when the URL is unsupported', async () => {
const response = await SERVER
.inject()
.post('/clip')
.body({
url: 'https://unsupported.com',
});

expect(response.statusCode).toBe(constants.HTTP_STATUS_UNPROCESSABLE_ENTITY);
});
});

+ 0
- 15
test/index.test.tsx View File

@@ -1,15 +0,0 @@
import { describe, it, expect } from 'vitest';
import SERVER from '../src/server';
import '../src/routes';

describe('Example', () => {
it('should have the expected content', async () => {
const response = await SERVER
.inject()
.get('/')
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(200);
});
});

Loading…
Cancel
Save