From d102de1b38f2877463529c3b27bd35ffef4fd8bf Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 21 Apr 2023 15:00:01 +0200 Subject: Add runner server tests --- shared/server-commands/runners/index.ts | 3 + .../server-commands/runners/runner-jobs-command.ts | 279 +++++++++++++++++++++ .../runners/runner-registration-tokens-command.ts | 55 ++++ shared/server-commands/runners/runners-command.ts | 77 ++++++ 4 files changed, 414 insertions(+) create mode 100644 shared/server-commands/runners/index.ts create mode 100644 shared/server-commands/runners/runner-jobs-command.ts create mode 100644 shared/server-commands/runners/runner-registration-tokens-command.ts create mode 100644 shared/server-commands/runners/runners-command.ts (limited to 'shared/server-commands/runners') diff --git a/shared/server-commands/runners/index.ts b/shared/server-commands/runners/index.ts new file mode 100644 index 000000000..9e8e1baf2 --- /dev/null +++ b/shared/server-commands/runners/index.ts @@ -0,0 +1,3 @@ +export * from './runner-jobs-command' +export * from './runner-registration-tokens-command' +export * from './runners-command' diff --git a/shared/server-commands/runners/runner-jobs-command.ts b/shared/server-commands/runners/runner-jobs-command.ts new file mode 100644 index 000000000..3b0f84b9d --- /dev/null +++ b/shared/server-commands/runners/runner-jobs-command.ts @@ -0,0 +1,279 @@ +import { omit, pick, wait } from '@shared/core-utils' +import { + AbortRunnerJobBody, + AcceptRunnerJobBody, + AcceptRunnerJobResult, + ErrorRunnerJobBody, + HttpStatusCode, + isHLSTranscodingPayloadSuccess, + isLiveRTMPHLSTranscodingUpdatePayload, + isWebVideoOrAudioMergeTranscodingPayloadSuccess, + RequestRunnerJobBody, + RequestRunnerJobResult, + ResultList, + RunnerJobAdmin, + RunnerJobLiveRTMPHLSTranscodingPayload, + RunnerJobPayload, + RunnerJobState, + RunnerJobSuccessBody, + RunnerJobSuccessPayload, + RunnerJobType, + RunnerJobUpdateBody, + RunnerJobVODPayload +} from '@shared/models' +import { unwrapBody } from '../requests' +import { waitJobs } from '../server' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class RunnerJobsCommand extends AbstractCommand { + + list (options: OverrideCommandOptions & { + start?: number + count?: number + sort?: string + search?: string + } = {}) { + const path = '/api/v1/runners/jobs' + + return this.getRequestBody>({ + ...options, + + path, + query: pick(options, [ 'start', 'count', 'sort', 'search' ]), + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + cancelByAdmin (options: OverrideCommandOptions & { jobUUID: string }) { + const path = '/api/v1/runners/jobs/' + options.jobUUID + '/cancel' + + return this.postBodyRequest({ + ...options, + + path, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + // --------------------------------------------------------------------------- + + request (options: OverrideCommandOptions & RequestRunnerJobBody) { + const path = '/api/v1/runners/jobs/request' + + return unwrapBody(this.postBodyRequest({ + ...options, + + path, + fields: pick(options, [ 'runnerToken' ]), + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + async requestVOD (options: OverrideCommandOptions & RequestRunnerJobBody) { + const vodTypes = new Set([ 'vod-audio-merge-transcoding', 'vod-hls-transcoding', 'vod-web-video-transcoding' ]) + + const { availableJobs } = await this.request(options) + + return { + availableJobs: availableJobs.filter(j => vodTypes.has(j.type)) + } as RequestRunnerJobResult + } + + async requestLive (options: OverrideCommandOptions & RequestRunnerJobBody) { + const vodTypes = new Set([ 'live-rtmp-hls-transcoding' ]) + + const { availableJobs } = await this.request(options) + + return { + availableJobs: availableJobs.filter(j => vodTypes.has(j.type)) + } as RequestRunnerJobResult + } + + // --------------------------------------------------------------------------- + + accept (options: OverrideCommandOptions & AcceptRunnerJobBody & { jobUUID: string }) { + const path = '/api/v1/runners/jobs/' + options.jobUUID + '/accept' + + return unwrapBody>(this.postBodyRequest({ + ...options, + + path, + fields: pick(options, [ 'runnerToken' ]), + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + abort (options: OverrideCommandOptions & AbortRunnerJobBody & { jobUUID: string }) { + const path = '/api/v1/runners/jobs/' + options.jobUUID + '/abort' + + return this.postBodyRequest({ + ...options, + + path, + fields: pick(options, [ 'reason', 'jobToken', 'runnerToken' ]), + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + update (options: OverrideCommandOptions & RunnerJobUpdateBody & { jobUUID: string }) { + const path = '/api/v1/runners/jobs/' + options.jobUUID + '/update' + + const { payload } = options + const attaches: { [id: string]: any } = {} + let payloadWithoutFiles = payload + + if (isLiveRTMPHLSTranscodingUpdatePayload(payload)) { + if (payload.masterPlaylistFile) { + attaches[`payload[masterPlaylistFile]`] = payload.masterPlaylistFile + } + + attaches[`payload[resolutionPlaylistFile]`] = payload.resolutionPlaylistFile + attaches[`payload[videoChunkFile]`] = payload.videoChunkFile + + payloadWithoutFiles = omit(payloadWithoutFiles as any, [ 'masterPlaylistFile', 'resolutionPlaylistFile', 'videoChunkFile' ]) + } + + return this.postUploadRequest({ + ...options, + + path, + fields: { + ...pick(options, [ 'progress', 'jobToken', 'runnerToken' ]), + + payload: payloadWithoutFiles + }, + attaches, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + error (options: OverrideCommandOptions & ErrorRunnerJobBody & { jobUUID: string }) { + const path = '/api/v1/runners/jobs/' + options.jobUUID + '/error' + + return this.postBodyRequest({ + ...options, + + path, + fields: pick(options, [ 'message', 'jobToken', 'runnerToken' ]), + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + success (options: OverrideCommandOptions & RunnerJobSuccessBody & { jobUUID: string }) { + const { payload } = options + + const path = '/api/v1/runners/jobs/' + options.jobUUID + '/success' + const attaches: { [id: string]: any } = {} + let payloadWithoutFiles = payload + + if ((isWebVideoOrAudioMergeTranscodingPayloadSuccess(payload) || isHLSTranscodingPayloadSuccess(payload)) && payload.videoFile) { + attaches[`payload[videoFile]`] = payload.videoFile + + payloadWithoutFiles = omit(payloadWithoutFiles as any, [ 'videoFile' ]) + } + + if (isHLSTranscodingPayloadSuccess(payload) && payload.resolutionPlaylistFile) { + attaches[`payload[resolutionPlaylistFile]`] = payload.resolutionPlaylistFile + + payloadWithoutFiles = omit(payloadWithoutFiles as any, [ 'resolutionPlaylistFile' ]) + } + + return this.postUploadRequest({ + ...options, + + path, + attaches, + fields: { + ...pick(options, [ 'jobToken', 'runnerToken' ]), + + payload: payloadWithoutFiles + }, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + getInputFile (options: OverrideCommandOptions & { url: string, jobToken: string, runnerToken: string }) { + const { host, protocol, pathname } = new URL(options.url) + + return this.postBodyRequest({ + url: `${protocol}//${host}`, + path: pathname, + + fields: pick(options, [ 'jobToken', 'runnerToken' ]), + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + // --------------------------------------------------------------------------- + + async autoAccept (options: OverrideCommandOptions & RequestRunnerJobBody & { type?: RunnerJobType }) { + const { availableJobs } = await this.request(options) + + const job = options.type + ? availableJobs.find(j => j.type === options.type) + : availableJobs[0] + + return this.accept({ ...options, jobUUID: job.uuid }) + } + + async autoProcessWebVideoJob (runnerToken: string, jobUUIDToProcess?: string) { + let jobUUID = jobUUIDToProcess + + if (!jobUUID) { + const { availableJobs } = await this.request({ runnerToken }) + jobUUID = availableJobs[0].uuid + } + + const { job } = await this.accept({ runnerToken, jobUUID }) + const jobToken = job.jobToken + + const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' } + await this.success({ runnerToken, jobUUID, jobToken, payload }) + + await waitJobs([ this.server ]) + + return job + } + + async cancelAllJobs (options: { state?: RunnerJobState } = {}) { + const { state } = options + + const { data } = await this.list({ count: 100 }) + + for (const job of data) { + if (state && job.state.id !== state) continue + + await this.cancelByAdmin({ jobUUID: job.uuid }) + } + } + + async getJob (options: OverrideCommandOptions & { uuid: string }) { + const { data } = await this.list({ ...options, count: 100, sort: '-updatedAt' }) + + return data.find(j => j.uuid === options.uuid) + } + + async requestLiveJob (runnerToken: string) { + let availableJobs: RequestRunnerJobResult['availableJobs'] = [] + + while (availableJobs.length === 0) { + const result = await this.requestLive({ runnerToken }) + availableJobs = result.availableJobs + + if (availableJobs.length === 1) break + + await wait(150) + } + + return availableJobs[0] + } +} diff --git a/shared/server-commands/runners/runner-registration-tokens-command.ts b/shared/server-commands/runners/runner-registration-tokens-command.ts new file mode 100644 index 000000000..e4f2e3d95 --- /dev/null +++ b/shared/server-commands/runners/runner-registration-tokens-command.ts @@ -0,0 +1,55 @@ +import { pick } from '@shared/core-utils' +import { HttpStatusCode, ResultList, RunnerRegistrationToken } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class RunnerRegistrationTokensCommand extends AbstractCommand { + + list (options: OverrideCommandOptions & { + start?: number + count?: number + sort?: string + } = {}) { + const path = '/api/v1/runners/registration-tokens' + + return this.getRequestBody>({ + ...options, + + path, + query: pick(options, [ 'start', 'count', 'sort' ]), + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + generate (options: OverrideCommandOptions = {}) { + const path = '/api/v1/runners/registration-tokens/generate' + + return this.postBodyRequest({ + ...options, + + path, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + delete (options: OverrideCommandOptions & { + id: number + }) { + const path = '/api/v1/runners/registration-tokens/' + options.id + + return this.deleteRequest({ + ...options, + + path, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + async getFirstRegistrationToken (options: OverrideCommandOptions = {}) { + const { data } = await this.list(options) + + return data[0].registrationToken + } +} diff --git a/shared/server-commands/runners/runners-command.ts b/shared/server-commands/runners/runners-command.ts new file mode 100644 index 000000000..ca9a1d7a3 --- /dev/null +++ b/shared/server-commands/runners/runners-command.ts @@ -0,0 +1,77 @@ +import { pick } from '@shared/core-utils' +import { HttpStatusCode, RegisterRunnerBody, RegisterRunnerResult, ResultList, Runner, UnregisterRunnerBody } from '@shared/models' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class RunnersCommand extends AbstractCommand { + + list (options: OverrideCommandOptions & { + start?: number + count?: number + sort?: string + } = {}) { + const path = '/api/v1/runners' + + return this.getRequestBody>({ + ...options, + + path, + query: pick(options, [ 'start', 'count', 'sort' ]), + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + register (options: OverrideCommandOptions & RegisterRunnerBody) { + const path = '/api/v1/runners/register' + + return unwrapBody(this.postBodyRequest({ + ...options, + + path, + fields: pick(options, [ 'name', 'registrationToken', 'description' ]), + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + unregister (options: OverrideCommandOptions & UnregisterRunnerBody) { + const path = '/api/v1/runners/unregister' + + return this.postBodyRequest({ + ...options, + + path, + fields: pick(options, [ 'runnerToken' ]), + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + delete (options: OverrideCommandOptions & { + id: number + }) { + const path = '/api/v1/runners/' + options.id + + return this.deleteRequest({ + ...options, + + path, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + // --------------------------------------------------------------------------- + + async autoRegisterRunner () { + const { data } = await this.server.runnerRegistrationTokens.list({ sort: 'createdAt' }) + + const { runnerToken } = await this.register({ + name: 'runner', + registrationToken: data[0].registrationToken + }) + + return runnerToken + } +} -- cgit v1.2.3