From: Chocobozzz Date: Fri, 7 Dec 2018 15:09:57 +0000 (+0100) Subject: Merge branch 'move-utils-to-shared' of https://github.com/buoyantair/PeerTube into... X-Git-Tag: v1.2.0-rc.1~88 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=2a8c5d0af13f3ccb9a505e1fbc9d324b9d33ba1f;p=oweals%2Fpeertube.git Merge branch 'move-utils-to-shared' of https://github.com/buoyantair/PeerTube into buoyantair-move-utils-to-shared --- 2a8c5d0af13f3ccb9a505e1fbc9d324b9d33ba1f diff --cc server/lib/activitypub/process/process-create.ts index cd7ea01aa,9a72cb899..df05ee452 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@@ -12,8 -12,9 +12,7 @@@ import { getOrCreateVideoAndAccountAndC import { forwardVideoRelatedActivity } from '../send/utils' import { Redis } from '../../redis' import { createOrUpdateCacheFile } from '../cache-file' -import { immutableAssign } from '../../../../shared/utils' import { getVideoDislikeActivityPubUrl } from '../url' --import { VideoModel } from '../../../models/video/video' async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { const activityObject = activity.object diff --cc server/tests/api/activitypub/client.ts index ea0682634,d45232c8d..6d90d8643 --- a/server/tests/api/activitypub/client.ts +++ b/server/tests/api/activitypub/client.ts @@@ -8,11 -6,12 +8,11 @@@ import flushTests, killallServers, makeActivityPubGetRequest, -- runServer, ServerInfo, - setAccessTokensToServers, uploadVideo - } from '../../utils' - setAccessTokensToServers ++ setAccessTokensToServers, ++ uploadVideo + } from '../../../../shared/utils' - const expect = chai.expect describe('Test activitypub', function () { diff --cc server/tests/api/activitypub/fetch.ts index a42c606c6,e84eb18bb..03609c1a9 --- a/server/tests/api/activitypub/fetch.ts +++ b/server/tests/api/activitypub/fetch.ts @@@ -11,12 -11,12 +11,13 @@@ import killallServers, ServerInfo, setAccessTokensToServers, ++ setActorField, ++ setVideoField, uploadVideo, -- userLogin - } from '../../utils' ++ userLogin, ++ waitJobs + } from '../../../../shared/utils' import * as chai from 'chai' --import { setActorField, setVideoField } from '../../utils/miscs/sql' - import { waitJobs } from '../../utils/server/jobs' -import { waitJobs } from '../../../../shared/utils/server/jobs' import { Video } from '../../../../shared/models/videos' const expect = chai.expect diff --cc server/tests/api/activitypub/helpers.ts index 51256a922,4c42f3d67..ac6e755c3 --- a/server/tests/api/activitypub/helpers.ts +++ b/server/tests/api/activitypub/helpers.ts @@@ -2,7 -2,7 +2,7 @@@ import 'mocha' import { expect } from 'chai' - import { buildRequestStub } from '../../utils' -import { buildRequestStub } from '../../utils/miscs/stubs' ++import { buildRequestStub } from '../../../../shared/utils/miscs/stubs' import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' import { cloneDeep } from 'lodash' import { buildSignedActivity } from '../../../helpers/activitypub' diff --cc server/tests/api/activitypub/refresher.ts index 67e04f79e,000000000..332ea7ed1 mode 100644,000000..100644 --- a/server/tests/api/activitypub/refresher.ts +++ b/server/tests/api/activitypub/refresher.ts @@@ -1,84 -1,0 +1,93 @@@ +/* tslint:disable:no-unused-expression */ + +import 'mocha' - import { doubleFollow, getVideo, reRunServer } from '../../utils' - import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo, wait } from '../../utils/index' - import { waitJobs } from '../../utils/server/jobs' - import { setVideoField } from '../../utils/miscs/sql' ++import { ++ doubleFollow, ++ flushAndRunMultipleServers, ++ getVideo, ++ killallServers, ++ reRunServer, ++ ServerInfo, ++ setAccessTokensToServers, ++ uploadVideo, ++ wait, ++ setVideoField, ++ waitJobs ++} from '../../../../shared/utils' + +describe('Test AP refresher', function () { + let servers: ServerInfo[] = [] + let videoUUID1: string + let videoUUID2: string + let videoUUID3: string + + before(async function () { + this.timeout(30000) + + servers = await flushAndRunMultipleServers(2) + + // Get the access tokens + await setAccessTokensToServers(servers) + + { + const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' }) + videoUUID1 = res.body.video.uuid + } + + { + const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' }) + videoUUID2 = res.body.video.uuid + } + + { + const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video3' }) + videoUUID3 = res.body.video.uuid + } + + await doubleFollow(servers[0], servers[1]) + }) + + it('Should remove a deleted remote video', async function () { + this.timeout(60000) + + await wait(10000) + + // Change UUID so the remote server returns a 404 + await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f') + + await getVideo(servers[0].url, videoUUID1) + await getVideo(servers[0].url, videoUUID2) + + await waitJobs(servers) + + await getVideo(servers[0].url, videoUUID1, 404) + await getVideo(servers[0].url, videoUUID2, 200) + }) + + it('Should not update a remote video if the remote instance is down', async function () { + this.timeout(60000) + + killallServers([ servers[1] ]) + + await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e') + + // Video will need a refresh + await wait(10000) + + await getVideo(servers[0].url, videoUUID3) + // The refresh should fail + await waitJobs([ servers[0] ]) + + await reRunServer(servers[1]) + + // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances) + await getVideo(servers[0].url, videoUUID3) + await waitJobs(servers) + + await getVideo(servers[0].url, videoUUID3, 200) + }) + + after(async function () { + killallServers(servers) + }) +}) diff --cc server/tests/api/activitypub/security.ts index 7349749f1,b71a61c8c..342ae0fa1 --- a/server/tests/api/activitypub/security.ts +++ b/server/tests/api/activitypub/security.ts @@@ -2,13 -2,18 +2,19 @@@ import 'mocha' - import { flushAndRunMultipleServers, flushTests, killallServers, ServerInfo } from '../../utils' + import { + flushAndRunMultipleServers, + flushTests, + killallServers, - ServerInfo ++ makeFollowRequest, ++ makePOSTAPRequest, ++ ServerInfo, ++ setActorField + } from '../../../../shared/utils' import { HTTP_SIGNATURE } from '../../../initializers' import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' import * as chai from 'chai' --import { setActorField } from '../../utils/miscs/sql' import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' --import { makeFollowRequest, makePOSTAPRequest } from '../../utils/requests/activitypub' const expect = chai.expect diff --cc server/tests/misc-endpoints.ts index 4de47d693,f948fdfd0..5f82719da --- a/server/tests/misc-endpoints.ts +++ b/server/tests/misc-endpoints.ts @@@ -2,18 -2,7 +2,18 @@@ import 'mocha' import * as chai from 'chai' -import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from '../../shared/utils' +import { + addVideoChannel, + createUser, + flushTests, + killallServers, + makeGetRequest, + runServer, + ServerInfo, + setAccessTokensToServers, + uploadVideo - } from './utils' ++} from '../../shared/utils' +import { VideoPrivacy } from '../../shared/models/videos' const expect = chai.expect diff --cc server/tests/utils/miscs/sql.ts index 027f78131,027f78131..000000000 deleted file mode 100644,100644 --- a/server/tests/utils/miscs/sql.ts +++ /dev/null @@@ -1,38 -1,38 +1,0 @@@ --import * as Sequelize from 'sequelize' -- --function getSequelize (serverNumber: number) { -- const dbname = 'peertube_test' + serverNumber -- const username = 'peertube' -- const password = 'peertube' -- const host = 'localhost' -- const port = 5432 -- -- return new Sequelize(dbname, username, password, { -- dialect: 'postgres', -- host, -- port, -- operatorsAliases: false, -- logging: false -- }) --} -- --function setActorField (serverNumber: number, to: string, field: string, value: string) { -- const seq = getSequelize(serverNumber) -- -- const options = { type: Sequelize.QueryTypes.UPDATE } -- -- return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) --} -- --function setVideoField (serverNumber: number, uuid: string, field: string, value: string) { -- const seq = getSequelize(serverNumber) -- -- const options = { type: Sequelize.QueryTypes.UPDATE } -- -- return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) --} -- --export { -- setVideoField, -- setActorField --} diff --cc server/tests/utils/miscs/stubs.ts index d1eb0e3b2,d1eb0e3b2..000000000 deleted file mode 100644,100644 --- a/server/tests/utils/miscs/stubs.ts +++ /dev/null @@@ -1,14 -1,14 +1,0 @@@ --function buildRequestStub (): any { -- return { } --} -- --function buildResponseStub (): any { -- return { -- locals: {} -- } --} -- --export { -- buildResponseStub, -- buildRequestStub --} diff --cc server/tests/utils/requests/activitypub.ts index 96fee60a8,96fee60a8..000000000 deleted file mode 100644,100644 --- a/server/tests/utils/requests/activitypub.ts +++ /dev/null @@@ -1,43 -1,43 +1,0 @@@ --import { doRequest } from '../../../helpers/requests' --import { HTTP_SIGNATURE } from '../../../initializers' --import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' --import { activityPubContextify } from '../../../helpers/activitypub' -- --function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { -- const options = { -- method: 'POST', -- uri: url, -- json: body, -- httpSignature, -- headers -- } -- -- return doRequest(options) --} -- --async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { -- const follow = { -- type: 'Follow', -- id: by.url + '/toto', -- actor: by.url, -- object: to.url -- } -- -- const body = activityPubContextify(follow) -- -- const httpSignature = { -- algorithm: HTTP_SIGNATURE.ALGORITHM, -- authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, -- keyId: by.url, -- key: by.privateKey, -- headers: HTTP_SIGNATURE.HEADERS_TO_SIGN -- } -- const headers = buildGlobalHeaders(body) -- -- return makePOSTAPRequest(to.url, body, httpSignature, headers) --} -- --export { -- makePOSTAPRequest, -- makeFollowRequest --} diff --cc shared/utils/index.ts index 000000000,897389824..e08bbfd2a mode 000000,100644..100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@@ -1,0 -1,18 +1,23 @@@ + export * from './server/activitypub' + export * from './cli/cli' + export * from './server/clients' + export * from './server/config' ++export * from './server/jobs' + export * from './users/login' + export * from './miscs/miscs' ++export * from './miscs/stubs' ++export * from './miscs/sql' + export * from './server/follows' ++export * from './requests/activitypub' + export * from './requests/requests' ++export * from './requests/check-api-params' + export * from './server/servers' + export * from './videos/services' + export * from './users/users' + export * from './videos/video-abuses' + export * from './videos/video-blacklist' + export * from './videos/video-channels' + export * from './videos/videos' + export * from './videos/video-change-ownership' + export * from './feeds/feeds' + export * from './search/videos' diff --cc shared/utils/miscs/miscs.ts index 000000000,589daa420..91a93b631 mode 000000,100644..100644 --- a/shared/utils/miscs/miscs.ts +++ b/shared/utils/miscs/miscs.ts @@@ -1,0 -1,101 +1,101 @@@ + /* tslint:disable:no-unused-expression */ + + import * as chai from 'chai' + import { isAbsolute, join } from 'path' + import * as request from 'supertest' + import * as WebTorrent from 'webtorrent' + import { pathExists, readFile } from 'fs-extra' + import * as ffmpeg from 'fluent-ffmpeg' + + const expect = chai.expect + let webtorrent = new WebTorrent() + + function immutableAssign (target: T, source: U) { + return Object.assign<{}, T, U>({}, target, source) + } + + // Default interval -> 5 minutes + function dateIsValid (dateString: string, interval = 300000) { + const dateToCheck = new Date(dateString) + const now = new Date() + + return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval + } + + function wait (milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)) + } + + function webtorrentAdd (torrent: string, refreshWebTorrent = false) { + if (refreshWebTorrent === true) webtorrent = new WebTorrent() + + return new Promise(res => webtorrent.add(torrent, res)) + } + + function root () { - // We are in server/tests/utils/miscs - return join(__dirname, '..', '..', '..', '..') ++ // We are in /shared/utils/miscs ++ return join(__dirname, '..', '..', '..') + } + + async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { + const res = await request(url) + .get(imagePath) + .expect(200) + + const body = res.body + - const data = await readFile(join(__dirname, '..', '..', 'fixtures', imageName + extension)) ++ const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension)) + const minLength = body.length - ((20 * body.length) / 100) + const maxLength = body.length + ((20 * body.length) / 100) + + expect(data.length).to.be.above(minLength) + expect(data.length).to.be.below(maxLength) + } + + function buildAbsoluteFixturePath (path: string, customTravisPath = false) { + if (isAbsolute(path)) { + return path + } + + if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path) + - return join(__dirname, '..', '..', 'fixtures', path) ++ return join(root(), 'server', 'tests', 'fixtures', path) + } + + async function generateHighBitrateVideo () { + const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) + + const exists = await pathExists(tempFixturePath) + if (!exists) { + + // Generate a random, high bitrate video on the fly, so we don't have to include + // a large file in the repo. The video needs to have a certain minimum length so + // that FFmpeg properly applies bitrate limits. + // https://stackoverflow.com/a/15795112 + return new Promise(async (res, rej) => { + ffmpeg() + .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ]) + .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) + .outputOptions([ '-maxrate 10M', '-bufsize 10M' ]) + .output(tempFixturePath) + .on('error', rej) + .on('end', () => res(tempFixturePath)) + .run() + }) + } + + return tempFixturePath + } + + // --------------------------------------------------------------------------- + + export { + dateIsValid, + wait, + webtorrentAdd, + immutableAssign, + testImage, + buildAbsoluteFixturePath, + root, + generateHighBitrateVideo + } diff --cc shared/utils/miscs/sql.ts index 000000000,000000000..027f78131 new file mode 100644 --- /dev/null +++ b/shared/utils/miscs/sql.ts @@@ -1,0 -1,0 +1,38 @@@ ++import * as Sequelize from 'sequelize' ++ ++function getSequelize (serverNumber: number) { ++ const dbname = 'peertube_test' + serverNumber ++ const username = 'peertube' ++ const password = 'peertube' ++ const host = 'localhost' ++ const port = 5432 ++ ++ return new Sequelize(dbname, username, password, { ++ dialect: 'postgres', ++ host, ++ port, ++ operatorsAliases: false, ++ logging: false ++ }) ++} ++ ++function setActorField (serverNumber: number, to: string, field: string, value: string) { ++ const seq = getSequelize(serverNumber) ++ ++ const options = { type: Sequelize.QueryTypes.UPDATE } ++ ++ return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) ++} ++ ++function setVideoField (serverNumber: number, uuid: string, field: string, value: string) { ++ const seq = getSequelize(serverNumber) ++ ++ const options = { type: Sequelize.QueryTypes.UPDATE } ++ ++ return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) ++} ++ ++export { ++ setVideoField, ++ setActorField ++} diff --cc shared/utils/miscs/stubs.ts index 000000000,000000000..d1eb0e3b2 new file mode 100644 --- /dev/null +++ b/shared/utils/miscs/stubs.ts @@@ -1,0 -1,0 +1,14 @@@ ++function buildRequestStub (): any { ++ return { } ++} ++ ++function buildResponseStub (): any { ++ return { ++ locals: {} ++ } ++} ++ ++export { ++ buildResponseStub, ++ buildRequestStub ++} diff --cc shared/utils/requests/activitypub.ts index 000000000,000000000..e2348ace0 new file mode 100644 --- /dev/null +++ b/shared/utils/requests/activitypub.ts @@@ -1,0 -1,0 +1,43 @@@ ++import { doRequest } from '../../../server/helpers/requests' ++import { HTTP_SIGNATURE } from '../../../server/initializers' ++import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils' ++import { activityPubContextify } from '../../../server/helpers/activitypub' ++ ++function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { ++ const options = { ++ method: 'POST', ++ uri: url, ++ json: body, ++ httpSignature, ++ headers ++ } ++ ++ return doRequest(options) ++} ++ ++async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { ++ const follow = { ++ type: 'Follow', ++ id: by.url + '/toto', ++ actor: by.url, ++ object: to.url ++ } ++ ++ const body = activityPubContextify(follow) ++ ++ const httpSignature = { ++ algorithm: HTTP_SIGNATURE.ALGORITHM, ++ authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, ++ keyId: by.url, ++ key: by.privateKey, ++ headers: HTTP_SIGNATURE.HEADERS_TO_SIGN ++ } ++ const headers = buildGlobalHeaders(body) ++ ++ return makePOSTAPRequest(to.url, body, httpSignature, headers) ++} ++ ++export { ++ makePOSTAPRequest, ++ makeFollowRequest ++} diff --cc shared/utils/requests/requests.ts index 000000000,5796540f7..77e9f6164 mode 000000,100644..100644 --- a/shared/utils/requests/requests.ts +++ b/shared/utils/requests/requests.ts @@@ -1,0 -1,168 +1,168 @@@ + import * as request from 'supertest' -import { buildAbsoluteFixturePath } from '../miscs/miscs' ++import { buildAbsoluteFixturePath, root } from '../miscs/miscs' + import { isAbsolute, join } from 'path' + + function makeGetRequest (options: { + url: string, + path: string, + query?: any, + token?: string, + statusCodeExpected?: number, + contentType?: string + }) { + if (!options.statusCodeExpected) options.statusCodeExpected = 400 + if (options.contentType === undefined) options.contentType = 'application/json' + + const req = request(options.url) + .get(options.path) + + if (options.contentType) req.set('Accept', options.contentType) + if (options.token) req.set('Authorization', 'Bearer ' + options.token) + if (options.query) req.query(options.query) + + return req.expect(options.statusCodeExpected) + } + + function makeDeleteRequest (options: { + url: string, + path: string, + token?: string, + statusCodeExpected?: number + }) { + if (!options.statusCodeExpected) options.statusCodeExpected = 400 + + const req = request(options.url) + .delete(options.path) + .set('Accept', 'application/json') + + if (options.token) req.set('Authorization', 'Bearer ' + options.token) + + return req.expect(options.statusCodeExpected) + } + + function makeUploadRequest (options: { + url: string, + method?: 'POST' | 'PUT', + path: string, + token?: string, + fields: { [ fieldName: string ]: any }, + attaches: { [ attachName: string ]: any | any[] }, + statusCodeExpected?: number + }) { + if (!options.statusCodeExpected) options.statusCodeExpected = 400 + + let req: request.Test + if (options.method === 'PUT') { + req = request(options.url).put(options.path) + } else { + req = request(options.url).post(options.path) + } + + req.set('Accept', 'application/json') + + if (options.token) req.set('Authorization', 'Bearer ' + options.token) + + Object.keys(options.fields).forEach(field => { + const value = options.fields[field] + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + req.field(field + '[' + i + ']', value[i]) + } + } else { + req.field(field, value) + } + }) + + Object.keys(options.attaches).forEach(attach => { + const value = options.attaches[attach] + if (Array.isArray(value)) { + req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) + } else { + req.attach(attach, buildAbsoluteFixturePath(value)) + } + }) + + return req.expect(options.statusCodeExpected) + } + + function makePostBodyRequest (options: { + url: string, + path: string, + token?: string, + fields?: { [ fieldName: string ]: any }, + statusCodeExpected?: number + }) { + if (!options.fields) options.fields = {} + if (!options.statusCodeExpected) options.statusCodeExpected = 400 + + const req = request(options.url) + .post(options.path) + .set('Accept', 'application/json') + + if (options.token) req.set('Authorization', 'Bearer ' + options.token) + + return req.send(options.fields) + .expect(options.statusCodeExpected) + } + + function makePutBodyRequest (options: { + url: string, + path: string, + token?: string, + fields: { [ fieldName: string ]: any }, + statusCodeExpected?: number + }) { + if (!options.statusCodeExpected) options.statusCodeExpected = 400 + + const req = request(options.url) + .put(options.path) + .set('Accept', 'application/json') + + if (options.token) req.set('Authorization', 'Bearer ' + options.token) + + return req.send(options.fields) + .expect(options.statusCodeExpected) + } + + function makeHTMLRequest (url: string, path: string) { + return request(url) + .get(path) + .set('Accept', 'text/html') + .expect(200) + } + + function updateAvatarRequest (options: { + url: string, + path: string, + accessToken: string, + fixture: string + }) { + let filePath = '' + if (isAbsolute(options.fixture)) { + filePath = options.fixture + } else { - filePath = join(__dirname, '..', '..', 'fixtures', options.fixture) ++ filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture) + } + + return makeUploadRequest({ + url: options.url, + path: options.path, + token: options.accessToken, + fields: {}, + attaches: { avatarfile: filePath }, + statusCodeExpected: 200 + }) + } + + // --------------------------------------------------------------------------- + + export { + makeHTMLRequest, + makeGetRequest, + makeUploadRequest, + makePostBodyRequest, + makePutBodyRequest, + makeDeleteRequest, + updateAvatarRequest + } diff --cc shared/utils/server/activitypub.ts index 000000000,cf3c1c3b3..eccb198ca mode 000000,100644..100644 --- a/shared/utils/server/activitypub.ts +++ b/shared/utils/server/activitypub.ts @@@ -1,0 -1,15 +1,14 @@@ + import * as request from 'supertest' + -function makeActivityPubGetRequest (url: string, path: string) { ++function makeActivityPubGetRequest (url: string, path: string, expectedStatus = 200) { + return request(url) + .get(path) + .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8') - .expect(200) - .expect('Content-Type', /json/) ++ .expect(expectedStatus) + } + + // --------------------------------------------------------------------------- + + export { + makeActivityPubGetRequest + } diff --cc shared/utils/server/servers.ts index 000000000,f358a21f1..88d2b390c mode 000000,100644..100644 --- a/shared/utils/server/servers.ts +++ b/shared/utils/server/servers.ts @@@ -1,0 -1,185 +1,185 @@@ + import { ChildProcess, exec, fork } from 'child_process' + import { join } from 'path' + import { root, wait } from '../miscs/miscs' + import { readFile } from 'fs-extra' + + interface ServerInfo { + app: ChildProcess, + url: string + host: string + serverNumber: number + + client: { + id: string, + secret: string + } + + user: { + username: string, + password: string, + email?: string + } + + accessToken?: string + + video?: { + id: number + uuid: string + name: string + account: { + name: string + } + } + + remoteVideo?: { + id: number + uuid: string + } + } + + function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) { + let apps = [] + let i = 0 + + return new Promise(res => { + function anotherServerDone (serverNumber, app) { + apps[serverNumber - 1] = app + i++ + if (i === totalServers) { + return res(apps) + } + } + + flushTests() + .then(() => { + for (let j = 1; j <= totalServers; j++) { + runServer(j, configOverride).then(app => anotherServerDone(j, app)) + } + }) + }) + } + + function flushTests () { + return new Promise((res, rej) => { + return exec('npm run clean:server:test', err => { + if (err) return rej(err) + + return res() + }) + }) + } + + function runServer (serverNumber: number, configOverride?: Object, args = []) { + const server: ServerInfo = { + app: null, + serverNumber: serverNumber, + url: `http://localhost:${9000 + serverNumber}`, + host: `localhost:${9000 + serverNumber}`, + client: { + id: null, + secret: null + }, + user: { + username: null, + password: null + } + } + + // These actions are async so we need to be sure that they have both been done + const serverRunString = { + 'Server listening': false + } + const key = 'Database peertube_test' + serverNumber + ' is ready' + serverRunString[key] = false + + const regexps = { + client_id: 'Client id: (.+)', + client_secret: 'Client secret: (.+)', + user_username: 'Username: (.+)', + user_password: 'User password: (.+)' + } + + // Share the environment + const env = Object.create(process.env) + env['NODE_ENV'] = 'test' + env['NODE_APP_INSTANCE'] = serverNumber.toString() + + if (configOverride !== undefined) { + env['NODE_CONFIG'] = JSON.stringify(configOverride) + } + + const options = { + silent: true, + env: env, + detached: true + } + + return new Promise(res => { - server.app = fork(join(__dirname, '..', '..', '..', '..', 'dist', 'server.js'), args, options) ++ server.app = fork(join(root(), 'dist', 'server.js'), args, options) + server.app.stdout.on('data', function onStdout (data) { + let dontContinue = false + + // Capture things if we want to + for (const key of Object.keys(regexps)) { + const regexp = regexps[key] + const matches = data.toString().match(regexp) + if (matches !== null) { + if (key === 'client_id') server.client.id = matches[1] + else if (key === 'client_secret') server.client.secret = matches[1] + else if (key === 'user_username') server.user.username = matches[1] + else if (key === 'user_password') server.user.password = matches[1] + } + } + + // Check if all required sentences are here + for (const key of Object.keys(serverRunString)) { + if (data.toString().indexOf(key) !== -1) serverRunString[key] = true + if (serverRunString[key] === false) dontContinue = true + } + + // If no, there is maybe one thing not already initialized (client/user credentials generation...) + if (dontContinue === true) return + + server.app.stdout.removeListener('data', onStdout) + res(server) + }) + }) + } + + async function reRunServer (server: ServerInfo, configOverride?: any) { + const newServer = await runServer(server.serverNumber, configOverride) + server.app = newServer.app + + return server + } + + function killallServers (servers: ServerInfo[]) { + for (const server of servers) { + process.kill(-server.app.pid) + } + } + + async function waitUntilLog (server: ServerInfo, str: string, count = 1) { + const logfile = join(root(), 'test' + server.serverNumber, 'logs/peertube.log') + + while (true) { + const buf = await readFile(logfile) + + const matches = buf.toString().match(new RegExp(str, 'g')) + if (matches && matches.length === count) return + + await wait(1000) + } + } + + // --------------------------------------------------------------------------- + + export { + ServerInfo, + flushAndRunMultipleServers, + flushTests, + runServer, + killallServers, + reRunServer, + waitUntilLog + } diff --cc shared/utils/users/blocklist.ts index 000000000,0ead5e5f6..5feb84179 mode 000000,100644..100644 --- a/shared/utils/users/blocklist.ts +++ b/shared/utils/users/blocklist.ts @@@ -1,0 -1,198 +1,197 @@@ + /* tslint:disable:no-unused-expression */ + -import { makeDeleteRequest, makePostBodyRequest } from '../requests/requests' -import { makeGetRequest } from '../requests/requests' ++import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests' + + function getAccountBlocklistByAccount ( + url: string, + token: string, + start: number, + count: number, + sort = '-createdAt', + statusCodeExpected = 200 + ) { + const path = '/api/v1/users/me/blocklist/accounts' + + return makeGetRequest({ + url, + token, + query: { start, count, sort }, + path, + statusCodeExpected + }) + } + + function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) { + const path = '/api/v1/users/me/blocklist/accounts' + + return makePostBodyRequest({ + url, + path, + token, + fields: { + accountName: accountToBlock + }, + statusCodeExpected + }) + } + + function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) { + const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock + + return makeDeleteRequest({ + url, + path, + token, + statusCodeExpected + }) + } + + function getServerBlocklistByAccount ( + url: string, + token: string, + start: number, + count: number, + sort = '-createdAt', + statusCodeExpected = 200 + ) { + const path = '/api/v1/users/me/blocklist/servers' + + return makeGetRequest({ + url, + token, + query: { start, count, sort }, + path, + statusCodeExpected + }) + } + + function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { + const path = '/api/v1/users/me/blocklist/servers' + + return makePostBodyRequest({ + url, + path, + token, + fields: { + host: serverToBlock + }, + statusCodeExpected + }) + } + + function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { + const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock + + return makeDeleteRequest({ + url, + path, + token, + statusCodeExpected + }) + } + + function getAccountBlocklistByServer ( + url: string, + token: string, + start: number, + count: number, + sort = '-createdAt', + statusCodeExpected = 200 + ) { + const path = '/api/v1/server/blocklist/accounts' + + return makeGetRequest({ + url, + token, + query: { start, count, sort }, + path, + statusCodeExpected + }) + } + + function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) { + const path = '/api/v1/server/blocklist/accounts' + + return makePostBodyRequest({ + url, + path, + token, + fields: { + accountName: accountToBlock + }, + statusCodeExpected + }) + } + + function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) { + const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock + + return makeDeleteRequest({ + url, + path, + token, + statusCodeExpected + }) + } + + function getServerBlocklistByServer ( + url: string, + token: string, + start: number, + count: number, + sort = '-createdAt', + statusCodeExpected = 200 + ) { + const path = '/api/v1/server/blocklist/servers' + + return makeGetRequest({ + url, + token, + query: { start, count, sort }, + path, + statusCodeExpected + }) + } + + function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { + const path = '/api/v1/server/blocklist/servers' + + return makePostBodyRequest({ + url, + path, + token, + fields: { + host: serverToBlock + }, + statusCodeExpected + }) + } + + function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { + const path = '/api/v1/server/blocklist/servers/' + serverToBlock + + return makeDeleteRequest({ + url, + path, + token, + statusCodeExpected + }) + } + + // --------------------------------------------------------------------------- + + export { + getAccountBlocklistByAccount, + addAccountToAccountBlocklist, + removeAccountFromAccountBlocklist, + getServerBlocklistByAccount, + addServerToAccountBlocklist, + removeServerFromAccountBlocklist, + + getAccountBlocklistByServer, + addAccountToServerBlocklist, + removeAccountFromServerBlocklist, + getServerBlocklistByServer, + addServerToServerBlocklist, + removeServerFromServerBlocklist + } diff --cc shared/utils/users/users.ts index 000000000,d5d62a507..554e42c01 mode 000000,100644..100644 --- a/shared/utils/users/users.ts +++ b/shared/utils/users/users.ts @@@ -1,0 -1,296 +1,298 @@@ + import * as request from 'supertest' + import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' + + import { UserRole } from '../../index' + import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type' + + function createUser ( + url: string, + accessToken: string, + username: string, + password: string, + videoQuota = 1000000, + videoQuotaDaily = -1, + role: UserRole = UserRole.USER, + specialStatus = 200 + ) { + const path = '/api/v1/users' + const body = { + username, + password, + role, + email: username + '@example.com', + videoQuota, + videoQuotaDaily + } + + return request(url) + .post(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .send(body) + .expect(specialStatus) + } + + function registerUser (url: string, username: string, password: string, specialStatus = 204) { + const path = '/api/v1/users/register' + const body = { + username, + password, + email: username + '@example.com' + } + + return request(url) + .post(path) + .set('Accept', 'application/json') + .send(body) + .expect(specialStatus) + } + + function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) { + const path = '/api/v1/users/me' + + return request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(specialStatus) + .expect('Content-Type', /json/) + } + + function deleteMe (url: string, accessToken: string, specialStatus = 204) { + const path = '/api/v1/users/me' + + return request(url) + .delete(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(specialStatus) + } + + function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) { + const path = '/api/v1/users/me/video-quota-used' + + return request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(specialStatus) + .expect('Content-Type', /json/) + } + + function getUserInformation (url: string, accessToken: string, userId: number) { + const path = '/api/v1/users/' + userId + + return request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(200) + .expect('Content-Type', /json/) + } + + function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = 200) { + const path = '/api/v1/users/me/videos/' + videoId + '/rating' + + return request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(specialStatus) + .expect('Content-Type', /json/) + } + + function getUsersList (url: string, accessToken: string) { + const path = '/api/v1/users' + + return request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(200) + .expect('Content-Type', /json/) + } + + function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) { + const path = '/api/v1/users' + + return request(url) + .get(path) + .query({ start }) + .query({ count }) + .query({ sort }) + .query({ search }) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(200) + .expect('Content-Type', /json/) + } + + function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { + const path = '/api/v1/users' + + return request(url) + .delete(path + '/' + userId) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(expectedStatus) + } + + function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) { + const path = '/api/v1/users' + let body: any + if (reason) body = { reason } + + return request(url) + .post(path + '/' + userId + '/block') + .send(body) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(expectedStatus) + } + + function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) { + const path = '/api/v1/users' + + return request(url) + .post(path + '/' + userId + '/unblock') + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(expectedStatus) + } + + function updateMyUser (options: { + url: string + accessToken: string, + currentPassword?: string, + newPassword?: string, + nsfwPolicy?: NSFWPolicyType, + email?: string, + autoPlayVideo?: boolean + displayName?: string, + description?: string + }) { + const path = '/api/v1/users/me' + + const toSend = {} + if (options.currentPassword !== undefined && options.currentPassword !== null) toSend['currentPassword'] = options.currentPassword + if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword + if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy + if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo + if (options.email !== undefined && options.email !== null) toSend['email'] = options.email + if (options.description !== undefined && options.description !== null) toSend['description'] = options.description + if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName + + return makePutBodyRequest({ + url: options.url, + path, + token: options.accessToken, + fields: toSend, + statusCodeExpected: 204 + }) + } + + function updateMyAvatar (options: { + url: string, + accessToken: string, + fixture: string + }) { + const path = '/api/v1/users/me/avatar/pick' + + return updateAvatarRequest(Object.assign(options, { path })) + } + + function updateUser (options: { + url: string + userId: number, + accessToken: string, + email?: string, ++ emailVerified?: boolean, + videoQuota?: number, + videoQuotaDaily?: number, + role?: UserRole + }) { + const path = '/api/v1/users/' + options.userId + + const toSend = {} + if (options.email !== undefined && options.email !== null) toSend['email'] = options.email ++ if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified + if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota + if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily + if (options.role !== undefined && options.role !== null) toSend['role'] = options.role + + return makePutBodyRequest({ + url: options.url, + path, + token: options.accessToken, + fields: toSend, + statusCodeExpected: 204 + }) + } + + function askResetPassword (url: string, email: string) { + const path = '/api/v1/users/ask-reset-password' + + return makePostBodyRequest({ + url, + path, + fields: { email }, + statusCodeExpected: 204 + }) + } + + function resetPassword (url: string, userId: number, verificationString: string, password: string, statusCodeExpected = 204) { + const path = '/api/v1/users/' + userId + '/reset-password' + + return makePostBodyRequest({ + url, + path, + fields: { password, verificationString }, + statusCodeExpected + }) + } + + function askSendVerifyEmail (url: string, email: string) { + const path = '/api/v1/users/ask-send-verify-email' + + return makePostBodyRequest({ + url, + path, + fields: { email }, + statusCodeExpected: 204 + }) + } + + function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) { + const path = '/api/v1/users/' + userId + '/verify-email' + + return makePostBodyRequest({ + url, + path, + fields: { verificationString }, + statusCodeExpected + }) + } + + // --------------------------------------------------------------------------- + + export { + createUser, + registerUser, + getMyUserInformation, + getMyUserVideoRating, + deleteMe, + getMyUserVideoQuotaUsed, + getUsersList, + getUsersListPaginationAndSort, + removeUser, + updateUser, + updateMyUser, + getUserInformation, + blockUser, + unblockUser, + askResetPassword, + resetPassword, + updateMyAvatar, + askSendVerifyEmail, + verifyEmail + } diff --cc shared/utils/videos/videos.ts index 000000000,1ab3e7c4b..f5fcc6a8a mode 000000,100644..100644 --- a/shared/utils/videos/videos.ts +++ b/shared/utils/videos/videos.ts @@@ -1,0 -1,577 +1,577 @@@ + /* tslint:disable:no-unused-expression */ + + import { expect } from 'chai' + import { existsSync, readdir, readFile } from 'fs-extra' + import * as parseTorrent from 'parse-torrent' + import { extname, join } from 'path' + import * as request from 'supertest' + import { + buildAbsoluteFixturePath, + getMyUserInformation, + immutableAssign, + makeGetRequest, + makePutBodyRequest, + makeUploadRequest, + root, + ServerInfo, + testImage + } from '../' + + import { VideoDetails, VideoPrivacy } from '../../models/videos' + import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' + import { dateIsValid, webtorrentAdd } from '../miscs/miscs' + + type VideoAttributes = { + name?: string + category?: number + licence?: number + language?: string + nsfw?: boolean + commentsEnabled?: boolean + waitTranscoding?: boolean + description?: string + tags?: string[] + channelId?: number + privacy?: VideoPrivacy + fixture?: string + thumbnailfile?: string + previewfile?: string + scheduleUpdate?: { + updateAt: string + privacy?: VideoPrivacy + } + } + + function getVideoCategories (url: string) { + const path = '/api/v1/videos/categories' + + return makeGetRequest({ + url, + path, + statusCodeExpected: 200 + }) + } + + function getVideoLicences (url: string) { + const path = '/api/v1/videos/licences' + + return makeGetRequest({ + url, + path, + statusCodeExpected: 200 + }) + } + + function getVideoLanguages (url: string) { + const path = '/api/v1/videos/languages' + + return makeGetRequest({ + url, + path, + statusCodeExpected: 200 + }) + } + + function getVideoPrivacies (url: string) { + const path = '/api/v1/videos/privacies' + + return makeGetRequest({ + url, + path, + statusCodeExpected: 200 + }) + } + + function getVideo (url: string, id: number | string, expectedStatus = 200) { + const path = '/api/v1/videos/' + id + + return request(url) + .get(path) + .set('Accept', 'application/json') + .expect(expectedStatus) + } + + function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) { + const path = '/api/v1/videos/' + id + '/views' + + const req = request(url) + .post(path) + .set('Accept', 'application/json') + + if (xForwardedFor) { + req.set('X-Forwarded-For', xForwardedFor) + } + + return req.expect(expectedStatus) + } + + function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) { + const path = '/api/v1/videos/' + id + + return request(url) + .get(path) + .set('Authorization', 'Bearer ' + token) + .set('Accept', 'application/json') + .expect(expectedStatus) + } + + function getVideoDescription (url: string, descriptionPath: string) { + return request(url) + .get(descriptionPath) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function getVideosList (url: string) { + const path = '/api/v1/videos' + + return request(url) + .get(path) + .query({ sort: 'name' }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) { + const path = '/api/v1/videos' + + return request(url) + .get(path) + .set('Authorization', 'Bearer ' + token) + .query(immutableAssign(query, { sort: 'name' })) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function getLocalVideos (url: string) { + const path = '/api/v1/videos' + + return request(url) + .get(path) + .query({ sort: 'name', filter: 'local' }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) { + const path = '/api/v1/users/me/videos' + + const req = request(url) + .get(path) + .query({ start: start }) + .query({ count: count }) + + if (sort) req.query({ sort }) + + return req.set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(200) + .expect('Content-Type', /json/) + } + + function getAccountVideos ( + url: string, + accessToken: string, + accountName: string, + start: number, + count: number, + sort?: string, + query: { nsfw?: boolean } = {} + ) { + const path = '/api/v1/accounts/' + accountName + '/videos' + + return makeGetRequest({ + url, + path, + query: immutableAssign(query, { + start, + count, + sort + }), + token: accessToken, + statusCodeExpected: 200 + }) + } + + function getVideoChannelVideos ( + url: string, + accessToken: string, + videoChannelName: string, + start: number, + count: number, + sort?: string, + query: { nsfw?: boolean } = {} + ) { + const path = '/api/v1/video-channels/' + videoChannelName + '/videos' + + return makeGetRequest({ + url, + path, + query: immutableAssign(query, { + start, + count, + sort + }), + token: accessToken, + statusCodeExpected: 200 + }) + } + + function getVideosListPagination (url: string, start: number, count: number, sort?: string) { + const path = '/api/v1/videos' + + const req = request(url) + .get(path) + .query({ start: start }) + .query({ count: count }) + + if (sort) req.query({ sort }) + + return req.set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function getVideosListSort (url: string, sort: string) { + const path = '/api/v1/videos' + + return request(url) + .get(path) + .query({ sort: sort }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { + const path = '/api/v1/videos' + + return request(url) + .get(path) + .query(query) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + } + + function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) { + const path = '/api/v1/videos' + + return request(url) + .delete(path + '/' + id) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus) + } + + async function checkVideoFilesWereRemoved ( + videoUUID: string, + serverNumber: number, + directories = [ 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ] + ) { + const testDirectory = 'test' + serverNumber + + for (const directory of directories) { + const directoryPath = join(root(), testDirectory, directory) + + const directoryExists = existsSync(directoryPath) + expect(directoryExists).to.be.true + + const files = await readdir(directoryPath) + for (const file of files) { + expect(file).to.not.contain(videoUUID) + } + } + } + + async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) { + const path = '/api/v1/videos/upload' + let defaultChannelId = '1' + + try { + const res = await getMyUserInformation(url, accessToken) + defaultChannelId = res.body.videoChannels[0].id + } catch (e) { /* empty */ } + + // Override default attributes + const attributes = Object.assign({ + name: 'my super video', + category: 5, + licence: 4, + language: 'zh', + channelId: defaultChannelId, + nsfw: true, + waitTranscoding: false, + description: 'my super description', + support: 'my super support text', + tags: [ 'tag' ], + privacy: VideoPrivacy.PUBLIC, + commentsEnabled: true, + fixture: 'video_short.webm' + }, videoAttributesArg) + + const req = request(url) + .post(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .field('name', attributes.name) + .field('nsfw', JSON.stringify(attributes.nsfw)) + .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) + .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding)) + .field('privacy', attributes.privacy.toString()) + .field('channelId', attributes.channelId) + + if (attributes.description !== undefined) { + req.field('description', attributes.description) + } + if (attributes.language !== undefined) { + req.field('language', attributes.language.toString()) + } + if (attributes.category !== undefined) { + req.field('category', attributes.category.toString()) + } + if (attributes.licence !== undefined) { + req.field('licence', attributes.licence.toString()) + } + + for (let i = 0; i < attributes.tags.length; i++) { + req.field('tags[' + i + ']', attributes.tags[i]) + } + + if (attributes.thumbnailfile !== undefined) { + req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile)) + } + if (attributes.previewfile !== undefined) { + req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile)) + } + + if (attributes.scheduleUpdate) { + req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) + + if (attributes.scheduleUpdate.privacy) { + req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) + } + } + + return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) + .expect(specialStatus) + } + + function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) { + const path = '/api/v1/videos/' + id + const body = {} + + if (attributes.name) body['name'] = attributes.name + if (attributes.category) body['category'] = attributes.category + if (attributes.licence) body['licence'] = attributes.licence + if (attributes.language) body['language'] = attributes.language + if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw) + if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled) + if (attributes.description) body['description'] = attributes.description + if (attributes.tags) body['tags'] = attributes.tags + if (attributes.privacy) body['privacy'] = attributes.privacy + if (attributes.channelId) body['channelId'] = attributes.channelId + if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate + + // Upload request + if (attributes.thumbnailfile || attributes.previewfile) { + const attaches: any = {} + if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile + if (attributes.previewfile) attaches.previewfile = attributes.previewfile + + return makeUploadRequest({ + url, + method: 'PUT', + path, + token: accessToken, + fields: body, + attaches, + statusCodeExpected + }) + } + + return makePutBodyRequest({ + url, + path, + fields: body, + token: accessToken, + statusCodeExpected + }) + } + + function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) { + const path = '/api/v1/videos/' + id + '/rate' + + return request(url) + .put(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .send({ rating }) + .expect(specialStatus) + } + + function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) { + return new Promise((res, rej) => { + const torrentName = videoUUID + '-' + resolution + '.torrent' - const torrentPath = join(__dirname, '..', '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName) ++ const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName) + readFile(torrentPath, (err, data) => { + if (err) return rej(err) + + return res(parseTorrent(data)) + }) + }) + } + + async function completeVideoCheck ( + url: string, + video: any, + attributes: { + name: string + category: number + licence: number + language: string + nsfw: boolean + commentsEnabled: boolean + description: string + publishedAt?: string + support: string + account: { + name: string + host: string + } + isLocal: boolean + tags: string[] + privacy: number + likes?: number + dislikes?: number + duration: number + channel: { + displayName: string + name: string + description + isLocal: boolean + } + fixture: string + files: { + resolution: number + size: number + }[], + thumbnailfile?: string + previewfile?: string + } + ) { + if (!attributes.likes) attributes.likes = 0 + if (!attributes.dislikes) attributes.dislikes = 0 + + expect(video.name).to.equal(attributes.name) + expect(video.category.id).to.equal(attributes.category) + expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc') + expect(video.licence.id).to.equal(attributes.licence) + expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown') + expect(video.language.id).to.equal(attributes.language) + expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown') + expect(video.privacy.id).to.deep.equal(attributes.privacy) + expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) + expect(video.nsfw).to.equal(attributes.nsfw) + expect(video.description).to.equal(attributes.description) + expect(video.account.id).to.be.a('number') + expect(video.account.uuid).to.be.a('string') + expect(video.account.host).to.equal(attributes.account.host) + expect(video.account.name).to.equal(attributes.account.name) + expect(video.channel.displayName).to.equal(attributes.channel.displayName) + expect(video.channel.name).to.equal(attributes.channel.name) + expect(video.likes).to.equal(attributes.likes) + expect(video.dislikes).to.equal(attributes.dislikes) + expect(video.isLocal).to.equal(attributes.isLocal) + expect(video.duration).to.equal(attributes.duration) + expect(dateIsValid(video.createdAt)).to.be.true + expect(dateIsValid(video.publishedAt)).to.be.true + expect(dateIsValid(video.updatedAt)).to.be.true + + if (attributes.publishedAt) { + expect(video.publishedAt).to.equal(attributes.publishedAt) + } + + const res = await getVideo(url, video.uuid) + const videoDetails: VideoDetails = res.body + + expect(videoDetails.files).to.have.lengthOf(attributes.files.length) + expect(videoDetails.tags).to.deep.equal(attributes.tags) + expect(videoDetails.account.name).to.equal(attributes.account.name) + expect(videoDetails.account.host).to.equal(attributes.account.host) + expect(video.channel.displayName).to.equal(attributes.channel.displayName) + expect(video.channel.name).to.equal(attributes.channel.name) + expect(videoDetails.channel.host).to.equal(attributes.account.host) + expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) + expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true + expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true + expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) + + for (const attributeFile of attributes.files) { + const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution) + expect(file).not.to.be.undefined + + let extension = extname(attributes.fixture) + // Transcoding enabled on server 2, extension will always be .mp4 + if (attributes.account.host === 'localhost:9002') extension = '.mp4' + + const magnetUri = file.magnetUri + expect(file.magnetUri).to.have.lengthOf.above(2) + expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) + expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`) + expect(file.resolution.id).to.equal(attributeFile.resolution) + expect(file.resolution.label).to.equal(attributeFile.resolution + 'p') + + const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) + const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) + expect(file.size, + 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')') + .to.be.above(minSize).and.below(maxSize) + + { + await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) + } + + if (attributes.previewfile) { + await testImage(url, attributes.previewfile, videoDetails.previewPath) + } + + const torrent = await webtorrentAdd(magnetUri, true) + expect(torrent.files).to.be.an('array') + expect(torrent.files.length).to.equal(1) + expect(torrent.files[0].path).to.exist.and.to.not.equal('') + } + } + + // --------------------------------------------------------------------------- + + export { + getVideoDescription, + getVideoCategories, + getVideoLicences, + getVideoPrivacies, + getVideoLanguages, + getMyVideos, + getAccountVideos, + getVideoChannelVideos, + getVideo, + getVideoWithToken, + getVideosList, + getVideosListPagination, + getVideosListSort, + removeVideo, + getVideosListWithToken, + uploadVideo, + getVideosWithFilters, + updateVideo, + rateVideo, + viewVideo, + parseTorrentVideo, + getLocalVideos, + completeVideoCheck, + checkVideoFilesWereRemoved + }