はじまりの大地
このコミットが含まれているのは:
@@ -0,0 +1,342 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test AP cleaner', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
const sqlCommands: SQLCommand[] = []
|
||||
|
||||
let videoUUID1: string
|
||||
let videoUUID2: string
|
||||
let videoUUID3: string
|
||||
|
||||
let videoUUIDs: string[]
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const config = {
|
||||
federation: {
|
||||
videos: { cleanup_remote_interactions: true }
|
||||
}
|
||||
}
|
||||
servers = await createMultipleServers(3, config)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
await Promise.all([
|
||||
doubleFollow(servers[0], servers[1]),
|
||||
doubleFollow(servers[1], servers[2]),
|
||||
doubleFollow(servers[0], servers[2])
|
||||
])
|
||||
|
||||
// Update 1 local share, check 6 shares
|
||||
|
||||
// Create 1 comment per video
|
||||
// Update 1 remote URL and 1 local URL on
|
||||
|
||||
videoUUID1 = (await servers[0].videos.quickUpload({ name: 'server 1' })).uuid
|
||||
videoUUID2 = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid
|
||||
videoUUID3 = (await servers[2].videos.quickUpload({ name: 'server 3' })).uuid
|
||||
|
||||
videoUUIDs = [ videoUUID1, videoUUID2, videoUUID3 ]
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
for (const uuid of videoUUIDs) {
|
||||
await server.videos.rate({ id: uuid, rating: 'like' })
|
||||
await server.comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
}
|
||||
|
||||
sqlCommands.push(new SQLCommand(server))
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the correct likes', async function () {
|
||||
for (const server of servers) {
|
||||
for (const uuid of videoUUIDs) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
expect(video.likes).to.equal(3)
|
||||
expect(video.dislikes).to.equal(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should destroy server 3 internal likes and correctly clean them', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await sqlCommands[2].deleteAll('accountVideoRate')
|
||||
for (const uuid of videoUUIDs) {
|
||||
await sqlCommands[2].setVideoField(uuid, 'likes', '0')
|
||||
}
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
// Updated rates of my video
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: videoUUID1 })
|
||||
expect(video.likes).to.equal(2)
|
||||
expect(video.dislikes).to.equal(0)
|
||||
}
|
||||
|
||||
// Did not update rates of a remote video
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: videoUUID2 })
|
||||
expect(video.likes).to.equal(3)
|
||||
expect(video.dislikes).to.equal(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should update rates to dislikes', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
for (const server of servers) {
|
||||
for (const uuid of videoUUIDs) {
|
||||
await server.videos.rate({ id: uuid, rating: 'dislike' })
|
||||
}
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
for (const uuid of videoUUIDs) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
expect(video.likes).to.equal(0)
|
||||
expect(video.dislikes).to.equal(3)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should destroy server 3 internal dislikes and correctly clean them', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await sqlCommands[2].deleteAll('accountVideoRate')
|
||||
|
||||
for (const uuid of videoUUIDs) {
|
||||
await sqlCommands[2].setVideoField(uuid, 'dislikes', '0')
|
||||
}
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
// Updated rates of my video
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: videoUUID1 })
|
||||
expect(video.likes).to.equal(0)
|
||||
expect(video.dislikes).to.equal(2)
|
||||
}
|
||||
|
||||
// Did not update rates of a remote video
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: videoUUID2 })
|
||||
expect(video.likes).to.equal(0)
|
||||
expect(video.dislikes).to.equal(3)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should destroy server 3 internal shares and correctly clean them', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
const preCount = await sqlCommands[0].getVideoShareCount()
|
||||
expect(preCount).to.equal(6)
|
||||
|
||||
await sqlCommands[2].deleteAll('videoShare')
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
// Still 6 because we don't have remote shares on local videos
|
||||
const postCount = await sqlCommands[0].getVideoShareCount()
|
||||
expect(postCount).to.equal(6)
|
||||
})
|
||||
|
||||
it('Should destroy server 3 internal comments and correctly clean them', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
|
||||
expect(total).to.equal(3)
|
||||
}
|
||||
|
||||
await sqlCommands[2].deleteAll('videoComment')
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
|
||||
expect(total).to.equal(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly update rate URLs', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
|
||||
const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` +
|
||||
`INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'`
|
||||
const res = await sqlCommands[0].selectQuery<{ url: string }>(query)
|
||||
|
||||
for (const rate of res) {
|
||||
const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`)
|
||||
expect(rate.url).to.match(matcher)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLocal () {
|
||||
const startsWith = 'http://' + servers[0].host + '%'
|
||||
// On local videos
|
||||
await check(startsWith, servers[0].url, '', 'false')
|
||||
// On remote videos
|
||||
await check(startsWith, servers[0].url, '', 'true')
|
||||
}
|
||||
|
||||
async function checkRemote (suffix: string) {
|
||||
const startsWith = 'http://' + servers[1].host + '%'
|
||||
// On local videos
|
||||
await check(startsWith, servers[1].url, suffix, 'false')
|
||||
// On remote videos, we should not update URLs so no suffix
|
||||
await check(startsWith, servers[1].url, '', 'true')
|
||||
}
|
||||
|
||||
await checkLocal()
|
||||
await checkRemote('')
|
||||
|
||||
{
|
||||
const query = `UPDATE "accountVideoRate" SET url = url || 'stan'`
|
||||
await sqlCommands[1].updateQuery(query)
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
await checkLocal()
|
||||
await checkRemote('stan')
|
||||
})
|
||||
|
||||
it('Should correctly update comment URLs', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
|
||||
const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` +
|
||||
`INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'`
|
||||
|
||||
const res = await sqlCommands[0].selectQuery<{ url: string, videoUUID: string }>(query)
|
||||
|
||||
for (const comment of res) {
|
||||
const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`)
|
||||
expect(comment.url).to.match(matcher)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLocal () {
|
||||
const startsWith = 'http://' + servers[0].host + '%'
|
||||
// On local videos
|
||||
await check(startsWith, servers[0].url, '', 'false')
|
||||
// On remote videos
|
||||
await check(startsWith, servers[0].url, '', 'true')
|
||||
}
|
||||
|
||||
async function checkRemote (suffix: string) {
|
||||
const startsWith = 'http://' + servers[1].host + '%'
|
||||
// On local videos
|
||||
await check(startsWith, servers[1].url, suffix, 'false')
|
||||
// On remote videos, we should not update URLs so no suffix
|
||||
await check(startsWith, servers[1].url, '', 'true')
|
||||
}
|
||||
|
||||
{
|
||||
const query = `UPDATE "videoComment" SET url = url || 'kyle'`
|
||||
await sqlCommands[1].updateQuery(query)
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
await checkLocal()
|
||||
await checkRemote('kyle')
|
||||
})
|
||||
|
||||
it('Should remove unavailable remote resources', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
async function expectNotDeleted () {
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
|
||||
expect(video.likes).to.equal(3)
|
||||
expect(video.dislikes).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const { total } = await servers[0].comments.listThreads({ videoId: uuid })
|
||||
expect(total).to.equal(3)
|
||||
}
|
||||
}
|
||||
|
||||
async function expectDeleted () {
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
|
||||
expect(video.likes).to.equal(2)
|
||||
expect(video.dislikes).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const { total } = await servers[0].comments.listThreads({ videoId: uuid })
|
||||
expect(total).to.equal(2)
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = (await servers[0].videos.quickUpload({ name: 'server 1 video 2' })).uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
await server.videos.rate({ id: uuid, rating: 'like' })
|
||||
await server.comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await expectNotDeleted()
|
||||
|
||||
await servers[1].kill()
|
||||
|
||||
await wait(5000)
|
||||
await expectNotDeleted()
|
||||
|
||||
let continueWhile = true
|
||||
|
||||
do {
|
||||
try {
|
||||
await expectDeleted()
|
||||
continueWhile = false
|
||||
} catch {
|
||||
}
|
||||
} while (continueWhile)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
for (const sql of sqlCommands) {
|
||||
await sql.cleanup()
|
||||
}
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { processViewersStats } from '@tests/shared/views.js'
|
||||
import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeActivityPubGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test activitypub', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let video: { id: number, uuid: string, shortUUID: string }
|
||||
let playlist: { id: number, uuid: string, shortUUID: string }
|
||||
|
||||
async function testAccount (path: string) {
|
||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||
const object = res.body
|
||||
|
||||
expect(object.type).to.equal('Person')
|
||||
expect(object.id).to.equal(servers[0].url + '/accounts/root')
|
||||
expect(object.name).to.equal('root')
|
||||
expect(object.preferredUsername).to.equal('root')
|
||||
}
|
||||
|
||||
async function testChannel (path: string) {
|
||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||
const object = res.body
|
||||
|
||||
expect(object.type).to.equal('Group')
|
||||
expect(object.id).to.equal(servers[0].url + '/video-channels/root_channel')
|
||||
expect(object.name).to.equal('Main root channel')
|
||||
expect(object.preferredUsername).to.equal('root_channel')
|
||||
}
|
||||
|
||||
async function testVideo (path: string) {
|
||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||
const object = res.body
|
||||
|
||||
expect(object.type).to.equal('Video')
|
||||
expect(object.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
|
||||
expect(object.name).to.equal('video')
|
||||
}
|
||||
|
||||
async function testPlaylist (path: string) {
|
||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||
const object = res.body
|
||||
|
||||
expect(object.type).to.equal('Playlist')
|
||||
expect(object.id).to.equal(servers[0].url + '/video-playlists/' + playlist.uuid)
|
||||
expect(object.name).to.equal('playlist')
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
{
|
||||
video = await servers[0].videos.quickUpload({ name: 'video' })
|
||||
}
|
||||
|
||||
{
|
||||
const attributes = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].store.channel.id }
|
||||
playlist = await servers[0].playlists.create({ attributes })
|
||||
}
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
it('Should return the account object', async function () {
|
||||
await testAccount('/accounts/root')
|
||||
await testAccount('/a/root')
|
||||
})
|
||||
|
||||
it('Should return the channel object', async function () {
|
||||
await testChannel('/video-channels/root_channel')
|
||||
await testChannel('/c/root_channel')
|
||||
})
|
||||
|
||||
it('Should return the video object', async function () {
|
||||
await testVideo('/videos/watch/' + video.id)
|
||||
await testVideo('/videos/watch/' + video.uuid)
|
||||
await testVideo('/videos/watch/' + video.shortUUID)
|
||||
await testVideo('/w/' + video.id)
|
||||
await testVideo('/w/' + video.uuid)
|
||||
await testVideo('/w/' + video.shortUUID)
|
||||
})
|
||||
|
||||
it('Should return the playlist object', async function () {
|
||||
await testPlaylist('/video-playlists/' + playlist.id)
|
||||
await testPlaylist('/video-playlists/' + playlist.uuid)
|
||||
await testPlaylist('/video-playlists/' + playlist.shortUUID)
|
||||
await testPlaylist('/w/p/' + playlist.id)
|
||||
await testPlaylist('/w/p/' + playlist.uuid)
|
||||
await testPlaylist('/w/p/' + playlist.shortUUID)
|
||||
await testPlaylist('/videos/watch/playlist/' + playlist.id)
|
||||
await testPlaylist('/videos/watch/playlist/' + playlist.uuid)
|
||||
await testPlaylist('/videos/watch/playlist/' + playlist.shortUUID)
|
||||
})
|
||||
|
||||
it('Should redirect to the origin video object', async function () {
|
||||
const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + video.uuid, HttpStatusCode.FOUND_302)
|
||||
|
||||
expect(res.header.location).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
|
||||
})
|
||||
|
||||
it('Should return the watch action', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
await servers[0].views.simulateViewer({ id: video.uuid, currentTimes: [ 0, 2 ] })
|
||||
await processViewersStats(servers)
|
||||
|
||||
const res = await makeActivityPubGetRequest(servers[0].url, '/videos/local-viewer/1', HttpStatusCode.OK_200)
|
||||
|
||||
const object: WatchActionObject = res.body
|
||||
expect(object.type).to.equal('WatchAction')
|
||||
expect(object.duration).to.equal('PT2S')
|
||||
expect(object.actionStatus).to.equal('CompletedActionStatus')
|
||||
expect(object.watchSections).to.have.lengthOf(1)
|
||||
expect(object.watchSections[0].startTimestamp).to.equal(0)
|
||||
expect(object.watchSections[0].endTimestamp).to.equal(2)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test ActivityPub fetcher', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let sqlCommandServer1: SQLCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
servers = await createMultipleServers(3)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
const user = { username: 'user1', password: 'password' }
|
||||
for (const server of servers) {
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
}
|
||||
|
||||
const userAccessToken = await servers[0].login.getAccessToken(user)
|
||||
|
||||
await servers[0].videos.upload({ attributes: { name: 'video root' } })
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'bad video root' } })
|
||||
await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'video user' } })
|
||||
|
||||
sqlCommandServer1 = new SQLCommand(servers[0])
|
||||
|
||||
{
|
||||
const to = servers[0].url + '/accounts/user1'
|
||||
const value = servers[1].url + '/accounts/user1'
|
||||
await sqlCommandServer1.setActorField(to, 'url', value)
|
||||
}
|
||||
|
||||
{
|
||||
const value = servers[2].url + '/videos/watch/' + uuid
|
||||
await sqlCommandServer1.setVideoField(uuid, 'url', value)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should add only the video with a valid actor URL', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const { total, data } = await servers[0].videos.list({ sort: 'createdAt' })
|
||||
|
||||
expect(total).to.equal(3)
|
||||
expect(data[0].name).to.equal('video root')
|
||||
expect(data[1].name).to.equal('bad video root')
|
||||
expect(data[2].name).to.equal('video user')
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[1].videos.list({ sort: 'createdAt' })
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data[0].name).to.equal('video root')
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await sqlCommandServer1.cleanup()
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import './cleaner.js'
|
||||
import './client.js'
|
||||
import './fetch.js'
|
||||
import './refresher.js'
|
||||
import './security.js'
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
killallServers,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test AP refresher', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let sqlCommandServer2: SQLCommand
|
||||
let videoUUID1: string
|
||||
let videoUUID2: string
|
||||
let videoUUID3: string
|
||||
let playlistUUID1: string
|
||||
let playlistUUID2: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
await server.config.disableTranscoding()
|
||||
}
|
||||
|
||||
{
|
||||
videoUUID1 = (await servers[1].videos.quickUpload({ name: 'video1' })).uuid
|
||||
videoUUID2 = (await servers[1].videos.quickUpload({ name: 'video2' })).uuid
|
||||
videoUUID3 = (await servers[1].videos.quickUpload({ name: 'video3' })).uuid
|
||||
}
|
||||
|
||||
{
|
||||
const token1 = await servers[1].users.generateUserAndToken('user1')
|
||||
await servers[1].videos.upload({ token: token1, attributes: { name: 'video4' } })
|
||||
|
||||
const token2 = await servers[1].users.generateUserAndToken('user2')
|
||||
await servers[1].videos.upload({ token: token2, attributes: { name: 'video5' } })
|
||||
}
|
||||
|
||||
{
|
||||
const attributes = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
|
||||
const created = await servers[1].playlists.create({ attributes })
|
||||
playlistUUID1 = created.uuid
|
||||
}
|
||||
|
||||
{
|
||||
const attributes = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
|
||||
const created = await servers[1].playlists.create({ attributes })
|
||||
playlistUUID2 = created.uuid
|
||||
}
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
sqlCommandServer2 = new SQLCommand(servers[1])
|
||||
})
|
||||
|
||||
describe('Videos refresher', function () {
|
||||
|
||||
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 sqlCommandServer2.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
|
||||
|
||||
await servers[0].videos.get({ id: videoUUID1 })
|
||||
await servers[0].videos.get({ id: videoUUID2 })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].videos.get({ id: videoUUID1, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await servers[0].videos.get({ id: videoUUID2 })
|
||||
})
|
||||
|
||||
it('Should not update a remote video if the remote instance is down', async function () {
|
||||
this.timeout(70000)
|
||||
|
||||
await killallServers([ servers[1] ])
|
||||
|
||||
await sqlCommandServer2.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
|
||||
|
||||
// Video will need a refresh
|
||||
await wait(10000)
|
||||
|
||||
await servers[0].videos.get({ id: videoUUID3 })
|
||||
// The refresh should fail
|
||||
await waitJobs([ servers[0] ])
|
||||
|
||||
await servers[1].run()
|
||||
|
||||
await servers[0].videos.get({ id: videoUUID3 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Actors refresher', function () {
|
||||
|
||||
it('Should remove a deleted actor', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const command = servers[0].accounts
|
||||
|
||||
await wait(10000)
|
||||
|
||||
// Change actor name so the remote server returns a 404
|
||||
const to = servers[1].url + '/accounts/user2'
|
||||
await sqlCommandServer2.setActorField(to, 'preferredUsername', 'toto')
|
||||
|
||||
await command.get({ accountName: 'user1@' + servers[1].host })
|
||||
await command.get({ accountName: 'user2@' + servers[1].host })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await command.get({ accountName: 'user1@' + servers[1].host, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await command.get({ accountName: 'user2@' + servers[1].host, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Playlist refresher', function () {
|
||||
|
||||
it('Should remove a deleted playlist', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await wait(10000)
|
||||
|
||||
// Change UUID so the remote server returns a 404
|
||||
await sqlCommandServer2.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
|
||||
|
||||
await servers[0].playlists.get({ playlistId: playlistUUID1 })
|
||||
await servers[0].playlists.get({ playlistId: playlistUUID2 })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].playlists.get({ playlistId: playlistUUID1, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await servers[0].playlists.get({ playlistId: playlistUUID2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await sqlCommandServer2.cleanup()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,364 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { PeerTubeServer, cleanupTests, createMultipleServers, killallServers } from '@peertube/peertube-server-commands'
|
||||
import {
|
||||
activityPubContextify,
|
||||
buildGlobalHTTPHeaders,
|
||||
signAndContextify
|
||||
} from '@peertube/peertube-server/core/helpers/activity-pub-utils.js'
|
||||
import { buildDigest } from '@peertube/peertube-server/core/helpers/peertube-crypto.js'
|
||||
import { signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
|
||||
import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@peertube/peertube-server/core/initializers/constants.js'
|
||||
import { makePOSTAPRequest } from '@tests/shared/requests.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { expect } from 'chai'
|
||||
import { readJsonSync } from 'fs-extra/esm'
|
||||
|
||||
function signJsonLDObjectWithoutAssertion (options: Parameters<typeof signJsonLDObject>[0]) {
|
||||
return signJsonLDObject({
|
||||
...options,
|
||||
|
||||
disableWorkerThreadAssertion: true
|
||||
})
|
||||
}
|
||||
|
||||
function fakeFilter () {
|
||||
return (data: any) => Promise.resolve(data)
|
||||
}
|
||||
|
||||
function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) {
|
||||
const url = ofServerUrl + '/accounts/peertube'
|
||||
|
||||
return Promise.all([
|
||||
onServer.setActorField(url, 'publicKey', publicKey),
|
||||
onServer.setActorField(url, 'privateKey', privateKey)
|
||||
])
|
||||
}
|
||||
|
||||
function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) {
|
||||
const url = ofServerUrl + '/accounts/peertube'
|
||||
|
||||
return Promise.all([
|
||||
onServer.setActorField(url, 'createdAt', updatedAt),
|
||||
onServer.setActorField(url, 'updatedAt', updatedAt)
|
||||
])
|
||||
}
|
||||
|
||||
function getAnnounceWithoutContext (server: PeerTubeServer) {
|
||||
const json = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
|
||||
const result: typeof json = {}
|
||||
|
||||
for (const key of Object.keys(json)) {
|
||||
if (Array.isArray(json[key])) {
|
||||
result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
|
||||
} else {
|
||||
result[key] = json[key].replace(':9002', `:${server.port}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
|
||||
const follow = {
|
||||
type: 'Follow',
|
||||
id: by.url + '/' + new Date().getTime(),
|
||||
actor: by.url,
|
||||
object: to.url
|
||||
}
|
||||
|
||||
const body = await activityPubContextify(follow, 'Follow', fakeFilter())
|
||||
|
||||
const httpSignature = {
|
||||
algorithm: HTTP_SIGNATURE.ALGORITHM,
|
||||
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
|
||||
keyId: by.url,
|
||||
key: by.privateKey,
|
||||
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
|
||||
}
|
||||
const headers = {
|
||||
'digest': buildDigest(body),
|
||||
'content-type': 'application/activity+json',
|
||||
'accept': ACTIVITY_PUB.ACCEPT_HEADER
|
||||
}
|
||||
|
||||
return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
|
||||
}
|
||||
|
||||
describe('Test ActivityPub security', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let sqlCommands: SQLCommand[] = []
|
||||
|
||||
let url: string
|
||||
|
||||
const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
|
||||
const invalidKeys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
|
||||
const baseHttpSignature = () => ({
|
||||
algorithm: HTTP_SIGNATURE.ALGORITHM,
|
||||
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
|
||||
keyId: 'acct:peertube@' + servers[1].host,
|
||||
key: keys.privateKey,
|
||||
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
servers = await createMultipleServers(3)
|
||||
|
||||
sqlCommands = servers.map(s => new SQLCommand(s))
|
||||
|
||||
url = servers[0].url + '/inbox'
|
||||
|
||||
await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null)
|
||||
await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
|
||||
|
||||
const to = { url: servers[0].url + '/accounts/peertube' }
|
||||
const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
|
||||
await makeFollowRequest(to, by)
|
||||
})
|
||||
|
||||
describe('When checking HTTP signature', function () {
|
||||
|
||||
it('Should fail with an invalid digest', async function () {
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = {
|
||||
Digest: buildDigest({ hello: 'coucou' })
|
||||
}
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid date', async function () {
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = buildGlobalHTTPHeaders(body, buildDigest)
|
||||
headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with bad keys', async function () {
|
||||
await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
|
||||
await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
|
||||
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = buildGlobalHTTPHeaders(body, buildDigest)
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should reject requests without appropriate signed headers', async function () {
|
||||
await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
|
||||
await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
|
||||
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = buildGlobalHTTPHeaders(body, buildDigest)
|
||||
|
||||
const signatureOptions = baseHttpSignature()
|
||||
const badHeadersMatrix = [
|
||||
[ '(request-target)', 'date', 'digest' ],
|
||||
[ 'host', 'date', 'digest' ],
|
||||
[ '(request-target)', 'host', 'digest' ]
|
||||
]
|
||||
|
||||
for (const badHeaders of badHeadersMatrix) {
|
||||
signatureOptions.headers = badHeaders
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, body, signatureOptions, headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = buildGlobalHTTPHeaders(body, buildDigest)
|
||||
|
||||
const signatureOptions = baseHttpSignature()
|
||||
signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
|
||||
|
||||
const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
|
||||
expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
|
||||
})
|
||||
|
||||
it('Should succeed with a valid HTTP signature', async function () {
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = buildGlobalHTTPHeaders(body, buildDigest)
|
||||
|
||||
const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
|
||||
expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
|
||||
})
|
||||
|
||||
it('Should refresh the actor keys', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
// Update keys of server 2 to invalid keys
|
||||
// Server 1 should refresh the actor and fail
|
||||
await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
|
||||
await setUpdatedAtOfServer(sqlCommands[0], servers[1].url, '2015-07-17 22:00:00+00')
|
||||
|
||||
// Invalid peertube actor cache
|
||||
await killallServers([ servers[1] ])
|
||||
await servers[1].run()
|
||||
|
||||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
|
||||
const headers = buildGlobalHTTPHeaders(body, buildDigest)
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When checking Linked Data Signature', function () {
|
||||
before(async function () {
|
||||
await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
|
||||
await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
|
||||
await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey)
|
||||
|
||||
const to = { url: servers[0].url + '/accounts/peertube' }
|
||||
const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
|
||||
await makeFollowRequest(to, by)
|
||||
})
|
||||
|
||||
it('Should fail with bad keys', async function () {
|
||||
await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
|
||||
await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
|
||||
|
||||
const body = getAnnounceWithoutContext(servers[1])
|
||||
body.actor = servers[2].url + '/accounts/peertube'
|
||||
|
||||
const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
|
||||
const signedBody = await signAndContextify({
|
||||
byActor: signer,
|
||||
data: body,
|
||||
contextType: 'Announce',
|
||||
contextFilter: fakeFilter(),
|
||||
signerFunction: signJsonLDObjectWithoutAssertion
|
||||
})
|
||||
|
||||
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an altered body', async function () {
|
||||
await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
|
||||
await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
|
||||
|
||||
const body = getAnnounceWithoutContext(servers[1])
|
||||
body.actor = servers[2].url + '/accounts/peertube'
|
||||
|
||||
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
|
||||
const signedBody: any = await signAndContextify({
|
||||
byActor: signer,
|
||||
data: body,
|
||||
contextType: 'Announce',
|
||||
contextFilter: fakeFilter(),
|
||||
signerFunction: signJsonLDObjectWithoutAssertion
|
||||
})
|
||||
|
||||
signedBody.actor = servers[2].url + '/account/peertube'
|
||||
|
||||
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with a valid signature', async function () {
|
||||
const body = getAnnounceWithoutContext(servers[1])
|
||||
body.actor = servers[2].url + '/accounts/peertube'
|
||||
|
||||
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
|
||||
const signedBody = await signAndContextify({
|
||||
byActor: signer,
|
||||
data: body,
|
||||
contextType: 'Announce',
|
||||
contextFilter: fakeFilter(),
|
||||
signerFunction: signJsonLDObjectWithoutAssertion
|
||||
})
|
||||
|
||||
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
|
||||
|
||||
const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
|
||||
expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
|
||||
})
|
||||
|
||||
it('Should refresh the actor keys', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
// Wait refresh invalidation
|
||||
await wait(10000)
|
||||
|
||||
// Update keys of server 3 to invalid keys
|
||||
// Server 1 should refresh the actor and fail
|
||||
await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
|
||||
|
||||
const body = getAnnounceWithoutContext(servers[1])
|
||||
body.actor = servers[2].url + '/accounts/peertube'
|
||||
|
||||
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
|
||||
const signedBody = await signAndContextify({
|
||||
byActor: signer,
|
||||
data: body,
|
||||
contextType: 'Announce',
|
||||
contextFilter: fakeFilter(),
|
||||
signerFunction: signJsonLDObjectWithoutAssertion
|
||||
})
|
||||
|
||||
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
|
||||
expect(true, 'Did not throw').to.be.false
|
||||
} catch (err) {
|
||||
expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
for (const sql of sqlCommands) {
|
||||
await sql.cleanup()
|
||||
}
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,438 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { AbuseCreate, AbuseState, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
AbusesCommand,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
doubleFollow,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test abuses API validators', function () {
|
||||
const basePath = '/api/v1/abuses/'
|
||||
|
||||
let server: PeerTubeServer
|
||||
|
||||
let userToken = ''
|
||||
let userToken2 = ''
|
||||
let abuseId: number
|
||||
let messageId: number
|
||||
|
||||
let command: AbusesCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user_1')
|
||||
userToken2 = await server.users.generateUserAndToken('user_2')
|
||||
|
||||
server.store.videoCreated = await server.videos.upload()
|
||||
|
||||
command = server.abuses
|
||||
})
|
||||
|
||||
describe('When listing abuses for admins', function () {
|
||||
const path = basePath
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad id filter', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad filter', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } })
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } })
|
||||
})
|
||||
|
||||
it('Should fail with bad predefined reason', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad state filter', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad videoIs filter', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const query = {
|
||||
id: 13,
|
||||
predefinedReason: 'violentOrRepulsive',
|
||||
filter: 'comment',
|
||||
state: 2,
|
||||
videoIs: 'deleted'
|
||||
}
|
||||
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing abuses for users', function () {
|
||||
const path = '/api/v1/users/me/abuses'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad id filter', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, query: { id: 'toto' } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad state filter', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 'toto' } })
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 0 } })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const query = {
|
||||
id: 13,
|
||||
state: 2
|
||||
}
|
||||
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When reporting an abuse', function () {
|
||||
const path = basePath
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a wrong video', async function () {
|
||||
const fields = { video: { id: 'blabla' }, reason: 'my super reason' }
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video', async function () {
|
||||
const fields = { video: { id: 42 }, reason: 'my super reason' }
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a wrong comment', async function () {
|
||||
const fields = { comment: { id: 'blabla' }, reason: 'my super reason' }
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown comment', async function () {
|
||||
const fields = { comment: { id: 42 }, reason: 'my super reason' }
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a wrong account', async function () {
|
||||
const fields = { account: { id: 'blabla' }, reason: 'my super reason' }
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
const fields = { account: { id: 42 }, reason: 'my super reason' }
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with not account, comment or video', async function () {
|
||||
const fields = { reason: 'my super reason' }
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
const fields = { video: { id: server.store.videoCreated.id }, reason: 'my super reason' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a reason too short', async function () {
|
||||
const fields = { video: { id: server.store.videoCreated.id }, reason: 'h' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too big reason', async function () {
|
||||
const fields = { video: { id: server.store.videoCreated.id }, reason: 'super'.repeat(605) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters (basic)', async function () {
|
||||
const fields: AbuseCreate = { video: { id: server.store.videoCreated.shortUUID }, reason: 'my super reason' }
|
||||
|
||||
const res = await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
abuseId = res.body.abuse.id
|
||||
})
|
||||
|
||||
it('Should fail with a wrong predefined reason', async function () {
|
||||
const fields = { video: server.store.videoCreated, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with negative timestamps', async function () {
|
||||
const fields = { video: { id: server.store.videoCreated.id, startAt: -1 }, reason: 'my super reason' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail mith misordered startAt/endAt', async function () {
|
||||
const fields = { video: { id: server.store.videoCreated.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters (advanced)', async function () {
|
||||
const fields: AbuseCreate = {
|
||||
video: {
|
||||
id: server.store.videoCreated.id,
|
||||
startAt: 1,
|
||||
endAt: 5
|
||||
},
|
||||
reason: 'my super reason',
|
||||
predefinedReasons: [ 'serverRules' ]
|
||||
}
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating an abuse', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.update({ token: 'blabla', abuseId, body: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await command.update({ token: userToken, abuseId, body: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad abuse id', async function () {
|
||||
await command.update({ abuseId: 45, body: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad state', async function () {
|
||||
const body = { state: 5 as any }
|
||||
await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad moderation comment', async function () {
|
||||
const body = { moderationComment: 'b'.repeat(3001) }
|
||||
await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const body = { state: AbuseState.ACCEPTED }
|
||||
await command.update({ abuseId, body })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When creating an abuse message', function () {
|
||||
const message = 'my super message'
|
||||
|
||||
it('Should fail with an invalid abuse id', async function () {
|
||||
await command.addMessage({ token: userToken2, abuseId: 888, message, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.addMessage({ token: 'fake_token', abuseId, message, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid logged in user', async function () {
|
||||
await command.addMessage({ token: userToken2, abuseId, message, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid message', async function () {
|
||||
await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const res = await command.addMessage({ token: userToken, abuseId, message })
|
||||
messageId = res.body.abuseMessage.id
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing abuse messages', function () {
|
||||
|
||||
it('Should fail with an invalid abuse id', async function () {
|
||||
await command.listMessages({ token: userToken, abuseId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.listMessages({ token: 'fake_token', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid logged in user', async function () {
|
||||
await command.listMessages({ token: userToken2, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.listMessages({ token: userToken, abuseId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting an abuse message', function () {
|
||||
it('Should fail with an invalid abuse id', async function () {
|
||||
await command.deleteMessage({ token: userToken, abuseId: 888, messageId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid message id', async function () {
|
||||
await command.deleteMessage({ token: userToken, abuseId, messageId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.deleteMessage({ token: 'fake_token', abuseId, messageId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid logged in user', async function () {
|
||||
await command.deleteMessage({ token: userToken2, abuseId, messageId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.deleteMessage({ token: userToken, abuseId, messageId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting a video abuse', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.delete({ token: 'blabla', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await command.delete({ token: userToken, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad abuse id', async function () {
|
||||
await command.delete({ abuseId: 45, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.delete({ abuseId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When trying to manage messages of a remote abuse', function () {
|
||||
let remoteAbuseId: number
|
||||
let anotherServer: PeerTubeServer
|
||||
|
||||
before(async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
anotherServer = await createSingleServer(2)
|
||||
await setAccessTokensToServers([ anotherServer ])
|
||||
|
||||
await doubleFollow(anotherServer, server)
|
||||
|
||||
const server2VideoId = await anotherServer.videos.getId({ uuid: server.store.videoCreated.uuid })
|
||||
await anotherServer.abuses.report({ reason: 'remote server', videoId: server2VideoId })
|
||||
|
||||
await waitJobs([ server, anotherServer ])
|
||||
|
||||
const body = await command.getAdminList({ sort: '-createdAt' })
|
||||
remoteAbuseId = body.data[0].id
|
||||
})
|
||||
|
||||
it('Should fail when listing abuse messages of a remote abuse', async function () {
|
||||
await command.listMessages({ abuseId: remoteAbuseId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail when creating abuse message of a remote abuse', async function () {
|
||||
await command.addMessage({ abuseId: remoteAbuseId, message: 'message', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ anotherServer ])
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test accounts API validators', function () {
|
||||
const path = '/api/v1/accounts/'
|
||||
let server: PeerTubeServer
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
})
|
||||
|
||||
describe('When listing accounts', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting an account', function () {
|
||||
|
||||
it('Should return 404 with a non existing name', async function () {
|
||||
await server.accounts.get({ accountName: 'arfaze', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer, setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test auto tag policies API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
let userToken: string
|
||||
let userToken2: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
userToken2 = await server.users.generateUserAndToken('user2')
|
||||
})
|
||||
|
||||
describe('When getting available account auto tags', function () {
|
||||
const baseParams = () => ({ accountName: 'user1', token: userToken })
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.autoTags.getAccountAvailable({ ...baseParams(), token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a user that cannot manage account', async function () {
|
||||
await server.autoTags.getAccountAvailable({ ...baseParams(), token: userToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await server.autoTags.getAccountAvailable({ ...baseParams(), accountName: 'user42', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.autoTags.getAccountAvailable(baseParams())
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting available server auto tags', function () {
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.autoTags.getServerAvailable({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a user that that does not have enought rights', async function () {
|
||||
await server.autoTags.getServerAvailable({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.autoTags.getServerAvailable()
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting auto tag policies', function () {
|
||||
const baseParams = () => ({ accountName: 'user1', token: userToken })
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.autoTags.getCommentPolicies({ ...baseParams(), token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a user that cannot manage account', async function () {
|
||||
await server.autoTags.getCommentPolicies({ ...baseParams(), token: userToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await server.autoTags.getCommentPolicies({ ...baseParams(), accountName: 'user42', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.autoTags.getCommentPolicies(baseParams())
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating auto tag policies', function () {
|
||||
const baseParams = () => ({ accountName: 'user1', review: [ 'external-link' ], token: userToken })
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.autoTags.updateCommentPolicies({
|
||||
...baseParams(),
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user that cannot manage account', async function () {
|
||||
await server.autoTags.updateCommentPolicies({
|
||||
...baseParams(),
|
||||
token: userToken2,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await server.autoTags.updateCommentPolicies({
|
||||
...baseParams(),
|
||||
accountName: 'user42',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid review array', async function () {
|
||||
await server.autoTags.updateCommentPolicies({
|
||||
...baseParams(),
|
||||
review: 'toto' as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with review array that does not contain available tags', async function () {
|
||||
await server.autoTags.updateCommentPolicies({
|
||||
...baseParams(),
|
||||
review: [ 'toto' ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.autoTags.updateCommentPolicies(baseParams())
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,556 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test blocklist API validators', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
server = servers[0]
|
||||
|
||||
const user = { username: 'user1', password: 'password' }
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
describe('When managing user blocklist', function () {
|
||||
|
||||
describe('When managing user accounts blocklist', function () {
|
||||
const path = '/api/v1/users/me/blocklist/accounts'
|
||||
|
||||
describe('When listing blocked accounts', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blocking an account', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { accountName: 'user1' },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user2' },
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to block ourselves', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'root' },
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user1' },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When unblocking an account', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account block', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user2',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When managing user servers blocklist', function () {
|
||||
const path = '/api/v1/users/me/blocklist/servers'
|
||||
|
||||
describe('When listing blocked servers', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blocking a server', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { host: '127.0.0.1:9002' },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with an unknown server', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { host: '127.0.0.1:9003' },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with our own server', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { host: server.host },
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { host: servers[1].host },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When unblocking a server', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/' + servers[1].host,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown server block', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/127.0.0.1:9004',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/' + servers[1].host,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When managing server blocklist', function () {
|
||||
|
||||
describe('When managing server accounts blocklist', function () {
|
||||
const path = '/api/v1/server/blocklist/accounts'
|
||||
|
||||
describe('When listing blocked accounts', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user without the appropriate rights', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blocking an account', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { accountName: 'user1' },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user without the appropriate rights', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path,
|
||||
fields: { accountName: 'user1' },
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user2' },
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to block ourselves', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'root' },
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user1' },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When unblocking an account', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user without the appropriate rights', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1',
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account block', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user2',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When managing server servers blocklist', function () {
|
||||
const path = '/api/v1/server/blocklist/servers'
|
||||
|
||||
describe('When listing blocked servers', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user without the appropriate rights', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blocking a server', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { host: servers[1].host },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user without the appropriate rights', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path,
|
||||
fields: { host: servers[1].host },
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with an unknown server', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { host: '127.0.0.1:9003' },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with our own server', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { host: server.host },
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { host: servers[1].host },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When unblocking a server', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/' + servers[1].host,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a user without the appropriate rights', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/' + servers[1].host,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown server block', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/127.0.0.1:9004',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/' + servers[1].host,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting blocklist status', function () {
|
||||
const path = '/api/v1/blocklist/status'
|
||||
|
||||
it('Should fail with a bad token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: 'false',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad accounts field', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
accounts: 1
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
accounts: [ 1 ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad hosts field', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
hosts: 1
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
hosts: [ 1 ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {},
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
hosts: [ 'example.com' ],
|
||||
accounts: [ 'john@example.com' ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test bulk API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = { username: 'user1', password: 'password' }
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When removing comments of', function () {
|
||||
const path = '/api/v1/bulk/remove-comments-of'
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { accountName: 'user1', scope: 'my-videos' },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user2', scope: 'my-videos' },
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid scope', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user1', scope: 'my-videoss' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to delete comments of the instance without the appropriate rights', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path,
|
||||
fields: { accountName: 'user1', scope: 'instance' },
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
fields: { accountName: 'user1', scope: 'instance' },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
ChannelsCommand,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test videos import in a channel API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
const userInfo = {
|
||||
accessToken: '',
|
||||
channelName: 'fake_channel',
|
||||
channelId: -1,
|
||||
id: -1,
|
||||
videoQuota: -1,
|
||||
videoQuotaDaily: -1,
|
||||
channelSyncId: -1
|
||||
}
|
||||
let command: ChannelsCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableVideoImports()
|
||||
await server.config.enableChannelSync()
|
||||
|
||||
const userCreds = {
|
||||
username: 'fake',
|
||||
password: 'fake_password'
|
||||
}
|
||||
|
||||
{
|
||||
const user = await server.users.create({ username: userCreds.username, password: userCreds.password })
|
||||
userInfo.id = user.id
|
||||
userInfo.accessToken = await server.login.getAccessToken(userCreds)
|
||||
|
||||
const info = await server.users.getMyInfo({ token: userInfo.accessToken })
|
||||
userInfo.channelId = info.videoChannels[0].id
|
||||
}
|
||||
|
||||
{
|
||||
const { videoChannelSync } = await server.channelSyncs.create({
|
||||
token: userInfo.accessToken,
|
||||
attributes: {
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
videoChannelId: userInfo.channelId
|
||||
}
|
||||
})
|
||||
userInfo.channelSyncId = videoChannelSync.id
|
||||
}
|
||||
|
||||
command = server.channels
|
||||
})
|
||||
|
||||
it('Should fail when HTTP upload is disabled', async function () {
|
||||
await server.config.disableChannelSync()
|
||||
await server.config.disableVideoImports()
|
||||
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
|
||||
await server.config.enableVideoImports()
|
||||
})
|
||||
|
||||
it('Should fail when externalChannelUrl is not provided', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: null,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when externalChannelUrl is malformed', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: 'not-a-url',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad sync id', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
videoChannelSyncId: 'toto' as any,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a unknown sync id', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
videoChannelSyncId: 42,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a sync id of another channel', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
videoChannelSyncId: userInfo.channelSyncId,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with no authentication', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when sync is not owned by the user', async function () {
|
||||
await command.importVideos({
|
||||
channelName: server.store.channel.name,
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
token: userInfo.accessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when the user has no quota', async function () {
|
||||
await server.users.update({
|
||||
userId: userInfo.id,
|
||||
videoQuota: 0
|
||||
})
|
||||
|
||||
await command.importVideos({
|
||||
channelName: 'fake_channel',
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
token: userInfo.accessToken,
|
||||
expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
|
||||
})
|
||||
|
||||
await server.users.update({
|
||||
userId: userInfo.id,
|
||||
videoQuota: userInfo.videoQuota
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when the user has no daily quota', async function () {
|
||||
await server.users.update({
|
||||
userId: userInfo.id,
|
||||
videoQuotaDaily: 0
|
||||
})
|
||||
|
||||
await command.importVideos({
|
||||
channelName: 'fake_channel',
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
token: userInfo.accessToken,
|
||||
expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
|
||||
})
|
||||
|
||||
await server.users.update({
|
||||
userId: userInfo.id,
|
||||
videoQuotaDaily: userInfo.videoQuotaDaily
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed when sync is run by its owner', async function () {
|
||||
if (!areHttpImportTestsDisabled()) return
|
||||
|
||||
await command.importVideos({
|
||||
channelName: 'fake_channel',
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
token: userInfo.accessToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed when sync is run with root and for another user\'s channel', async function () {
|
||||
if (!areHttpImportTestsDisabled()) return
|
||||
|
||||
await command.importVideos({
|
||||
channelName: 'fake_channel',
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,305 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
import merge from 'lodash-es/merge.js'
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { ActorImageType, CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
|
||||
describe('Test config API validators', function () {
|
||||
const path = '/api/v1/config/custom'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
|
||||
let updateParams: CustomConfig
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
updateParams = await server.config.getCustomConfig()
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When getting the configuration', function () {
|
||||
it('Should fail without token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating the configuration', function () {
|
||||
it('Should fail without token', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: updateParams,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: updateParams,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if it misses a key', async function () {
|
||||
const newUpdateParams = { ...updateParams, admin: omit(updateParams.admin, [ 'email' ]) }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: newUpdateParams,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad default NSFW policy', async function () {
|
||||
const newUpdateParams = {
|
||||
...updateParams,
|
||||
|
||||
instance: {
|
||||
defaultNSFWPolicy: 'hello'
|
||||
}
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: newUpdateParams,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if email disabled and signup requires email verification', async function () {
|
||||
// opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts
|
||||
const newUpdateParams = {
|
||||
...updateParams,
|
||||
|
||||
signup: {
|
||||
enabled: true,
|
||||
limit: 5,
|
||||
requiresApproval: true,
|
||||
requiresEmailVerification: true
|
||||
}
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: newUpdateParams,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a disabled web videos & hls transcoding', async function () {
|
||||
const newUpdateParams = {
|
||||
...updateParams,
|
||||
|
||||
transcoding: {
|
||||
hls: {
|
||||
enabled: false
|
||||
},
|
||||
web_videos: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: newUpdateParams,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a disabled http upload & enabled sync', async function () {
|
||||
const newUpdateParams: CustomConfig = merge({}, updateParams, {
|
||||
import: {
|
||||
videos: {
|
||||
http: { enabled: false }
|
||||
},
|
||||
videoChannelSynchronization: { enabled: true }
|
||||
}
|
||||
})
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: newUpdateParams,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: updateParams,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting the configuration', function () {
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Updating instance image', function () {
|
||||
const toTest = [
|
||||
{ path: '/api/v1/config/instance-banner/pick', attachName: 'bannerfile' },
|
||||
{ path: '/api/v1/config/instance-avatar/pick', attachName: 'avatarfile' }
|
||||
]
|
||||
|
||||
it('Should fail with an incorrect input file', async function () {
|
||||
for (const { attachName, path } of toTest) {
|
||||
const attaches = { [attachName]: buildAbsoluteFixturePath('video_short.mp4') }
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields: {}, attaches })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a big file', async function () {
|
||||
for (const { attachName, path } of toTest) {
|
||||
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar-big.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
for (const { attachName, path } of toTest) {
|
||||
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail without the appropriate rights', async function () {
|
||||
for (const { attachName, path } of toTest) {
|
||||
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
for (const { attachName, path } of toTest) {
|
||||
const attaches = { [attachName]: buildAbsoluteFixturePath('avatar.png') }
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: {},
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deleting instance image', function () {
|
||||
const types = [ ActorImageType.BANNER, ActorImageType.AVATAR ]
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
for (const type of types) {
|
||||
await server.config.deleteInstanceImage({ type, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail without the appropriate rights', async function () {
|
||||
for (const type of types) {
|
||||
await server.config.deleteInstanceImage({ type, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
for (const type of types) {
|
||||
await server.config.deleteInstanceImage({ type })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
ConfigCommand,
|
||||
ContactFormCommand,
|
||||
createSingleServer,
|
||||
killallServers,
|
||||
PeerTubeServer
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test contact form API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
const emails: object[] = []
|
||||
const defaultBody = {
|
||||
fromName: 'super name',
|
||||
fromEmail: 'toto@example.com',
|
||||
subject: 'my subject',
|
||||
body: 'Hello, how are you?'
|
||||
}
|
||||
let emailPort: number
|
||||
let command: ContactFormCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
emailPort = await MockSmtpServer.Instance.collectEmails(emails)
|
||||
|
||||
// Email is disabled
|
||||
server = await createSingleServer(1)
|
||||
command = server.contactForm
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if emails are disabled', async function () {
|
||||
await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if it is disabled in the configuration', async function () {
|
||||
this.timeout(25000)
|
||||
|
||||
await killallServers([ server ])
|
||||
|
||||
// Contact form is disabled
|
||||
await server.run({ ...ConfigCommand.getEmailOverrideConfig(emailPort), contact_form: { enabled: false } })
|
||||
await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if from email is invalid', async function () {
|
||||
this.timeout(25000)
|
||||
|
||||
await killallServers([ server ])
|
||||
|
||||
// Email & contact form enabled
|
||||
await server.run(ConfigCommand.getEmailOverrideConfig(emailPort))
|
||||
|
||||
await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.send({ ...defaultBody, fromEmail: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if from name is invalid', async function () {
|
||||
await command.send({ ...defaultBody, fromName: 'name'.repeat(100), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.send({ ...defaultBody, fromName: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.send({ ...defaultBody, fromName: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should not accept a contact form if body is invalid', async function () {
|
||||
await command.send({ ...defaultBody, body: 'body'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.send({ ...defaultBody, body: 'a', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.send({ ...defaultBody, body: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should accept a contact form with the correct parameters', async function () {
|
||||
await command.send(defaultBody)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test custom pages validators', function () {
|
||||
const path = '/api/v1/custom-pages/homepage/instance'
|
||||
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = { username: 'user1', password: 'password' }
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When updating instance homepage', function () {
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { content: 'super content' },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
fields: { content: 'super content' },
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { content: 'super content' },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting instance homapage', function () {
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test debug API validators', function () {
|
||||
const path = '/api/v1/server/debug'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'my super password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When getting debug endpoint', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: { startDate: new Date().toISOString() },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,369 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test server follows API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
describe('When managing following', function () {
|
||||
let userAccessToken = null
|
||||
|
||||
before(async function () {
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
})
|
||||
|
||||
describe('When adding follows', function () {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if hosts is not composed by hosts', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { hosts: [ '127.0.0.1:9002', '127.0.0.1:coucou' ] },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if hosts is composed with http schemes', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { hosts: [ '127.0.0.1:9002', 'http://127.0.0.1:9003' ] },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if hosts are not unique', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { urls: [ '127.0.0.1:9002', '127.0.0.1:9002' ] },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if handles is not composed by handles', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { handles: [ 'hello@example.com', '127.0.0.1:9001' ] },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if handles are not unique', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { urls: [ 'hello@example.com', 'hello@example.com' ] },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { hosts: [ '127.0.0.1:9002' ] },
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { hosts: [ '127.0.0.1:9002' ] },
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing followings', function () {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect state', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
state: 'blabla'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect actor type', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
actorType: 'blabla'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.OK_200,
|
||||
query: {
|
||||
state: 'accepted',
|
||||
actorType: 'Application'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing followers', function () {
|
||||
const path = '/api/v1/server/followers'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect actor type', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
actorType: 'blabla'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect state', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
state: 'blabla',
|
||||
actorType: 'Application'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.OK_200,
|
||||
query: {
|
||||
state: 'accepted'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing a follower', function () {
|
||||
const path = '/api/v1/server/followers'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9002',
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9002',
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid follower', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown follower', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9003',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When accepting a follower', function () {
|
||||
const path = '/api/v1/server/followers'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9002/accept',
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9002/accept',
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid follower', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto/accept',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown follower', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9003/accept',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When rejecting a follower', function () {
|
||||
const path = '/api/v1/server/followers'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9002/reject',
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9002/reject',
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid follower', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto/reject',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown follower', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '/toto@127.0.0.1:9003/reject',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing following', function () {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/127.0.0.1:9002',
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/127.0.0.1:9002',
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if we do not follow this server', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/example.com',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import './abuses.js'
|
||||
import './accounts.js'
|
||||
import './auto-tags.js'
|
||||
import './blocklist.js'
|
||||
import './bulk.js'
|
||||
import './channel-import-videos.js'
|
||||
import './config.js'
|
||||
import './contact-form.js'
|
||||
import './custom-pages.js'
|
||||
import './debug.js'
|
||||
import './follows.js'
|
||||
import './jobs.js'
|
||||
import './live.js'
|
||||
import './logs.js'
|
||||
import './metrics.js'
|
||||
import './my-user.js'
|
||||
import './plugins.js'
|
||||
import './redundancy.js'
|
||||
import './registrations.js'
|
||||
import './runners.js'
|
||||
import './search.js'
|
||||
import './services.js'
|
||||
import './transcoding.js'
|
||||
import './two-factor.js'
|
||||
import './upload-quota.js'
|
||||
import './user-export.js'
|
||||
import './user-import.js'
|
||||
import './user-notifications.js'
|
||||
import './user-subscriptions.js'
|
||||
import './users-admin.js'
|
||||
import './users-emails.js'
|
||||
import './video-blacklist.js'
|
||||
import './video-captions.js'
|
||||
import './video-channel-syncs.js'
|
||||
import './video-channels.js'
|
||||
import './video-chapters.js'
|
||||
import './video-comments.js'
|
||||
import './video-files.js'
|
||||
import './video-imports.js'
|
||||
import './video-playlists.js'
|
||||
import './video-source.js'
|
||||
import './video-storyboards.js'
|
||||
import './video-studio.js'
|
||||
import './video-token.js'
|
||||
import './video-transcription.js'
|
||||
import './videos-common-filters.js'
|
||||
import './videos-history.js'
|
||||
import './videos-overviews.js'
|
||||
import './videos.js'
|
||||
import './views.js'
|
||||
import './watched-words.js'
|
||||
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test jobs API validators', function () {
|
||||
const path = '/api/v1/jobs/failed'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'my super password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When listing jobs', function () {
|
||||
|
||||
it('Should fail with a bad state', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: path + 'ade'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect job type', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: {
|
||||
jobType: 'toto'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When pausing/resuming the job queue', async function () {
|
||||
const commands = [ 'pause', 'resume' ]
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
for (const command of commands) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/jobs/' + command,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
for (const command of commands) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/jobs/' + command,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
for (const command of commands) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/jobs/' + command,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,576 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
VideoCommentPolicy,
|
||||
VideoCreateResult,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
LiveCommand,
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
makeUploadRequest,
|
||||
sendRTMPStream,
|
||||
setAccessTokensToServers,
|
||||
stopFfmpeg
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test video lives API validator', function () {
|
||||
const path = '/api/v1/videos/live'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
let channelId: number
|
||||
let video: VideoCreateResult
|
||||
let videoIdNotLive: number
|
||||
let command: LiveCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.config.enableMinimumTranscoding()
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
maxInstanceLives: 20,
|
||||
maxUserLives: 20,
|
||||
allowReplay: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const username = 'user1'
|
||||
const password = 'my super password'
|
||||
await server.users.create({ username, password })
|
||||
userAccessToken = await server.login.getAccessToken({ username, password })
|
||||
|
||||
{
|
||||
const { videoChannels } = await server.users.getMyInfo()
|
||||
channelId = videoChannels[0].id
|
||||
}
|
||||
|
||||
{
|
||||
videoIdNotLive = (await server.videos.quickUpload({ name: 'not live' })).id
|
||||
}
|
||||
|
||||
command = server.live
|
||||
})
|
||||
|
||||
describe('When creating a live', function () {
|
||||
let baseCorrectParams: LiveVideoCreate
|
||||
|
||||
before(function () {
|
||||
baseCorrectParams = {
|
||||
name: 'my super name',
|
||||
category: 5,
|
||||
licence: 1,
|
||||
language: 'pt',
|
||||
nsfw: false,
|
||||
commentsPolicy: VideoCommentPolicy.ENABLED,
|
||||
downloadEnabled: true,
|
||||
waitTranscoding: true,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag1', 'tag2' ],
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId,
|
||||
saveReplay: false,
|
||||
replaySettings: undefined,
|
||||
permanentLive: true,
|
||||
latencyMode: LiveVideoLatencyMode.DEFAULT
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long name', async function () {
|
||||
const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad category', async function () {
|
||||
const fields = { ...baseCorrectParams, category: 125 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad licence', async function () {
|
||||
const fields = { ...baseCorrectParams, licence: 125 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad language', async function () {
|
||||
const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with bad comments policy', async function () {
|
||||
const fields = { ...baseCorrectParams, commentsPolicy: 42 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long support text', async function () {
|
||||
const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without a channel', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'channelId' ])
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel', async function () {
|
||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad privacy for replay settings', async function () {
|
||||
const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with another user channel', async function () {
|
||||
const user = {
|
||||
username: 'fake',
|
||||
password: 'fake_password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
|
||||
const accessTokenUser = await server.login.getAccessToken(user)
|
||||
const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
|
||||
const customChannelId = videoChannels[0].id
|
||||
|
||||
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with too many tags', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too low', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too big', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a big thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a big preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with bad latency setting', async function () {
|
||||
const fields = { ...baseCorrectParams, latencyMode: 42 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail to set latency if the server does not allow it', async function () {
|
||||
const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const res = await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
video = res.body.video
|
||||
})
|
||||
|
||||
it('Should forbid if live is disabled', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should forbid to save replay if not enabled by the admin', async function () {
|
||||
const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
|
||||
|
||||
await server.config.enableLive({ allowReplay: false, transcoding: false })
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should allow to save replay if enabled by the admin', async function () {
|
||||
const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
|
||||
|
||||
await server.config.enableLive({ allowReplay: true, transcoding: false })
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not allow live if max instance lives is reached', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
maxInstanceLives: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not allow live if max user lives is reached', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
maxInstanceLives: 20,
|
||||
maxUserLives: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting live information', function () {
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should not display private information without access token', async function () {
|
||||
const live = await command.get({ token: '', videoId: video.id })
|
||||
|
||||
expect(live.rtmpUrl).to.not.exist
|
||||
expect(live.streamKey).to.not.exist
|
||||
expect(live.latencyMode).to.exist
|
||||
})
|
||||
|
||||
it('Should not display private information with token of another user', async function () {
|
||||
const live = await command.get({ token: userAccessToken, videoId: video.id })
|
||||
|
||||
expect(live.rtmpUrl).to.not.exist
|
||||
expect(live.streamKey).to.not.exist
|
||||
expect(live.latencyMode).to.exist
|
||||
})
|
||||
|
||||
it('Should display private information with appropriate token', async function () {
|
||||
const live = await command.get({ videoId: video.id })
|
||||
|
||||
expect(live.rtmpUrl).to.exist
|
||||
expect(live.streamKey).to.exist
|
||||
expect(live.latencyMode).to.exist
|
||||
})
|
||||
|
||||
it('Should fail with a bad video id', async function () {
|
||||
await command.get({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video id', async function () {
|
||||
await command.get({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non live video', async function () {
|
||||
await command.get({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.get({ videoId: video.id })
|
||||
await command.get({ videoId: video.uuid })
|
||||
await command.get({ videoId: video.shortUUID })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting live sessions', function () {
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
await command.listSessions({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await command.listSessions({ token: null, videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with the token of another user', async function () {
|
||||
await command.listSessions({ token: userAccessToken, videoId: video.id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad video id', async function () {
|
||||
await command.listSessions({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video id', async function () {
|
||||
await command.listSessions({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non live video', async function () {
|
||||
await command.listSessions({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.listSessions({ videoId: video.id })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting live session of a replay', function () {
|
||||
|
||||
it('Should fail with a bad video id', async function () {
|
||||
await command.getReplaySession({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video id', async function () {
|
||||
await command.getReplaySession({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non replay video', async function () {
|
||||
await command.getReplaySession({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating live information', async function () {
|
||||
|
||||
it('Should fail without access token', async function () {
|
||||
await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
await command.update({ token: 'toto', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with access token of another user', async function () {
|
||||
await command.update({ token: userAccessToken, videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad video id', async function () {
|
||||
await command.update({ videoId: 'toto', fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video id', async function () {
|
||||
await command.update({ videoId: 454555, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non live video', async function () {
|
||||
await command.update({ videoId: videoIdNotLive, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with bad latency setting', async function () {
|
||||
const fields = { latencyMode: 42 as any }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad privacy for replay settings', async function () {
|
||||
const fields = { saveReplay: true, replaySettings: { privacy: 999 as any } }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with save replay enabled but without replay settings', async function () {
|
||||
await server.config.enableLive({ allowReplay: true, transcoding: false })
|
||||
|
||||
const fields = { saveReplay: true }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with save replay disabled and replay settings', async function () {
|
||||
const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with only replay settings when save replay is disabled', async function () {
|
||||
const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail to set latency if the server does not allow it', async function () {
|
||||
const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
|
||||
|
||||
await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.update({ videoId: video.id, fields: { saveReplay: false } })
|
||||
await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
|
||||
await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } })
|
||||
|
||||
await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
|
||||
|
||||
})
|
||||
|
||||
it('Should fail to update replay status if replay is not allowed on the instance', async function () {
|
||||
await server.config.enableLive({ allowReplay: false, transcoding: false })
|
||||
|
||||
await command.update({ videoId: video.id, fields: { saveReplay: true }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed to live attributes if it has already started', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
const live = await command.get({ videoId: video.id })
|
||||
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
|
||||
await command.waitUntilPublished({ videoId: video.id })
|
||||
await command.update({ videoId: video.id, fields: { permanentLive: false }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should fail to change live privacy if it has already started', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
const live = await command.get({ videoId: video.id })
|
||||
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
|
||||
await command.waitUntilPublished({ videoId: video.id })
|
||||
|
||||
await server.videos.update({
|
||||
id: video.id,
|
||||
attributes: { privacy: VideoPrivacy.PUBLIC } // Same privacy, it's fine
|
||||
})
|
||||
|
||||
await server.videos.update({
|
||||
id: video.id,
|
||||
attributes: { privacy: VideoPrivacy.UNLISTED },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should fail to stream twice in the save live', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
const live = await command.get({ videoId: video.id })
|
||||
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
|
||||
await command.waitUntilPublished({ videoId: video.id })
|
||||
|
||||
await command.runAndTestStreamError({ videoId: video.id, shouldHaveError: true })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test logs API validators', function () {
|
||||
const path = '/api/v1/server/logs'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'my super password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When getting logs', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a missing startDate query', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad startDate query', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: { startDate: 'toto' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad endDate query', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: { startDate: new Date().toISOString(), endDate: 'toto' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad level parameter', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: { startDate: new Date().toISOString(), level: 'toto' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: { startDate: new Date().toISOString() },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When creating client logs', function () {
|
||||
const base = {
|
||||
level: 'warn' as 'warn',
|
||||
message: 'my super message',
|
||||
url: 'https://example.com/toto'
|
||||
}
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
it('Should fail with an invalid level', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, level: '' as any }, expectedStatus })
|
||||
await server.logs.createLogClient({ payload: { ...base, level: undefined }, expectedStatus })
|
||||
await server.logs.createLogClient({ payload: { ...base, level: 'toto' as any }, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid message', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, message: undefined }, expectedStatus })
|
||||
await server.logs.createLogClient({ payload: { ...base, message: '' }, expectedStatus })
|
||||
await server.logs.createLogClient({ payload: { ...base, message: 'm'.repeat(2500) }, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid url', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, url: undefined }, expectedStatus })
|
||||
await server.logs.createLogClient({ payload: { ...base, url: 'toto' }, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid stackTrace', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, stackTrace: 's'.repeat(20000) }, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid userAgent', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, userAgent: 's'.repeat(500) }, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid meta', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, meta: 's'.repeat(20000) }, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.logs.createLogClient({ payload: { ...base, stackTrace: 'stackTrace', meta: '{toto}', userAgent: 'userAgent' } })
|
||||
})
|
||||
|
||||
it('Should rate limit log creation', async function () {
|
||||
let fail = false
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try {
|
||||
await server.logs.createLogClient({ token: null, payload: base })
|
||||
} catch {
|
||||
fail = true
|
||||
}
|
||||
}
|
||||
|
||||
expect(fail).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,214 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test metrics API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let videoUUID: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1, {
|
||||
open_telemetry: {
|
||||
metrics: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
videoUUID = uuid
|
||||
})
|
||||
|
||||
describe('When adding playback metrics', function () {
|
||||
const path = '/api/v1/metrics/playback'
|
||||
let baseParams: PlaybackMetricCreate
|
||||
|
||||
before(function () {
|
||||
baseParams = {
|
||||
playerMode: 'p2p-media-loader',
|
||||
resolution: VideoResolution.H_1080P,
|
||||
fps: 30,
|
||||
resolutionChanges: 1,
|
||||
errors: 2,
|
||||
p2pEnabled: true,
|
||||
downloadedBytesP2P: 0,
|
||||
downloadedBytesHTTP: 0,
|
||||
uploadedBytesP2P: 0,
|
||||
videoId: videoUUID
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid resolution', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, resolution: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid fps', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, fps: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a missing/invalid player mode', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'playerMode' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, playerMode: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an missing/invalid resolution changes', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'resolutionChanges' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, resolutionChanges: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an missing/invalid errors', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'errors' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, errors: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an missing/invalid downloadedBytesP2P', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'downloadedBytesP2P' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, downloadedBytesP2P: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an missing/invalid downloadedBytesHTTP', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'downloadedBytesHTTP' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, downloadedBytesHTTP: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an missing/invalid uploadedBytesP2P', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'uploadedBytesP2P' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, uploadedBytesP2P: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a missing/invalid p2pEnabled', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: omit(baseParams, [ 'p2pEnabled' ])
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, p2pEnabled: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid totalPeers', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, p2pPeers: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad video id', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, videoId: 'toto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, videoId: 42 },
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: baseParams,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { ...baseParams, p2pEnabled: false, totalPeers: 32 },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,492 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode, UserRole, VideoCreateResult } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
UsersCommand
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test my user API validators', function () {
|
||||
const path = '/api/v1/users/'
|
||||
let userId: number
|
||||
let rootId: number
|
||||
let moderatorId: number
|
||||
let video: VideoCreateResult
|
||||
let server: PeerTubeServer
|
||||
let userToken = ''
|
||||
let moderatorToken = ''
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
{
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('user1')
|
||||
userToken = result.token
|
||||
userId = result.userId
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('moderator1', UserRole.MODERATOR)
|
||||
moderatorToken = result.token
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('moderator2', UserRole.MODERATOR)
|
||||
moderatorId = result.userId
|
||||
}
|
||||
|
||||
{
|
||||
video = await server.videos.upload()
|
||||
}
|
||||
})
|
||||
|
||||
describe('When updating my account', function () {
|
||||
|
||||
it('Should fail with an invalid email attribute', async function () {
|
||||
const fields = {
|
||||
email: 'blabla'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too small password', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'bla'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too long password', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'super'.repeat(61)
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without the current password', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'super'.repeat(61)
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid current password', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'my super password fail',
|
||||
password: 'super'.repeat(61)
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'me',
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid NSFW policy attribute', async function () {
|
||||
const fields = {
|
||||
nsfwPolicy: 'hello'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid autoPlayVideo attribute', async function () {
|
||||
const fields = {
|
||||
autoPlayVideo: -1
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid autoPlayNextVideo attribute', async function () {
|
||||
const fields = {
|
||||
autoPlayNextVideo: -1
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
|
||||
const fields = {
|
||||
videosHistoryEnabled: -1
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an non authenticated user', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'my super password'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'me',
|
||||
token: 'supertoken',
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a too long description', async function () {
|
||||
const fields = {
|
||||
description: 'super'.repeat(201)
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videoLanguages attribute', async function () {
|
||||
{
|
||||
const fields = {
|
||||
videoLanguages: 'toto'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
}
|
||||
|
||||
{
|
||||
const languages = []
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
languages.push('fr')
|
||||
}
|
||||
|
||||
const fields = {
|
||||
videoLanguages: languages
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid theme', async function () {
|
||||
const fields = { theme: 'invalid' }
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown theme', async function () {
|
||||
const fields = { theme: 'peertube-theme-unknown' }
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with invalid no modal attributes', async function () {
|
||||
const keys = [
|
||||
'noInstanceConfigWarningModal',
|
||||
'noAccountSetupWarningModal',
|
||||
'noWelcomeModal'
|
||||
]
|
||||
|
||||
for (const key of keys) {
|
||||
const fields = {
|
||||
[key]: -1
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed to change password with the correct params', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'my super password',
|
||||
nsfwPolicy: 'blur',
|
||||
autoPlayVideo: false,
|
||||
email: 'super_email@example.com',
|
||||
theme: 'default',
|
||||
noInstanceConfigWarningModal: true,
|
||||
noWelcomeModal: true,
|
||||
noAccountSetupWarningModal: true
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'me',
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed without password change with the correct params', async function () {
|
||||
const fields = {
|
||||
nsfwPolicy: 'blur',
|
||||
autoPlayVideo: false
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'me',
|
||||
token: userToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating my avatar', function () {
|
||||
it('Should fail without an incorrect input file', async function () {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
avatarfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a big file', async function () {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
avatarfile: buildAbsoluteFixturePath('avatar-big.png')
|
||||
}
|
||||
await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
avatarfile: buildAbsoluteFixturePath('avatar.png')
|
||||
}
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path: path + '/me/avatar/pick',
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
avatarfile: buildAbsoluteFixturePath('avatar.png')
|
||||
}
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path: path + '/me/avatar/pick',
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When managing my scoped tokens', function () {
|
||||
|
||||
it('Should fail to get my scoped tokens with an non authenticated user', async function () {
|
||||
await server.users.getMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail to get my scoped tokens with a bad token', async function () {
|
||||
await server.users.getMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
|
||||
})
|
||||
|
||||
it('Should succeed to get my scoped tokens', async function () {
|
||||
await server.users.getMyScopedTokens()
|
||||
})
|
||||
|
||||
it('Should fail to renew my scoped tokens with an non authenticated user', async function () {
|
||||
await server.users.renewMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail to renew my scoped tokens with a bad token', async function () {
|
||||
await server.users.renewMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should succeed to renew my scoped tokens', async function () {
|
||||
await server.users.renewMyScopedTokens()
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting my information', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await server.users.getMyInfo({ token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await server.users.getMyInfo({ token: userToken })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting my video rating', function () {
|
||||
let command: UsersCommand
|
||||
|
||||
before(function () {
|
||||
command = server.users
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.getMyRating({ token: 'fake_token', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video uuid', async function () {
|
||||
await command.getMyRating({ videoId: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video', async function () {
|
||||
await command.getMyRating({ videoId: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await command.getMyRating({ videoId: video.id })
|
||||
await command.getMyRating({ videoId: video.uuid })
|
||||
await command.getMyRating({ videoId: video.shortUUID })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When retrieving my global ratings', function () {
|
||||
const path = '/api/v1/accounts/user1/ratings'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a unauthenticated user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a another user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad type', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
query: { rating: 'toto ' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting my global followers', function () {
|
||||
const path = '/api/v1/accounts/user1/followers'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, userToken)
|
||||
})
|
||||
|
||||
it('Should fail with a unauthenticated user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a another user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blocking/unblocking/removing user', function () {
|
||||
|
||||
it('Should fail with an incorrect id', async function () {
|
||||
const options = { userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
|
||||
await server.users.remove(options)
|
||||
await server.users.banUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.users.unbanUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with the root user', async function () {
|
||||
const options = { userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
|
||||
await server.users.remove(options)
|
||||
await server.users.banUser(options)
|
||||
await server.users.unbanUser(options)
|
||||
})
|
||||
|
||||
it('Should return 404 with a non existing id', async function () {
|
||||
const options = { userId: 4545454, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await server.users.remove(options)
|
||||
await server.users.banUser(options)
|
||||
await server.users.unbanUser(options)
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
const options = { userId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
|
||||
|
||||
await server.users.remove(options)
|
||||
await server.users.banUser(options)
|
||||
await server.users.unbanUser(options)
|
||||
})
|
||||
|
||||
it('Should fail on a moderator with a moderator', async function () {
|
||||
const options = { userId: moderatorId, token: moderatorToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
|
||||
|
||||
await server.users.remove(options)
|
||||
await server.users.banUser(options)
|
||||
await server.users.unbanUser(options)
|
||||
})
|
||||
|
||||
it('Should succeed on a user with a moderator', async function () {
|
||||
const options = { userId, token: moderatorToken }
|
||||
|
||||
await server.users.banUser(options)
|
||||
await server.users.unbanUser(options)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting our account', function () {
|
||||
|
||||
it('Should fail with with the root account', async function () {
|
||||
await server.users.deleteMe({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,490 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode, PeerTubePlugin, PluginType } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test server plugins API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = null
|
||||
|
||||
const npmPlugin = 'peertube-plugin-hello-world'
|
||||
const pluginName = 'hello-world'
|
||||
let npmVersion: string
|
||||
|
||||
const themePlugin = 'peertube-theme-background-red'
|
||||
const themeName = 'background-red'
|
||||
let themeVersion: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
|
||||
{
|
||||
const res = await server.plugins.install({ npmName: npmPlugin })
|
||||
const plugin = res.body as PeerTubePlugin
|
||||
npmVersion = plugin.version
|
||||
}
|
||||
|
||||
{
|
||||
const res = await server.plugins.install({ npmName: themePlugin })
|
||||
const plugin = res.body as PeerTubePlugin
|
||||
themeVersion = plugin.version
|
||||
}
|
||||
})
|
||||
|
||||
describe('With static plugin routes', function () {
|
||||
it('Should fail with an unknown plugin name/plugin version', async function () {
|
||||
const paths = [
|
||||
'/plugins/' + pluginName + '/0.0.1/auth/fake-auth',
|
||||
'/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png',
|
||||
'/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js',
|
||||
'/themes/' + themeName + '/0.0.1/static/images/chocobo.png',
|
||||
'/themes/' + themeName + '/0.0.1/client-scripts/client/video-watch-client-plugin.js',
|
||||
'/themes/' + themeName + '/0.0.1/css/assets/style1.css'
|
||||
]
|
||||
|
||||
for (const p of paths) {
|
||||
await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail when requesting a plugin in the theme path', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/themes/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid versions', async function () {
|
||||
const paths = [
|
||||
'/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth',
|
||||
'/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png',
|
||||
'/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js',
|
||||
'/themes/' + themeName + '/1/static/images/chocobo.png',
|
||||
'/themes/' + themeName + '/0.0.1000a/client-scripts/client/video-watch-client-plugin.js',
|
||||
'/themes/' + themeName + '/0.a.1/css/assets/style1.css'
|
||||
]
|
||||
|
||||
for (const p of paths) {
|
||||
await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with invalid paths', async function () {
|
||||
const paths = [
|
||||
'/plugins/' + pluginName + '/' + npmVersion + '/static/images/../chocobo.png',
|
||||
'/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/h/o/../client/common-client-plugin.js',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/static/hola/a/../images/chocobo.png',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js/..',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/css/hiha//j../assets/style1.css'
|
||||
]
|
||||
|
||||
for (const p of paths) {
|
||||
await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown auth name', async function () {
|
||||
const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth'
|
||||
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown static file', async function () {
|
||||
const paths = [
|
||||
'/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png',
|
||||
'/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/fake.js',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/static/fake/chocobo.png',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/fake.js'
|
||||
]
|
||||
|
||||
for (const p of paths) {
|
||||
await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown CSS file', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/themes/' + themeName + '/' + themeVersion + '/css/assets/fake.css',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const paths = [
|
||||
'/plugins/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png',
|
||||
'/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/common-client-plugin.js',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/static/images/chocobo.png',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js',
|
||||
'/themes/' + themeName + '/' + themeVersion + '/css/assets/style1.css'
|
||||
]
|
||||
|
||||
for (const p of paths) {
|
||||
await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth'
|
||||
await makeGetRequest({ url: server.url, path: authPath, expectedStatus: HttpStatusCode.FOUND_302 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing available plugins/themes', function () {
|
||||
const path = '/api/v1/plugins/available'
|
||||
const baseQuery = {
|
||||
search: 'super search',
|
||||
pluginType: PluginType.PLUGIN,
|
||||
currentPeerTubeEngine: '1.2.3'
|
||||
}
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: 'fake_token',
|
||||
query: baseQuery,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
query: baseQuery,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid plugin type', async function () {
|
||||
const query = { ...baseQuery, pluginType: 5 }
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid current peertube engine', async function () {
|
||||
const query = { ...baseQuery, currentPeerTubeEngine: '1.0' }
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: baseQuery,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing local plugins/themes', function () {
|
||||
const path = '/api/v1/plugins'
|
||||
const baseQuery = {
|
||||
pluginType: PluginType.THEME
|
||||
}
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: 'fake_token',
|
||||
query: baseQuery,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
query: baseQuery,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid plugin type', async function () {
|
||||
const query = { ...baseQuery, pluginType: 5 }
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
query: baseQuery,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting a plugin or the registered settings or public settings', function () {
|
||||
const path = '/api/v1/plugins/'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid npm name', async function () {
|
||||
for (const suffix of [ 'toto', 'toto/registered-settings', 'toto/public-settings' ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
for (const suffix of [ 'peertube-plugin-TOTO', 'peertube-plugin-TOTO/registered-settings', 'peertube-plugin-TOTO/public-settings' ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown plugin', async function () {
|
||||
for (const suffix of [ 'peertube-plugin-toto', 'peertube-plugin-toto/registered-settings', 'peertube-plugin-toto/public-settings' ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings`, `${npmPlugin}/public-settings` ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating plugin settings', function () {
|
||||
const path = '/api/v1/plugins/'
|
||||
const settings = { setting1: 'value1' }
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + npmPlugin + '/settings',
|
||||
fields: { settings },
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + npmPlugin + '/settings',
|
||||
fields: { settings },
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid npm name', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'toto/settings',
|
||||
fields: { settings },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'peertube-plugin-TOTO/settings',
|
||||
fields: { settings },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown plugin', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + 'peertube-plugin-toto/settings',
|
||||
fields: { settings },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + npmPlugin + '/settings',
|
||||
fields: { settings },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When installing/updating/uninstalling a plugin', function () {
|
||||
const path = '/api/v1/plugins/'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
for (const suffix of [ 'install', 'update', 'uninstall' ]) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
fields: { npmName: npmPlugin },
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
for (const suffix of [ 'install', 'update', 'uninstall' ]) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
fields: { npmName: npmPlugin },
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid npm name', async function () {
|
||||
for (const suffix of [ 'install', 'update', 'uninstall' ]) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
fields: { npmName: 'toto' },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
for (const suffix of [ 'install', 'update', 'uninstall' ]) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + suffix,
|
||||
fields: { npmName: 'peertube-plugin-TOTO' },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const it = [
|
||||
{ suffix: 'install', status: HttpStatusCode.OK_200 },
|
||||
{ suffix: 'update', status: HttpStatusCode.OK_200 },
|
||||
{ suffix: 'uninstall', status: HttpStatusCode.NO_CONTENT_204 }
|
||||
]
|
||||
|
||||
for (const obj of it) {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + obj.suffix,
|
||||
fields: { npmName: npmPlugin },
|
||||
token: server.accessToken,
|
||||
expectedStatus: obj.status
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,240 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode, VideoCreateResult } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test server redundancy API validators', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let userAccessToken = null
|
||||
let videoIdLocal: number
|
||||
let videoRemote: VideoCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
await servers[0].users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await servers[0].login.getAccessToken(user)
|
||||
|
||||
videoIdLocal = (await servers[0].videos.quickUpload({ name: 'video' })).id
|
||||
|
||||
const remoteUUID = (await servers[1].videos.quickUpload({ name: 'video' })).uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
videoRemote = await servers[0].videos.get({ id: remoteUUID })
|
||||
})
|
||||
|
||||
describe('When listing redundancies', function () {
|
||||
const path = '/api/v1/server/redundancy/videos'
|
||||
|
||||
let url: string
|
||||
let token: string
|
||||
|
||||
before(function () {
|
||||
url = servers[0].url
|
||||
token = servers[0].accessToken
|
||||
})
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makeGetRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeGetRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(url, path, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(url, path, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(url, path, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad target', async function () {
|
||||
await makeGetRequest({ url, path, token, query: { target: 'bad target' } })
|
||||
})
|
||||
|
||||
it('Should fail without target', async function () {
|
||||
await makeGetRequest({ url, path, token })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When manually adding a redundancy', function () {
|
||||
const path = '/api/v1/server/redundancy/videos'
|
||||
|
||||
let url: string
|
||||
let token: string
|
||||
|
||||
before(function () {
|
||||
url = servers[0].url
|
||||
token = servers[0].accessToken
|
||||
})
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makePostBodyRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePostBodyRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail without a video id', async function () {
|
||||
await makePostBodyRequest({ url, path, token })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video id', async function () {
|
||||
await makePostBodyRequest({ url, path, token, fields: { videoId: 'peertube' } })
|
||||
})
|
||||
|
||||
it('Should fail with a not found video id', async function () {
|
||||
await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a local a video id', async function () {
|
||||
await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdLocal } })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
fields: { videoId: videoRemote.shortUUID },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the video is already duplicated', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await makePostBodyRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
fields: { videoId: videoRemote.uuid },
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When manually removing a redundancy', function () {
|
||||
const path = '/api/v1/server/redundancy/videos/'
|
||||
|
||||
let url: string
|
||||
let token: string
|
||||
|
||||
before(function () {
|
||||
url = servers[0].url
|
||||
token = servers[0].accessToken
|
||||
})
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video id', async function () {
|
||||
await makeDeleteRequest({ url, path: path + 'toto', token })
|
||||
})
|
||||
|
||||
it('Should fail with a not found video redundancy', async function () {
|
||||
await makeDeleteRequest({ url, path: path + '454545', token, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating server redundancy', function () {
|
||||
const path = '/api/v1/server/redundancy'
|
||||
|
||||
it('Should fail with an invalid token', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path: path + '/' + servers[1].host,
|
||||
fields: { redundancyAllowed: true },
|
||||
token: 'fake_token',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user is not an administrator', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path: path + '/' + servers[1].host,
|
||||
fields: { redundancyAllowed: true },
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if we do not follow this server', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path: path + '/example.com',
|
||||
fields: { redundancyAllowed: true },
|
||||
token: servers[0].accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without de redundancyAllowed param', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path: path + '/' + servers[1].host,
|
||||
fields: { blabla: true },
|
||||
token: servers[0].accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path: path + '/' + servers[1].host,
|
||||
fields: { redundancyAllowed: true },
|
||||
token: servers[0].accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,446 @@
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, HttpStatusCodeType, UserRole } from '@peertube/peertube-models'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
setDefaultChannelAvatar
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test registrations API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let userToken: string
|
||||
let moderatorToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultAccountAvatar([ server ])
|
||||
await setDefaultChannelAvatar([ server ])
|
||||
|
||||
await server.config.enableSignup(false);
|
||||
|
||||
({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR));
|
||||
({ token: userToken } = await server.users.generate('user', UserRole.USER))
|
||||
})
|
||||
|
||||
describe('Register', function () {
|
||||
const registrationPath = '/api/v1/users/register'
|
||||
const registrationRequestPath = '/api/v1/users/registrations/request'
|
||||
|
||||
const baseCorrectParams = {
|
||||
username: 'user3',
|
||||
displayName: 'super user',
|
||||
email: 'test3@example.com',
|
||||
password: 'my super password',
|
||||
registrationReason: 'my super registration reason'
|
||||
}
|
||||
|
||||
describe('When registering a new user or requesting user registration', function () {
|
||||
|
||||
async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
|
||||
await server.config.enableSignup(false)
|
||||
await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus })
|
||||
|
||||
await server.config.enableSignup(true)
|
||||
await makePostBodyRequest({ url: server.url, path: registrationRequestPath, fields, expectedStatus })
|
||||
}
|
||||
|
||||
it('Should fail with a too small username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: '' }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a too long username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'super'.repeat(50) }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'my username' }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a missing email', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'email' ])
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid email', async function () {
|
||||
const fields = { ...baseCorrectParams, email: 'test_example.com' }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a too small password', async function () {
|
||||
const fields = { ...baseCorrectParams, password: 'bla' }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a too long password', async function () {
|
||||
const fields = { ...baseCorrectParams, password: 'super'.repeat(61) }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail if we register a user with the same username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'root' }
|
||||
|
||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||
})
|
||||
|
||||
it('Should fail with a "peertube" username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'peertube' }
|
||||
|
||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||
})
|
||||
|
||||
it('Should fail if we register a user with the same email', async function () {
|
||||
const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' }
|
||||
|
||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||
})
|
||||
|
||||
it('Should fail with a bad display name', async function () {
|
||||
const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel name', async function () {
|
||||
const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel display name', async function () {
|
||||
const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with a channel name that is the same as username', async function () {
|
||||
const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } }
|
||||
const fields = { ...baseCorrectParams, ...source }
|
||||
|
||||
await check(fields)
|
||||
})
|
||||
|
||||
it('Should fail with an existing channel', async function () {
|
||||
const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' }
|
||||
await server.channels.create({ attributes })
|
||||
|
||||
const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } }
|
||||
|
||||
await check(fields, HttpStatusCode.CONFLICT_409)
|
||||
})
|
||||
|
||||
it('Should fail on a server with registration disabled', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
signup: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await server.registrations.register({ username: 'user4', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user4',
|
||||
registrationReason: 'reason',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the user limit is reached', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { total } = await server.users.list()
|
||||
|
||||
await server.config.enableSignup(false, total)
|
||||
await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
|
||||
await server.config.enableSignup(true, total)
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user42',
|
||||
registrationReason: 'reason',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed if the user limit is not reached', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { total } = await server.users.list()
|
||||
|
||||
await server.config.enableSignup(false, total + 1)
|
||||
await server.registrations.register({ username: 'user43', expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
|
||||
await server.config.enableSignup(true, total + 2)
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user44',
|
||||
registrationReason: 'reason',
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('On direct registration', function () {
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.config.enableSignup(false)
|
||||
|
||||
const fields = {
|
||||
username: 'user_direct_1',
|
||||
displayName: 'super user direct 1',
|
||||
email: 'user_direct_1@example.com',
|
||||
password: 'my super password',
|
||||
channel: { name: 'super_user_direct_1_channel', displayName: 'super user direct 1 channel' }
|
||||
}
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
})
|
||||
|
||||
it('Should fail if the instance requires approval', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.config.enableSignup(true)
|
||||
await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('On registration request', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.config.enableSignup(true)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid registration reason', async function () {
|
||||
for (const registrationReason of [ '', 't', 't'.repeat(5000) ]) {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user_request_1',
|
||||
registrationReason,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user_request_2',
|
||||
registrationReason: 'tt',
|
||||
channel: {
|
||||
displayName: 'my user request 2 channel',
|
||||
name: 'user_request_2_channel'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the username is already awaiting registration approval', async function () {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user_request_2',
|
||||
registrationReason: 'tt',
|
||||
channel: {
|
||||
displayName: 'my user request 42 channel',
|
||||
name: 'user_request_42_channel'
|
||||
},
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the email is already awaiting registration approval', async function () {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user42',
|
||||
email: 'user_request_2@example.com',
|
||||
registrationReason: 'tt',
|
||||
channel: {
|
||||
displayName: 'my user request 42 channel',
|
||||
name: 'user_request_42_channel'
|
||||
},
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the channel is already awaiting registration approval', async function () {
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user42',
|
||||
registrationReason: 'tt',
|
||||
channel: {
|
||||
displayName: 'my user request 2 channel',
|
||||
name: 'user_request_2_channel'
|
||||
},
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if the instance does not require approval', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.config.enableSignup(false)
|
||||
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'user42',
|
||||
registrationReason: 'toto',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Registrations accept/reject', function () {
|
||||
let id1: number
|
||||
let id2: number
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.config.enableSignup(true);
|
||||
|
||||
({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' }));
|
||||
({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' }))
|
||||
})
|
||||
|
||||
it('Should fail to accept/reject registration without token', async function () {
|
||||
const options = { id: id1, moderationResponse: 'tt', token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }
|
||||
await server.registrations.accept(options)
|
||||
await server.registrations.reject(options)
|
||||
})
|
||||
|
||||
it('Should fail to accept/reject registration with a non moderator user', async function () {
|
||||
const options = { id: id1, moderationResponse: 'tt', token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
|
||||
await server.registrations.accept(options)
|
||||
await server.registrations.reject(options)
|
||||
})
|
||||
|
||||
it('Should fail to accept/reject registration with a bad registration id', async function () {
|
||||
{
|
||||
const options = { id: 't' as any, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
await server.registrations.accept(options)
|
||||
await server.registrations.reject(options)
|
||||
}
|
||||
|
||||
{
|
||||
const options = { id: 42, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
await server.registrations.accept(options)
|
||||
await server.registrations.reject(options)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail to accept/reject registration with a bad moderation resposne', async function () {
|
||||
for (const moderationResponse of [ '', 't', 't'.repeat(5000) ]) {
|
||||
const options = { id: id1, moderationResponse, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
await server.registrations.accept(options)
|
||||
await server.registrations.reject(options)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed to accept a registration', async function () {
|
||||
await server.registrations.accept({ id: id1, moderationResponse: 'tt', token: moderatorToken })
|
||||
})
|
||||
|
||||
it('Should succeed to reject a registration', async function () {
|
||||
await server.registrations.reject({ id: id2, moderationResponse: 'tt', token: moderatorToken })
|
||||
})
|
||||
|
||||
it('Should fail to accept/reject a registration that was already accepted/rejected', async function () {
|
||||
for (const id of [ id1, id2 ]) {
|
||||
const options = { id, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.CONFLICT_409 }
|
||||
await server.registrations.accept(options)
|
||||
await server.registrations.reject(options)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Registrations deletion', function () {
|
||||
let id1: number
|
||||
let id2: number
|
||||
let id3: number
|
||||
|
||||
before(async function () {
|
||||
({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' }));
|
||||
({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' }));
|
||||
({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' }))
|
||||
|
||||
await server.registrations.accept({ id: id2, moderationResponse: 'tt' })
|
||||
await server.registrations.reject({ id: id3, moderationResponse: 'tt' })
|
||||
})
|
||||
|
||||
it('Should fail to delete registration without token', async function () {
|
||||
await server.registrations.delete({ id: id1, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail to delete registration with a non moderator user', async function () {
|
||||
await server.registrations.delete({ id: id1, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail to delete registration with a bad registration id', async function () {
|
||||
await server.registrations.delete({ id: 't' as any, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.registrations.delete({ id: 42, token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.registrations.delete({ id: id1, token: moderatorToken })
|
||||
await server.registrations.delete({ id: id2, token: moderatorToken })
|
||||
await server.registrations.delete({ id: id3, token: moderatorToken })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Listing registrations', function () {
|
||||
const path = '/api/v1/users/registrations'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await server.registrations.list({
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await server.registrations.list({
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.registrations.list({
|
||||
token: moderatorToken,
|
||||
search: 'toto'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,911 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
import { basename } from 'path'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
isVideoStudioTaskIntro,
|
||||
RunnerJob,
|
||||
RunnerJobState,
|
||||
RunnerJobStudioTranscodingPayload,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobUpdatePayload,
|
||||
VideoPrivacy,
|
||||
VideoStudioTaskIntro
|
||||
} from '@peertube/peertube-models'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
sendRTMPStream,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
VideoStudioCommand,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
|
||||
|
||||
describe('Test managing runners', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
let userToken: string
|
||||
|
||||
let registrationTokenId: number
|
||||
let registrationToken: string
|
||||
|
||||
let runnerToken: string
|
||||
let runnerToken2: string
|
||||
|
||||
let completedJobToken: string
|
||||
let completedJobUUID: string
|
||||
|
||||
let cancelledJobToken: string
|
||||
let cancelledJobUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const config = {
|
||||
rates_limit: {
|
||||
api: {
|
||||
max: 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server = await createSingleServer(1, config)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
const { data } = await server.runnerRegistrationTokens.list()
|
||||
registrationToken = data[0].registrationToken
|
||||
registrationTokenId = data[0].id
|
||||
|
||||
await server.config.enableTranscoding({ hls: true, webVideo: true })
|
||||
await server.config.enableStudio()
|
||||
await server.config.enableRemoteTranscoding()
|
||||
await server.config.enableRemoteStudio()
|
||||
|
||||
runnerToken = await server.runners.autoRegisterRunner()
|
||||
runnerToken2 = await server.runners.autoRegisterRunner()
|
||||
|
||||
{
|
||||
await server.videos.quickUpload({ name: 'video 1' })
|
||||
await server.videos.quickUpload({ name: 'video 2' })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
{
|
||||
const job = await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
|
||||
completedJobToken = job.jobToken
|
||||
completedJobUUID = job.uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken })
|
||||
cancelledJobToken = job.jobToken
|
||||
cancelledJobUUID = job.uuid
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('Managing runner registration tokens', function () {
|
||||
|
||||
describe('Common', function () {
|
||||
|
||||
it('Should fail to generate, list or delete runner registration token without oauth token', async function () {
|
||||
const expectedStatus = HttpStatusCode.UNAUTHORIZED_401
|
||||
|
||||
await server.runnerRegistrationTokens.generate({ token: null, expectedStatus })
|
||||
await server.runnerRegistrationTokens.list({ token: null, expectedStatus })
|
||||
await server.runnerRegistrationTokens.delete({ token: null, id: registrationTokenId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail to generate, list or delete runner registration token without admin rights', async function () {
|
||||
const expectedStatus = HttpStatusCode.FORBIDDEN_403
|
||||
|
||||
await server.runnerRegistrationTokens.generate({ token: userToken, expectedStatus })
|
||||
await server.runnerRegistrationTokens.list({ token: userToken, expectedStatus })
|
||||
await server.runnerRegistrationTokens.delete({ token: userToken, id: registrationTokenId, expectedStatus })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete', function () {
|
||||
|
||||
it('Should fail to delete with a bad id', async function () {
|
||||
await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', function () {
|
||||
const path = '/api/v1/runners/registration-tokens'
|
||||
|
||||
it('Should fail to list with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail to list with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail to list with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should succeed to list with the correct params', async function () {
|
||||
await server.runnerRegistrationTokens.list({ start: 0, count: 5, sort: '-createdAt' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Managing runners', function () {
|
||||
let toDeleteId: number
|
||||
|
||||
describe('Register', function () {
|
||||
const name = 'runner name'
|
||||
|
||||
it('Should fail with a bad registration token', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await server.runners.register({ name, registrationToken: 'a'.repeat(4000), expectedStatus })
|
||||
await server.runners.register({ name, registrationToken: null, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown registration token', async function () {
|
||||
await server.runners.register({ name, registrationToken: 'aaa', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad name', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await server.runners.register({ name: '', registrationToken, expectedStatus })
|
||||
await server.runners.register({ name: 'a'.repeat(200), registrationToken, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid description', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await server.runners.register({ name, description: '', registrationToken, expectedStatus })
|
||||
await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const { id } = await server.runners.register({ name, description: 'super description', registrationToken })
|
||||
|
||||
toDeleteId = id
|
||||
})
|
||||
|
||||
it('Should fail with the same runner name', async function () {
|
||||
await server.runners.register({
|
||||
name,
|
||||
description: 'super description',
|
||||
registrationToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete', function () {
|
||||
|
||||
it('Should fail without oauth token', async function () {
|
||||
await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without admin rights', async function () {
|
||||
await server.runners.delete({ token: userToken, id: toDeleteId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad id', async function () {
|
||||
await server.runners.delete({ id: 'hi' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await server.runners.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.runners.delete({ id: toDeleteId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', function () {
|
||||
const path = '/api/v1/runners'
|
||||
|
||||
it('Should fail without oauth token', async function () {
|
||||
await server.runners.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without admin rights', async function () {
|
||||
await server.runners.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail to list with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail to list with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail to list with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid state', async function () {
|
||||
await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
|
||||
})
|
||||
|
||||
it('Should succeed to list with the correct params', async function () {
|
||||
await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Runner jobs by admin', function () {
|
||||
|
||||
describe('Cancel', function () {
|
||||
let jobUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
jobUUID = availableJobs[0].uuid
|
||||
})
|
||||
|
||||
it('Should fail without oauth token', async function () {
|
||||
await server.runnerJobs.cancelByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without admin rights', async function () {
|
||||
await server.runnerJobs.cancelByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad job uuid', async function () {
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown job uuid', async function () {
|
||||
const jobUUID = badUUID
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an already cancelled job', async function () {
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID })
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', function () {
|
||||
const path = '/api/v1/runners/jobs'
|
||||
|
||||
it('Should fail without oauth token', async function () {
|
||||
await server.runnerJobs.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without admin rights', async function () {
|
||||
await server.runnerJobs.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail to list with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail to list with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail to list with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid state', async function () {
|
||||
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any })
|
||||
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete', function () {
|
||||
let jobUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
jobUUID = availableJobs[0].uuid
|
||||
})
|
||||
|
||||
it('Should fail without oauth token', async function () {
|
||||
await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail without admin rights', async function () {
|
||||
await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad job uuid', async function () {
|
||||
await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown job uuid', async function () {
|
||||
const jobUUID = badUUID
|
||||
await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.runnerJobs.deleteByAdmin({ jobUUID })
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Runner jobs by runners', function () {
|
||||
let jobUUID: string
|
||||
let jobToken: string
|
||||
let videoUUID: string
|
||||
|
||||
let jobUUID2: string
|
||||
let jobToken2: string
|
||||
|
||||
let videoUUID2: string
|
||||
|
||||
let pendingUUID: string
|
||||
|
||||
let videoStudioUUID: string
|
||||
let studioFile: string
|
||||
|
||||
let liveAcceptedJob: RunnerJob & { jobToken: string }
|
||||
let studioAcceptedJob: RunnerJob & { jobToken: string }
|
||||
|
||||
async function fetchVideoInputFiles (options: {
|
||||
jobUUID: string
|
||||
videoUUID: string
|
||||
runnerToken: string
|
||||
jobToken: string
|
||||
expectedStatus: HttpStatusCodeType
|
||||
}) {
|
||||
const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options
|
||||
|
||||
const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID
|
||||
const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ]
|
||||
|
||||
for (const path of paths) {
|
||||
await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStudioFiles (options: {
|
||||
jobUUID: string
|
||||
videoUUID: string
|
||||
runnerToken: string
|
||||
jobToken: string
|
||||
studioFile?: string
|
||||
expectedStatus: HttpStatusCodeType
|
||||
}) {
|
||||
const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken, studioFile } = options
|
||||
|
||||
const path = `/api/v1/runners/jobs/${jobUUID}/files/videos/${videoUUID}/studio/task-files/${studioFile}`
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
{
|
||||
await server.runnerJobs.cancelAllJobs({ state: RunnerJobState.PENDING })
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken })
|
||||
jobUUID = job.uuid
|
||||
jobToken = job.jobToken
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
videoUUID2 = uuid
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken: runnerToken2 })
|
||||
jobUUID2 = job.uuid
|
||||
jobToken2 = job.jobToken
|
||||
}
|
||||
|
||||
{
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
pendingUUID = availableJobs[0].uuid
|
||||
}
|
||||
|
||||
{
|
||||
await server.config.disableTranscoding()
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video studio' })
|
||||
videoStudioUUID = uuid
|
||||
|
||||
await server.config.enableTranscoding({ hls: true, webVideo: true })
|
||||
await server.config.enableStudio()
|
||||
|
||||
await server.videoStudio.createEditionTasks({
|
||||
videoId: videoStudioUUID,
|
||||
tasks: VideoStudioCommand.getComplexTask()
|
||||
})
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-studio-transcoding' })
|
||||
studioAcceptedJob = job
|
||||
|
||||
const tasks = (job.payload as RunnerJobStudioTranscodingPayload).tasks
|
||||
const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string
|
||||
studioFile = basename(fileUrl)
|
||||
}
|
||||
|
||||
{
|
||||
await server.config.enableLive({
|
||||
allowReplay: false,
|
||||
resolutions: 'max',
|
||||
transcoding: true
|
||||
})
|
||||
|
||||
const { live } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
|
||||
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
await waitJobs([ server ])
|
||||
|
||||
await server.runnerJobs.requestLiveJob(runnerToken)
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
|
||||
liveAcceptedJob = job
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
}
|
||||
})
|
||||
|
||||
describe('Common runner tokens validations', function () {
|
||||
|
||||
async function testEndpoints (options: {
|
||||
jobUUID: string
|
||||
runnerToken: string
|
||||
jobToken: string
|
||||
expectedStatus: HttpStatusCodeType
|
||||
}) {
|
||||
await server.runnerJobs.abort({ ...options, reason: 'reason' })
|
||||
await server.runnerJobs.update({ ...options })
|
||||
await server.runnerJobs.error({ ...options, message: 'message' })
|
||||
await server.runnerJobs.success({ ...options, payload: { videoFile: 'video_short.mp4' } })
|
||||
}
|
||||
|
||||
it('Should fail with an invalid job uuid', async function () {
|
||||
const options = { jobUUID: 'a', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
|
||||
await testEndpoints({ ...options, jobToken })
|
||||
await fetchVideoInputFiles({ ...options, videoUUID, jobToken })
|
||||
await fetchStudioFiles({ ...options, videoUUID, jobToken: studioAcceptedJob.jobToken, studioFile })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown job uuid', async function () {
|
||||
const options = { jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await testEndpoints({ ...options, jobToken })
|
||||
await fetchVideoInputFiles({ ...options, videoUUID, jobToken })
|
||||
await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID, studioFile })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid runner token', async function () {
|
||||
const options = { runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
|
||||
await testEndpoints({ ...options, jobUUID, jobToken })
|
||||
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
|
||||
await fetchStudioFiles({
|
||||
...options,
|
||||
jobToken: studioAcceptedJob.jobToken,
|
||||
jobUUID: studioAcceptedJob.uuid,
|
||||
videoUUID: videoStudioUUID,
|
||||
studioFile
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown runner token', async function () {
|
||||
const options = { runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await testEndpoints({ ...options, jobUUID, jobToken })
|
||||
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
|
||||
await fetchStudioFiles({
|
||||
...options,
|
||||
jobToken: studioAcceptedJob.jobToken,
|
||||
jobUUID: studioAcceptedJob.uuid,
|
||||
videoUUID: videoStudioUUID,
|
||||
studioFile
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid job token job uuid', async function () {
|
||||
const options = { runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
|
||||
|
||||
await testEndpoints({ ...options, jobUUID })
|
||||
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
|
||||
await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown job token job uuid', async function () {
|
||||
const options = { runnerToken, jobToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await testEndpoints({ ...options, jobUUID })
|
||||
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
|
||||
await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
|
||||
})
|
||||
|
||||
it('Should fail with a runner token not associated to this job', async function () {
|
||||
const options = { runnerToken: runnerToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await testEndpoints({ ...options, jobUUID, jobToken })
|
||||
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
|
||||
await fetchStudioFiles({
|
||||
...options,
|
||||
jobToken: studioAcceptedJob.jobToken,
|
||||
jobUUID: studioAcceptedJob.uuid,
|
||||
videoUUID: videoStudioUUID,
|
||||
studioFile
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a job uuid not associated to the job token', async function () {
|
||||
{
|
||||
const options = { jobUUID: jobUUID2, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await testEndpoints({ ...options, jobToken })
|
||||
await fetchVideoInputFiles({ ...options, jobToken, videoUUID })
|
||||
await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID: videoStudioUUID, studioFile })
|
||||
}
|
||||
|
||||
{
|
||||
const options = { runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
|
||||
|
||||
await testEndpoints({ ...options, jobUUID })
|
||||
await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
|
||||
await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Unregister', function () {
|
||||
|
||||
it('Should fail without a runner token', async function () {
|
||||
await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad a runner token', async function () {
|
||||
await server.runners.unregister({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown runner token', async function () {
|
||||
await server.runners.unregister({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Request', function () {
|
||||
|
||||
it('Should fail without a runner token', async function () {
|
||||
await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad a runner token', async function () {
|
||||
await server.runnerJobs.request({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown runner token', async function () {
|
||||
await server.runnerJobs.request({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accept', function () {
|
||||
|
||||
it('Should fail with a bad a job uuid', async function () {
|
||||
await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown job uuid', async function () {
|
||||
await server.runnerJobs.accept({ jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a job not in pending state', async function () {
|
||||
await server.runnerJobs.accept({ jobUUID: completedJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.runnerJobs.accept({ jobUUID: cancelledJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail without a runner token', async function () {
|
||||
await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad a runner token', async function () {
|
||||
await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown runner token', async function () {
|
||||
await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Abort', function () {
|
||||
|
||||
it('Should fail without a reason', async function () {
|
||||
await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad reason', async function () {
|
||||
const reason = 'reason'.repeat(5000)
|
||||
await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a job not in processing state', async function () {
|
||||
await server.runnerJobs.abort({
|
||||
jobUUID: completedJobUUID,
|
||||
jobToken: completedJobToken,
|
||||
runnerToken,
|
||||
reason: 'reason',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update', function () {
|
||||
|
||||
describe('Common', function () {
|
||||
|
||||
it('Should fail with an invalid progress', async function () {
|
||||
await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a job not in processing state', async function () {
|
||||
await server.runnerJobs.update({
|
||||
jobUUID: cancelledJobUUID,
|
||||
jobToken: cancelledJobToken,
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Live RTMP to HLS', function () {
|
||||
const base: RunnerJobUpdatePayload = {
|
||||
masterPlaylistFile: 'live/master.m3u8',
|
||||
resolutionPlaylistFilename: '0.m3u8',
|
||||
resolutionPlaylistFile: 'live/1.m3u8',
|
||||
type: 'add-chunk',
|
||||
videoChunkFile: 'live/1-000069.ts',
|
||||
videoChunkFilename: '1-000068.ts'
|
||||
}
|
||||
|
||||
function testUpdate (payload: RunnerJobUpdatePayload) {
|
||||
return server.runnerJobs.update({
|
||||
jobUUID: liveAcceptedJob.uuid,
|
||||
jobToken: liveAcceptedJob.jobToken,
|
||||
payload,
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
it('Should fail with an invalid resolutionPlaylistFilename', async function () {
|
||||
await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
|
||||
await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
|
||||
await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videoChunkFilename', async function () {
|
||||
await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
|
||||
await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
|
||||
await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid type', async function () {
|
||||
await testUpdate({ ...base, type: undefined })
|
||||
await testUpdate({ ...base, type: 'toto' as any })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error', function () {
|
||||
|
||||
it('Should fail with a missing error message', async function () {
|
||||
await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid error messgae', async function () {
|
||||
const message = 'a'.repeat(6000)
|
||||
await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a job not in processing state', async function () {
|
||||
await server.runnerJobs.error({
|
||||
jobUUID: completedJobUUID,
|
||||
jobToken: completedJobToken,
|
||||
message: 'my message',
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Success', function () {
|
||||
let vodJobUUID: string
|
||||
let vodJobToken: string
|
||||
|
||||
describe('Common', function () {
|
||||
|
||||
it('Should fail with a job not in processing state', async function () {
|
||||
await server.runnerJobs.success({
|
||||
jobUUID: completedJobUUID,
|
||||
jobToken: completedJobToken,
|
||||
payload: { videoFile: 'video_short.mp4' },
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('VOD', function () {
|
||||
|
||||
it('Should fail with an invalid vod web video payload', async function () {
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' })
|
||||
|
||||
await server.runnerJobs.success({
|
||||
jobUUID: job.uuid,
|
||||
jobToken: job.jobToken,
|
||||
payload: { hello: 'video_short.mp4' } as any,
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
vodJobUUID = job.uuid
|
||||
vodJobToken = job.jobToken
|
||||
})
|
||||
|
||||
it('Should fail with an invalid vod hls payload', async function () {
|
||||
// To create HLS jobs
|
||||
const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' }
|
||||
await server.runnerJobs.success({ runnerToken, jobUUID: vodJobUUID, jobToken: vodJobToken, payload })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-hls-transcoding' })
|
||||
|
||||
await server.runnerJobs.success({
|
||||
jobUUID: job.uuid,
|
||||
jobToken: job.jobToken,
|
||||
payload: { videoFile: 'video_short.mp4' } as any,
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid vod audio merge payload', async function () {
|
||||
const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
|
||||
await server.videos.upload({ attributes, mode: 'legacy' })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-audio-merge-transcoding' })
|
||||
|
||||
await server.runnerJobs.success({
|
||||
jobUUID: job.uuid,
|
||||
jobToken: job.jobToken,
|
||||
payload: { hello: 'video_short.mp4' } as any,
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video studio', function () {
|
||||
|
||||
it('Should fail with an invalid video studio transcoding payload', async function () {
|
||||
await server.runnerJobs.success({
|
||||
jobUUID: studioAcceptedJob.uuid,
|
||||
jobToken: studioAcceptedJob.jobToken,
|
||||
payload: { hello: 'video_short.mp4' } as any,
|
||||
runnerToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Job files', function () {
|
||||
|
||||
describe('Check video param for common job file routes', function () {
|
||||
|
||||
async function fetchFiles (options: {
|
||||
videoUUID?: string
|
||||
expectedStatus: HttpStatusCodeType
|
||||
}) {
|
||||
await fetchVideoInputFiles({ videoUUID, ...options, jobToken, jobUUID, runnerToken })
|
||||
|
||||
await fetchStudioFiles({
|
||||
videoUUID: videoStudioUUID,
|
||||
|
||||
...options,
|
||||
|
||||
jobToken: studioAcceptedJob.jobToken,
|
||||
jobUUID: studioAcceptedJob.uuid,
|
||||
runnerToken,
|
||||
studioFile
|
||||
})
|
||||
}
|
||||
|
||||
it('Should fail with an invalid video id', async function () {
|
||||
await fetchFiles({
|
||||
videoUUID: 'a',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video id', async function () {
|
||||
const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
|
||||
|
||||
await fetchFiles({
|
||||
videoUUID,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a video id not associated to this job', async function () {
|
||||
await fetchFiles({
|
||||
videoUUID: videoUUID2,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await fetchFiles({ expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video studio tasks file routes', function () {
|
||||
|
||||
it('Should fail with an invalid studio filename', async function () {
|
||||
await fetchStudioFiles({
|
||||
videoUUID: videoStudioUUID,
|
||||
jobUUID: studioAcceptedJob.uuid,
|
||||
runnerToken,
|
||||
jobToken: studioAcceptedJob.jobToken,
|
||||
studioFile: 'toto',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,278 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
function updateSearchIndex (server: PeerTubeServer, enabled: boolean, disableLocalSearch = false) {
|
||||
return server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
search: {
|
||||
searchIndex: {
|
||||
enabled,
|
||||
disableLocalSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Test videos API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
describe('When searching videos', function () {
|
||||
const path = '/api/v1/search/videos/'
|
||||
|
||||
const query = {
|
||||
search: 'coucou'
|
||||
}
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid category', async function () {
|
||||
const customQuery1 = { ...query, categoryOneOf: [ 'aa', 'b' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const customQuery2 = { ...query, categoryOneOf: 'a' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with a valid category', async function () {
|
||||
const customQuery1 = { ...query, categoryOneOf: [ 1, 7 ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const customQuery2 = { ...query, categoryOneOf: 1 }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid licence', async function () {
|
||||
const customQuery1 = { ...query, licenceOneOf: [ 'aa', 'b' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const customQuery2 = { ...query, licenceOneOf: 'a' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with a valid licence', async function () {
|
||||
const customQuery1 = { ...query, licenceOneOf: [ 1, 2 ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const customQuery2 = { ...query, licenceOneOf: 1 }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should succeed with a valid language', async function () {
|
||||
const customQuery1 = { ...query, languageOneOf: [ 'fr', 'en' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const customQuery2 = { ...query, languageOneOf: 'fr' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should succeed with valid tags', async function () {
|
||||
const customQuery1 = { ...query, tagsOneOf: [ 'tag1', 'tag2' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const customQuery2 = { ...query, tagsOneOf: 'tag1' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const customQuery3 = { ...query, tagsAllOf: [ 'tag1', 'tag2' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const customQuery4 = { ...query, tagsAllOf: 'tag1' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should fail with invalid durations', async function () {
|
||||
const customQuery1 = { ...query, durationMin: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const customQuery2 = { ...query, durationMax: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with invalid dates', async function () {
|
||||
const customQuery1 = { ...query, startDate: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const customQuery2 = { ...query, endDate: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const customQuery3 = { ...query, originallyPublishedStartDate: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
const customQuery4 = { ...query, originallyPublishedEndDate: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid host', async function () {
|
||||
const customQuery = { ...query, host: '6565' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with a host', async function () {
|
||||
const customQuery = { ...query, host: 'example.com' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should fail with invalid uuids', async function () {
|
||||
const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with valid uuids', async function () {
|
||||
const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When searching video playlists', function () {
|
||||
const path = '/api/v1/search/video-playlists/'
|
||||
|
||||
const query = {
|
||||
search: 'coucou',
|
||||
host: 'example.com'
|
||||
}
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid host', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with invalid uuids', async function () {
|
||||
const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When searching video channels', function () {
|
||||
const path = '/api/v1/search/video-channels/'
|
||||
|
||||
const query = {
|
||||
search: 'coucou',
|
||||
host: 'example.com'
|
||||
}
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, null, query)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid host', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with invalid handles', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search target', function () {
|
||||
|
||||
it('Should fail/succeed depending on the search target', async function () {
|
||||
const query = { search: 'coucou' }
|
||||
const paths = [
|
||||
'/api/v1/search/video-playlists/',
|
||||
'/api/v1/search/video-channels/',
|
||||
'/api/v1/search/videos/'
|
||||
]
|
||||
|
||||
for (const path of paths) {
|
||||
{
|
||||
const customQuery = { ...query, searchTarget: 'hello' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
|
||||
{
|
||||
const customQuery = { ...query, searchTarget: undefined }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
{
|
||||
const customQuery = { ...query, searchTarget: 'local' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
{
|
||||
const customQuery = { ...query, searchTarget: 'search-index' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
|
||||
await updateSearchIndex(server, true, true)
|
||||
|
||||
{
|
||||
const customQuery = { ...query, searchTarget: 'search-index' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
await updateSearchIndex(server, true, false)
|
||||
|
||||
{
|
||||
const customQuery = { ...query, searchTarget: 'local' }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
await updateSearchIndex(server, false, false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,207 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
VideoCreateResult,
|
||||
VideoPlaylistCreateResult,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test services API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let playlistUUID: string
|
||||
|
||||
let privateVideo: VideoCreateResult
|
||||
let unlistedVideo: VideoCreateResult
|
||||
|
||||
let privatePlaylist: VideoPlaylistCreateResult
|
||||
let unlistedPlaylist: VideoPlaylistCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
server.store.videoCreated = await server.videos.upload({ attributes: { name: 'my super name' } })
|
||||
|
||||
privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })
|
||||
unlistedVideo = await server.videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })
|
||||
|
||||
{
|
||||
const created = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'super playlist',
|
||||
privacy: VideoPlaylistPrivacy.PUBLIC,
|
||||
videoChannelId: server.store.channel.id
|
||||
}
|
||||
})
|
||||
|
||||
playlistUUID = created.uuid
|
||||
|
||||
privatePlaylist = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'private',
|
||||
privacy: VideoPlaylistPrivacy.PRIVATE,
|
||||
videoChannelId: server.store.channel.id
|
||||
}
|
||||
})
|
||||
|
||||
unlistedPlaylist = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'unlisted',
|
||||
privacy: VideoPlaylistPrivacy.UNLISTED,
|
||||
videoChannelId: server.store.channel.id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('Test oEmbed API validators', function () {
|
||||
|
||||
it('Should fail with an invalid url', async function () {
|
||||
const embedUrl = 'hello.com'
|
||||
await checkParamEmbed(server, embedUrl)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid host', async function () {
|
||||
const embedUrl = 'http://hello.com/videos/watch/' + server.store.videoCreated.uuid
|
||||
await checkParamEmbed(server, embedUrl)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid element id', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/blabla`
|
||||
await checkParamEmbed(server, embedUrl)
|
||||
})
|
||||
|
||||
it('Should fail with an unknown element', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c`
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_FOUND_404)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid path', async function () {
|
||||
const embedUrl = `${server.url}/videos/watchs/${server.store.videoCreated.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid max height', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxheight: 'hello' })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid max width', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxwidth: 'hello' })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid format', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { format: 'blabla' })
|
||||
})
|
||||
|
||||
it('Should fail with a non supported format', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_IMPLEMENTED_501, { format: 'xml' })
|
||||
})
|
||||
|
||||
it('Should fail with a private video', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${privateVideo.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should fail with an unlisted video with the int id', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${unlistedVideo.id}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should succeed with an unlisted video using the uuid id', async function () {
|
||||
for (const uuid of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) {
|
||||
const embedUrl = `${server.url}/videos/watch/${uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a private playlist', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/playlist/${privatePlaylist.uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should fail with an unlisted playlist using the int id', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/playlist/${unlistedPlaylist.id}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should succeed with an unlisted playlist using the uuid id', async function () {
|
||||
for (const uuid of [ unlistedPlaylist.uuid, unlistedPlaylist.shortUUID ]) {
|
||||
const embedUrl = `${server.url}/videos/watch/playlist/${uuid}`
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params with a video', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
|
||||
const query = {
|
||||
format: 'json',
|
||||
maxheight: 400,
|
||||
maxwidth: 400
|
||||
}
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query)
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params with a playlist', async function () {
|
||||
const embedUrl = `${server.url}/videos/watch/playlist/${playlistUUID}`
|
||||
const query = {
|
||||
format: 'json',
|
||||
maxheight: 400,
|
||||
maxwidth: 400
|
||||
}
|
||||
|
||||
await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
function checkParamEmbed (
|
||||
server: PeerTubeServer,
|
||||
embedUrl: string,
|
||||
expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400,
|
||||
query = {}
|
||||
) {
|
||||
const path = '/services/oembed'
|
||||
|
||||
return makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: Object.assign(query, { url: embedUrl }),
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test transcoding API validators', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
|
||||
let userToken: string
|
||||
let moderatorToken: string
|
||||
|
||||
let remoteId: string
|
||||
let validId: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER)
|
||||
moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR)
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
|
||||
remoteId = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' })
|
||||
validId = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].config.enableTranscoding()
|
||||
})
|
||||
|
||||
it('Should not run transcoding of a unknown video', async function () {
|
||||
await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'web-video', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not run transcoding of a remote video', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus })
|
||||
await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'web-video', expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not run transcoding by a non admin user', async function () {
|
||||
const expectedStatus = HttpStatusCode.FORBIDDEN_403
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus })
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', token: moderatorToken, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not run transcoding without transcoding type', async function () {
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should not run transcoding with an incorrect transcoding type', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'toto' as any, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not run transcoding if the instance disabled it', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await servers[0].config.disableTranscoding()
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus })
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus })
|
||||
})
|
||||
|
||||
it('Should run transcoding', async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
await servers[0].config.enableTranscoding()
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' })
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not run transcoding on a video that is already being transcoded if forceTranscoding is not set', async function () {
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' })
|
||||
|
||||
const expectedStatus = HttpStatusCode.CONFLICT_409
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus })
|
||||
|
||||
await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,294 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
TwoFactorCommand
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test two factor API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
let rootId: number
|
||||
let rootPassword: string
|
||||
let rootRequestToken: string
|
||||
let rootOTPToken: string
|
||||
|
||||
let userId: number
|
||||
let userToken = ''
|
||||
let userPassword: string
|
||||
let userRequestToken: string
|
||||
let userOTPToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
{
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('user1')
|
||||
userToken = result.token
|
||||
userId = result.userId
|
||||
userPassword = result.password
|
||||
}
|
||||
|
||||
{
|
||||
const { id } = await server.users.getMyInfo()
|
||||
rootId = id
|
||||
rootPassword = server.store.user.password
|
||||
}
|
||||
})
|
||||
|
||||
describe('When requesting two factor', function () {
|
||||
|
||||
it('Should fail with an unknown user id', async function () {
|
||||
await server.twoFactor.request({ userId: 42, currentPassword: rootPassword, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid user id', async function () {
|
||||
await server.twoFactor.request({
|
||||
userId: 'invalid' as any,
|
||||
currentPassword: rootPassword,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to request another user two factor without the appropriate rights', async function () {
|
||||
await server.twoFactor.request({
|
||||
userId: rootId,
|
||||
token: userToken,
|
||||
currentPassword: userPassword,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to request another user two factor with the appropriate rights', async function () {
|
||||
await server.twoFactor.request({ userId, currentPassword: rootPassword })
|
||||
})
|
||||
|
||||
it('Should fail to request two factor without a password', async function () {
|
||||
await server.twoFactor.request({
|
||||
userId,
|
||||
token: userToken,
|
||||
currentPassword: undefined,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to request two factor with an incorrect password', async function () {
|
||||
await server.twoFactor.request({
|
||||
userId,
|
||||
token: userToken,
|
||||
currentPassword: rootPassword,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () {
|
||||
await server.twoFactor.request({ userId })
|
||||
})
|
||||
|
||||
it('Should fail to request two factor without a password when targeting myself with an admin account', async function () {
|
||||
await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed to request my two factor auth', async function () {
|
||||
{
|
||||
const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
|
||||
userRequestToken = otpRequest.requestToken
|
||||
userOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
|
||||
}
|
||||
|
||||
{
|
||||
const { otpRequest } = await server.twoFactor.request({ userId: rootId, currentPassword: rootPassword })
|
||||
rootRequestToken = otpRequest.requestToken
|
||||
rootOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When confirming two factor request', function () {
|
||||
|
||||
it('Should fail with an unknown user id', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId: 42,
|
||||
requestToken: rootRequestToken,
|
||||
otpToken: rootOTPToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid user id', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId: 'invalid' as any,
|
||||
requestToken: rootRequestToken,
|
||||
otpToken: rootOTPToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to confirm another user two factor request without the appropriate rights', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId: rootId,
|
||||
token: userToken,
|
||||
requestToken: rootRequestToken,
|
||||
otpToken: rootOTPToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without request token', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
requestToken: undefined,
|
||||
otpToken: userOTPToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid request token', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
requestToken: 'toto',
|
||||
otpToken: userOTPToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with request token of another user', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
requestToken: rootRequestToken,
|
||||
otpToken: userOTPToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without an otp token', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
requestToken: userRequestToken,
|
||||
otpToken: undefined,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad otp token', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
requestToken: userRequestToken,
|
||||
otpToken: '123456',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to confirm another user two factor request with the appropriate rights', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
requestToken: userRequestToken,
|
||||
otpToken: userOTPToken
|
||||
})
|
||||
|
||||
// Reinit
|
||||
await server.twoFactor.disable({ userId, currentPassword: rootPassword })
|
||||
})
|
||||
|
||||
it('Should succeed to confirm my two factor request', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
token: userToken,
|
||||
requestToken: userRequestToken,
|
||||
otpToken: userOTPToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to confirm again two factor request', async function () {
|
||||
await server.twoFactor.confirmRequest({
|
||||
userId,
|
||||
token: userToken,
|
||||
requestToken: userRequestToken,
|
||||
otpToken: userOTPToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When disabling two factor', function () {
|
||||
|
||||
it('Should fail with an unknown user id', async function () {
|
||||
await server.twoFactor.disable({
|
||||
userId: 42,
|
||||
currentPassword: rootPassword,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid user id', async function () {
|
||||
await server.twoFactor.disable({
|
||||
userId: 'invalid' as any,
|
||||
currentPassword: rootPassword,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to disable another user two factor without the appropriate rights', async function () {
|
||||
await server.twoFactor.disable({
|
||||
userId: rootId,
|
||||
token: userToken,
|
||||
currentPassword: userPassword,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to disable two factor with an incorrect password', async function () {
|
||||
await server.twoFactor.disable({
|
||||
userId,
|
||||
token: userToken,
|
||||
currentPassword: rootPassword,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () {
|
||||
await server.twoFactor.disable({ userId })
|
||||
await server.twoFactor.requestAndConfirm({ userId })
|
||||
})
|
||||
|
||||
it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () {
|
||||
await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed to disable another user two factor with the appropriate rights', async function () {
|
||||
await server.twoFactor.disable({ userId, currentPassword: rootPassword })
|
||||
|
||||
await server.twoFactor.requestAndConfirm({ userId })
|
||||
})
|
||||
|
||||
it('Should succeed to update my two factor auth', async function () {
|
||||
await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword })
|
||||
})
|
||||
|
||||
it('Should fail to disable again two factor', async function () {
|
||||
await server.twoFactor.disable({
|
||||
userId,
|
||||
token: userToken,
|
||||
currentPassword: userPassword,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { randomInt } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoImportState, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
VideosCommand,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test upload quota', function () {
|
||||
let server: PeerTubeServer
|
||||
let rootId: number
|
||||
let command: VideosCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
const user = await server.users.getMyInfo()
|
||||
rootId = user.id
|
||||
|
||||
await server.users.update({ userId: rootId, videoQuota: 42 })
|
||||
|
||||
command = server.videos
|
||||
})
|
||||
|
||||
describe('When having a video quota', function () {
|
||||
|
||||
it('Should fail with a registered user having too many videos with legacy upload', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
|
||||
await server.registrations.register(user)
|
||||
const userToken = await server.login.getAccessToken(user)
|
||||
|
||||
const attributes = { fixture: 'video_short2.webm' }
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await command.upload({ token: userToken, attributes })
|
||||
}
|
||||
|
||||
await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
|
||||
})
|
||||
|
||||
it('Should fail with a registered user having too many videos with resumable upload', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
|
||||
await server.registrations.register(user)
|
||||
const userToken = await server.login.getAccessToken(user)
|
||||
|
||||
const attributes = { fixture: 'video_short2.webm' }
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await command.upload({ token: userToken, attributes })
|
||||
}
|
||||
|
||||
await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
|
||||
})
|
||||
|
||||
it('Should fail to import with HTTP/Torrent/magnet', async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
const baseAttributes = {
|
||||
channelId: server.store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
await server.videoImports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } })
|
||||
await server.videoImports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } })
|
||||
await server.videoImports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { total, data: videoImports } = await server.videoImports.getMyVideoImports()
|
||||
expect(total).to.equal(3)
|
||||
|
||||
expect(videoImports).to.have.lengthOf(3)
|
||||
|
||||
for (const videoImport of videoImports) {
|
||||
expect(videoImport.state.id).to.equal(VideoImportState.FAILED)
|
||||
expect(videoImport.error).not.to.be.undefined
|
||||
expect(videoImport.error).to.contain('user video quota is exceeded')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When having a daily video quota', function () {
|
||||
|
||||
it('Should fail with a user having too many videos daily', async function () {
|
||||
await server.users.update({ userId: rootId, videoQuotaDaily: 42 })
|
||||
|
||||
await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
|
||||
await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When having an absolute and daily video quota', function () {
|
||||
it('Should fail if exceeding total quota', async function () {
|
||||
await server.users.update({
|
||||
userId: rootId,
|
||||
videoQuota: 42,
|
||||
videoQuotaDaily: 1024 * 1024 * 1024
|
||||
})
|
||||
|
||||
await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
|
||||
await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
|
||||
})
|
||||
|
||||
it('Should fail if exceeding daily quota', async function () {
|
||||
await server.users.update({
|
||||
userId: rootId,
|
||||
videoQuota: 1024 * 1024 * 1024,
|
||||
videoQuotaDaily: 42
|
||||
})
|
||||
|
||||
await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
|
||||
await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,339 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test user export API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let rootId: number
|
||||
|
||||
let userId: number
|
||||
let userToken: string
|
||||
|
||||
let exportId: number
|
||||
let userExportId: number
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
{
|
||||
const user = await server.users.getMyInfo()
|
||||
rootId = user.id
|
||||
}
|
||||
|
||||
{
|
||||
userToken = await server.users.generateUserAndToken('user')
|
||||
const user = await server.users.getMyInfo({ token: userToken })
|
||||
userId = user.id
|
||||
}
|
||||
})
|
||||
|
||||
describe('Request export', function () {
|
||||
|
||||
it('Should fail if export is disabled', async function () {
|
||||
await server.config.disableUserExport()
|
||||
|
||||
await server.userExports.request({ userId: rootId, withVideoFiles: false, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await server.config.enableUserExport()
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.userExports.request({
|
||||
userId: rootId,
|
||||
withVideoFiles: false,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid token', async function () {
|
||||
await server.userExports.request({
|
||||
userId: rootId,
|
||||
withVideoFiles: false,
|
||||
token: 'hello',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a token of another user', async function () {
|
||||
await server.userExports.request({
|
||||
userId: rootId,
|
||||
withVideoFiles: false,
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown user', async function () {
|
||||
await server.userExports.request({ userId: 404, withVideoFiles: false, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail if user quota is too big', async function () {
|
||||
const { videoQuotaUsed } = await server.users.getMyQuotaUsed()
|
||||
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
export: {
|
||||
users: { maxUserVideoQuota: videoQuotaUsed - 1 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await server.userExports.request({ userId: rootId, withVideoFiles: true, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await server.userExports.request({ userId: rootId, withVideoFiles: false, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
// Cleanup
|
||||
await server.userExports.waitForCreation({ userId: rootId })
|
||||
await server.userExports.deleteAllArchives({ userId: rootId })
|
||||
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
export: {
|
||||
users: { maxUserVideoQuota: 1000 * 1000 * 1000 * 1000 }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the appropriate token', async function () {
|
||||
const { export: { id } } = await server.userExports.request({ userId: rootId, withVideoFiles: false })
|
||||
|
||||
exportId = id
|
||||
})
|
||||
|
||||
it('Should fail if there is already an export', async function () {
|
||||
await server.userExports.request({
|
||||
userId: rootId,
|
||||
withVideoFiles: false,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed after a delete with an admin token', async function () {
|
||||
await server.userExports.waitForCreation({ userId: rootId })
|
||||
await server.userExports.delete({ userId: rootId, exportId })
|
||||
|
||||
const { export: { id } } = await server.userExports.request({ userId: rootId, withVideoFiles: false })
|
||||
exportId = id
|
||||
})
|
||||
})
|
||||
|
||||
describe('List exports', function () {
|
||||
|
||||
it('Should fail if export is disabled', async function () {
|
||||
await server.config.disableUserExport()
|
||||
|
||||
await server.userExports.list({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await server.config.enableUserExport()
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.userExports.list({
|
||||
userId: rootId,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid token', async function () {
|
||||
await server.userExports.list({
|
||||
userId: rootId,
|
||||
token: 'toto',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a token of another user', async function () {
|
||||
await server.userExports.list({
|
||||
userId: rootId,
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown user', async function () {
|
||||
await server.userExports.list({ userId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
// User token
|
||||
await server.userExports.list({ userId, token: userToken })
|
||||
// Root token
|
||||
await server.userExports.list({ userId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deleting export', function () {
|
||||
|
||||
before(async function () {
|
||||
const { export: { id } } = await server.userExports.request({ userId, withVideoFiles: true })
|
||||
userExportId = id
|
||||
|
||||
await server.userExports.waitForCreation({ userId })
|
||||
})
|
||||
|
||||
it('Should fail if export is disabled', async function () {
|
||||
await server.config.disableUserExport()
|
||||
|
||||
await server.userExports.delete({ userId, exportId: userExportId, token: userToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await server.config.enableUserExport()
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.userExports.delete({
|
||||
userId: rootId,
|
||||
exportId,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid token', async function () {
|
||||
await server.userExports.delete({
|
||||
userId: rootId,
|
||||
exportId,
|
||||
token: 'toto',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a token of another user', async function () {
|
||||
await server.userExports.delete({
|
||||
userId: rootId,
|
||||
exportId,
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an export id of another user', async function () {
|
||||
await server.userExports.delete({
|
||||
userId,
|
||||
exportId,
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown user', async function () {
|
||||
await server.userExports.delete({
|
||||
userId: 404,
|
||||
exportId,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown export id', async function () {
|
||||
await server.userExports.delete({
|
||||
userId,
|
||||
exportId: 404,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await server.userExports.delete({
|
||||
userId,
|
||||
exportId: userExportId,
|
||||
token: userToken
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Downloading an export', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.userExports.request({ userId, withVideoFiles: true })
|
||||
await server.userExports.waitForCreation({ userId })
|
||||
})
|
||||
|
||||
it('Should fail without jwt token', async function () {
|
||||
const { data } = await server.userExports.list({ userId })
|
||||
|
||||
const url = data[0].privateDownloadUrl.replace('jwt=', 'toto=')
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a wrong jwt token', async function () {
|
||||
const { data } = await server.userExports.list({ userId })
|
||||
|
||||
// Invalid format
|
||||
{
|
||||
const url = data[0].privateDownloadUrl.replace('jwt=', 'jwt=hello.coucou')
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
|
||||
// Invalid content
|
||||
{
|
||||
const url = data[0].privateDownloadUrl.replace('jwt=', 'jwt=a')
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a jwt token of another export', async function () {
|
||||
let userQuery: string
|
||||
|
||||
// Save user JWT token
|
||||
{
|
||||
const { data } = await server.userExports.list({ userId })
|
||||
|
||||
const { pathname, search } = new URL(data[0].privateDownloadUrl)
|
||||
const rawQuery = search.replace('?', '')
|
||||
userQuery = rawQuery
|
||||
|
||||
await makeGetRequest({ url: server.url, path: pathname, rawQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
// This user JWT token must not be used to download an export of another user
|
||||
{
|
||||
const { data } = await server.userExports.list({ userId: rootId })
|
||||
|
||||
const { pathname, search } = new URL(data[0].privateDownloadUrl)
|
||||
const rawQuery = search.replace('?', '')
|
||||
|
||||
await makeGetRequest({ url: server.url, path: pathname, rawQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeGetRequest({ url: server.url, path: pathname, rawQuery: userQuery, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid filename', async function () {
|
||||
const { data } = await server.userExports.list({ userId })
|
||||
|
||||
const url = data[0].privateDownloadUrl.replace('.zip', '.tar')
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an expired JWT token', async function () {
|
||||
const { data } = await server.userExports.list({ userId })
|
||||
|
||||
await wait(3000)
|
||||
await makeRawRequest({ url: data[0].privateDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const { data } = await server.userExports.list({ userId })
|
||||
await makeRawRequest({ url: data[0].privateDownloadUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,169 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer, PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test user import API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let userId: number
|
||||
let rootId: number
|
||||
let token: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
{
|
||||
const result = await server.users.generate('user')
|
||||
userId = result.userId
|
||||
token = result.token
|
||||
}
|
||||
|
||||
{
|
||||
const { id } = await server.users.getMyInfo()
|
||||
rootId = id
|
||||
}
|
||||
})
|
||||
|
||||
describe('Request import', function () {
|
||||
|
||||
it('Should fail if import is disabled', async function () {
|
||||
await server.config.disableUserImport()
|
||||
|
||||
await server.userImports.importArchive({
|
||||
userId,
|
||||
fixture: 'export-without-files.zip',
|
||||
token,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await server.config.enableUserImport()
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.userImports.importArchive({
|
||||
userId,
|
||||
fixture: 'export-without-files.zip',
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid token', async function () {
|
||||
await server.userImports.importArchive({
|
||||
userId,
|
||||
fixture: 'export-without-files.zip',
|
||||
token: 'invalid',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a token of another user', async function () {
|
||||
await server.userImports.importArchive({
|
||||
userId: rootId,
|
||||
fixture: 'export-without-files.zip',
|
||||
token,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown user', async function () {
|
||||
await server.userImports.importArchive({
|
||||
userId: 404,
|
||||
fixture: 'export-without-files.zip',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if user quota is exceeded', async function () {
|
||||
await server.users.update({ userId, videoQuota: 100 })
|
||||
|
||||
await server.userImports.importArchive({
|
||||
userId,
|
||||
fixture: 'export-without-files.zip',
|
||||
expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
|
||||
})
|
||||
|
||||
await server.users.update({ userId, videoQuota: -1 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.userImports.importArchive({ userId, fixture: 'export-without-files.zip' })
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
it('Should fail with an import that is already being processed', async function () {
|
||||
await server.userImports.importArchive({ userId, fixture: 'export-without-files.zip' })
|
||||
await server.userImports.importArchive({
|
||||
userId,
|
||||
fixture: 'export-without-files.zip',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid ZIPs', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const toTest = [
|
||||
'export-bad-video-file.zip',
|
||||
'export-bad-video.zip',
|
||||
'export-without-videos.zip',
|
||||
'export-bad-structure.zip',
|
||||
'export-bad-structure.zip'
|
||||
]
|
||||
|
||||
const tokens: string[] = []
|
||||
|
||||
for (let i = 0; i < toTest.length; i++) {
|
||||
const { token, userId } = await server.users.generate('import' + i)
|
||||
await server.userImports.importArchive({ userId, token, fixture: toTest[i] })
|
||||
}
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
for (const token of tokens) {
|
||||
const { data } = await server.videos.listMyVideos({ token })
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Get latest import status', function () {
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.userImports.getLatestImport({ userId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with invalid token', async function () {
|
||||
await server.userImports.getLatestImport({ userId, token: 'invalid', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown user', async function () {
|
||||
await server.userImports.getLatestImport({ userId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a token of another user', async function () {
|
||||
await server.userImports.getLatestImport({ userId: rootId, token, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await server.userImports.getLatestImport({ userId, token })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,291 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { io } from 'socket.io-client'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test user notifications API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
describe('When listing my notifications', function () {
|
||||
const path = '/api/v1/users/me/notifications'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect unread parameter', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
unread: 'toto'
|
||||
},
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When marking as read my notifications', function () {
|
||||
const path = '/api/v1/users/me/notifications/read'
|
||||
|
||||
it('Should fail with wrong ids parameters', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {
|
||||
ids: [ 'hello' ]
|
||||
},
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {
|
||||
ids: [ ]
|
||||
},
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {
|
||||
ids: 5
|
||||
},
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {
|
||||
ids: [ 5 ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: {
|
||||
ids: [ 5 ]
|
||||
},
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When marking as read my notifications', function () {
|
||||
const path = '/api/v1/users/me/notifications/read-all'
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating my notification settings', function () {
|
||||
const path = '/api/v1/users/me/notification-settings'
|
||||
const correctFields: UserNotificationSetting = {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
abuseAsModerator: UserNotificationSettingValue.WEB,
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB,
|
||||
commentMention: UserNotificationSettingValue.WEB,
|
||||
newFollow: UserNotificationSettingValue.WEB,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB,
|
||||
newInstanceFollower: UserNotificationSettingValue.WEB,
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB,
|
||||
abuseNewMessage: UserNotificationSettingValue.WEB,
|
||||
abuseStateChange: UserNotificationSettingValue.WEB,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB,
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB,
|
||||
myVideoTranscriptionGenerated: UserNotificationSettingValue.WEB,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
it('Should fail with missing fields', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with incorrect field values', async function () {
|
||||
{
|
||||
const fields = { ...correctFields, newCommentOnMyVideo: 15 }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const fields = { ...correctFields, newCommentOnMyVideo: 'toto' }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: correctFields,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: correctFields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When connecting to my notification socket', function () {
|
||||
|
||||
it('Should fail with no token', function (next) {
|
||||
const socket = io(`${server.url}/user-notifications`, { reconnection: false })
|
||||
|
||||
socket.once('connect_error', function () {
|
||||
socket.disconnect()
|
||||
next()
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.disconnect()
|
||||
next(new Error('Connected with a missing token.'))
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid token', function (next) {
|
||||
const socket = io(`${server.url}/user-notifications`, {
|
||||
query: { accessToken: 'bad_access_token' },
|
||||
reconnection: false
|
||||
})
|
||||
|
||||
socket.once('connect_error', function () {
|
||||
socket.disconnect()
|
||||
next()
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.disconnect()
|
||||
next(new Error('Connected with an invalid token.'))
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct token', function (next) {
|
||||
const socket = io(`${server.url}/user-notifications`, {
|
||||
query: { accessToken: server.accessToken },
|
||||
reconnection: false
|
||||
})
|
||||
|
||||
function errorListener (err) {
|
||||
next(new Error('Error in connection: ' + err))
|
||||
}
|
||||
|
||||
socket.on('connect_error', errorListener)
|
||||
|
||||
socket.once('connect', async () => {
|
||||
socket.disconnect()
|
||||
|
||||
await wait(500)
|
||||
next()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,298 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js'
|
||||
|
||||
describe('Test user subscriptions API validators', function () {
|
||||
const path = '/api/v1/users/me/subscriptions'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const user = {
|
||||
username: 'user1',
|
||||
password: 'my super password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
})
|
||||
|
||||
describe('When listing my subscriptions', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing my subscriptions videos', function () {
|
||||
const path = '/api/v1/users/me/subscriptions/videos'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a subscription', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields: { uri: 'user1_channel@' + server.host },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with bad URIs', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { uri: 'root' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { uri: 'root@' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { uri: 'root@hello@' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: { uri: 'user1_channel@' + server.host },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting a subscription', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1_channel@' + server.host,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with bad URIs', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '/root',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '/root@',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '/root@hello@',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown subscription', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '/root1@' + server.host,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1_channel@' + server.host,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When checking if subscriptions exist', function () {
|
||||
const existPath = path + '/exist'
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: existPath,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with bad URIs', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: existPath,
|
||||
query: { uris: 'toto' },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: existPath,
|
||||
query: { 'uris[]': 1 },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: existPath,
|
||||
query: { 'uris[]': 'coucou@' + server.host },
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing a subscription', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1_channel@' + server.host,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with bad URIs', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/root',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/root@',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/root@hello@',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown subscription', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/root1@' + server.host,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '/user1_channel@' + server.host,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,457 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserAdminFlag, UserRole } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
ConfigCommand,
|
||||
createSingleServer,
|
||||
killallServers,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test users admin API validators', function () {
|
||||
const path = '/api/v1/users/'
|
||||
let userId: number
|
||||
let rootId: number
|
||||
let moderatorId: number
|
||||
let server: PeerTubeServer
|
||||
let userToken = ''
|
||||
let moderatorToken = ''
|
||||
let emailPort: number
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const emails: object[] = []
|
||||
emailPort = await MockSmtpServer.Instance.collectEmails(emails)
|
||||
|
||||
{
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('user1')
|
||||
userToken = result.token
|
||||
userId = result.userId
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('moderator1', UserRole.MODERATOR)
|
||||
moderatorToken = result.token
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.users.generate('moderator2', UserRole.MODERATOR)
|
||||
moderatorId = result.userId
|
||||
}
|
||||
})
|
||||
|
||||
describe('When listing users', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a new user', function () {
|
||||
const baseCorrectParams = {
|
||||
username: 'user2',
|
||||
email: 'test@example.com',
|
||||
password: 'my super password',
|
||||
videoQuota: -1,
|
||||
videoQuotaDaily: -1,
|
||||
role: UserRole.USER,
|
||||
adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST
|
||||
}
|
||||
|
||||
it('Should fail with a too small username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: '' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too long username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'super'.repeat(50) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a not lowercase username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'Toto' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'my username' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a missing email', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'email' ])
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid email', async function () {
|
||||
const fields = { ...baseCorrectParams, email: 'test_example.com' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too small password', async function () {
|
||||
const fields = { ...baseCorrectParams, password: 'bla' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too long password', async function () {
|
||||
const fields = { ...baseCorrectParams, password: 'super'.repeat(61) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with empty password and no smtp configured', async function () {
|
||||
const fields = { ...baseCorrectParams, password: '' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with no password on a server with smtp enabled', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await killallServers([ server ])
|
||||
|
||||
await server.run(ConfigCommand.getEmailOverrideConfig(emailPort))
|
||||
|
||||
const fields = {
|
||||
...baseCorrectParams,
|
||||
|
||||
password: '',
|
||||
username: 'create_password',
|
||||
email: 'create_password@example.com'
|
||||
}
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid admin flags', async function () {
|
||||
const fields = { ...baseCorrectParams, adminFlags: 'toto' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an non authenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: 'supertoken',
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if we add a user with the same username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'user1' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if we add a user with the same email', async function () {
|
||||
const fields = { ...baseCorrectParams, email: 'user1@example.com' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videoQuota', async function () {
|
||||
const fields = { ...baseCorrectParams, videoQuota: -5 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videoQuotaDaily', async function () {
|
||||
const fields = { ...baseCorrectParams, videoQuotaDaily: -7 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without a user role', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'role' ])
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid user role', async function () {
|
||||
const fields = { ...baseCorrectParams, role: 88989 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a "peertube" username', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'peertube' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to create a moderator or an admin with a moderator', async function () {
|
||||
for (const role of [ UserRole.MODERATOR, UserRole.ADMINISTRATOR ]) {
|
||||
const fields = { ...baseCorrectParams, role }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: moderatorToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed to create a user with a moderator', async function () {
|
||||
const fields = { ...baseCorrectParams, username: 'a4656', email: 'a4656@example.com', role: UserRole.USER }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: moderatorToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
const user = { username: 'user1' }
|
||||
userToken = await server.login.getAccessToken(user)
|
||||
|
||||
const fields = {
|
||||
username: 'user3',
|
||||
email: 'test@example.com',
|
||||
password: 'my super password',
|
||||
videoQuota: 42000000
|
||||
}
|
||||
await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting a user', function () {
|
||||
|
||||
it('Should fail with an non authenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + userId,
|
||||
token: 'supertoken',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, path: path + userId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating a user', function () {
|
||||
|
||||
it('Should fail with an invalid email attribute', async function () {
|
||||
const fields = {
|
||||
email: 'blabla'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid emailVerified attribute', async function () {
|
||||
const fields = {
|
||||
emailVerified: 'yes'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid videoQuota attribute', async function () {
|
||||
const fields = {
|
||||
videoQuota: -90
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid user role attribute', async function () {
|
||||
const fields = {
|
||||
role: 54878
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too small password', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'bla'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a too long password', async function () {
|
||||
const fields = {
|
||||
currentPassword: 'password',
|
||||
password: 'super'.repeat(61)
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an non authenticated user', async function () {
|
||||
const fields = {
|
||||
videoQuota: 42
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + userId,
|
||||
token: 'supertoken',
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when updating root role', async function () {
|
||||
const fields = {
|
||||
role: UserRole.MODERATOR
|
||||
}
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + rootId, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with invalid admin flags', async function () {
|
||||
const fields = { adminFlags: 'toto' }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail to update an admin with a moderator', async function () {
|
||||
const fields = {
|
||||
videoQuota: 42
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + moderatorId,
|
||||
token: moderatorToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to update a user with a moderator', async function () {
|
||||
const fields = {
|
||||
videoQuota: 42
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + userId,
|
||||
token: moderatorToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const fields = {
|
||||
email: 'email@example.com',
|
||||
emailVerified: true,
|
||||
videoQuota: 42,
|
||||
role: UserRole.USER
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + userId,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test users API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1, {
|
||||
rates_limit: {
|
||||
ask_send_email: {
|
||||
max: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await server.config.enableSignup(true)
|
||||
|
||||
await server.users.generate('moderator2', UserRole.MODERATOR)
|
||||
|
||||
await server.registrations.requestRegistration({
|
||||
username: 'request1',
|
||||
registrationReason: 'tt'
|
||||
})
|
||||
})
|
||||
|
||||
describe('When asking a password reset', function () {
|
||||
const path = '/api/v1/users/ask-reset-password'
|
||||
|
||||
it('Should fail with a missing email', async function () {
|
||||
const fields = {}
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid email', async function () {
|
||||
const fields = { email: 'hello' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields })
|
||||
})
|
||||
|
||||
it('Should success with the correct params', async function () {
|
||||
const fields = { email: 'admin@example.com' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When asking for an account verification email', function () {
|
||||
const path = '/api/v1/users/ask-send-verify-email'
|
||||
|
||||
it('Should fail with a missing email', async function () {
|
||||
const fields = {}
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid email', async function () {
|
||||
const fields = { email: 'hello' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const fields = { email: 'admin@example.com' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When asking for a registration verification email', function () {
|
||||
const path = '/api/v1/users/registrations/ask-send-verify-email'
|
||||
|
||||
it('Should fail with a missing email', async function () {
|
||||
const fields = {}
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid email', async function () {
|
||||
const fields = { email: 'hello' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const fields = { email: 'request1@example.com' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,292 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode, VideoBlacklistType } from '@peertube/peertube-models'
|
||||
import {
|
||||
BlacklistCommand,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video blacklist API validators', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let notBlacklistedVideoId: string
|
||||
let remoteVideoUUID: string
|
||||
let userAccessToken1 = ''
|
||||
let userAccessToken2 = ''
|
||||
let command: BlacklistCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
{
|
||||
const username = 'user1'
|
||||
const password = 'my super password'
|
||||
await servers[0].users.create({ username, password })
|
||||
userAccessToken1 = await servers[0].login.getAccessToken({ username, password })
|
||||
}
|
||||
|
||||
{
|
||||
const username = 'user2'
|
||||
const password = 'my super password'
|
||||
await servers[0].users.create({ username, password })
|
||||
userAccessToken2 = await servers[0].login.getAccessToken({ username, password })
|
||||
}
|
||||
|
||||
{
|
||||
servers[0].store.videoCreated = await servers[0].videos.upload({ token: userAccessToken1 })
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.upload()
|
||||
notBlacklistedVideoId = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.upload()
|
||||
remoteVideoUUID = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
command = servers[0].blacklist
|
||||
})
|
||||
|
||||
describe('When adding a video in blacklist', function () {
|
||||
const basePath = '/api/v1/videos/'
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated + '/blacklist'
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a wrong video', async function () {
|
||||
const wrongPath = '/api/v1/videos/blabla/blacklist'
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated + '/blacklist'
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated + '/blacklist'
|
||||
const fields = {}
|
||||
await makePostBodyRequest({
|
||||
url: servers[0].url,
|
||||
path,
|
||||
token: userAccessToken2,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid reason', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
|
||||
const fields = { reason: 'a'.repeat(305) }
|
||||
|
||||
await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail to unfederate a remote video', async function () {
|
||||
const path = basePath + remoteVideoUUID + '/blacklist'
|
||||
const fields = { unfederate: true }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: servers[0].url,
|
||||
path,
|
||||
token: servers[0].accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
|
||||
const fields = {}
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: servers[0].url,
|
||||
path,
|
||||
token: servers[0].accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating a video in blacklist', function () {
|
||||
const basePath = '/api/v1/videos/'
|
||||
|
||||
it('Should fail with a wrong video', async function () {
|
||||
const wrongPath = '/api/v1/videos/blabla/blacklist'
|
||||
const fields = {}
|
||||
await makePutBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a video not blacklisted', async function () {
|
||||
const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist'
|
||||
const fields = {}
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path,
|
||||
token: servers[0].accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated + '/blacklist'
|
||||
const fields = {}
|
||||
await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated + '/blacklist'
|
||||
const fields = {}
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path,
|
||||
token: userAccessToken2,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid reason', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
|
||||
const fields = { reason: 'a'.repeat(305) }
|
||||
|
||||
await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const path = basePath + servers[0].store.videoCreated.shortUUID + '/blacklist'
|
||||
const fields = { reason: 'hello' }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: servers[0].url,
|
||||
path,
|
||||
token: servers[0].accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting blacklisted video', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await servers[0].videos.get({ id: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another user', async function () {
|
||||
await servers[0].videos.getWithToken({
|
||||
token: userAccessToken2,
|
||||
id: servers[0].store.videoCreated.uuid,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the owner authenticated user', async function () {
|
||||
const video = await servers[0].videos.getWithToken({ token: userAccessToken1, id: servers[0].store.videoCreated.uuid })
|
||||
expect(video.blacklisted).to.be.true
|
||||
})
|
||||
|
||||
it('Should succeed with an admin', async function () {
|
||||
const video = servers[0].store.videoCreated
|
||||
|
||||
for (const id of [ video.id, video.uuid, video.shortUUID ]) {
|
||||
const video = await servers[0].videos.getWithToken({ id, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(video.blacklisted).to.be.true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing a video in blacklist', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.remove({
|
||||
token: 'faketoken',
|
||||
videoId: servers[0].store.videoCreated.uuid,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await command.remove({
|
||||
token: userAccessToken2,
|
||||
videoId: servers[0].store.videoCreated.uuid,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect id', async function () {
|
||||
await command.remove({ videoId: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a not blacklisted video', async function () {
|
||||
// The video was not added to the blacklist so it should fail
|
||||
await command.remove({ videoId: notBlacklistedVideoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.remove({ videoId: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing videos in blacklist', function () {
|
||||
const basePath = '/api/v1/videos/blacklist/'
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await servers[0].blacklist.list({ token: 'faketoken', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a non admin user', async function () {
|
||||
await servers[0].blacklist.list({ token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(servers[0].url, basePath, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(servers[0].url, basePath, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid type', async function () {
|
||||
await servers[0].blacklist.list({ type: 0 as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await servers[0].blacklist.list({ type: VideoBlacklistType.MANUAL })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,312 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makeUploadRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video captions API validator', function () {
|
||||
const path = '/api/v1/videos/'
|
||||
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
let video: VideoCreateResult
|
||||
let privateVideo: VideoCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
video = await server.videos.upload()
|
||||
privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
})
|
||||
|
||||
describe('When adding video caption', function () {
|
||||
const fields = { }
|
||||
const attaches = {
|
||||
captionfile: buildAbsoluteFixturePath('subtitle-good1.vtt')
|
||||
}
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr',
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr',
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: 404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a missing language in path', async function () {
|
||||
const captionPath = path + video.uuid + '/captions'
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown language', async function () {
|
||||
const captionPath = path + video.uuid + '/captions/15'
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without access token', async function () {
|
||||
const captionPath = path + video.uuid + '/captions/fr'
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
const captionPath = path + video.uuid + '/captions/fr'
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: 'blabla',
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user token', async function () {
|
||||
const captionPath = path + video.uuid + '/captions/fr'
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: userAccessToken,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
// We accept any file now
|
||||
// it('Should fail with an invalid captionfile extension', async function () {
|
||||
// const attaches = {
|
||||
// 'captionfile': buildAbsoluteFixturePath('subtitle-bad.txt')
|
||||
// }
|
||||
//
|
||||
// const captionPath = path + video.uuid + '/captions/fr'
|
||||
// await makeUploadRequest({
|
||||
// method: 'PUT',
|
||||
// url: server.url,
|
||||
// path: captionPath,
|
||||
// token: server.accessToken,
|
||||
// fields,
|
||||
// attaches,
|
||||
// expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
// })
|
||||
// })
|
||||
|
||||
// We don't check the extension yet
|
||||
// it('Should fail with an invalid captionfile extension and octet-stream mime type', async function () {
|
||||
// await createVideoCaption({
|
||||
// url: server.url,
|
||||
// accessToken: server.accessToken,
|
||||
// language: 'zh',
|
||||
// videoId: video.uuid,
|
||||
// fixture: 'subtitle-bad.txt',
|
||||
// mimeType: 'application/octet-stream',
|
||||
// expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
// })
|
||||
// })
|
||||
|
||||
it('Should succeed with a valid captionfile extension and octet-stream mime type', async function () {
|
||||
await server.captions.add({
|
||||
language: 'zh',
|
||||
videoId: video.uuid,
|
||||
fixture: 'subtitle-good.srt',
|
||||
mimeType: 'application/octet-stream'
|
||||
})
|
||||
})
|
||||
|
||||
// We don't check the file validity yet
|
||||
// it('Should fail with an invalid captionfile srt', async function () {
|
||||
// const attaches = {
|
||||
// 'captionfile': buildAbsoluteFixturePath('subtitle-bad.srt')
|
||||
// }
|
||||
//
|
||||
// const captionPath = path + video.uuid + '/captions/fr'
|
||||
// await makeUploadRequest({
|
||||
// method: 'PUT',
|
||||
// url: server.url,
|
||||
// path: captionPath,
|
||||
// token: server.accessToken,
|
||||
// fields,
|
||||
// attaches,
|
||||
// expectedStatus: HttpStatusCode.INTERNAL_SERVER_ERROR_500
|
||||
// })
|
||||
// })
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
const captionPath = path + video.uuid + '/captions/fr'
|
||||
await makeUploadRequest({
|
||||
method: 'PUT',
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing video captions', function () {
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await makeGetRequest({ url: server.url, path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions' })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a private video without token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + privateVideo.shortUUID + '/captions',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path: path + privateVideo.shortUUID + '/captions',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: path + privateVideo.shortUUID + '/captions',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting video caption', function () {
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr',
|
||||
token: server.accessToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid language', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/16',
|
||||
token: server.accessToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a missing language', async function () {
|
||||
const captionPath = path + video.shortUUID + '/captions'
|
||||
await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown language', async function () {
|
||||
const captionPath = path + video.shortUUID + '/captions/15'
|
||||
await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken })
|
||||
})
|
||||
|
||||
it('Should fail without access token', async function () {
|
||||
const captionPath = path + video.shortUUID + '/captions/fr'
|
||||
await makeDeleteRequest({ url: server.url, path: captionPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
const captionPath = path + video.shortUUID + '/captions/fr'
|
||||
await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another user', async function () {
|
||||
const captionPath = path + video.shortUUID + '/captions/fr'
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
const captionPath = path + video.shortUUID + '/captions/fr'
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: captionPath,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,319 @@
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models'
|
||||
import {
|
||||
ChannelSyncsCommand,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video channel sync API validator', () => {
|
||||
const path = '/api/v1/video-channel-syncs'
|
||||
let server: PeerTubeServer
|
||||
let command: ChannelSyncsCommand
|
||||
let rootChannelId: number
|
||||
let rootChannelSyncId: number
|
||||
const userInfo = {
|
||||
accessToken: '',
|
||||
username: 'user1',
|
||||
id: -1,
|
||||
channelId: -1,
|
||||
syncId: -1
|
||||
}
|
||||
|
||||
async function withChannelSyncDisabled<T> (callback: () => Promise<T>): Promise<void> {
|
||||
try {
|
||||
await server.config.disableChannelSync()
|
||||
await callback()
|
||||
} finally {
|
||||
await server.config.enableChannelSync()
|
||||
}
|
||||
}
|
||||
|
||||
async function withMaxSyncsPerUser<T> (maxSync: number, callback: () => Promise<T>): Promise<void> {
|
||||
const origConfig = await server.config.getCustomConfig()
|
||||
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
import: {
|
||||
videoChannelSynchronization: {
|
||||
maxPerUser: maxSync
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await callback()
|
||||
} finally {
|
||||
await server.config.updateCustomConfig({ newCustomConfig: origConfig })
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30_000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
command = server.channelSyncs
|
||||
|
||||
rootChannelId = server.store.channel.id
|
||||
|
||||
{
|
||||
userInfo.accessToken = await server.users.generateUserAndToken(userInfo.username)
|
||||
|
||||
const { videoChannels, id: userId } = await server.users.getMyInfo({ token: userInfo.accessToken })
|
||||
userInfo.id = userId
|
||||
userInfo.channelId = videoChannels[0].id
|
||||
}
|
||||
|
||||
await server.config.enableChannelSync()
|
||||
})
|
||||
|
||||
describe('When creating a sync', function () {
|
||||
let baseCorrectParams: VideoChannelSyncCreate
|
||||
|
||||
before(function () {
|
||||
baseCorrectParams = {
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
videoChannelId: rootChannelId
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail when sync is disabled', async function () {
|
||||
await withChannelSyncDisabled(async () => {
|
||||
await command.create({
|
||||
token: server.accessToken,
|
||||
attributes: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with no authentication', async function () {
|
||||
await command.create({
|
||||
token: null,
|
||||
attributes: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without a target url', async function () {
|
||||
const attributes: VideoChannelSyncCreate = {
|
||||
...baseCorrectParams,
|
||||
externalChannelUrl: null
|
||||
}
|
||||
await command.create({
|
||||
token: server.accessToken,
|
||||
attributes,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without a channelId', async function () {
|
||||
const attributes: VideoChannelSyncCreate = {
|
||||
...baseCorrectParams,
|
||||
videoChannelId: null
|
||||
}
|
||||
await command.create({
|
||||
token: server.accessToken,
|
||||
attributes,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a channelId referring nothing', async function () {
|
||||
const attributes: VideoChannelSyncCreate = {
|
||||
...baseCorrectParams,
|
||||
videoChannelId: 42
|
||||
}
|
||||
await command.create({
|
||||
token: server.accessToken,
|
||||
attributes,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to create a sync when the user does not own the channel', async function () {
|
||||
await command.create({
|
||||
token: userInfo.accessToken,
|
||||
attributes: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to create a sync with root and for another user\'s channel', async function () {
|
||||
const { videoChannelSync } = await command.create({
|
||||
token: server.accessToken,
|
||||
attributes: {
|
||||
...baseCorrectParams,
|
||||
videoChannelId: userInfo.channelId
|
||||
},
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
userInfo.syncId = videoChannelSync.id
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const { videoChannelSync } = await command.create({
|
||||
token: server.accessToken,
|
||||
attributes: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
rootChannelSyncId = videoChannelSync.id
|
||||
})
|
||||
|
||||
it('Should fail when the user exceeds allowed number of synchronizations', async function () {
|
||||
await withMaxSyncsPerUser(1, async () => {
|
||||
await command.create({
|
||||
token: server.accessToken,
|
||||
attributes: {
|
||||
...baseCorrectParams,
|
||||
videoChannelId: userInfo.channelId
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing my channel syncs', function () {
|
||||
const myPath = '/api/v1/accounts/root/video-channel-syncs'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, myPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, myPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, myPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await command.listByAccount({
|
||||
accountName: 'root',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with no authentication', async function () {
|
||||
await command.listByAccount({
|
||||
accountName: 'root',
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when a simple user lists another user\'s synchronizations', async function () {
|
||||
await command.listByAccount({
|
||||
accountName: 'root',
|
||||
token: userInfo.accessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed when root lists another user\'s synchronizations', async function () {
|
||||
await command.listByAccount({
|
||||
accountName: userInfo.username,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed even with synchronization disabled', async function () {
|
||||
await withChannelSyncDisabled(async function () {
|
||||
await command.listByAccount({
|
||||
accountName: 'root',
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When triggering deletion', function () {
|
||||
it('should fail with no authentication', async function () {
|
||||
await command.delete({
|
||||
channelSyncId: userInfo.syncId,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when channelSyncId does not refer to any sync', async function () {
|
||||
await command.delete({
|
||||
channelSyncId: 42,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when sync is not owned by the user', async function () {
|
||||
await command.delete({
|
||||
channelSyncId: rootChannelSyncId,
|
||||
token: userInfo.accessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed when root delete a sync they do not own', async function () {
|
||||
await command.delete({
|
||||
channelSyncId: userInfo.syncId,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed when user delete a sync they own', async function () {
|
||||
const { videoChannelSync } = await command.create({
|
||||
attributes: {
|
||||
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||
videoChannelId: userInfo.channelId
|
||||
},
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await command.delete({
|
||||
channelSyncId: videoChannelSync.id,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed even when synchronization is disabled', async function () {
|
||||
await withChannelSyncDisabled(async function () {
|
||||
await command.delete({
|
||||
channelSyncId: rootChannelSyncId,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await server?.kill()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,379 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
ChannelsCommand,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video channels API validator', function () {
|
||||
const videoChannelPath = '/api/v1/video-channels'
|
||||
let server: PeerTubeServer
|
||||
const userInfo = {
|
||||
accessToken: '',
|
||||
channelName: 'fake_channel',
|
||||
id: -1,
|
||||
videoQuota: -1,
|
||||
videoQuotaDaily: -1
|
||||
}
|
||||
let command: ChannelsCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const userCreds = {
|
||||
username: 'fake',
|
||||
password: 'fake_password'
|
||||
}
|
||||
|
||||
{
|
||||
const user = await server.users.create({ username: userCreds.username, password: userCreds.password })
|
||||
userInfo.id = user.id
|
||||
userInfo.accessToken = await server.login.getAccessToken(userCreds)
|
||||
}
|
||||
|
||||
command = server.channels
|
||||
})
|
||||
|
||||
describe('When listing a video channels', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, videoChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, videoChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing account video channels', function () {
|
||||
const accountChannelPath = '/api/v1/accounts/fake/video-channels'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, accountChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, accountChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, accountChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a unknown account', async function () {
|
||||
await server.channels.listByAccount({ accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: accountChannelPath,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a video channel', function () {
|
||||
const baseCorrectParams = {
|
||||
name: 'super_channel',
|
||||
displayName: 'hello',
|
||||
description: 'super description',
|
||||
support: 'super support text'
|
||||
}
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath,
|
||||
token: 'none',
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without a name', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'name' ])
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad name', async function () {
|
||||
const fields = { ...baseCorrectParams, name: 'super name' }
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without a name', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'displayName' ])
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long name', async function () {
|
||||
const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) }
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = { ...baseCorrectParams, description: 'super'.repeat(201) }
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long support text', async function () {
|
||||
const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
|
||||
await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when adding a channel with the same username', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating a video channel', function () {
|
||||
const baseCorrectParams: VideoChannelUpdate = {
|
||||
displayName: 'hello',
|
||||
description: 'super description',
|
||||
support: 'toto',
|
||||
bulkVideosSupportUpdate: false
|
||||
}
|
||||
let path: string
|
||||
|
||||
before(async function () {
|
||||
path = videoChannelPath + '/super_channel'
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: 'hi',
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another authenticated user', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: userInfo.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a long name', async function () {
|
||||
const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) }
|
||||
await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = { ...baseCorrectParams, description: 'super'.repeat(201) }
|
||||
await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long support text', async function () {
|
||||
const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
|
||||
await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad bulkVideosSupportUpdate field', async function () {
|
||||
const fields = { ...baseCorrectParams, bulkVideosSupportUpdate: 'super' }
|
||||
await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating video channel avatars/banners', function () {
|
||||
const types = [ 'avatar', 'banner' ]
|
||||
let path: string
|
||||
|
||||
before(async function () {
|
||||
path = videoChannelPath + '/super_channel'
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect input file', async function () {
|
||||
for (const type of types) {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
[type + 'file']: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a big file', async function () {
|
||||
for (const type of types) {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
[type + 'file']: buildAbsoluteFixturePath('avatar-big.png')
|
||||
}
|
||||
await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
for (const type of types) {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
[type + 'file']: buildAbsoluteFixturePath('avatar.png')
|
||||
}
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path: `${path}/${type}/pick`,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
for (const type of types) {
|
||||
const fields = {}
|
||||
const attaches = {
|
||||
[type + 'file']: buildAbsoluteFixturePath('avatar.png')
|
||||
}
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path: `${path}/${type}/pick`,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting a video channel', function () {
|
||||
it('Should return the list of the video channels with nothing', async function () {
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
expect(res.body.data).to.be.an('array')
|
||||
})
|
||||
|
||||
it('Should return 404 with an incorrect video channel', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath + '/super_channel2',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath + '/super_channel',
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting channel followers', function () {
|
||||
const path = '/api/v1/video-channels/super_channel/followers'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a unauthenticated user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a another user', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: userInfo.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting a video channel', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another authenticated user', async function () {
|
||||
await command.delete({ token: userInfo.accessToken, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video channel id', async function () {
|
||||
await command.delete({ channelName: 'super_channel2', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await command.delete({ channelName: 'super_channel' })
|
||||
})
|
||||
|
||||
it('Should fail to delete the last user video channel', async function () {
|
||||
await command.delete({ channelName: 'root_channel', expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,172 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, Video, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test videos chapters API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
let video: VideoCreateResult
|
||||
let live: Video
|
||||
let privateVideo: VideoCreateResult
|
||||
let userAccessToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
video = await server.videos.upload()
|
||||
privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
await server.config.enableLive({ allowReplay: false })
|
||||
|
||||
const res = await server.live.quickCreate({ saveReplay: false, permanentLive: false })
|
||||
live = res.video
|
||||
})
|
||||
|
||||
describe('When updating chapters', function () {
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.chapters.update({ videoId: '4da6fd', chapters: [], expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: 'ce0801ef-7124-48df-9b22-b473ace78797',
|
||||
chapters: [],
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without access token', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [],
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [],
|
||||
token: 'toto',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a another user access token', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [],
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a wrong chapters param', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: 'hello' as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad chapter title', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [ { title: 'hello', timecode: 21 }, { title: '', timecode: 21 } ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [ { title: 'hello', timecode: 21 }, { title: 'a'.repeat(150), timecode: 21 } ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad timecode', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [ { title: 'hello', timecode: 21 }, { title: 'title', timecode: -5 } ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [ { title: 'hello', timecode: 21 }, { title: 'title', timecode: 'hi' as any } ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with non unique timecodes', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [ { title: 'hello', timecode: 21 }, { title: 'title', timecode: 22 }, { title: 'hello', timecode: 21 } ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to create chapters on a live', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: live.id,
|
||||
chapters: [],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: []
|
||||
})
|
||||
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
chapters: [ { title: 'hello', timecode: 21 }, { title: 'hello 2', timecode: 35 } ]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing chapters', function () {
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.chapters.list({ videoId: '4da6fd', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await server.chapters.list({ videoId: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not list private chapters to anyone', async function () {
|
||||
await server.chapters.list({ videoId: privateVideo.uuid, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should not list private chapters to another user', async function () {
|
||||
await server.chapters.list({ videoId: privateVideo.uuid, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should list chapters', async function () {
|
||||
await server.chapters.list({ videoId: privateVideo.uuid })
|
||||
await server.chapters.list({ videoId: video.uuid })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,576 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, VideoCommentPolicy, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test video comments API validator', function () {
|
||||
let pathThread: string
|
||||
let pathComment: string
|
||||
|
||||
let server: PeerTubeServer
|
||||
|
||||
let video: VideoCreateResult
|
||||
|
||||
let userAccessToken: string
|
||||
let userAccessToken2: string
|
||||
|
||||
let commentId: number
|
||||
let privateCommentId: number
|
||||
let privateVideo: VideoCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
{
|
||||
video = await server.videos.upload({ attributes: {} })
|
||||
pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads'
|
||||
}
|
||||
|
||||
{
|
||||
privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
|
||||
}
|
||||
|
||||
{
|
||||
const created = await server.comments.createThread({ videoId: video.uuid, text: 'coucou' })
|
||||
commentId = created.id
|
||||
pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId
|
||||
}
|
||||
|
||||
{
|
||||
const created = await server.comments.createThread({ videoId: privateVideo.uuid, text: 'coucou' })
|
||||
privateCommentId = created.id
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'user1', password: 'my super password' }
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'user2', password: 'my super password' }
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken2 = await server.login.getAccessToken(user)
|
||||
}
|
||||
})
|
||||
|
||||
describe('When listing video comment threads', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, pathThread, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, pathThread, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, pathThread, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a private video without token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing comments of a thread', function () {
|
||||
it('Should fail with an incorrect video', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads/' + commentId,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thread id', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a private video without token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: userAccessToken,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a video thread', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
const fields = {
|
||||
text: 'text'
|
||||
}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: pathThread,
|
||||
token: 'none',
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a short comment', async function () {
|
||||
const fields = {
|
||||
text: ''
|
||||
}
|
||||
await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long comment', async function () {
|
||||
const fields = {
|
||||
text: 'h'.repeat(10001)
|
||||
}
|
||||
await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video', async function () {
|
||||
const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads'
|
||||
const fields = { text: 'super comment' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a private video of another user', async function () {
|
||||
const fields = { text: 'super comment' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
|
||||
token: userAccessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const fields = { text: 'super comment' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: pathThread,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a comment to a thread', function () {
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
const fields = {
|
||||
text: 'text'
|
||||
}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: pathComment,
|
||||
token: 'none',
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a short comment', async function () {
|
||||
const fields = {
|
||||
text: ''
|
||||
}
|
||||
await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long comment', async function () {
|
||||
const fields = {
|
||||
text: 'h'.repeat(10001)
|
||||
}
|
||||
await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video', async function () {
|
||||
const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId
|
||||
const fields = {
|
||||
text: 'super comment'
|
||||
}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a private video of another user', async function () {
|
||||
const fields = { text: 'super comment' }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: '/api/v1/videos/' + privateVideo.uuid + '/comments/' + privateCommentId,
|
||||
token: userAccessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect comment', async function () {
|
||||
const path = '/api/v1/videos/' + video.uuid + '/comments/124'
|
||||
const fields = {
|
||||
text: 'super comment'
|
||||
}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const fields = {
|
||||
text: 'super comment'
|
||||
}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: pathComment,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing video comments', function () {
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another user', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: pathComment,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video', async function () {
|
||||
const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId
|
||||
await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect comment', async function () {
|
||||
const path = '/api/v1/videos/' + video.uuid + '/comments/124'
|
||||
await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the same user', async function () {
|
||||
let commentToDelete: number
|
||||
|
||||
{
|
||||
const created = await server.comments.createThread({ videoId: video.uuid, token: userAccessToken, text: 'hello' })
|
||||
commentToDelete = created.id
|
||||
}
|
||||
|
||||
const path = '/api/v1/videos/' + video.uuid + '/comments/' + commentToDelete
|
||||
|
||||
await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
})
|
||||
|
||||
it('Should succeed with the owner of the video', async function () {
|
||||
let commentToDelete: number
|
||||
let anotherVideoUUID: string
|
||||
|
||||
{
|
||||
const { uuid } = await server.videos.upload({ token: userAccessToken, attributes: { name: 'video' } })
|
||||
anotherVideoUUID = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const created = await server.comments.createThread({ videoId: anotherVideoUUID, text: 'hello' })
|
||||
commentToDelete = created.id
|
||||
}
|
||||
|
||||
const path = '/api/v1/videos/' + anotherVideoUUID + '/comments/' + commentToDelete
|
||||
|
||||
await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path: pathComment,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When a video has comments disabled', function () {
|
||||
|
||||
before(async function () {
|
||||
video = await server.videos.upload({ attributes: { commentsPolicy: VideoCommentPolicy.DISABLED } })
|
||||
pathThread = `/api/v1/videos/${video.uuid}/comment-threads`
|
||||
})
|
||||
|
||||
it('Should return an empty thread list', async function () {
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path: pathThread,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
expect(res.body.total).to.equal(0)
|
||||
expect(res.body.data).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should return an thread comments list')
|
||||
|
||||
it('Should return conflict on thread add', async function () {
|
||||
const fields = {
|
||||
text: 'super comment'
|
||||
}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: pathThread,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return conflict on comment thread add')
|
||||
})
|
||||
|
||||
describe('When listing admin/user comments', function () {
|
||||
const paths = [ '/api/v1/videos/comments', '/api/v1/users/me/videos/comments' ]
|
||||
|
||||
it('Should fail with a bad start/count pagination of invalid sort', async function () {
|
||||
for (const path of paths) {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await server.comments.listForAdmin({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await server.comments.listCommentsOnMyVideos({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail to list admin comments with a non admin user', async function () {
|
||||
await server.comments.listForAdmin({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid video', async function () {
|
||||
await server.comments.listForAdmin({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.comments.listCommentsOnMyVideos({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await server.comments.listForAdmin({ videoId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await server.comments.listCommentsOnMyVideos({ videoId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid channel', async function () {
|
||||
await server.comments.listForAdmin({ videoChannelId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.comments.listCommentsOnMyVideos({ videoChannelId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await server.comments.listForAdmin({ videoChannelId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await server.comments.listCommentsOnMyVideos({ videoChannelId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail to list comments on my videos with non owned video or channel', async function () {
|
||||
await server.comments.listCommentsOnMyVideos({
|
||||
videoId: video.uuid,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
|
||||
await server.comments.listCommentsOnMyVideos({
|
||||
videoChannelId: server.store.channel.id,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
const base = {
|
||||
search: 'toto',
|
||||
searchAccount: 'toto',
|
||||
searchVideo: 'toto',
|
||||
videoId: video.uuid,
|
||||
videoChannelId: server.store.channel.id,
|
||||
autoTagOneOf: [ 'external-link' ]
|
||||
}
|
||||
|
||||
await server.comments.listForAdmin({ ...base, isLocal: false })
|
||||
await server.comments.listCommentsOnMyVideos(base)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When approving a comment', function () {
|
||||
let videoId: string
|
||||
let commentId: number
|
||||
let deletedCommentId: number
|
||||
let userAccessToken3: string
|
||||
|
||||
before(async function () {
|
||||
userAccessToken3 = await server.users.generateUserAndToken('user3')
|
||||
|
||||
{
|
||||
const res = await server.videos.upload({
|
||||
token: userAccessToken,
|
||||
attributes: {
|
||||
name: 'review policy',
|
||||
commentsPolicy: VideoCommentPolicy.REQUIRES_APPROVAL
|
||||
}
|
||||
})
|
||||
|
||||
videoId = res.uuid
|
||||
}
|
||||
|
||||
{
|
||||
const res = await server.comments.createThread({ text: 'thread', videoId, token: userAccessToken2 })
|
||||
commentId = res.id
|
||||
}
|
||||
|
||||
{
|
||||
const res = await server.comments.createThread({ text: 'deleted', videoId, token: userAccessToken2 })
|
||||
deletedCommentId = res.id
|
||||
|
||||
await server.comments.delete({ commentId: deletedCommentId, videoId })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a non authenticated user', async function () {
|
||||
await server.comments.approve({ token: 'none', commentId, videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another user', async function () {
|
||||
await server.comments.approve({ token: userAccessToken3, commentId, videoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with the owner', async function () {
|
||||
await server.comments.approve({ token: userAccessToken2, commentId, videoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect video', async function () {
|
||||
await server.comments.approve({ token: userAccessToken, commentId, videoId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect comment', async function () {
|
||||
await server.comments.approve({ token: userAccessToken, commentId: 42, videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a deleted comment', async function () {
|
||||
await server.comments.approve({
|
||||
token: userAccessToken,
|
||||
commentId: deletedCommentId,
|
||||
videoId,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.comments.approve({ token: userAccessToken, commentId, videoId })
|
||||
})
|
||||
|
||||
it('Should fail with an already held for review comment', async function () {
|
||||
await server.comments.approve({ token: userAccessToken, commentId, videoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { getAllFiles } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRole, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test videos files', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
|
||||
let userToken: string
|
||||
let moderatorToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(300_000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER)
|
||||
moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR)
|
||||
})
|
||||
|
||||
describe('Getting metadata', function () {
|
||||
let video: VideoDetails
|
||||
|
||||
before(async function () {
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
|
||||
video = await servers[0].videos.getWithToken({ id: uuid })
|
||||
})
|
||||
|
||||
it('Should not get metadata of private video without token', async function () {
|
||||
for (const file of getAllFiles(video)) {
|
||||
await makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not get metadata of private video without the appropriate token', async function () {
|
||||
for (const file of getAllFiles(video)) {
|
||||
await makeRawRequest({ url: file.metadataUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should get metadata of private video with the appropriate token', async function () {
|
||||
for (const file of getAllFiles(video)) {
|
||||
await makeRawRequest({ url: file.metadataUrl, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deleting files', function () {
|
||||
let webVideoId: string
|
||||
let hlsId: string
|
||||
let remoteId: string
|
||||
|
||||
let validId1: string
|
||||
let validId2: string
|
||||
|
||||
let hlsFileId: number
|
||||
let webVideoFileId: number
|
||||
|
||||
let remoteHLSFileId: number
|
||||
let remoteWebVideoFileId: number
|
||||
|
||||
before(async function () {
|
||||
this.timeout(300_000)
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await servers[1].videos.get({ id: uuid })
|
||||
remoteId = video.uuid
|
||||
remoteHLSFileId = video.streamingPlaylists[0].files[0].id
|
||||
remoteWebVideoFileId = video.files[0].id
|
||||
}
|
||||
|
||||
{
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
validId1 = video.uuid
|
||||
hlsFileId = video.streamingPlaylists[0].files[0].id
|
||||
webVideoFileId = video.files[0].id
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' })
|
||||
validId2 = uuid
|
||||
}
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' })
|
||||
hlsId = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
await servers[0].config.enableTranscoding({ webVideo: true, hls: false })
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' })
|
||||
webVideoId = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not delete files of a unknown video', async function () {
|
||||
const expectedStatus = HttpStatusCode.NOT_FOUND_404
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus })
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: 404, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus })
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: 404, fileId: webVideoFileId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete unknown files', async function () {
|
||||
const expectedStatus = HttpStatusCode.NOT_FOUND_404
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webVideoFileId, expectedStatus })
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: hlsFileId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete files of a remote video', async function () {
|
||||
const expectedStatus = HttpStatusCode.BAD_REQUEST_400
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus })
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: remoteId, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus })
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: remoteId, fileId: remoteWebVideoFileId, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete files by a non admin user', async function () {
|
||||
const expectedStatus = HttpStatusCode.FORBIDDEN_403
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus })
|
||||
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: userToken, expectedStatus })
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: moderatorToken, expectedStatus })
|
||||
})
|
||||
|
||||
it('Should not delete files if the files are not available', async function () {
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not delete files if no both versions are available', async function () {
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should delete files if both versions are available', async function () {
|
||||
await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId })
|
||||
await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId })
|
||||
|
||||
await servers[0].videos.removeHLSPlaylist({ videoId: validId1 })
|
||||
await servers[0].videos.removeAllWebVideoFiles({ videoId: validId2 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,439 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoCommentPolicy, VideoImportCreate, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makeUploadRequest,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
|
||||
describe('Test video imports API validator', function () {
|
||||
const path = '/api/v1/videos/imports'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
let channelId: number
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
const username = 'user1'
|
||||
const password = 'my super password'
|
||||
await server.users.create({ username, password })
|
||||
userAccessToken = await server.login.getAccessToken({ username, password })
|
||||
|
||||
{
|
||||
const { videoChannels } = await server.users.getMyInfo()
|
||||
channelId = videoChannels[0].id
|
||||
}
|
||||
})
|
||||
|
||||
describe('When listing my video imports', function () {
|
||||
const myPath = '/api/v1/users/me/videos/imports'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, myPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, myPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, myPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad videoChannelSyncId param', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: myPath,
|
||||
query: { videoChannelSyncId: 'toto' },
|
||||
token: server.accessToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a video import', function () {
|
||||
let baseCorrectParams: VideoImportCreate
|
||||
|
||||
before(function () {
|
||||
baseCorrectParams = {
|
||||
targetUrl: FIXTURE_URLS.goodVideo,
|
||||
name: 'my super name',
|
||||
category: 5,
|
||||
licence: 1,
|
||||
language: 'pt',
|
||||
nsfw: false,
|
||||
commentsPolicy: VideoCommentPolicy.ENABLED,
|
||||
downloadEnabled: true,
|
||||
waitTranscoding: true,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag1', 'tag2' ],
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without a target url', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'targetUrl' ])
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad target url', async function () {
|
||||
const fields = { ...baseCorrectParams, targetUrl: 'htt://hello' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with localhost', async function () {
|
||||
const fields = { ...baseCorrectParams, targetUrl: 'http://localhost:8000' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a private IP target urls', async function () {
|
||||
const targetUrls = [
|
||||
'http://127.0.0.1:8000',
|
||||
'http://127.0.0.1',
|
||||
'http://127.0.0.1/hello',
|
||||
'https://192.168.1.42',
|
||||
'http://192.168.1.42',
|
||||
'http://127.0.0.1.cpy.re'
|
||||
]
|
||||
|
||||
for (const targetUrl of targetUrls) {
|
||||
const fields = { ...baseCorrectParams, targetUrl }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a long name', async function () {
|
||||
const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad category', async function () {
|
||||
const fields = { ...baseCorrectParams, category: 125 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad licence', async function () {
|
||||
const fields = { ...baseCorrectParams, licence: 125 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad language', async function () {
|
||||
const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad commentsPolicy', async function () {
|
||||
const fields = { ...baseCorrectParams, commentsPolicy: 42 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long support text', async function () {
|
||||
const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without a channel', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'channelId' ])
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel', async function () {
|
||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with another user channel', async function () {
|
||||
const user = {
|
||||
username: 'fake',
|
||||
password: 'fake_password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
|
||||
const accessTokenUser = await server.login.getAccessToken(user)
|
||||
const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
|
||||
const customChannelId = videoChannels[0].id
|
||||
|
||||
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with too many tags', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too low', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too big', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a big thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a big preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid torrent file', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'targetUrl' ])
|
||||
const attaches = {
|
||||
torrentfile: buildAbsoluteFixturePath('avatar-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid magnet URI', async function () {
|
||||
let fields = omit(baseCorrectParams, [ 'targetUrl' ])
|
||||
fields = { ...fields, magnetUri: 'blabla' }
|
||||
|
||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should forbid to import http videos', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
enabled: false
|
||||
},
|
||||
torrent: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields: baseCorrectParams,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
|
||||
it('Should forbid to import torrent videos', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
enabled: true
|
||||
},
|
||||
torrent: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let fields = omit(baseCorrectParams, [ 'targetUrl' ])
|
||||
fields = { ...fields, magnetUri: FIXTURE_URLS.magnet }
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
|
||||
fields = omit(fields, [ 'magnetUri' ])
|
||||
const attaches = {
|
||||
torrentfile: buildAbsoluteFixturePath('video-720p.torrent')
|
||||
}
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches,
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deleting/cancelling a video import', function () {
|
||||
let importId: number
|
||||
|
||||
async function importVideo () {
|
||||
const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
|
||||
const res = await server.videoImports.importVideo({ attributes })
|
||||
|
||||
return res.id
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
importId = await importVideo()
|
||||
})
|
||||
|
||||
it('Should fail with an invalid import id', async function () {
|
||||
await server.videoImports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await server.videoImports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown import id', async function () {
|
||||
await server.videoImports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await server.videoImports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.videoImports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await server.videoImports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another user token', async function () {
|
||||
await server.videoImports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await server.videoImports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail to cancel non pending import', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
await server.videoImports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||
})
|
||||
|
||||
it('Should succeed to delete an import', async function () {
|
||||
await server.videoImports.delete({ importId })
|
||||
})
|
||||
|
||||
it('Should fail to delete a pending import', async function () {
|
||||
await server.jobs.pauseJobQueue()
|
||||
|
||||
importId = await importVideo()
|
||||
|
||||
await server.videoImports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||
})
|
||||
|
||||
it('Should succeed to cancel an import', async function () {
|
||||
importId = await importVideo()
|
||||
|
||||
await server.videoImports.cancel({ importId })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,605 @@
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
PeerTubeProblemDocument,
|
||||
ServerErrorCode,
|
||||
VideoCommentPolicy,
|
||||
VideoCreateResult,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makePostBodyRequest,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { checkUploadVideoParam } from '@tests/shared/videos.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test video passwords validator', function () {
|
||||
let path: string
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
let video: VideoCreateResult
|
||||
let channelId: number
|
||||
let publicVideo: VideoCreateResult
|
||||
let commentId: number
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
allowReplay: false
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
http:{
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
{
|
||||
const body = await server.users.getMyInfo()
|
||||
channelId = body.videoChannels[0].id
|
||||
}
|
||||
|
||||
{
|
||||
video = await server.videos.quickUpload({
|
||||
name: 'password protected video',
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
videoPasswords: [ 'password1', 'password2' ]
|
||||
})
|
||||
}
|
||||
path = '/api/v1/videos/'
|
||||
})
|
||||
|
||||
async function checkVideoPasswordOptions (options: {
|
||||
server: PeerTubeServer
|
||||
token: string
|
||||
videoPasswords: string[]
|
||||
expectedStatus: HttpStatusCodeType
|
||||
mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live'
|
||||
}) {
|
||||
const { server, token, videoPasswords, expectedStatus = HttpStatusCode.OK_200, mode } = options
|
||||
const attaches = {
|
||||
fixture: buildAbsoluteFixturePath('video_short.webm')
|
||||
}
|
||||
const baseCorrectParams = {
|
||||
name: 'my super name',
|
||||
category: 5,
|
||||
licence: 1,
|
||||
language: 'pt',
|
||||
nsfw: false,
|
||||
commentsPolicy: VideoCommentPolicy.ENABLED,
|
||||
downloadEnabled: true,
|
||||
waitTranscoding: true,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag1', 'tag2' ],
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
channelId,
|
||||
originallyPublishedAt: new Date().toISOString()
|
||||
}
|
||||
if (mode === 'uploadLegacy') {
|
||||
const fields = { ...baseCorrectParams, videoPasswords }
|
||||
return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'legacy' })
|
||||
}
|
||||
|
||||
if (mode === 'uploadResumable') {
|
||||
const fields = { ...baseCorrectParams, videoPasswords }
|
||||
return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'resumable' })
|
||||
}
|
||||
|
||||
if (mode === 'import') {
|
||||
const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords }
|
||||
return server.videoImports.importVideo({ attributes, expectedStatus })
|
||||
}
|
||||
|
||||
if (mode === 'updateVideo') {
|
||||
const attributes = { ...baseCorrectParams, videoPasswords }
|
||||
return server.videos.update({ token, expectedStatus, id: video.id, attributes })
|
||||
}
|
||||
|
||||
if (mode === 'updatePasswords') {
|
||||
return server.videoPasswords.updateAll({ token, expectedStatus, videoId: video.id, passwords: videoPasswords })
|
||||
}
|
||||
|
||||
if (mode === 'live') {
|
||||
const fields = { ...baseCorrectParams, videoPasswords }
|
||||
|
||||
return server.live.create({ fields, expectedStatus })
|
||||
}
|
||||
}
|
||||
|
||||
function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') {
|
||||
|
||||
it('Should fail with a password protected privacy without providing a password', async function () {
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: server.accessToken,
|
||||
videoPasswords: undefined,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a password protected privacy and an empty password list', async function () {
|
||||
const videoPasswords = []
|
||||
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: server.accessToken,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a password protected privacy and a too short password', async function () {
|
||||
const videoPasswords = [ 'p' ]
|
||||
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: server.accessToken,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a password protected privacy and a too long password', async function () {
|
||||
const videoPasswords = [ 'Very very very very very very very very very very very very very very very very very very long password' ]
|
||||
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: server.accessToken,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a password protected privacy and an empty password', async function () {
|
||||
const videoPasswords = [ '' ]
|
||||
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: server.accessToken,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a password protected privacy and duplicated passwords', async function () {
|
||||
const videoPasswords = [ 'password', 'password' ]
|
||||
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: server.accessToken,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
if (mode === 'updatePasswords') {
|
||||
it('Should fail for an unauthenticated user', async function () {
|
||||
const videoPasswords = [ 'password' ]
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: null,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
|
||||
mode
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail for an unauthorized user', async function () {
|
||||
const videoPasswords = [ 'password' ]
|
||||
await checkVideoPasswordOptions({
|
||||
server,
|
||||
token: userAccessToken,
|
||||
videoPasswords,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
mode
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it('Should succeed with a password protected privacy and correct passwords', async function () {
|
||||
const videoPasswords = [ 'password1', 'password2' ]
|
||||
const expectedStatus = mode === 'updatePasswords' || mode === 'updateVideo'
|
||||
? HttpStatusCode.NO_CONTENT_204
|
||||
: HttpStatusCode.OK_200
|
||||
|
||||
await checkVideoPasswordOptions({ server, token: server.accessToken, videoPasswords, expectedStatus, mode })
|
||||
})
|
||||
}
|
||||
|
||||
describe('When adding or updating a video', function () {
|
||||
describe('Resumable upload', function () {
|
||||
validateVideoPasswordList('uploadResumable')
|
||||
})
|
||||
|
||||
describe('Legacy upload', function () {
|
||||
validateVideoPasswordList('uploadLegacy')
|
||||
})
|
||||
|
||||
describe('When importing a video', function () {
|
||||
validateVideoPasswordList('import')
|
||||
})
|
||||
|
||||
describe('When updating a video', function () {
|
||||
validateVideoPasswordList('updateVideo')
|
||||
})
|
||||
|
||||
describe('When updating the password list of a video', function () {
|
||||
validateVideoPasswordList('updatePasswords')
|
||||
})
|
||||
|
||||
describe('When creating a live', function () {
|
||||
validateVideoPasswordList('live')
|
||||
})
|
||||
})
|
||||
|
||||
async function checkVideoAccessOptions (options: {
|
||||
server: PeerTubeServer
|
||||
token?: string
|
||||
videoPassword?: string
|
||||
expectedStatus: HttpStatusCodeType
|
||||
mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token'
|
||||
}) {
|
||||
const { server, token = null, videoPassword, expectedStatus, mode } = options
|
||||
|
||||
if (mode === 'get') {
|
||||
return server.videos.get({ id: video.id, expectedStatus })
|
||||
}
|
||||
|
||||
if (mode === 'getWithToken') {
|
||||
return server.videos.getWithToken({
|
||||
id: video.id,
|
||||
token,
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'getWithPassword') {
|
||||
return server.videos.getWithPassword({
|
||||
id: video.id,
|
||||
token,
|
||||
expectedStatus,
|
||||
password: videoPassword
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'rate') {
|
||||
return server.videos.rate({
|
||||
id: video.id,
|
||||
token,
|
||||
expectedStatus,
|
||||
rating: 'like',
|
||||
videoPassword
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'createThread') {
|
||||
const fields = { text: 'super comment' }
|
||||
const headers = videoPassword !== undefined && videoPassword !== null
|
||||
? { 'x-peertube-video-password': videoPassword }
|
||||
: undefined
|
||||
const body = await makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + video.uuid + '/comment-threads',
|
||||
token,
|
||||
fields,
|
||||
headers,
|
||||
expectedStatus
|
||||
})
|
||||
return JSON.parse(body.text)
|
||||
}
|
||||
|
||||
if (mode === 'replyThread') {
|
||||
const fields = { text: 'super reply' }
|
||||
const headers = videoPassword !== undefined && videoPassword !== null
|
||||
? { 'x-peertube-video-password': videoPassword }
|
||||
: undefined
|
||||
return makePostBodyRequest({
|
||||
url: server.url,
|
||||
path: path + video.uuid + '/comments/' + commentId,
|
||||
token,
|
||||
fields,
|
||||
headers,
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
if (mode === 'listThreads') {
|
||||
return server.comments.listThreads({
|
||||
videoId: video.id,
|
||||
token,
|
||||
expectedStatus,
|
||||
videoPassword
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'listCaptions') {
|
||||
return server.captions.list({
|
||||
videoId: video.id,
|
||||
token,
|
||||
expectedStatus,
|
||||
videoPassword
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'token') {
|
||||
return server.videoToken.create({
|
||||
videoId: video.id,
|
||||
token,
|
||||
expectedStatus,
|
||||
videoPassword
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function checkVideoError (error: any, mode: 'providePassword' | 'incorrectPassword') {
|
||||
const serverCode = mode === 'providePassword'
|
||||
? ServerErrorCode.VIDEO_REQUIRES_PASSWORD
|
||||
: ServerErrorCode.INCORRECT_VIDEO_PASSWORD
|
||||
|
||||
const message = mode === 'providePassword'
|
||||
? 'Please provide a password to access this password protected video'
|
||||
: 'Incorrect video password. Access to the video is denied.'
|
||||
|
||||
if (!error.code) {
|
||||
error = JSON.parse(error.text)
|
||||
}
|
||||
|
||||
expect(error.code).to.equal(serverCode)
|
||||
expect(error.detail).to.equal(message)
|
||||
expect(error.error).to.equal(message)
|
||||
|
||||
expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') {
|
||||
const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode)
|
||||
let tokens: string[]
|
||||
if (!requiresUserAuth) {
|
||||
it('Should fail without providing a password for an unlogged user', async function () {
|
||||
const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode })
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
checkVideoError(error, 'providePassword')
|
||||
})
|
||||
}
|
||||
|
||||
it('Should fail without providing a password for an unauthorised user', async function () {
|
||||
const tmp = mode === 'get' ? 'getWithToken' : mode
|
||||
|
||||
const body = await checkVideoAccessOptions({
|
||||
server,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
mode: tmp
|
||||
})
|
||||
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
checkVideoError(error, 'providePassword')
|
||||
})
|
||||
|
||||
it('Should fail if a wrong password is entered', async function () {
|
||||
const tmp = mode === 'get' ? 'getWithPassword' : mode
|
||||
tokens = [ userAccessToken, server.accessToken ]
|
||||
|
||||
if (!requiresUserAuth) tokens.push(null)
|
||||
|
||||
for (const token of tokens) {
|
||||
const body = await checkVideoAccessOptions({
|
||||
server,
|
||||
token,
|
||||
videoPassword: 'toto',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
mode: tmp
|
||||
})
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
checkVideoError(error, 'incorrectPassword')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail if an empty password is entered', async function () {
|
||||
const tmp = mode === 'get' ? 'getWithPassword' : mode
|
||||
|
||||
for (const token of tokens) {
|
||||
const body = await checkVideoAccessOptions({
|
||||
server,
|
||||
token,
|
||||
videoPassword: '',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
mode: tmp
|
||||
})
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
checkVideoError(error, 'incorrectPassword')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail if an inccorect password containing the correct password is entered', async function () {
|
||||
const tmp = mode === 'get' ? 'getWithPassword' : mode
|
||||
|
||||
for (const token of tokens) {
|
||||
const body = await checkVideoAccessOptions({
|
||||
server,
|
||||
token,
|
||||
videoPassword: 'password11',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
mode: tmp
|
||||
})
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
checkVideoError(error, 'incorrectPassword')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed without providing a password for an authorised user', async function () {
|
||||
const tmp = mode === 'get' ? 'getWithToken' : mode
|
||||
const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200
|
||||
|
||||
const body = await checkVideoAccessOptions({ server, token: server.accessToken, expectedStatus, mode: tmp })
|
||||
|
||||
if (mode === 'createThread') commentId = body.comment.id
|
||||
})
|
||||
|
||||
it('Should succeed using correct passwords', async function () {
|
||||
const tmp = mode === 'get' ? 'getWithPassword' : mode
|
||||
const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200
|
||||
|
||||
for (const token of tokens) {
|
||||
await checkVideoAccessOptions({ server, videoPassword: 'password1', token, expectedStatus, mode: tmp })
|
||||
await checkVideoAccessOptions({ server, videoPassword: 'password2', token, expectedStatus, mode: tmp })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('When accessing password protected video', function () {
|
||||
|
||||
describe('For getting a password protected video', function () {
|
||||
validateVideoAccess('get')
|
||||
})
|
||||
|
||||
describe('For rating a video', function () {
|
||||
validateVideoAccess('rate')
|
||||
})
|
||||
|
||||
describe('For creating a thread', function () {
|
||||
validateVideoAccess('createThread')
|
||||
})
|
||||
|
||||
describe('For replying to a thread', function () {
|
||||
validateVideoAccess('replyThread')
|
||||
})
|
||||
|
||||
describe('For listing threads', function () {
|
||||
validateVideoAccess('listThreads')
|
||||
})
|
||||
|
||||
describe('For getting captions', function () {
|
||||
validateVideoAccess('listCaptions')
|
||||
})
|
||||
|
||||
describe('For creating video file token', function () {
|
||||
validateVideoAccess('token')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing passwords', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path + video.uuid + '/passwords', server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path + video.uuid + '/passwords', server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path + video.uuid + '/passwords', server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail for unauthenticated user', async function () {
|
||||
await server.videoPasswords.list({
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
|
||||
videoId: video.id
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail for unauthorized user', async function () {
|
||||
await server.videoPasswords.list({
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
videoId: video.id
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await server.videoPasswords.list({
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200,
|
||||
videoId: video.id
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting a password', async function () {
|
||||
const passwords = (await server.videoPasswords.list({ videoId: video.id })).data
|
||||
|
||||
it('Should fail with wrong password id', async function () {
|
||||
await server.videoPasswords.remove({ id: -1, videoId: video.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail for unauthenticated user', async function () {
|
||||
await server.videoPasswords.remove({
|
||||
id: passwords[0].id,
|
||||
token: null,
|
||||
videoId: video.id,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail for unauthorized user', async function () {
|
||||
await server.videoPasswords.remove({
|
||||
id: passwords[0].id,
|
||||
token: userAccessToken,
|
||||
videoId: video.id,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail for non password protected video', async function () {
|
||||
publicVideo = await server.videos.quickUpload({ name: 'public video' })
|
||||
await server.videoPasswords.remove({ id: passwords[0].id, videoId: publicVideo.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail for password not linked to correct video', async function () {
|
||||
const video2 = await server.videos.quickUpload({
|
||||
name: 'password protected video',
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
videoPasswords: [ 'password1', 'password2' ]
|
||||
})
|
||||
await server.videoPasswords.remove({ id: passwords[0].id, videoId: video2.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with correct parameter', async function () {
|
||||
await server.videoPasswords.remove({ id: passwords[0].id, videoId: video.id, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
})
|
||||
|
||||
it('Should fail for last password of a video', async function () {
|
||||
await server.videoPasswords.remove({ id: passwords[1].id, videoId: video.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,695 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoPlaylistCreate,
|
||||
VideoPlaylistCreateResult,
|
||||
VideoPlaylistElementCreate,
|
||||
VideoPlaylistElementUpdate,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPlaylistReorder,
|
||||
VideoPlaylistType
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
PlaylistsCommand,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video playlists API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
|
||||
let playlist: VideoPlaylistCreateResult
|
||||
let privatePlaylistUUID: string
|
||||
|
||||
let watchLaterPlaylistId: number
|
||||
let videoId: number
|
||||
let elementId: number
|
||||
|
||||
let command: PlaylistsCommand
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
videoId = (await server.videos.quickUpload({ name: 'video 1' })).id
|
||||
|
||||
command = server.playlists
|
||||
|
||||
{
|
||||
const { data } = await command.listByAccount({
|
||||
token: server.accessToken,
|
||||
handle: 'root',
|
||||
start: 0,
|
||||
count: 5,
|
||||
playlistType: VideoPlaylistType.WATCH_LATER
|
||||
})
|
||||
watchLaterPlaylistId = data[0].id
|
||||
}
|
||||
|
||||
{
|
||||
playlist = await command.create({
|
||||
attributes: {
|
||||
displayName: 'super playlist',
|
||||
privacy: VideoPlaylistPrivacy.PUBLIC,
|
||||
videoChannelId: server.store.channel.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const created = await command.create({
|
||||
attributes: {
|
||||
displayName: 'private',
|
||||
privacy: VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
})
|
||||
privatePlaylistUUID = created.uuid
|
||||
}
|
||||
})
|
||||
|
||||
describe('When listing playlists', function () {
|
||||
const globalPath = '/api/v1/video-playlists'
|
||||
const accountPath = '/api/v1/accounts/root/video-playlists'
|
||||
const videoChannelPath = '/api/v1/video-channels/root_channel/video-playlists'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, globalPath, server.accessToken)
|
||||
await checkBadStartPagination(server.url, accountPath, server.accessToken)
|
||||
await checkBadStartPagination(server.url, videoChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, globalPath, server.accessToken)
|
||||
await checkBadCountPagination(server.url, accountPath, server.accessToken)
|
||||
await checkBadCountPagination(server.url, videoChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, globalPath, server.accessToken)
|
||||
await checkBadSortPagination(server.url, accountPath, server.accessToken)
|
||||
await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad playlist type', async function () {
|
||||
await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } })
|
||||
await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } })
|
||||
await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad account parameter', async function () {
|
||||
const accountPath = '/api/v1/accounts/root2/video-playlists'
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: accountPath,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404,
|
||||
token: server.accessToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad video channel parameter', async function () {
|
||||
const accountPath = '/api/v1/video-channels/bad_channel/video-playlists'
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: accountPath,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404,
|
||||
token: server.accessToken
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path: globalPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
|
||||
await makeGetRequest({ url: server.url, path: accountPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: videoChannelPath,
|
||||
expectedStatus: HttpStatusCode.OK_200,
|
||||
token: server.accessToken
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing videos of a playlist', function () {
|
||||
const path = '/api/v1/video-playlists/'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken)
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting a video playlist', function () {
|
||||
it('Should fail with a bad id or uuid', async function () {
|
||||
await command.get({ playlistId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown playlist', async function () {
|
||||
await command.get({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail to get an unlisted playlist with the number id', async function () {
|
||||
const playlist = await command.create({
|
||||
attributes: {
|
||||
displayName: 'super playlist',
|
||||
videoChannelId: server.store.channel.id,
|
||||
privacy: VideoPlaylistPrivacy.UNLISTED
|
||||
}
|
||||
})
|
||||
|
||||
await command.get({ playlistId: playlist.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When creating/updating a video playlist', function () {
|
||||
const getBase = (
|
||||
attributes?: Partial<VideoPlaylistCreate>,
|
||||
wrapper?: Partial<Parameters<PlaylistsCommand['create']>[0]>
|
||||
) => {
|
||||
return {
|
||||
attributes: {
|
||||
displayName: 'display name',
|
||||
privacy: VideoPlaylistPrivacy.UNLISTED,
|
||||
thumbnailfile: 'custom-thumbnail.jpg',
|
||||
videoChannelId: server.store.channel.id,
|
||||
|
||||
...attributes
|
||||
},
|
||||
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
|
||||
...wrapper
|
||||
}
|
||||
}
|
||||
const getUpdate = (params: any, playlistId: number | string) => {
|
||||
return { ...params, playlistId }
|
||||
}
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail without displayName', async function () {
|
||||
const params = getBase({ displayName: undefined })
|
||||
|
||||
await command.create(params)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect display name', async function () {
|
||||
const params = getBase({ displayName: 's'.repeat(300) })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect description', async function () {
|
||||
const params = getBase({ description: 't' })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect privacy', async function () {
|
||||
const params = getBase({ privacy: 45 as any })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video channel id', async function () {
|
||||
const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thumbnail file', async function () {
|
||||
const params = getBase({ thumbnailfile: 'video_short.mp4' })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail with a thumbnail file too big', async function () {
|
||||
const params = getBase({ thumbnailfile: 'custom-preview-big.png' })
|
||||
|
||||
await command.create(params)
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail to set "public" a playlist not assigned to a channel', async function () {
|
||||
const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: undefined })
|
||||
const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' as any })
|
||||
const params3 = getBase({ privacy: undefined, videoChannelId: 'null' as any })
|
||||
|
||||
await command.create(params)
|
||||
await command.create(params2)
|
||||
await command.update(getUpdate(params, privatePlaylistUUID))
|
||||
await command.update(getUpdate(params2, playlist.shortUUID))
|
||||
await command.update(getUpdate(params3, playlist.shortUUID))
|
||||
})
|
||||
|
||||
it('Should fail with an unknown playlist to update', async function () {
|
||||
await command.update(getUpdate(
|
||||
getBase({}, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }),
|
||||
42
|
||||
))
|
||||
})
|
||||
|
||||
it('Should fail to update a playlist of another user', async function () {
|
||||
await command.update(getUpdate(
|
||||
getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }),
|
||||
playlist.shortUUID
|
||||
))
|
||||
})
|
||||
|
||||
it('Should fail to update the watch later playlist', async function () {
|
||||
await command.update(getUpdate(
|
||||
getBase({}, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 }),
|
||||
watchLaterPlaylistId
|
||||
))
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
{
|
||||
const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 })
|
||||
await command.create(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
await command.update(getUpdate(params, playlist.shortUUID))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding an element in a playlist', function () {
|
||||
const getBase = (
|
||||
attributes?: Partial<VideoPlaylistElementCreate>,
|
||||
wrapper?: Partial<Parameters<PlaylistsCommand['addElement']>[0]>
|
||||
) => {
|
||||
return {
|
||||
attributes: {
|
||||
videoId,
|
||||
startTimestamp: 2,
|
||||
stopTimestamp: 3,
|
||||
|
||||
...attributes
|
||||
},
|
||||
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
playlistId: playlist.id,
|
||||
|
||||
...wrapper
|
||||
}
|
||||
}
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await command.addElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with the playlist of another user', async function () {
|
||||
const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await command.addElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with an unknown or incorrect playlist id', async function () {
|
||||
{
|
||||
const params = getBase({}, { playlistId: 'toto' })
|
||||
await command.addElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.addElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown or incorrect video id', async function () {
|
||||
const params = getBase({ videoId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.addElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with a bad start/stop timestamp', async function () {
|
||||
{
|
||||
const params = getBase({ startTimestamp: -42 })
|
||||
await command.addElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ stopTimestamp: 'toto' as any })
|
||||
await command.addElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Succeed with the correct params', async function () {
|
||||
const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 })
|
||||
const created = await command.addElement(params)
|
||||
elementId = created.id
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating an element in a playlist', function () {
|
||||
const getBase = (
|
||||
attributes?: Partial<VideoPlaylistElementUpdate>,
|
||||
wrapper?: Partial<Parameters<PlaylistsCommand['updateElement']>[0]>
|
||||
) => {
|
||||
return {
|
||||
attributes: {
|
||||
startTimestamp: 1,
|
||||
stopTimestamp: 2,
|
||||
|
||||
...attributes
|
||||
},
|
||||
|
||||
elementId,
|
||||
playlistId: playlist.id,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
|
||||
...wrapper
|
||||
}
|
||||
}
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await command.updateElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with the playlist of another user', async function () {
|
||||
const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await command.updateElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with an unknown or incorrect playlist id', async function () {
|
||||
{
|
||||
const params = getBase({}, { playlistId: 'toto' })
|
||||
await command.updateElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.updateElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown or incorrect playlistElement id', async function () {
|
||||
{
|
||||
const params = getBase({}, { elementId: 'toto' })
|
||||
await command.updateElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({}, { elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.updateElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a bad start/stop timestamp', async function () {
|
||||
{
|
||||
const params = getBase({ startTimestamp: 'toto' as any })
|
||||
await command.updateElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ stopTimestamp: -42 })
|
||||
await command.updateElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown element', async function () {
|
||||
const params = getBase({}, { elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.updateElement(params)
|
||||
})
|
||||
|
||||
it('Succeed with the correct params', async function () {
|
||||
const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
await command.updateElement(params)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When reordering elements of a playlist', function () {
|
||||
let videoId3: number
|
||||
let videoId4: number
|
||||
|
||||
const getBase = (
|
||||
attributes?: Partial<VideoPlaylistReorder>,
|
||||
wrapper?: Partial<Parameters<PlaylistsCommand['reorderElements']>[0]>
|
||||
) => {
|
||||
return {
|
||||
attributes: {
|
||||
startPosition: 1,
|
||||
insertAfterPosition: 2,
|
||||
reorderLength: 3,
|
||||
|
||||
...attributes
|
||||
},
|
||||
|
||||
playlistId: playlist.shortUUID,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
|
||||
...wrapper
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
videoId3 = (await server.videos.quickUpload({ name: 'video 3' })).id
|
||||
videoId4 = (await server.videos.quickUpload({ name: 'video 4' })).id
|
||||
|
||||
for (const id of [ videoId3, videoId4 ]) {
|
||||
await command.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: id } })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await command.reorderElements(params)
|
||||
})
|
||||
|
||||
it('Should fail with the playlist of another user', async function () {
|
||||
const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await command.reorderElements(params)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid playlist', async function () {
|
||||
{
|
||||
const params = getBase({}, { playlistId: 'toto' })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid start position', async function () {
|
||||
{
|
||||
const params = getBase({ startPosition: -1 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ startPosition: 'toto' as any })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ startPosition: 42 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid insert after position', async function () {
|
||||
{
|
||||
const params = getBase({ insertAfterPosition: 'toto' as any })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ insertAfterPosition: -2 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ insertAfterPosition: 42 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid reorder length', async function () {
|
||||
{
|
||||
const params = getBase({ reorderLength: 'toto' as any })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ reorderLength: -2 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ reorderLength: 42 })
|
||||
await command.reorderElements(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Succeed with the correct params', async function () {
|
||||
const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
await command.reorderElements(params)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When checking exists in playlist endpoint', function () {
|
||||
const path = '/api/v1/users/me/video-playlists/videos-exist'
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: { videoIds: [ 1, 2 ] },
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid video ids', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { videoIds: 'toto' }
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { videoIds: [ 'toto' ] }
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { videoIds: [ 1, 'toto' ] }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { videoIds: [ 1, 2 ] },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting an element in a playlist', function () {
|
||||
const getBase = (wrapper: Partial<Parameters<PlaylistsCommand['removeElement']>[0]>) => {
|
||||
return {
|
||||
elementId,
|
||||
playlistId: playlist.uuid,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
|
||||
...wrapper
|
||||
}
|
||||
}
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
const params = getBase({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await command.removeElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with the playlist of another user', async function () {
|
||||
const params = getBase({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await command.removeElement(params)
|
||||
})
|
||||
|
||||
it('Should fail with an unknown or incorrect playlist id', async function () {
|
||||
{
|
||||
const params = getBase({ playlistId: 'toto' })
|
||||
await command.removeElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.removeElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown or incorrect video id', async function () {
|
||||
{
|
||||
const params = getBase({ elementId: 'toto' as any })
|
||||
await command.removeElement(params)
|
||||
}
|
||||
|
||||
{
|
||||
const params = getBase({ elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.removeElement(params)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an unknown element', async function () {
|
||||
const params = getBase({ elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await command.removeElement(params)
|
||||
})
|
||||
|
||||
it('Succeed with the correct params', async function () {
|
||||
const params = getBase({ expectedStatus: HttpStatusCode.NO_CONTENT_204 })
|
||||
await command.removeElement(params)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting a playlist', function () {
|
||||
it('Should fail with an unknown playlist', async function () {
|
||||
await command.delete({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a playlist of another user', async function () {
|
||||
await command.delete({ token: userAccessToken, playlistId: playlist.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with the watch later playlist', async function () {
|
||||
await command.delete({ playlistId: watchLaterPlaylistId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await command.delete({ playlistId: playlist.uuid })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,247 @@
|
||||
import { HttpStatusCode, VideoSource } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeRawRequest,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video sources API validator', function () {
|
||||
let server: PeerTubeServer = null
|
||||
let uuid: string
|
||||
let userToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
})
|
||||
|
||||
describe('When getting latest source', function () {
|
||||
|
||||
before(async function () {
|
||||
const created = await server.videos.quickUpload({ name: 'video' })
|
||||
uuid = created.uuid
|
||||
})
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should receive 404 when passing a non existing video id', async function () {
|
||||
await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not get the source as unauthenticated', async function () {
|
||||
await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
|
||||
})
|
||||
|
||||
it('Should not get the source with another user', async function () {
|
||||
await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters get the source as another user', async function () {
|
||||
await server.videos.getSource({ id: uuid })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating source video file', function () {
|
||||
let userAccessToken: string
|
||||
let userId: number
|
||||
|
||||
let videoId: string
|
||||
let userVideoId: string
|
||||
|
||||
before(async function () {
|
||||
const res = await server.users.generate('user2')
|
||||
userAccessToken = res.token
|
||||
userId = res.userId
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
videoId = uuid
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
it('Should fail if not enabled on the instance', async function () {
|
||||
await server.config.disableFileUpdate()
|
||||
|
||||
await server.videos.replaceSourceFile({ videoId, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail on an unknown video', async function () {
|
||||
await server.config.enableFileUpdate()
|
||||
|
||||
await server.videos.replaceSourceFile({ videoId: 404, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid video', async function () {
|
||||
await server.config.enableLive({ allowReplay: false })
|
||||
|
||||
const { video } = await server.live.quickCreate({ saveReplay: false, permanentLive: true })
|
||||
await server.videos.replaceSourceFile({
|
||||
videoId: video.uuid,
|
||||
fixture: 'video_short.mp4',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.videos.replaceSourceFile({
|
||||
token: null,
|
||||
videoId,
|
||||
fixture: 'video_short.mp4',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user', async function () {
|
||||
await server.videos.replaceSourceFile({
|
||||
token: userAccessToken,
|
||||
videoId,
|
||||
fixture: 'video_short.mp4',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect input file', async function () {
|
||||
await server.videos.replaceSourceFile({
|
||||
fixture: 'video_short_fake.webm',
|
||||
videoId,
|
||||
completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422
|
||||
})
|
||||
|
||||
await server.videos.replaceSourceFile({
|
||||
fixture: 'video_short.mkv',
|
||||
videoId,
|
||||
expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if quota is exceeded', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'user video' })
|
||||
userVideoId = uuid
|
||||
await waitJobs([ server ])
|
||||
|
||||
await server.users.update({ userId, videoQuota: 1 })
|
||||
await server.videos.replaceSourceFile({
|
||||
token: userAccessToken,
|
||||
videoId: uuid,
|
||||
fixture: 'video_short.mp4',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.users.update({ userId, videoQuota: 1000 * 1000 * 1000 })
|
||||
await server.videos.replaceSourceFile({ videoId: userVideoId, fixture: 'video_short.mp4' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When downloading the source file', function () {
|
||||
let videoFileToken: string
|
||||
let videoId: string
|
||||
let source: VideoSource
|
||||
let user3: string
|
||||
let user4: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
user3 = await server.users.generateUserAndToken('user3')
|
||||
user4 = await server.users.generateUserAndToken('user4')
|
||||
|
||||
await server.config.enableMinimumTranscoding({ hls: true, keepOriginal: true, webVideo: true })
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video', token: user3 })
|
||||
|
||||
videoId = uuid
|
||||
videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid, token: user3 })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
source = await server.videos.getSource({ id: videoId, token: user3 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid filename', async function () {
|
||||
await makeRawRequest({ url: server.url + '/download/original-video-files/hello.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail without header token or video file token', async function () {
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid header token', async function () {
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, token: 'toto', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid video file token', async function () {
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken: 'toto' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with header token of another user', async function () {
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, token: user4, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with video file token of another user', async function () {
|
||||
const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid, token: user4 })
|
||||
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with a valid header token', async function () {
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, token: user3, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should succeed with a valid header token', async function () {
|
||||
await makeRawRequest({ url: source.fileDownloadUrl, query: { videoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting video source file', function () {
|
||||
let userAccessToken: string
|
||||
|
||||
let videoId: string
|
||||
|
||||
before(async function () {
|
||||
userAccessToken = await server.users.generateUserAndToken('user56')
|
||||
|
||||
await server.config.enableMinimumTranscoding({ keepOriginal: true })
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'with source' })
|
||||
videoId = uuid
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await server.videos.deleteSource({ id: videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another user', async function () {
|
||||
await server.videos.deleteSource({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video', async function () {
|
||||
await server.videos.deleteSource({ id: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await server.videos.deleteSource({ id: videoId })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video storyboards API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
let publicVideo: { uuid: string }
|
||||
let privateVideo: { uuid: string }
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
publicVideo = await server.videos.quickUpload({ name: 'public' })
|
||||
privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })
|
||||
})
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should receive 404 when passing a non existing video id', async function () {
|
||||
await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not get the private storyboard without the appropriate token', async function () {
|
||||
await server.storyboard.list({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
|
||||
await server.storyboard.list({ id: publicVideo.uuid, expectedStatus: HttpStatusCode.OK_200, token: null })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await server.storyboard.list({ id: privateVideo.uuid })
|
||||
await server.storyboard.list({ id: publicVideo.uuid })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,392 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, HttpStatusCodeType, VideoStudioTask } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
VideoStudioCommand,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video studio API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
let command: VideoStudioCommand
|
||||
let userAccessToken: string
|
||||
let videoUUID: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
await server.config.enableMinimumTranscoding()
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
videoUUID = uuid
|
||||
|
||||
command = server.videoStudio
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
describe('Task creation', function () {
|
||||
|
||||
describe('Config settings', function () {
|
||||
|
||||
it('Should fail if studio is disabled', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
videoStudio: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to enable studio if transcoding is disabled', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
videoStudio: {
|
||||
enabled: true
|
||||
},
|
||||
transcoding: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to enable video studio', async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
videoStudio: {
|
||||
enabled: true
|
||||
},
|
||||
transcoding: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Common tasks', function () {
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await command.createEditionTasks({
|
||||
token: null,
|
||||
videoId: videoUUID,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user token', async function () {
|
||||
await command.createEditionTasks({
|
||||
token: userAccessToken,
|
||||
videoId: videoUUID,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid video', async function () {
|
||||
await command.createEditionTasks({
|
||||
videoId: 'tintin',
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown video', async function () {
|
||||
await command.createEditionTasks({
|
||||
videoId: 42,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an already in transcoding state video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'transcoded video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
await server.jobs.pauseJobQueue()
|
||||
await server.videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
|
||||
|
||||
await command.createEditionTasks({
|
||||
videoId: uuid,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
|
||||
await server.jobs.resumeJobQueue()
|
||||
})
|
||||
|
||||
it('Should fail with a bad complex task', async function () {
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: [
|
||||
{
|
||||
name: 'cut',
|
||||
options: {
|
||||
start: 1,
|
||||
end: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'hadock',
|
||||
options: {
|
||||
start: 1,
|
||||
end: 2
|
||||
}
|
||||
}
|
||||
] as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without task', async function () {
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: [],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with too many tasks', async function () {
|
||||
const tasks: VideoStudioTask[] = []
|
||||
|
||||
for (let i = 0; i < 110; i++) {
|
||||
tasks.push({
|
||||
name: 'cut',
|
||||
options: {
|
||||
start: 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with correct parameters', async function () {
|
||||
await server.jobs.pauseJobQueue()
|
||||
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a video that is already waiting for edition', async function () {
|
||||
this.timeout(360000)
|
||||
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: VideoStudioCommand.getComplexTask(),
|
||||
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||
})
|
||||
|
||||
await server.jobs.resumeJobQueue()
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cut task', function () {
|
||||
|
||||
async function cut (start: number, end: number, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: [
|
||||
{
|
||||
name: 'cut',
|
||||
options: {
|
||||
start,
|
||||
end
|
||||
}
|
||||
}
|
||||
],
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
|
||||
it('Should fail with bad start/end', async function () {
|
||||
const invalid = [
|
||||
'tintin',
|
||||
-1,
|
||||
undefined
|
||||
]
|
||||
|
||||
for (const value of invalid) {
|
||||
await cut(value as any, undefined)
|
||||
await cut(undefined, value as any)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with the same start/end', async function () {
|
||||
await cut(2, 2)
|
||||
})
|
||||
|
||||
it('Should fail with inconsistents start/end', async function () {
|
||||
await cut(2, 1)
|
||||
})
|
||||
|
||||
it('Should fail without start and end', async function () {
|
||||
await cut(undefined, undefined)
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
this.timeout(360000)
|
||||
|
||||
await cut(0, 2, HttpStatusCode.NO_CONTENT_204)
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Watermark task', function () {
|
||||
|
||||
async function addWatermark (file: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: [
|
||||
{
|
||||
name: 'add-watermark',
|
||||
options: {
|
||||
file
|
||||
}
|
||||
}
|
||||
],
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
|
||||
it('Should fail without waterkmark', async function () {
|
||||
await addWatermark(undefined)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid watermark', async function () {
|
||||
await addWatermark('video_short.mp4')
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
this.timeout(360000)
|
||||
|
||||
await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204)
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Intro/Outro task', function () {
|
||||
|
||||
async function addIntroOutro (
|
||||
type: 'add-intro' | 'add-outro',
|
||||
file: string,
|
||||
expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400
|
||||
) {
|
||||
await command.createEditionTasks({
|
||||
videoId: videoUUID,
|
||||
tasks: [
|
||||
{
|
||||
name: type,
|
||||
options: {
|
||||
file
|
||||
}
|
||||
}
|
||||
],
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
|
||||
it('Should fail without file', async function () {
|
||||
await addIntroOutro('add-intro', undefined)
|
||||
await addIntroOutro('add-outro', undefined)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid file', async function () {
|
||||
await addIntroOutro('add-intro', 'custom-thumbnail.jpg')
|
||||
await addIntroOutro('add-outro', 'custom-thumbnail.jpg')
|
||||
})
|
||||
|
||||
it('Should fail with a file that does not contain video stream', async function () {
|
||||
await addIntroOutro('add-intro', 'sample.ogg')
|
||||
await addIntroOutro('add-outro', 'sample.ogg')
|
||||
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
this.timeout(360000)
|
||||
|
||||
await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204)
|
||||
await waitJobs([ server ])
|
||||
|
||||
await addIntroOutro('add-outro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204)
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
it('Should check total quota when creating the task', async function () {
|
||||
this.timeout(360000)
|
||||
|
||||
const user = await server.users.create({ username: 'user_quota_1' })
|
||||
const token = await server.login.getAccessToken('user_quota_1')
|
||||
const { uuid } = await server.videos.quickUpload({ token, name: 'video_quota_1', fixture: 'video_short.mp4' })
|
||||
|
||||
const addIntroOutroByUser = (type: 'add-intro' | 'add-outro', expectedStatus: HttpStatusCodeType) => {
|
||||
return command.createEditionTasks({
|
||||
token,
|
||||
videoId: uuid,
|
||||
tasks: [
|
||||
{
|
||||
name: type,
|
||||
options: {
|
||||
file: 'video_short.mp4'
|
||||
}
|
||||
}
|
||||
],
|
||||
expectedStatus
|
||||
})
|
||||
}
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { videoQuotaUsed } = await server.users.getMyQuotaUsed({ token })
|
||||
await server.users.update({ userId: user.id, videoQuota: Math.round(videoQuotaUsed * 2.5) })
|
||||
|
||||
// Still valid
|
||||
await addIntroOutroByUser('add-intro', HttpStatusCode.NO_CONTENT_204)
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
// Too much quota
|
||||
await addIntroOutroByUser('add-intro', HttpStatusCode.PAYLOAD_TOO_LARGE_413)
|
||||
await addIntroOutroByUser('add-outro', HttpStatusCode.PAYLOAD_TOO_LARGE_413)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video tokens', function () {
|
||||
let server: PeerTubeServer
|
||||
let privateVideoId: string
|
||||
let passwordProtectedVideoId: string
|
||||
let userToken: string
|
||||
|
||||
const videoPassword = 'password'
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(300_000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE })
|
||||
privateVideoId = uuid
|
||||
}
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({
|
||||
name: 'password protected video',
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
videoPasswords: [ videoPassword ]
|
||||
})
|
||||
passwordProtectedVideoId = uuid
|
||||
}
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
})
|
||||
|
||||
it('Should not generate tokens on private video for unauthenticated user', async function () {
|
||||
await server.videoToken.create({ videoId: privateVideoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should not generate tokens of unknown video', async function () {
|
||||
await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not generate tokens with incorrect password', async function () {
|
||||
await server.videoToken.create({
|
||||
videoId: passwordProtectedVideoId,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403,
|
||||
videoPassword: 'incorrectPassword'
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not generate tokens of a non owned video', async function () {
|
||||
await server.videoToken.create({ videoId: privateVideoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should generate token', async function () {
|
||||
await server.videoToken.create({ videoId: privateVideoId })
|
||||
})
|
||||
|
||||
it('Should generate token on password protected video', async function () {
|
||||
await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: null })
|
||||
await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: userToken })
|
||||
await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video transcription API validator', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
|
||||
let userToken: string
|
||||
let anotherUserToken: string
|
||||
|
||||
let remoteId: string
|
||||
let validId: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER)
|
||||
anotherUserToken = await servers[0].users.generateUserAndToken('user2', UserRole.USER)
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
|
||||
remoteId = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1', token: userToken })
|
||||
validId = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].config.enableTranscription()
|
||||
})
|
||||
|
||||
it('Should not run transcription of an unknown video', async function () {
|
||||
await servers[0].captions.runGenerate({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should not run transcription of a remote video', async function () {
|
||||
await servers[0].captions.runGenerate({ videoId: remoteId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should not run transcription by a owner/moderator user', async function () {
|
||||
await servers[0].captions.runGenerate({ videoId: validId, token: anotherUserToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should not run transcription if a caption file already exists', async function () {
|
||||
await servers[0].captions.add({
|
||||
language: 'en',
|
||||
videoId: validId,
|
||||
fixture: 'subtitle-good1.vtt'
|
||||
})
|
||||
|
||||
await servers[0].captions.runGenerate({ videoId: validId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await servers[0].captions.delete({ language: 'en', videoId: validId })
|
||||
})
|
||||
|
||||
it('Should not run transcription if the instance disabled it', async function () {
|
||||
await servers[0].config.disableTranscription()
|
||||
|
||||
await servers[0].captions.runGenerate({ videoId: validId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
await servers[0].config.enableTranscription()
|
||||
})
|
||||
|
||||
it('Should succeed to run transcription', async function () {
|
||||
await servers[0].captions.runGenerate({ videoId: validId, token: userToken })
|
||||
})
|
||||
|
||||
it('Should fail to run transcription twice', async function () {
|
||||
await servers[0].captions.runGenerate({ videoId: validId, token: userToken, expectedStatus: HttpStatusCode.CONFLICT_409 })
|
||||
})
|
||||
|
||||
it('Should fail to run transcription twice with a non-admin user with the forceTranscription boolean', async function () {
|
||||
await servers[0].captions.runGenerate({
|
||||
videoId: validId,
|
||||
token: userToken,
|
||||
forceTranscription: true,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to run transcription twice with the forceTranscription boolean', async function () {
|
||||
await servers[0].captions.runGenerate({ videoId: validId, forceTranscription: true })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,196 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
UserRole,
|
||||
VideoInclude,
|
||||
VideoIncludeType,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video filters validators', function () {
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken: string
|
||||
let moderatorAccessToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
const user = { username: 'user1', password: 'my super password' }
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
|
||||
const moderator = { username: 'moderator', password: 'my super password' }
|
||||
await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
|
||||
|
||||
moderatorAccessToken = await server.login.getAccessToken(moderator)
|
||||
})
|
||||
|
||||
describe('When setting video filters', function () {
|
||||
|
||||
const validIncludes = [
|
||||
VideoInclude.NONE,
|
||||
VideoInclude.BLOCKED_OWNER,
|
||||
VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED,
|
||||
VideoInclude.SOURCE
|
||||
]
|
||||
|
||||
async function testEndpoints (options: {
|
||||
token?: string
|
||||
isLocal?: boolean
|
||||
include?: VideoIncludeType
|
||||
privacyOneOf?: VideoPrivacyType[]
|
||||
autoTagOneOf?: string[]
|
||||
expectedStatus: HttpStatusCodeType
|
||||
excludeAlreadyWatched?: boolean
|
||||
unauthenticatedUser?: boolean
|
||||
filter?: string
|
||||
}) {
|
||||
const paths = [
|
||||
'/api/v1/video-channels/root_channel/videos',
|
||||
'/api/v1/accounts/root/videos',
|
||||
'/api/v1/videos',
|
||||
'/api/v1/search/videos'
|
||||
]
|
||||
|
||||
for (const path of paths) {
|
||||
const token = options.unauthenticatedUser
|
||||
? undefined
|
||||
: options.token || server.accessToken
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token,
|
||||
query: {
|
||||
isLocal: options.isLocal,
|
||||
privacyOneOf: options.privacyOneOf,
|
||||
autoTagOneOf: options.autoTagOneOf,
|
||||
include: options.include,
|
||||
excludeAlreadyWatched: options.excludeAlreadyWatched,
|
||||
filter: options.filter
|
||||
},
|
||||
expectedStatus: options.expectedStatus
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
it('Should fail with the old filter query param', async function () {
|
||||
await testEndpoints({ filter: 'all-local', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad privacyOneOf', async function () {
|
||||
await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with a good privacyOneOf', async function () {
|
||||
await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should fail to use privacyOneOf with a simple user', async function () {
|
||||
await testEndpoints({
|
||||
privacyOneOf: [ VideoPrivacy.INTERNAL ],
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to use autoTagOneOf with a simple user', async function () {
|
||||
await testEndpoints({
|
||||
autoTagOneOf: [ 'test' ],
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to use autoTagOneOf with a moderator', async function () {
|
||||
await testEndpoints({
|
||||
autoTagOneOf: [ 'test' ],
|
||||
token: moderatorAccessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad include', async function () {
|
||||
await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with a good include', async function () {
|
||||
for (const include of validIncludes) {
|
||||
await testEndpoints({ include, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail to include more videos with a simple user', async function () {
|
||||
for (const include of validIncludes) {
|
||||
await testEndpoints({ token: userAccessToken, include, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed to list all local/all with a moderator', async function () {
|
||||
for (const include of validIncludes) {
|
||||
await testEndpoints({ token: moderatorAccessToken, include, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed to list all local/all with an admin', async function () {
|
||||
for (const include of validIncludes) {
|
||||
await testEndpoints({ token: server.accessToken, include, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
// Because we cannot authenticate the user on the RSS endpoint
|
||||
it('Should fail on the feeds endpoint with the all filter', async function () {
|
||||
for (const include of [ VideoInclude.NOT_PUBLISHED_STATE ]) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/feeds/videos.json',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
|
||||
query: {
|
||||
include
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed on the feeds endpoint with the local filter', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: '/feeds/videos.json',
|
||||
expectedStatus: HttpStatusCode.OK_200,
|
||||
query: {
|
||||
isLocal: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail when trying to exclude already watched videos for an unlogged user', async function () {
|
||||
await testEndpoints({ excludeAlreadyWatched: true, unauthenticatedUser: true, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed when trying to exclude already watched videos for a logged user', async function () {
|
||||
await testEndpoints({ token: userAccessToken, excludeAlreadyWatched: true, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { checkBadCountPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test videos history API validator', function () {
|
||||
const myHistoryPath = '/api/v1/users/me/history/videos'
|
||||
const myHistoryRemove = myHistoryPath + '/remove'
|
||||
let viewPath: string
|
||||
let server: PeerTubeServer
|
||||
let videoId: number
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const { id, uuid } = await server.videos.upload()
|
||||
viewPath = '/api/v1/videos/' + uuid + '/views'
|
||||
videoId = id
|
||||
})
|
||||
|
||||
describe('When notifying a user is watching a video', function () {
|
||||
|
||||
it('Should fail with a bad token', async function () {
|
||||
const fields = { currentTime: 5 }
|
||||
await makePutBodyRequest({ url: server.url, path: viewPath, fields, token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const fields = { currentTime: 5 }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: viewPath,
|
||||
fields,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing user videos history', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, myHistoryPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, myHistoryPath, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeGetRequest({ url: server.url, path: myHistoryPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing a specific user video history element', function () {
|
||||
let path: string
|
||||
|
||||
before(function () {
|
||||
path = myHistoryPath + '/' + videoId
|
||||
})
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad videoId parameter', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: myHistoryRemove + '/hi',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing all user videos history', function () {
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad beforeDate parameter', async function () {
|
||||
const body = { beforeDate: '15' }
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: myHistoryRemove,
|
||||
fields: body,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with a valid beforeDate param', async function () {
|
||||
const body = { beforeDate: new Date().toISOString() }
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: myHistoryRemove,
|
||||
fields: body,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed without body', async function () {
|
||||
await makePostBodyRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path: myHistoryRemove,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test videos overview API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
})
|
||||
|
||||
describe('When getting videos overview', function () {
|
||||
|
||||
it('Should fail with a bad pagination', async function () {
|
||||
await server.overviews.getVideos({ page: 0, expectedStatus: 400 })
|
||||
await server.overviews.getVideos({ page: 100, expectedStatus: 400 })
|
||||
})
|
||||
|
||||
it('Should succeed with a good pagination', async function () {
|
||||
await server.overviews.getVideos({ page: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,897 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { omit, randomInt } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
PeerTubeProblemDocument,
|
||||
VideoCommentPolicy,
|
||||
VideoCreateResult,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { checkUploadVideoParam } from '@tests/shared/videos.js'
|
||||
import { expect } from 'chai'
|
||||
import { join } from 'path'
|
||||
|
||||
describe('Test videos API validator', function () {
|
||||
const path = '/api/v1/videos/'
|
||||
let server: PeerTubeServer
|
||||
let userAccessToken = ''
|
||||
let accountName: string
|
||||
let channelId: number
|
||||
let channelName: string
|
||||
let video: VideoCreateResult
|
||||
let privateVideo: VideoCreateResult
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
{
|
||||
const body = await server.users.getMyInfo()
|
||||
channelId = body.videoChannels[0].id
|
||||
channelName = body.videoChannels[0].name
|
||||
accountName = body.account.name + '@' + body.account.host
|
||||
}
|
||||
|
||||
{
|
||||
privateVideo = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE })
|
||||
}
|
||||
})
|
||||
|
||||
describe('When listing videos', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path)
|
||||
})
|
||||
|
||||
it('Should fail with a bad skipVideos query', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: 'toto' } })
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: false } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When searching a video', function () {
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path: join(path, 'search'),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, join(path, 'search', 'test'))
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, join(path, 'search', 'test'))
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, join(path, 'search', 'test'))
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing my videos', function () {
|
||||
const path = '/api/v1/users/me/videos'
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an invalid channel', async function () {
|
||||
await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown channel', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { channelId: 89898 },
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing account videos', function () {
|
||||
let path: string
|
||||
|
||||
before(async function () {
|
||||
path = '/api/v1/accounts/' + accountName + '/videos'
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing video channel videos', function () {
|
||||
let path: string
|
||||
|
||||
before(async function () {
|
||||
path = '/api/v1/video-channels/' + channelName + '/videos'
|
||||
})
|
||||
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should success with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding a video', function () {
|
||||
const baseCorrectParams = {
|
||||
name: 'my super name',
|
||||
category: 5,
|
||||
licence: 1,
|
||||
language: 'pt',
|
||||
nsfw: false,
|
||||
commentsPolicy: VideoCommentPolicy.ENABLED,
|
||||
downloadEnabled: true,
|
||||
waitTranscoding: true,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag1', 'tag2' ],
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: -1,
|
||||
originallyPublishedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
const baseCorrectAttaches = {
|
||||
fixture: buildAbsoluteFixturePath('video_short.webm')
|
||||
}
|
||||
|
||||
before(function () {
|
||||
// Put in before to have channelId
|
||||
baseCorrectParams.channelId = channelId
|
||||
})
|
||||
|
||||
function runSuite (mode: 'legacy' | 'resumable') {
|
||||
|
||||
const baseOptions = () => {
|
||||
return {
|
||||
server,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400,
|
||||
mode
|
||||
}
|
||||
}
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
const attaches = {}
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail without name', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'name' ])
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a long name', async function () {
|
||||
const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad category', async function () {
|
||||
const fields = { ...baseCorrectParams, category: 125 }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad licence', async function () {
|
||||
const fields = { ...baseCorrectParams, licence: 125 }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad language', async function () {
|
||||
const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with bad commentsPolicy', async function () {
|
||||
const fields = { ...baseCorrectParams, commentsPolicy: 42 as any }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a long support text', async function () {
|
||||
const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail without a channel', async function () {
|
||||
const fields = omit(baseCorrectParams, [ 'channelId' ])
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel', async function () {
|
||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with another user channel', async function () {
|
||||
const user = {
|
||||
username: 'fake' + randomInt(0, 1500),
|
||||
password: 'fake_password'
|
||||
}
|
||||
await server.users.create({ username: user.username, password: user.password })
|
||||
|
||||
const accessTokenUser = await server.login.getAccessToken(user)
|
||||
const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
|
||||
const customChannelId = videoChannels[0].id
|
||||
|
||||
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
token: userAccessToken,
|
||||
attributes: { ...fields, ...attaches }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with too many tags', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too low', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too big', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (miss updateAt)', async function () {
|
||||
const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } as any }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (wrong updateAt)', async function () {
|
||||
const fields = {
|
||||
...baseCorrectParams,
|
||||
|
||||
scheduleUpdate: {
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
updateAt: 'toto'
|
||||
}
|
||||
}
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a bad originally published at attribute', async function () {
|
||||
const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail without an input file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {}
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect input file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
let attaches = { fixture: buildAbsoluteFixturePath('video_short_fake.webm') }
|
||||
|
||||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
attributes: { ...fields, ...attaches },
|
||||
// 200 for the init request, 422 when the file has finished being uploaded
|
||||
expectedStatus: undefined,
|
||||
completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422
|
||||
})
|
||||
|
||||
attaches = { fixture: buildAbsoluteFixturePath('video_short.mkv') }
|
||||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
attributes: { ...fields, ...attaches },
|
||||
expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('video_short.mp4'),
|
||||
fixture: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a big thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png'),
|
||||
fixture: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('video_short.mp4'),
|
||||
fixture: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should fail with a big preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('custom-preview-big.png'),
|
||||
fixture: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
||||
})
|
||||
|
||||
it('Should report the appropriate error', async function () {
|
||||
const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
const attributes = { ...fields, ...attaches }
|
||||
const body = await checkUploadVideoParam({ ...baseOptions(), attributes })
|
||||
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
if (mode === 'legacy') {
|
||||
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
|
||||
} else {
|
||||
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit')
|
||||
}
|
||||
|
||||
expect(error.type).to.equal('about:blank')
|
||||
expect(error.title).to.equal('Bad Request')
|
||||
|
||||
expect(error.detail).to.equal('Incorrect request parameters: language')
|
||||
expect(error.error).to.equal('Incorrect request parameters: language')
|
||||
|
||||
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||
expect(error['invalid-params'].language).to.exist
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const fields = baseCorrectParams
|
||||
|
||||
{
|
||||
const attaches = baseCorrectAttaches
|
||||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
attributes: { ...fields, ...attaches },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const attaches = {
|
||||
...baseCorrectAttaches,
|
||||
|
||||
videofile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
attributes: { ...fields, ...attaches },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const attaches = {
|
||||
...baseCorrectAttaches,
|
||||
|
||||
videofile: buildAbsoluteFixturePath('video_short.ogv')
|
||||
}
|
||||
|
||||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
attributes: { ...fields, ...attaches },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Resumable upload', function () {
|
||||
runSuite('resumable')
|
||||
})
|
||||
|
||||
describe('Legacy upload', function () {
|
||||
runSuite('legacy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating a video', function () {
|
||||
const baseCorrectParams = {
|
||||
name: 'my super name',
|
||||
category: 5,
|
||||
licence: 2,
|
||||
language: 'pt',
|
||||
nsfw: false,
|
||||
commentsPolicy: VideoCommentPolicy.DISABLED,
|
||||
downloadEnabled: false,
|
||||
description: 'my super description',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
tags: [ 'tag1', 'tag2' ]
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
const { data } = await server.videos.list()
|
||||
video = data[0]
|
||||
})
|
||||
|
||||
it('Should fail with nothing', async function () {
|
||||
const fields = {}
|
||||
await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
const fields = baseCorrectParams
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'blabla', token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
const fields = baseCorrectParams
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df5630b06',
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a long name', async function () {
|
||||
const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad category', async function () {
|
||||
const fields = { ...baseCorrectParams, category: 125 }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad licence', async function () {
|
||||
const fields = { ...baseCorrectParams, licence: 125 }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad language', async function () {
|
||||
const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a long support text', async function () {
|
||||
const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel', async function () {
|
||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with too many tags', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too low', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a tag length too big', async function () {
|
||||
const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (miss updateAt)', async function () {
|
||||
const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad schedule update (wrong updateAt)', async function () {
|
||||
const fields = { ...baseCorrectParams, scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a bad originally published at param', async function () {
|
||||
const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
method: 'PUT',
|
||||
path: path + video.shortUUID,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a big thumbnail file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
method: 'PUT',
|
||||
path: path + video.shortUUID,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('video_short.mp4')
|
||||
}
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
method: 'PUT',
|
||||
path: path + video.shortUUID,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a big preview file', async function () {
|
||||
const fields = baseCorrectParams
|
||||
const attaches = {
|
||||
previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
|
||||
}
|
||||
|
||||
await makeUploadRequest({
|
||||
url: server.url,
|
||||
method: 'PUT',
|
||||
path: path + video.shortUUID,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a video of another user without the appropriate right', async function () {
|
||||
const fields = baseCorrectParams
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + video.shortUUID,
|
||||
token: userAccessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a video of another server')
|
||||
|
||||
it('Shoud report the appropriate error', async function () {
|
||||
const fields = { ...baseCorrectParams, licence: 125 }
|
||||
|
||||
const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
||||
const error = res.body as PeerTubeProblemDocument
|
||||
|
||||
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo')
|
||||
|
||||
expect(error.type).to.equal('about:blank')
|
||||
expect(error.title).to.equal('Bad Request')
|
||||
|
||||
expect(error.detail).to.equal('Incorrect request parameters: licence')
|
||||
expect(error.error).to.equal('Incorrect request parameters: licence')
|
||||
|
||||
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||
expect(error['invalid-params'].licence).to.exist
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const fields = baseCorrectParams
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + video.shortUUID,
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting a video', function () {
|
||||
it('Should return the list of the videos with nothing', async function () {
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
expect(res.body.data).to.be.an('array')
|
||||
expect(res.body.data.length).to.equal(6)
|
||||
})
|
||||
|
||||
it('Should fail without a correct uuid', async function () {
|
||||
await server.videos.get({ id: 'coucou', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should return 404 with an incorrect video', async function () {
|
||||
await server.videos.get({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Shoud report the appropriate error', async function () {
|
||||
const body = await server.videos.get({ id: 'hi', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
const error = body as unknown as PeerTubeProblemDocument
|
||||
|
||||
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
|
||||
|
||||
expect(error.type).to.equal('about:blank')
|
||||
expect(error.title).to.equal('Bad Request')
|
||||
|
||||
expect(error.detail).to.equal('Incorrect request parameters: id')
|
||||
expect(error.error).to.equal('Incorrect request parameters: id')
|
||||
|
||||
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||
expect(error['invalid-params'].id).to.exist
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await server.videos.get({ id: video.shortUUID })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When rating a video', function () {
|
||||
let videoId: number
|
||||
|
||||
before(async function () {
|
||||
const { data } = await server.videos.list()
|
||||
videoId = data[0].id
|
||||
})
|
||||
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
const fields = {
|
||||
rating: 'like'
|
||||
}
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'blabla/rate', token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
const fields = {
|
||||
rating: 'like'
|
||||
}
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate',
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a wrong rating', async function () {
|
||||
const fields = {
|
||||
rating: 'likes'
|
||||
}
|
||||
await makePutBodyRequest({ url: server.url, path: path + videoId + '/rate', token: server.accessToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with a private video of another user', async function () {
|
||||
const fields = {
|
||||
rating: 'like'
|
||||
}
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + privateVideo.uuid + '/rate',
|
||||
token: userAccessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
const fields = {
|
||||
rating: 'like'
|
||||
}
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + videoId + '/rate',
|
||||
token: server.accessToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing a video', function () {
|
||||
it('Should have 404 with nothing', async function () {
|
||||
await makeDeleteRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without a correct uuid', async function () {
|
||||
await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with a video which does not exist', async function () {
|
||||
await server.videos.remove({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a video of another user without the appropriate right', async function () {
|
||||
await server.videos.remove({ token: userAccessToken, id: video.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail with a video of another server')
|
||||
|
||||
it('Shoud report the appropriate error', async function () {
|
||||
const body = await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
const error = body as PeerTubeProblemDocument
|
||||
|
||||
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
|
||||
|
||||
expect(error.type).to.equal('about:blank')
|
||||
expect(error.title).to.equal('Bad Request')
|
||||
|
||||
expect(error.detail).to.equal('Incorrect request parameters: id')
|
||||
expect(error.error).to.equal('Incorrect request parameters: id')
|
||||
|
||||
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||
expect(error['invalid-params'].id).to.exist
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await server.videos.remove({ id: video.uuid })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,228 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test videos views API validators', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let liveVideoId: string
|
||||
let videoId: string
|
||||
let remoteVideoId: string
|
||||
let userAccessToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await servers[0].config.enableLive({ allowReplay: false, transcoding: false });
|
||||
|
||||
({ uuid: videoId } = await servers[0].videos.quickUpload({ name: 'video' }));
|
||||
({ uuid: remoteVideoId } = await servers[1].videos.quickUpload({ name: 'video' }));
|
||||
({ uuid: liveVideoId } = await servers[0].live.create({
|
||||
fields: {
|
||||
name: 'live',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: servers[0].store.channel.id
|
||||
}
|
||||
}))
|
||||
|
||||
userAccessToken = await servers[0].users.generateUserAndToken('user')
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
describe('When viewing a video', async function () {
|
||||
|
||||
it('Should fail without current time', async function () {
|
||||
await servers[0].views.view({ id: videoId, currentTime: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid current time', async function () {
|
||||
await servers[0].views.view({ id: videoId, currentTime: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].views.view({ id: videoId, currentTime: -1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await servers[0].views.view({ id: videoId, currentTime: 10, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with correct parameters', async function () {
|
||||
await servers[0].views.view({ id: videoId, currentTime: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting overall stats', function () {
|
||||
|
||||
it('Should fail with a remote video', async function () {
|
||||
await servers[0].videoStats.getOverallStats({ videoId: remoteVideoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await servers[0].videoStats.getOverallStats({ videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with another token', async function () {
|
||||
await servers[0].videoStats.getOverallStats({
|
||||
videoId,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid start date', async function () {
|
||||
await servers[0].videoStats.getOverallStats({
|
||||
videoId,
|
||||
startDate: 'fake' as any,
|
||||
endDate: new Date().toISOString(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid end date', async function () {
|
||||
await servers[0].videoStats.getOverallStats({
|
||||
videoId,
|
||||
startDate: new Date().toISOString(),
|
||||
endDate: 'fake' as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await servers[0].videoStats.getOverallStats({
|
||||
videoId,
|
||||
startDate: new Date().toISOString(),
|
||||
endDate: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting timeserie stats', function () {
|
||||
|
||||
it('Should fail with a remote video', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId: remoteVideoId,
|
||||
metric: 'viewers',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
token: null,
|
||||
metric: 'viewers',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another token', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
token: userAccessToken,
|
||||
metric: 'viewers',
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid metric', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid start date', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
metric: 'viewers',
|
||||
startDate: 'fake' as any,
|
||||
endDate: new Date(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid end date', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
metric: 'viewers',
|
||||
startDate: new Date(),
|
||||
endDate: 'fake' as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if start date is specified but not end date', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
metric: 'viewers',
|
||||
startDate: new Date(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail if end date is specified but not start date', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
metric: 'viewers',
|
||||
endDate: new Date(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a too big interval', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({
|
||||
videoId,
|
||||
metric: 'viewers',
|
||||
startDate: new Date('2000-04-07T08:31:57.126Z'),
|
||||
endDate: new Date(),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When getting retention stats', function () {
|
||||
|
||||
it('Should fail with a remote video', async function () {
|
||||
await servers[0].videoStats.getRetentionStats({
|
||||
videoId: remoteVideoId,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without token', async function () {
|
||||
await servers[0].videoStats.getRetentionStats({
|
||||
videoId,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another token', async function () {
|
||||
await servers[0].videoStats.getRetentionStats({
|
||||
videoId,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail on live video', async function () {
|
||||
await servers[0].videoStats.getRetentionStats({ videoId: liveVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await servers[0].videoStats.getRetentionStats({ videoId })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,254 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
WatchedWordsCommand,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeGetRequest,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
|
||||
describe('Test watched words API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
let userToken: string
|
||||
let userToken2: string
|
||||
let moderatorToken: string
|
||||
|
||||
let command: WatchedWordsCommand
|
||||
|
||||
let accountListId: number
|
||||
let serverListId: number
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultAccountAvatar([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
userToken2 = await server.users.generateUserAndToken('user2')
|
||||
moderatorToken = await server.users.generateUserAndToken('moderator', UserRole.MODERATOR)
|
||||
|
||||
command = server.watchedWordsLists
|
||||
|
||||
{
|
||||
const { watchedWordsList } = await command.createList({
|
||||
accountName: 'user1',
|
||||
token: userToken,
|
||||
listName: 'default',
|
||||
words: [ 'word1' ]
|
||||
})
|
||||
accountListId = watchedWordsList.id
|
||||
}
|
||||
|
||||
{
|
||||
const { watchedWordsList } = await command.createList({
|
||||
listName: 'default',
|
||||
words: [ 'word1' ]
|
||||
})
|
||||
serverListId = watchedWordsList.id
|
||||
}
|
||||
})
|
||||
|
||||
describe('Account & server watched words', function () {
|
||||
|
||||
describe('When listing watched words', function () {
|
||||
const paths = [
|
||||
'/api/v1/watched-words/accounts/user1/lists',
|
||||
'/api/v1/watched-words/server/lists'
|
||||
]
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
for (const path of paths) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with the wrong token', async function () {
|
||||
for (const path of paths) {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: userToken2,
|
||||
path,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with a bad start/count pagination or incorrect sort', async function () {
|
||||
for (const path of paths) {
|
||||
await checkBadStartPagination(server.url, path, userToken)
|
||||
await checkBadCountPagination(server.url, path, userToken)
|
||||
await checkBadSortPagination(server.url, path, userToken)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding/updating watched words', function () {
|
||||
const baseParams = () => ([
|
||||
{
|
||||
token: userToken,
|
||||
accountName: 'user1',
|
||||
listName: 'list',
|
||||
words: [ 'word1' ],
|
||||
listId: accountListId
|
||||
},
|
||||
{
|
||||
token: moderatorToken,
|
||||
listName: 'list',
|
||||
words: [ 'word1' ],
|
||||
listId: serverListId
|
||||
}
|
||||
])
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.createList({ ...baseParam, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await command.updateList({ ...baseParam, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with the wrong token', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.createList({ ...baseParam, token: userToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await command.updateList({ ...baseParam, token: userToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid listName', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.createList({ ...baseParam, listName: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
for (const listName of [ '', 'a'.repeat(500) ]) {
|
||||
await command.createList({ ...baseParam, listName, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.updateList({ ...baseParam, listName, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with invalid words', async function () {
|
||||
const bigArray: string[] = []
|
||||
for (let i = 0; i < 550; i++) {
|
||||
bigArray.push(`word${i}`)
|
||||
}
|
||||
|
||||
const toTest = [
|
||||
[],
|
||||
bigArray,
|
||||
[ 'a'.repeat(102) ],
|
||||
[ '' ],
|
||||
[ '', 'word' ]
|
||||
]
|
||||
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.createList({ ...baseParam, words: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
||||
for (const words of toTest) {
|
||||
await command.createList({ ...baseParam, words, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.updateList({ ...baseParam, words, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.createList(baseParam)
|
||||
await command.updateList({ ...baseParam, listName: 'updated-list' })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed to update a list with the same name', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.updateList({ ...baseParam, listName: 'updated-list' })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail to add a list with an already existing name', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.createList({ ...baseParam, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
await command.updateList({ ...baseParam, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting watched words', function () {
|
||||
const baseParams = () => ([
|
||||
{
|
||||
token: userToken,
|
||||
accountName: 'user1',
|
||||
listId: accountListId
|
||||
},
|
||||
{
|
||||
token: moderatorToken,
|
||||
listId: serverListId
|
||||
}
|
||||
])
|
||||
|
||||
it('Should fail with an unauthenticated user', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.deleteList({ ...baseParam, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with the wrong token', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.deleteList({ ...baseParam, token: userToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
for (const baseParam of baseParams()) {
|
||||
await command.deleteList(baseParam)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Account specific watched words', function () {
|
||||
|
||||
describe('When listing watched words', function () {
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await command.listWordsLists({ accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When adding/updating watched words', function () {
|
||||
const baseParams = () => ({
|
||||
token: userToken,
|
||||
accountName: 'user1',
|
||||
listName: 'list',
|
||||
words: [ 'word1' ]
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await command.createList({ ...baseParams(), accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting watched words', function () {
|
||||
const baseParams = () => ({
|
||||
listId: accountListId,
|
||||
token: userToken,
|
||||
accountName: 'user1'
|
||||
})
|
||||
|
||||
it('Should fail with an unknown account', async function () {
|
||||
await command.deleteList({ ...baseParams(), accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import './live-constraints.js'
|
||||
import './live-fast-restream.js'
|
||||
import './live-socket-messages.js'
|
||||
import './live-privacy-update.js'
|
||||
import './live-permanent.js'
|
||||
import './live-rtmps.js'
|
||||
import './live-save-replay.js'
|
||||
import './live.js'
|
||||
@@ -0,0 +1,235 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { LiveVideoError, UserVideoQuota, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests, createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs,
|
||||
waitUntilLiveReplacedByReplayOnAllServers,
|
||||
waitUntilLiveWaitingOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
import { checkLiveCleanup } from '../../shared/live.js'
|
||||
|
||||
describe('Test live constraints', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let userId: number
|
||||
let userAccessToken: string
|
||||
let userChannelId: number
|
||||
|
||||
async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) {
|
||||
const { replay, permanent } = options
|
||||
|
||||
const liveAttributes = {
|
||||
name: 'user live',
|
||||
channelId: userChannelId,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay: replay,
|
||||
replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
|
||||
permanentLive: permanent
|
||||
}
|
||||
|
||||
const { uuid } = await servers[0].live.create({ token: userAccessToken, fields: liveAttributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
async function checkSaveReplay (videoId: string, resolutions = [ 720 ]) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoId })
|
||||
expect(video.isLive).to.be.false
|
||||
expect(video.duration).to.be.greaterThan(0)
|
||||
}
|
||||
|
||||
await checkLiveCleanup({ server: servers[0], permanent: false, videoUUID: videoId, savedResolutions: resolutions })
|
||||
}
|
||||
|
||||
function updateQuota (options: { total: number, daily: number }) {
|
||||
return servers[0].users.update({
|
||||
userId,
|
||||
videoQuota: options.total,
|
||||
videoQuotaDaily: options.daily
|
||||
})
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
await servers[0].config.enableLive({ allowReplay: true, transcoding: false })
|
||||
|
||||
{
|
||||
const res = await servers[0].users.generate('user1')
|
||||
userId = res.userId
|
||||
userChannelId = res.userChannelId
|
||||
userAccessToken = res.token
|
||||
|
||||
await updateQuota({ total: 1, daily: -1 })
|
||||
}
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
it('Should not have size limit if save replay is disabled', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: false, permanent: false })
|
||||
await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false })
|
||||
})
|
||||
|
||||
it('Should have size limit depending on user global quota if save replay is enabled on non permanent live', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
// Wait for user quota memoize cache invalidation
|
||||
await wait(5000)
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
|
||||
await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkSaveReplay(userVideoLiveoId)
|
||||
|
||||
const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId })
|
||||
expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED)
|
||||
})
|
||||
|
||||
it('Should have size limit depending on user global quota if save replay is enabled on a permanent live', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
// Wait for user quota memoize cache invalidation
|
||||
await wait(5000)
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: true })
|
||||
await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
|
||||
|
||||
await waitJobs(servers)
|
||||
await waitUntilLiveWaitingOnAllServers(servers, userVideoLiveoId)
|
||||
|
||||
const session = await servers[0].live.findLatestSession({ videoId: userVideoLiveoId })
|
||||
expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED)
|
||||
})
|
||||
|
||||
it('Should have size limit depending on user daily quota if save replay is enabled', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
// Wait for user quota memoize cache invalidation
|
||||
await wait(5000)
|
||||
|
||||
await updateQuota({ total: -1, daily: 1 })
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
|
||||
await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkSaveReplay(userVideoLiveoId)
|
||||
|
||||
const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId })
|
||||
expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED)
|
||||
})
|
||||
|
||||
it('Should succeed without quota limit', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
// Wait for user quota memoize cache invalidation
|
||||
await wait(5000)
|
||||
|
||||
await updateQuota({ total: 10 * 1000 * 1000, daily: -1 })
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
|
||||
await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false })
|
||||
})
|
||||
|
||||
it('Should have the same quota in admin and as a user', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ token: userAccessToken, videoId: userVideoLiveoId })
|
||||
|
||||
await servers[0].live.waitUntilPublished({ videoId: userVideoLiveoId })
|
||||
// Wait previous live cleanups
|
||||
await wait(3000)
|
||||
|
||||
const baseQuota = await servers[0].users.getMyQuotaUsed({ token: userAccessToken })
|
||||
|
||||
let quotaUser: UserVideoQuota
|
||||
|
||||
do {
|
||||
await wait(500)
|
||||
|
||||
quotaUser = await servers[0].users.getMyQuotaUsed({ token: userAccessToken })
|
||||
} while (quotaUser.videoQuotaUsed <= baseQuota.videoQuotaUsed)
|
||||
|
||||
const { data } = await servers[0].users.list()
|
||||
const quotaAdmin = data.find(u => u.username === 'user1')
|
||||
|
||||
expect(quotaUser.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed)
|
||||
expect(quotaUser.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily)
|
||||
|
||||
expect(quotaAdmin.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed)
|
||||
expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily)
|
||||
|
||||
expect(quotaUser.videoQuotaUsed).to.be.above(10)
|
||||
expect(quotaUser.videoQuotaUsedDaily).to.be.above(10)
|
||||
expect(quotaAdmin.videoQuotaUsed).to.be.above(10)
|
||||
expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(10)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have max duration limit', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
maxDuration: 15,
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
resolutions: {
|
||||
'144p': true,
|
||||
'240p': true,
|
||||
'360p': false,
|
||||
'480p': false,
|
||||
'720p': true,
|
||||
'1080p': false,
|
||||
'1440p': false,
|
||||
'2160p': false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
|
||||
await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkSaveReplay(userVideoLiveoId, [ 720, 240, 144 ])
|
||||
|
||||
const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId })
|
||||
expect(session.error).to.equal(LiveVideoError.DURATION_EXCEEDED)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Fast restream in live', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) {
|
||||
const attributes: LiveVideoCreate = {
|
||||
channelId: server.store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'my super live',
|
||||
saveReplay: options.replay,
|
||||
replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
|
||||
permanentLive: options.permanent
|
||||
}
|
||||
|
||||
const { uuid } = await server.live.create({ fields: attributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
async function fastRestreamWrapper ({ replay }: { replay: boolean }) {
|
||||
const liveVideoUUID = await createLiveWrapper({ permanent: true, replay })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const rtmpOptions = {
|
||||
videoId: liveVideoUUID,
|
||||
copyCodecs: true,
|
||||
fixtureName: 'video_short.mp4'
|
||||
}
|
||||
|
||||
// Streaming session #1
|
||||
let ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions)
|
||||
await server.live.waitUntilPublished({ videoId: liveVideoUUID })
|
||||
|
||||
const video = await server.videos.get({ id: liveVideoUUID })
|
||||
const session1PlaylistId = video.streamingPlaylists[0].id
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await server.live.waitUntilWaiting({ videoId: liveVideoUUID })
|
||||
|
||||
// Streaming session #2
|
||||
ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions)
|
||||
|
||||
let hasNewPlaylist = false
|
||||
do {
|
||||
const video = await server.videos.get({ id: liveVideoUUID })
|
||||
hasNewPlaylist = video.streamingPlaylists.length === 1 && video.streamingPlaylists[0].id !== session1PlaylistId
|
||||
|
||||
await wait(100)
|
||||
} while (!hasNewPlaylist)
|
||||
|
||||
await server.live.waitUntilSegmentGeneration({
|
||||
server,
|
||||
videoUUID: liveVideoUUID,
|
||||
segment: 1,
|
||||
playlistNumber: 0
|
||||
})
|
||||
|
||||
return { ffmpegCommand, liveVideoUUID }
|
||||
}
|
||||
|
||||
async function ensureLastLiveWorks (liveId: string) {
|
||||
// Equivalent to PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const video = await server.videos.get({ id: liveId })
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
try {
|
||||
await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 })
|
||||
await server.streamingPlaylists.get({ url: video.streamingPlaylists[0].playlistUrl })
|
||||
await server.streamingPlaylists.getSegmentSha256({ url: video.streamingPlaylists[0].segmentsSha256Url })
|
||||
} catch (err) {
|
||||
// FIXME: try to debug error in CI "Unexpected end of JSON input"
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest (replay: boolean) {
|
||||
const { ffmpegCommand, liveVideoUUID } = await fastRestreamWrapper({ replay })
|
||||
|
||||
// TODO: remove, we try to debug a test timeout failure here
|
||||
console.log('Ensuring last live works')
|
||||
|
||||
await ensureLastLiveWorks(liveVideoUUID)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await server.live.waitUntilWaiting({ videoId: liveVideoUUID })
|
||||
|
||||
// Wait for replays
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { total, data: sessions } = await server.live.listSessions({ videoId: liveVideoUUID })
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(sessions).to.have.lengthOf(2)
|
||||
|
||||
for (const session of sessions) {
|
||||
expect(session.error).to.be.null
|
||||
|
||||
if (replay) {
|
||||
expect(session.replayVideo).to.exist
|
||||
|
||||
await server.videos.get({ id: session.replayVideo.uuid })
|
||||
} else {
|
||||
expect(session.replayVideo).to.not.exist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const env = { PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY: '10000' }
|
||||
server = await createSingleServer(1, {}, { env })
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableMinimumTranscoding({ webVideo: false, hls: true })
|
||||
await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
|
||||
})
|
||||
|
||||
it('Should correctly fast restream in a permanent live with and without save replay', async function () {
|
||||
this.timeout(480000)
|
||||
|
||||
// A test can take a long time, so prefer to run them in parallel
|
||||
await Promise.all([
|
||||
runTest(true),
|
||||
runTest(false)
|
||||
])
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,205 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { LiveVideoCreate, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models'
|
||||
import {
|
||||
ConfigCommand,
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkLiveCleanup } from '@tests/shared/live.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Permanent live', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let videoUUID: string
|
||||
|
||||
async function createLiveWrapper (permanentLive: boolean) {
|
||||
const attributes: LiveVideoCreate = {
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'my super live',
|
||||
saveReplay: false,
|
||||
permanentLive
|
||||
}
|
||||
|
||||
const { uuid } = await servers[0].live.create({ fields: attributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
async function checkVideoState (videoId: string, state: VideoStateType) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoId })
|
||||
expect(video.state.id).to.equal(state)
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
await servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
maxDuration: -1,
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should create a non permanent live and update it to be a permanent live', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
const videoUUID = await createLiveWrapper(false)
|
||||
|
||||
{
|
||||
const live = await servers[0].live.get({ videoId: videoUUID })
|
||||
expect(live.permanentLive).to.be.false
|
||||
}
|
||||
|
||||
await servers[0].live.update({ videoId: videoUUID, fields: { permanentLive: true } })
|
||||
|
||||
{
|
||||
const live = await servers[0].live.get({ videoId: videoUUID })
|
||||
expect(live.permanentLive).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('Should create a permanent live', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
videoUUID = await createLiveWrapper(true)
|
||||
|
||||
const live = await servers[0].live.get({ videoId: videoUUID })
|
||||
expect(live.permanentLive).to.be.true
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should stream into this permanent live', async function () {
|
||||
this.timeout(240_000)
|
||||
|
||||
const beforePublication = new Date()
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
|
||||
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilPublished({ videoId: videoUUID })
|
||||
}
|
||||
|
||||
await checkVideoState(videoUUID, VideoState.PUBLISHED)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
expect(new Date(video.publishedAt)).greaterThan(beforePublication)
|
||||
}
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await servers[0].live.waitUntilWaiting({ videoId: videoUUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have cleaned up this live', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const videoDetails = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID })
|
||||
})
|
||||
|
||||
it('Should have set this live to waiting for live state', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await checkVideoState(videoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
})
|
||||
|
||||
it('Should be able to stream again in the permanent live', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
maxDuration: -1,
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
|
||||
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilPublished({ videoId: videoUUID })
|
||||
}
|
||||
|
||||
await checkVideoState(videoUUID, VideoState.PUBLISHED)
|
||||
|
||||
const count = await servers[0].live.countPlaylists({ videoUUID })
|
||||
// master playlist and 720p playlist
|
||||
expect(count).to.equal(2)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have appropriate sessions', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].live.waitUntilWaiting({ videoId: videoUUID })
|
||||
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: videoUUID })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
for (const session of data) {
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
}
|
||||
})
|
||||
|
||||
it('Should remove the live and have cleaned up the directory', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].videos.remove({ id: videoUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests, createSingleServer, makeRawRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs,
|
||||
waitUntilLivePublishedOnAllServers,
|
||||
waitUntilLiveReplacedByReplayOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function testVideoFiles (server: PeerTubeServer, uuid: string) {
|
||||
const video = await server.videos.getWithToken({ id: uuid })
|
||||
|
||||
const expectedStatus = HttpStatusCode.OK_200
|
||||
|
||||
await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, token: server.accessToken, expectedStatus })
|
||||
await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, token: server.accessToken, expectedStatus })
|
||||
}
|
||||
|
||||
describe('Live privacy update', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableMinimumTranscoding()
|
||||
await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
|
||||
})
|
||||
|
||||
describe('Normal live', function () {
|
||||
let uuid: string
|
||||
|
||||
it('Should create a public live with private replay', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const fields: LiveVideoCreate = {
|
||||
name: 'live',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
permanentLive: false,
|
||||
replaySettings: { privacy: VideoPrivacy.PRIVATE },
|
||||
saveReplay: true,
|
||||
channelId: server.store.channel.id
|
||||
}
|
||||
|
||||
const video = await server.live.create({ fields })
|
||||
uuid = video.uuid
|
||||
|
||||
const ffmpegCommand = await server.live.sendRTMPStreamInVideo({ videoId: uuid })
|
||||
await waitUntilLivePublishedOnAllServers([ server ], uuid)
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers([ server ], uuid)
|
||||
await waitJobs([ server ])
|
||||
|
||||
await testVideoFiles(server, uuid)
|
||||
})
|
||||
|
||||
it('Should update the replay to public and re-update it to private', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
await waitJobs([ server ])
|
||||
await testVideoFiles(server, uuid)
|
||||
|
||||
await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PRIVATE } })
|
||||
await waitJobs([ server ])
|
||||
await testVideoFiles(server, uuid)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
sendRTMPStream,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
testFfmpegStreamError,
|
||||
waitUntilLivePublishedOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test live RTMPS', function () {
|
||||
let server: PeerTubeServer
|
||||
let rtmpUrl: string
|
||||
let rtmpsUrl: string
|
||||
|
||||
async function createLiveWrapper () {
|
||||
const liveAttributes = {
|
||||
name: 'live',
|
||||
channelId: server.store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay: false
|
||||
}
|
||||
|
||||
const { uuid } = await server.live.create({ fields: liveAttributes })
|
||||
|
||||
const live = await server.live.get({ videoId: uuid })
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
return Object.assign(video, live)
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableMinimumTranscoding()
|
||||
await server.config.enableLive({ allowReplay: true, transcoding: false })
|
||||
|
||||
rtmpUrl = 'rtmp://' + server.hostname + ':' + server.rtmpPort + '/live'
|
||||
rtmpsUrl = 'rtmps://' + server.hostname + ':' + server.rtmpsPort + '/live'
|
||||
})
|
||||
|
||||
it('Should enable RTMPS endpoint only', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await server.kill()
|
||||
await server.run({
|
||||
live: {
|
||||
rtmp: {
|
||||
enabled: false
|
||||
},
|
||||
rtmps: {
|
||||
enabled: true,
|
||||
port: server.rtmpsPort,
|
||||
key_file: buildAbsoluteFixturePath('rtmps.key'),
|
||||
cert_file: buildAbsoluteFixturePath('rtmps.cert')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
const liveVideo = await createLiveWrapper()
|
||||
|
||||
expect(liveVideo.rtmpUrl).to.not.exist
|
||||
expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl)
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey })
|
||||
await testFfmpegStreamError(command, true)
|
||||
}
|
||||
|
||||
{
|
||||
const liveVideo = await createLiveWrapper()
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey })
|
||||
await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
|
||||
await stopFfmpeg(command)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should enable both RTMP and RTMPS', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await server.kill()
|
||||
await server.run({
|
||||
live: {
|
||||
rtmp: {
|
||||
enabled: true,
|
||||
port: server.rtmpPort
|
||||
},
|
||||
rtmps: {
|
||||
enabled: true,
|
||||
port: server.rtmpsPort,
|
||||
key_file: buildAbsoluteFixturePath('rtmps.key'),
|
||||
cert_file: buildAbsoluteFixturePath('rtmps.cert')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
const liveVideo = await createLiveWrapper()
|
||||
|
||||
expect(liveVideo.rtmpUrl).to.equal(rtmpUrl)
|
||||
expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl)
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey })
|
||||
await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
|
||||
await stopFfmpeg(command)
|
||||
}
|
||||
|
||||
{
|
||||
const liveVideo = await createLiveWrapper()
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey })
|
||||
await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
|
||||
await stopFfmpeg(command)
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,606 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
LiveVideoCreate,
|
||||
LiveVideoError,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideoState,
|
||||
VideoStateType
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
ConfigCommand,
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
findExternalSavedVideo,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
testFfmpegStreamError,
|
||||
waitJobs,
|
||||
waitUntilLivePublishedOnAllServers,
|
||||
waitUntilLiveReplacedByReplayOnAllServers,
|
||||
waitUntilLiveWaitingOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkLiveCleanup } from '@tests/shared/live.js'
|
||||
import { expect } from 'chai'
|
||||
import { FfmpegCommand } from 'fluent-ffmpeg'
|
||||
|
||||
describe('Save replay setting', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let liveVideoUUID: string
|
||||
let ffmpegCommand: FfmpegCommand
|
||||
|
||||
async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
|
||||
if (liveVideoUUID) {
|
||||
try {
|
||||
await servers[0].videos.remove({ id: liveVideoUUID })
|
||||
await waitJobs(servers)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const attributes: LiveVideoCreate = {
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'live'.repeat(30),
|
||||
saveReplay: options.replay,
|
||||
replaySettings: options.replaySettings,
|
||||
permanentLive: options.permanent
|
||||
}
|
||||
|
||||
const { uuid } = await servers[0].live.create({ fields: attributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
|
||||
liveVideoUUID = await createLiveWrapper(options)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
|
||||
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkVideosExist(liveVideoUUID, null, HttpStatusCode.OK_200)
|
||||
|
||||
return { ffmpegCommand, liveDetails }
|
||||
}
|
||||
|
||||
async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
|
||||
const { ffmpegCommand, liveDetails } = await publishLive(options)
|
||||
|
||||
await Promise.all([
|
||||
servers[0].videos.remove({ id: liveVideoUUID }),
|
||||
testFfmpegStreamError(ffmpegCommand, true)
|
||||
])
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
return { liveDetails }
|
||||
}
|
||||
|
||||
async function publishLiveAndBlacklist (options: {
|
||||
permanent: boolean
|
||||
replay: boolean
|
||||
replaySettings?: { privacy: VideoPrivacyType }
|
||||
}) {
|
||||
const { ffmpegCommand, liveDetails } = await publishLive(options)
|
||||
|
||||
await Promise.all([
|
||||
servers[0].blacklist.add({ videoId: liveVideoUUID, reason: 'bad live', unfederate: true }),
|
||||
testFfmpegStreamError(ffmpegCommand, true)
|
||||
])
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
return { liveDetails }
|
||||
}
|
||||
|
||||
async function checkVideosExist (videoId: string, videosLength: number, expectedStatus?: HttpStatusCodeType) {
|
||||
for (const server of servers) {
|
||||
const { data, total } = await server.videos.list()
|
||||
|
||||
if (videosLength !== null) {
|
||||
expect(data).to.have.lengthOf(videosLength)
|
||||
expect(total).to.equal(videosLength)
|
||||
}
|
||||
|
||||
if (expectedStatus) {
|
||||
await server.videos.get({ id: videoId, expectedStatus })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVideoState (videoId: string, state: VideoStateType) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoId })
|
||||
expect(video.state.id).to.equal(state)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacyType) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoId })
|
||||
expect(video.privacy.id).to.equal(privacy)
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
await servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
maxDuration: -1,
|
||||
transcoding: {
|
||||
enabled: false,
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('With save replay disabled', function () {
|
||||
let sessionStartDateMin: Date
|
||||
let sessionStartDateMax: Date
|
||||
let sessionEndDateMin: Date
|
||||
|
||||
it('Should correctly create and federate the "waiting for stream" live', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: false, replay: false })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
|
||||
sessionStartDateMin = new Date()
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
sessionStartDateMax = new Date()
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
})
|
||||
|
||||
it('Should correctly delete the video files after the stream ended', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
sessionEndDateMin = new Date()
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilEnded({ videoId: liveVideoUUID })
|
||||
}
|
||||
await waitJobs(servers)
|
||||
|
||||
// Live still exist, but cannot be played anymore
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED)
|
||||
|
||||
// No resolutions saved since we did not save replay
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
|
||||
it('Should have appropriate ended session', async function () {
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const session = data[0]
|
||||
|
||||
const startDate = new Date(session.startDate)
|
||||
expect(startDate).to.be.above(sessionStartDateMin)
|
||||
expect(startDate).to.be.below(sessionStartDateMax)
|
||||
|
||||
expect(session.endDate).to.exist
|
||||
expect(new Date(session.endDate)).to.be.above(sessionEndDateMin)
|
||||
|
||||
expect(session.saveReplay).to.be.false
|
||||
expect(session.error).to.not.exist
|
||||
expect(session.replayVideo).to.not.exist
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on blacklist and delete the live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await publishLiveAndBlacklist({ permanent: false, replay: false })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0)
|
||||
|
||||
await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
|
||||
it('Should have blacklisted session error', async function () {
|
||||
const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.error).to.equal(LiveVideoError.BLACKLISTED)
|
||||
expect(session.replayVideo).to.not.exist
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on delete and delete the video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await publishLiveAndDelete({ permanent: false, replay: false })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.NOT_FOUND_404)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('With save replay enabled on non permanent live', function () {
|
||||
|
||||
it('Should correctly create and federate the "waiting for stream" live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
|
||||
expect(session.endDate).to.not.exist
|
||||
expect(session.endingProcessed).to.be.false
|
||||
expect(session.saveReplay).to.be.true
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
// Live has been transcoded
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED)
|
||||
})
|
||||
|
||||
it('Should find the replay live session', async function () {
|
||||
const session = await servers[0].live.getReplaySession({ videoId: liveVideoUUID })
|
||||
|
||||
expect(session).to.exist
|
||||
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
expect(session.saveReplay).to.be.true
|
||||
expect(session.endingProcessed).to.be.true
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
expect(session.replayVideo.shortUUID).to.exist
|
||||
expect(session.replayVideo.uuid).to.equal(liveVideoUUID)
|
||||
})
|
||||
|
||||
it('Should update the saved live and correctly federate the updated attributes', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } })
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: liveVideoUUID })
|
||||
expect(video.name).to.equal('video updated')
|
||||
expect(video.isLive).to.be.false
|
||||
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have cleaned up the live files', async function () {
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] })
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0)
|
||||
|
||||
await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] })
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on delete and delete the video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.NOT_FOUND_404)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('With save replay enabled on permanent live', function () {
|
||||
let lastReplayUUID: string
|
||||
|
||||
describe('With a first live and its replay', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await servers[0].kill()
|
||||
await servers[0].run({
|
||||
federation: {
|
||||
videos: {
|
||||
federate_unlisted: false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should correctly create and federate the "waiting for stream" live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have saved the live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(video).to.exist
|
||||
|
||||
await servers[0].videos.get({ id: video.uuid })
|
||||
await servers[1].videos.get({ id: video.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
|
||||
lastReplayUUID = video.uuid
|
||||
})
|
||||
|
||||
it('Should federate the replay after updating its privacy to public', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await servers[0].videos.update({ id: lastReplayUUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[1].videos.get({ id: lastReplayUUID, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should have appropriate ended session and replay live session', async function () {
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const sessionFromLive = data[0]
|
||||
const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
|
||||
|
||||
for (const session of [ sessionFromLive, sessionFromReplay ]) {
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
expect(session.replayVideo.shortUUID).to.exist
|
||||
expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have the first live replay with correct settings', async function () {
|
||||
await checkVideosExist(lastReplayUUID, 1, HttpStatusCode.OK_200)
|
||||
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With a second live and its replay', function () {
|
||||
|
||||
it('Should update the replay settings', async function () {
|
||||
await servers[0].live.update({ videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
|
||||
await waitJobs(servers)
|
||||
|
||||
const live = await servers[0].live.get({ videoId: liveVideoUUID })
|
||||
|
||||
expect(live.saveReplay).to.be.true
|
||||
expect(live.replaySettings).to.exist
|
||||
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
|
||||
})
|
||||
|
||||
it('Should correctly have updated the live and federated it when streaming in the live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 2, HttpStatusCode.OK_200)
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should correctly have saved the live and federated it after the streaming', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(video).to.exist
|
||||
|
||||
for (const server of servers) {
|
||||
await server.videos.get({ id: video.uuid })
|
||||
}
|
||||
|
||||
lastReplayUUID = video.uuid
|
||||
})
|
||||
|
||||
it('Should have appropriate ended session and replay live session', async function () {
|
||||
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
const sessionFromLive = data[1]
|
||||
const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
|
||||
|
||||
for (const session of [ sessionFromLive, sessionFromReplay ]) {
|
||||
expect(session.startDate).to.exist
|
||||
expect(session.endDate).to.exist
|
||||
|
||||
expect(session.replaySettings).to.exist
|
||||
expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
|
||||
expect(session.error).to.not.exist
|
||||
|
||||
expect(session.replayVideo).to.exist
|
||||
expect(session.replayVideo.id).to.exist
|
||||
expect(session.replayVideo.shortUUID).to.exist
|
||||
expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have the first live replay with correct settings', async function () {
|
||||
await checkVideosExist(lastReplayUUID, 2, HttpStatusCode.OK_200)
|
||||
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
|
||||
})
|
||||
|
||||
it('Should have cleaned up the live files', async function () {
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await servers[0].videos.remove({ id: lastReplayUUID })
|
||||
const { liveDetails } = await publishLiveAndBlacklist({
|
||||
permanent: true,
|
||||
replay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC }
|
||||
})
|
||||
|
||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(replay).to.exist
|
||||
|
||||
for (const videoId of [ liveVideoUUID, replay.uuid ]) {
|
||||
await checkVideosExist(videoId, 1)
|
||||
|
||||
await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
|
||||
it('Should correctly terminate the stream on delete and not save the video', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { liveDetails } = await publishLiveAndDelete({
|
||||
permanent: true,
|
||||
replay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC }
|
||||
})
|
||||
|
||||
const replay = await findExternalSavedVideo(servers[0], liveDetails)
|
||||
expect(replay).to.not.exist
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.NOT_FOUND_404)
|
||||
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,181 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { LiveVideoEventPayload, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs,
|
||||
waitUntilLivePublishedOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test live socket messages', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
await servers[0].config.enableLive({ allowReplay: true, transcoding: false })
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
describe('Live socket messages', function () {
|
||||
|
||||
async function createLiveWrapper () {
|
||||
const liveAttributes = {
|
||||
name: 'live video',
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
|
||||
const { uuid } = await servers[0].live.create({ fields: liveAttributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
it('Should correctly send a message when the live starts and ends', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const localStateChanges: VideoStateType[] = []
|
||||
const remoteStateChanges: VideoStateType[] = []
|
||||
|
||||
const liveVideoUUID = await createLiveWrapper()
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
|
||||
|
||||
const localSocket = servers[0].socketIO.getLiveNotificationSocket()
|
||||
localSocket.on('state-change', data => localStateChanges.push(data.state))
|
||||
localSocket.emit('subscribe', { videoId })
|
||||
}
|
||||
|
||||
{
|
||||
const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID })
|
||||
|
||||
const remoteSocket = servers[1].socketIO.getLiveNotificationSocket()
|
||||
remoteSocket.on('state-change', data => remoteStateChanges.push(data.state))
|
||||
remoteSocket.emit('subscribe', { videoId })
|
||||
}
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
// Ensure remote server doesn't send multiple times the state change event to viewers
|
||||
await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'my new live name' } })
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
|
||||
expect(stateChanges).to.have.lengthOf(1)
|
||||
expect(stateChanges[0]).to.equal(VideoState.PUBLISHED)
|
||||
}
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilEnded({ videoId: liveVideoUUID })
|
||||
}
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
|
||||
expect(stateChanges).to.have.length.at.least(2)
|
||||
expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.LIVE_ENDED)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly send views change notification', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
let localLastVideoViews = 0
|
||||
let remoteLastVideoViews = 0
|
||||
|
||||
const liveVideoUUID = await createLiveWrapper()
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
|
||||
|
||||
const localSocket = servers[0].socketIO.getLiveNotificationSocket()
|
||||
localSocket.on('views-change', (data: LiveVideoEventPayload) => { localLastVideoViews = data.viewers })
|
||||
localSocket.emit('subscribe', { videoId })
|
||||
}
|
||||
|
||||
{
|
||||
const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID })
|
||||
|
||||
const remoteSocket = servers[1].socketIO.getLiveNotificationSocket()
|
||||
remoteSocket.on('views-change', (data: LiveVideoEventPayload) => { remoteLastVideoViews = data.viewers })
|
||||
remoteSocket.emit('subscribe', { videoId })
|
||||
}
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
expect(localLastVideoViews).to.equal(0)
|
||||
expect(remoteLastVideoViews).to.equal(0)
|
||||
|
||||
await servers[0].views.simulateView({ id: liveVideoUUID })
|
||||
await servers[1].views.simulateView({ id: liveVideoUUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
expect(localLastVideoViews).to.equal(2)
|
||||
expect(remoteLastVideoViews).to.equal(2)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should not receive a notification after unsubscribe', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const stateChanges: VideoStateType[] = []
|
||||
|
||||
const liveVideoUUID = await createLiveWrapper()
|
||||
await waitJobs(servers)
|
||||
|
||||
const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
|
||||
|
||||
const socket = servers[0].socketIO.getLiveNotificationSocket()
|
||||
socket.on('state-change', data => stateChanges.push(data.state))
|
||||
socket.emit('subscribe', { videoId })
|
||||
|
||||
const command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
|
||||
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
// Notifier waits before sending a notification
|
||||
await wait(10000)
|
||||
|
||||
expect(stateChanges).to.have.lengthOf(1)
|
||||
socket.emit('unsubscribe', { videoId })
|
||||
|
||||
await stopFfmpeg(command)
|
||||
await waitJobs(servers)
|
||||
|
||||
expect(stateChanges).to.have.lengthOf(1)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,782 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { basename, join } from 'path'
|
||||
import { getAllFiles, wait } from '@peertube/peertube-core-utils'
|
||||
import { ffprobePromise, getVideoStream } from '@peertube/peertube-ffmpeg'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
VideoCommentPolicy,
|
||||
VideoDetails,
|
||||
VideoPrivacy,
|
||||
VideoState,
|
||||
VideoStreamingPlaylistType
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
killallServers,
|
||||
LiveCommand,
|
||||
makeGetRequest,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
sendRTMPStream,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
testFfmpegStreamError,
|
||||
waitJobs,
|
||||
waitUntilLivePublishedOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
|
||||
import { testLiveVideoResolutions } from '@tests/shared/live.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
|
||||
describe('Test live', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let commands: LiveCommand[]
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
await servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
maxUserLives: -1,
|
||||
latencySetting: {
|
||||
enabled: true
|
||||
},
|
||||
transcoding: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
commands = servers.map(s => s.live)
|
||||
})
|
||||
|
||||
describe('Live creation, update and delete', function () {
|
||||
let liveVideoUUID: string
|
||||
|
||||
it('Should create a live with the appropriate parameters', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
const attributes: LiveVideoCreate = {
|
||||
category: 1,
|
||||
licence: 2,
|
||||
language: 'fr',
|
||||
description: 'super live description',
|
||||
support: 'support field',
|
||||
channelId: servers[0].store.channel.id,
|
||||
nsfw: false,
|
||||
waitTranscoding: false,
|
||||
name: 'my super live',
|
||||
tags: [ 'tag1', 'tag2' ],
|
||||
commentsPolicy: VideoCommentPolicy.DISABLED,
|
||||
downloadEnabled: false,
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
previewfile: 'video_short1-preview.webm.jpg',
|
||||
thumbnailfile: 'video_short1.webm.jpg'
|
||||
}
|
||||
|
||||
const live = await commands[0].create({ fields: attributes })
|
||||
liveVideoUUID = live.uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: liveVideoUUID })
|
||||
|
||||
expect(video.category.id).to.equal(1)
|
||||
expect(video.licence.id).to.equal(2)
|
||||
expect(video.language.id).to.equal('fr')
|
||||
expect(video.description).to.equal('super live description')
|
||||
expect(video.support).to.equal('support field')
|
||||
|
||||
expect(video.channel.name).to.equal(servers[0].store.channel.name)
|
||||
expect(video.channel.host).to.equal(servers[0].store.channel.host)
|
||||
|
||||
expect(video.isLive).to.be.true
|
||||
|
||||
expect(video.aspectRatio).to.not.exist
|
||||
|
||||
expect(video.nsfw).to.be.false
|
||||
expect(video.waitTranscoding).to.be.false
|
||||
expect(video.name).to.equal('my super live')
|
||||
expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ])
|
||||
expect(video.commentsEnabled).to.be.false
|
||||
expect(video.downloadEnabled).to.be.false
|
||||
expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
|
||||
|
||||
await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath)
|
||||
await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath)
|
||||
|
||||
const live = await server.live.get({ videoId: liveVideoUUID })
|
||||
|
||||
if (server.url === servers[0].url) {
|
||||
expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
|
||||
expect(live.streamKey).to.not.be.empty
|
||||
|
||||
expect(live.replaySettings).to.exist
|
||||
expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
|
||||
} else {
|
||||
expect(live.rtmpUrl).to.not.exist
|
||||
expect(live.streamKey).to.not.exist
|
||||
}
|
||||
|
||||
expect(live.saveReplay).to.be.true
|
||||
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have a default preview and thumbnail', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
const attributes: LiveVideoCreate = {
|
||||
name: 'default live thumbnail',
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.UNLISTED,
|
||||
nsfw: true
|
||||
}
|
||||
|
||||
const live = await commands[0].create({ fields: attributes })
|
||||
const videoId = live.uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoId })
|
||||
expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
|
||||
expect(video.nsfw).to.be.true
|
||||
|
||||
await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have the live listed since nobody streams into', async function () {
|
||||
for (const server of servers) {
|
||||
const { total, data } = await server.videos.list()
|
||||
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not be able to update a live of another server', async function () {
|
||||
await commands[1].update({ videoId: liveVideoUUID, fields: { saveReplay: false }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should update the live', async function () {
|
||||
await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Have the live updated', async function () {
|
||||
for (const server of servers) {
|
||||
const live = await server.live.get({ videoId: liveVideoUUID })
|
||||
|
||||
if (server.url === servers[0].url) {
|
||||
expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
|
||||
expect(live.streamKey).to.not.be.empty
|
||||
} else {
|
||||
expect(live.rtmpUrl).to.not.exist
|
||||
expect(live.streamKey).to.not.exist
|
||||
}
|
||||
|
||||
expect(live.saveReplay).to.be.false
|
||||
expect(live.replaySettings).to.not.exist
|
||||
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
|
||||
}
|
||||
})
|
||||
|
||||
it('Delete the live', async function () {
|
||||
await servers[0].videos.remove({ id: liveVideoUUID })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the live deleted', async function () {
|
||||
for (const server of servers) {
|
||||
await server.videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await server.live.get({ videoId: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Live filters', function () {
|
||||
let ffmpegCommand: any
|
||||
let liveVideoId: string
|
||||
let vodVideoId: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid
|
||||
|
||||
const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id }
|
||||
const live = await commands[0].create({ fields: liveOptions })
|
||||
liveVideoId = live.uuid
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should only display lives', async function () {
|
||||
const { data, total } = await servers[0].videos.list({ isLive: true })
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].name).to.equal('live')
|
||||
})
|
||||
|
||||
it('Should not display lives', async function () {
|
||||
const { data, total } = await servers[0].videos.list({ isLive: false })
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].name).to.equal('vod video')
|
||||
})
|
||||
|
||||
it('Should display my lives', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].videos.listMyVideos({ isLive: true })
|
||||
|
||||
const result = data.every(v => v.isLive)
|
||||
expect(result).to.be.true
|
||||
})
|
||||
|
||||
it('Should not display my lives', async function () {
|
||||
const { data } = await servers[0].videos.listMyVideos({ isLive: false })
|
||||
|
||||
const result = data.every(v => !v.isLive)
|
||||
expect(result).to.be.true
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await servers[0].videos.remove({ id: vodVideoId })
|
||||
await servers[0].videos.remove({ id: liveVideoId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Stream checks', function () {
|
||||
let liveVideo: LiveVideo & VideoDetails
|
||||
let rtmpUrl: string
|
||||
|
||||
before(function () {
|
||||
rtmpUrl = 'rtmp://' + servers[0].hostname + ':' + servers[0].rtmpPort + ''
|
||||
})
|
||||
|
||||
async function createLiveWrapper (token?: string, channelId?: number) {
|
||||
const { uuid } = await commands[0].create({
|
||||
token,
|
||||
fields: {
|
||||
name: 'user live',
|
||||
channelId: channelId ?? servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay: false
|
||||
}
|
||||
})
|
||||
|
||||
const live = await commands[0].get({ videoId: uuid })
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
|
||||
return Object.assign(video, live)
|
||||
}
|
||||
|
||||
it('Should not allow a stream without the appropriate path', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
liveVideo = await createLiveWrapper()
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey })
|
||||
await testFfmpegStreamError(command, true)
|
||||
})
|
||||
|
||||
it('Should not allow a stream without the appropriate stream key', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' })
|
||||
await testFfmpegStreamError(command, true)
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
|
||||
await testFfmpegStreamError(command, false)
|
||||
})
|
||||
|
||||
it('Should list this live now someone stream into it', async function () {
|
||||
for (const server of servers) {
|
||||
const { total, data } = await server.videos.list()
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const video = data[0]
|
||||
expect(video.name).to.equal('user live')
|
||||
expect(video.isLive).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not allow a stream on a live that was blacklisted', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
liveVideo = await createLiveWrapper()
|
||||
|
||||
await servers[0].blacklist.add({ videoId: liveVideo.uuid })
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
|
||||
await testFfmpegStreamError(command, true)
|
||||
})
|
||||
|
||||
it('Should not allow a stream on if the owner has been blocked', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { token, userId, userChannelId } = await servers[0].users.generate('user1')
|
||||
liveVideo = await createLiveWrapper(token, userChannelId)
|
||||
|
||||
await servers[0].users.banUser({ userId })
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
|
||||
await testFfmpegStreamError(command, true)
|
||||
})
|
||||
|
||||
it('Should not allow a stream on a live that was deleted', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
liveVideo = await createLiveWrapper()
|
||||
|
||||
await servers[0].videos.remove({ id: liveVideo.uuid })
|
||||
|
||||
const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
|
||||
await testFfmpegStreamError(command, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Live transcoding', function () {
|
||||
let liveVideoId: string
|
||||
let sqlCommandServer1: SQLCommand
|
||||
|
||||
async function createLiveWrapper (saveReplay: boolean) {
|
||||
const liveAttributes = {
|
||||
name: 'live video',
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay,
|
||||
replaySettings: saveReplay
|
||||
? { privacy: VideoPrivacy.PUBLIC }
|
||||
: undefined
|
||||
}
|
||||
|
||||
const { uuid } = await commands[0].create({ fields: liveAttributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
function updateConf (resolutions: number[]) {
|
||||
return servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: true,
|
||||
maxDuration: -1,
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
resolutions: {
|
||||
'144p': resolutions.includes(144),
|
||||
'240p': resolutions.includes(240),
|
||||
'360p': resolutions.includes(360),
|
||||
'480p': resolutions.includes(480),
|
||||
'720p': resolutions.includes(720),
|
||||
'1080p': resolutions.includes(1080),
|
||||
'2160p': resolutions.includes(2160)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
await updateConf([])
|
||||
|
||||
sqlCommandServer1 = new SQLCommand(servers[0])
|
||||
})
|
||||
|
||||
it('Should enable transcoding without additional resolutions', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
liveVideoId = await createLiveWrapper(false)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId,
|
||||
resolutions: [ 720 ],
|
||||
transcoded: true
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should transcode audio only RTMP stream', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
liveVideoId = await createLiveWrapper(false)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short_no_audio.mp4' })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should enable transcoding with some resolutions', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const resolutions = [ 240, 480 ]
|
||||
await updateConf(resolutions)
|
||||
liveVideoId = await createLiveWrapper(false)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId,
|
||||
resolutions: resolutions.concat([ 720 ]),
|
||||
transcoded: true
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should correctly set the appropriate bitrate depending on the input', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
liveVideoId = await createLiveWrapper(false)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({
|
||||
videoId: liveVideoId,
|
||||
fixtureName: 'video_short.mp4',
|
||||
copyCodecs: true
|
||||
})
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await servers[0].videos.get({ id: liveVideoId })
|
||||
|
||||
const masterPlaylist = video.streamingPlaylists[0].playlistUrl
|
||||
const probe = await ffprobePromise(masterPlaylist)
|
||||
|
||||
const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate))
|
||||
for (const bitrate of bitrates) {
|
||||
expect(bitrate).to.exist
|
||||
expect(isNaN(bitrate)).to.be.false
|
||||
expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate
|
||||
}
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should enable transcoding with some resolutions and correctly save them', async function () {
|
||||
this.timeout(500_000)
|
||||
|
||||
const resolutions = [ 240, 360, 720 ]
|
||||
|
||||
await updateConf(resolutions)
|
||||
liveVideoId = await createLiveWrapper(true)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId,
|
||||
resolutions,
|
||||
transcoded: true
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await commands[0].waitUntilEnded({ videoId: liveVideoId })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
|
||||
const maxBitrateLimits = {
|
||||
720: 6500 * 1000, // 60FPS
|
||||
360: 1250 * 1000,
|
||||
240: 700 * 1000
|
||||
}
|
||||
|
||||
const minBitrateLimits = {
|
||||
720: 4800 * 1000,
|
||||
360: 1000 * 1000,
|
||||
240: 550 * 1000
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: liveVideoId })
|
||||
|
||||
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||
expect(video.duration).to.be.greaterThan(1)
|
||||
expect(video.aspectRatio).to.equal(1.7778)
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
|
||||
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
|
||||
await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
// We should have generated random filenames
|
||||
expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8')
|
||||
expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json')
|
||||
|
||||
expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length)
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
|
||||
|
||||
expect(file).to.exist
|
||||
expect(file.size).to.be.greaterThan(1)
|
||||
|
||||
if (resolution >= 720) {
|
||||
expect(file.fps).to.be.approximately(60, 10)
|
||||
} else {
|
||||
expect(file.fps).to.be.approximately(30, 3)
|
||||
}
|
||||
|
||||
const filename = basename(file.fileUrl)
|
||||
expect(filename).to.not.contain(video.uuid)
|
||||
|
||||
const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename))
|
||||
|
||||
const probe = await ffprobePromise(segmentPath)
|
||||
const videoStream = await getVideoStream(segmentPath, probe)
|
||||
|
||||
expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height])
|
||||
expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height])
|
||||
|
||||
await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not generate an upper resolution than original file', async function () {
|
||||
this.timeout(500_000)
|
||||
|
||||
const resolutions = [ 240, 480 ]
|
||||
await updateConf(resolutions)
|
||||
|
||||
await servers[0].config.updateExistingConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
transcoding: {
|
||||
alwaysTranscodeOriginalResolution: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
liveVideoId = await createLiveWrapper(true)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId,
|
||||
resolutions,
|
||||
transcoded: true
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await commands[0].waitUntilEnded({ videoId: liveVideoId })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
|
||||
const video = await servers[0].videos.get({ id: liveVideoId })
|
||||
const hlsFiles = video.streamingPlaylists[0].files
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
expect(hlsFiles).to.have.lengthOf(resolutions.length)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
|
||||
expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions)
|
||||
})
|
||||
|
||||
it('Should only keep the original resolution if all resolutions are disabled', async function () {
|
||||
this.timeout(600_000)
|
||||
|
||||
await updateConf([])
|
||||
liveVideoId = await createLiveWrapper(true)
|
||||
|
||||
const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
await waitJobs(servers)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId,
|
||||
resolutions: [ 720 ],
|
||||
transcoded: true
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await commands[0].waitUntilEnded({ videoId: liveVideoId })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
|
||||
|
||||
const video = await servers[0].videos.get({ id: liveVideoId })
|
||||
const hlsFiles = video.streamingPlaylists[0].files
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
expect(hlsFiles).to.have.lengthOf(1)
|
||||
|
||||
expect(hlsFiles[0].resolution.id).to.equal(720)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await sqlCommandServer1.cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
describe('After a server restart', function () {
|
||||
let liveVideoId: string
|
||||
let liveVideoReplayId: string
|
||||
let permanentLiveVideoReplayId: string
|
||||
|
||||
let permanentLiveReplayName: string
|
||||
|
||||
let beforeServerRestart: Date
|
||||
|
||||
async function createLiveWrapper (options: { saveReplay: boolean, permanent: boolean }) {
|
||||
const liveAttributes: LiveVideoCreate = {
|
||||
name: 'live video',
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
saveReplay: options.saveReplay,
|
||||
permanentLive: options.permanent
|
||||
}
|
||||
|
||||
const { uuid } = await commands[0].create({ fields: liveAttributes })
|
||||
return uuid
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(600_000)
|
||||
|
||||
liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false })
|
||||
liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false })
|
||||
permanentLiveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: true })
|
||||
|
||||
await Promise.all([
|
||||
commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }),
|
||||
commands[0].sendRTMPStreamInVideo({ videoId: permanentLiveVideoReplayId }),
|
||||
commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId })
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
commands[0].waitUntilPublished({ videoId: liveVideoId }),
|
||||
commands[0].waitUntilPublished({ videoId: permanentLiveVideoReplayId }),
|
||||
commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
|
||||
])
|
||||
|
||||
for (const videoUUID of [ liveVideoId, liveVideoReplayId, permanentLiveVideoReplayId ]) {
|
||||
await commands[0].waitUntilSegmentGeneration({
|
||||
server: servers[0],
|
||||
videoUUID,
|
||||
playlistNumber: 0,
|
||||
segment: 2
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: permanentLiveVideoReplayId })
|
||||
permanentLiveReplayName = video.name + ' - ' + new Date(video.publishedAt).toLocaleString()
|
||||
}
|
||||
|
||||
await killallServers([ servers[0] ])
|
||||
|
||||
beforeServerRestart = new Date()
|
||||
await servers[0].run()
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should cleanup lives', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await commands[0].waitUntilEnded({ videoId: liveVideoId })
|
||||
await commands[0].waitUntilWaiting({ videoId: permanentLiveVideoReplayId })
|
||||
})
|
||||
|
||||
it('Should save a non permanent live replay', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
|
||||
|
||||
const session = await commands[0].getReplaySession({ videoId: liveVideoReplayId })
|
||||
expect(session.endDate).to.exist
|
||||
expect(new Date(session.endDate)).to.be.above(beforeServerRestart)
|
||||
})
|
||||
|
||||
it('Should have saved a permanent live replay', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { data } = await servers[0].videos.listMyVideos({ sort: '-publishedAt' })
|
||||
expect(data.find(v => v.name === permanentLiveReplayName)).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,887 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@peertube/peertube-models'
|
||||
import {
|
||||
AbusesCommand,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
setDefaultChannelAvatar,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test abuses', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let abuseServer1: AdminAbuse
|
||||
let abuseServer2: AdminAbuse
|
||||
let commands: AbusesCommand[]
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
// Run servers
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultChannelAvatar(servers)
|
||||
await setDefaultAccountAvatar(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
commands = servers.map(s => s.abuses)
|
||||
})
|
||||
|
||||
describe('Video abuses', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
// Upload some videos on each servers
|
||||
{
|
||||
const attributes = {
|
||||
name: 'my super name for server 1',
|
||||
description: 'my super description for server 1'
|
||||
}
|
||||
await servers[0].videos.upload({ attributes })
|
||||
}
|
||||
|
||||
{
|
||||
const attributes = {
|
||||
name: 'my super name for server 2',
|
||||
description: 'my super description for server 2'
|
||||
}
|
||||
await servers[1].videos.upload({ attributes })
|
||||
}
|
||||
|
||||
// Wait videos propagation, server 2 has transcoding enabled
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].videos.list()
|
||||
expect(data.length).to.equal(2)
|
||||
|
||||
servers[0].store.videoCreated = data.find(video => video.name === 'my super name for server 1')
|
||||
servers[1].store.videoCreated = data.find(video => video.name === 'my super name for server 2')
|
||||
})
|
||||
|
||||
it('Should not have abuses', async function () {
|
||||
const body = await commands[0].getAdminList()
|
||||
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.be.an('array')
|
||||
expect(body.data.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should report abuse on a local video', async function () {
|
||||
this.timeout(15000)
|
||||
|
||||
const reason = 'my super bad reason'
|
||||
await commands[0].report({ videoId: servers[0].store.videoCreated.id, reason })
|
||||
|
||||
// We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
|
||||
{
|
||||
const body = await commands[0].getAdminList()
|
||||
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data).to.be.an('array')
|
||||
expect(body.data.length).to.equal(1)
|
||||
|
||||
const abuse = body.data[0]
|
||||
expect(abuse.reason).to.equal('my super bad reason')
|
||||
|
||||
expect(abuse.reporterAccount.name).to.equal('root')
|
||||
expect(abuse.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse.video.id).to.equal(servers[0].store.videoCreated.id)
|
||||
expect(abuse.video.channel).to.exist
|
||||
|
||||
expect(abuse.comment).to.be.null
|
||||
|
||||
expect(abuse.flaggedAccount.name).to.equal('root')
|
||||
expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse.video.countReports).to.equal(1)
|
||||
expect(abuse.video.nthReport).to.equal(1)
|
||||
|
||||
expect(abuse.countReportsForReporter).to.equal(1)
|
||||
expect(abuse.countReportsForReportee).to.equal(1)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList()
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.be.an('array')
|
||||
expect(body.data.length).to.equal(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should report abuse on a remote video', async function () {
|
||||
const reason = 'my super bad reason 2'
|
||||
const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid })
|
||||
await commands[0].report({ videoId, reason })
|
||||
|
||||
// We wait requests propagation
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
|
||||
{
|
||||
const body = await commands[0].getAdminList()
|
||||
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data.length).to.equal(2)
|
||||
|
||||
const abuse1 = body.data[0]
|
||||
expect(abuse1.reason).to.equal('my super bad reason')
|
||||
expect(abuse1.reporterAccount.name).to.equal('root')
|
||||
expect(abuse1.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse1.video.id).to.equal(servers[0].store.videoCreated.id)
|
||||
expect(abuse1.video.countReports).to.equal(1)
|
||||
expect(abuse1.video.nthReport).to.equal(1)
|
||||
|
||||
expect(abuse1.comment).to.be.null
|
||||
|
||||
expect(abuse1.flaggedAccount.name).to.equal('root')
|
||||
expect(abuse1.flaggedAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse1.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuse1.state.label).to.equal('Pending')
|
||||
expect(abuse1.moderationComment).to.be.null
|
||||
|
||||
const abuse2 = body.data[1]
|
||||
expect(abuse2.reason).to.equal('my super bad reason 2')
|
||||
|
||||
expect(abuse2.reporterAccount.name).to.equal('root')
|
||||
expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse2.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
|
||||
|
||||
expect(abuse2.comment).to.be.null
|
||||
|
||||
expect(abuse2.flaggedAccount.name).to.equal('root')
|
||||
expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
|
||||
|
||||
expect(abuse2.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuse2.state.label).to.equal('Pending')
|
||||
expect(abuse2.moderationComment).to.be.null
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList()
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data.length).to.equal(1)
|
||||
|
||||
abuseServer2 = body.data[0]
|
||||
expect(abuseServer2.reason).to.equal('my super bad reason 2')
|
||||
expect(abuseServer2.reporterAccount.name).to.equal('root')
|
||||
expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuseServer2.flaggedAccount.name).to.equal('root')
|
||||
expect(abuseServer2.flaggedAccount.host).to.equal(servers[1].host)
|
||||
|
||||
expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuseServer2.state.label).to.equal('Pending')
|
||||
expect(abuseServer2.moderationComment).to.be.null
|
||||
}
|
||||
})
|
||||
|
||||
it('Should hide video abuses from blocked accounts', async function () {
|
||||
{
|
||||
const videoId = await servers[1].videos.getId({ uuid: servers[0].store.videoCreated.uuid })
|
||||
await commands[1].report({ videoId, reason: 'will mute this' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const body = await commands[0].getAdminList()
|
||||
expect(body.total).to.equal(3)
|
||||
}
|
||||
|
||||
const accountToBlock = 'root@' + servers[1].host
|
||||
|
||||
{
|
||||
await servers[0].blocklist.addToServerBlocklist({ account: accountToBlock })
|
||||
|
||||
const body = await commands[0].getAdminList()
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const abuse = body.data.find(a => a.reason === 'will mute this')
|
||||
expect(abuse).to.be.undefined
|
||||
}
|
||||
|
||||
{
|
||||
await servers[0].blocklist.removeFromServerBlocklist({ account: accountToBlock })
|
||||
|
||||
const body = await commands[0].getAdminList()
|
||||
expect(body.total).to.equal(3)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should hide video abuses from blocked servers', async function () {
|
||||
const serverToBlock = servers[1].host
|
||||
|
||||
{
|
||||
await servers[0].blocklist.addToServerBlocklist({ server: serverToBlock })
|
||||
|
||||
const body = await commands[0].getAdminList()
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const abuse = body.data.find(a => a.reason === 'will mute this')
|
||||
expect(abuse).to.be.undefined
|
||||
}
|
||||
|
||||
{
|
||||
await servers[0].blocklist.removeFromServerBlocklist({ server: serverToBlock })
|
||||
|
||||
const body = await commands[0].getAdminList()
|
||||
expect(body.total).to.equal(3)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should keep the video abuse when deleting the video', async function () {
|
||||
await servers[1].videos.remove({ id: abuseServer2.video.uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const body = await commands[1].getAdminList()
|
||||
expect(body.total).to.equal(2, 'wrong number of videos returned')
|
||||
expect(body.data).to.have.lengthOf(2, 'wrong number of videos returned')
|
||||
|
||||
const abuse = body.data[0]
|
||||
expect(abuse.id).to.equal(abuseServer2.id, 'wrong origin server id for first video')
|
||||
expect(abuse.video.id).to.equal(abuseServer2.video.id, 'wrong video id')
|
||||
expect(abuse.video.channel).to.exist
|
||||
expect(abuse.video.deleted).to.be.true
|
||||
})
|
||||
|
||||
it('Should include counts of reports from reporter and reportee', async function () {
|
||||
// register a second user to have two reporters/reportees
|
||||
const user = { username: 'user2', password: 'password' }
|
||||
await servers[0].users.create({ ...user })
|
||||
const userAccessToken = await servers[0].login.getAccessToken(user)
|
||||
|
||||
// upload a third video via this user
|
||||
const attributes = {
|
||||
name: 'my second super name for server 1',
|
||||
description: 'my second super description for server 1'
|
||||
}
|
||||
const { id } = await servers[0].videos.upload({ token: userAccessToken, attributes })
|
||||
const video3Id = id
|
||||
|
||||
// resume with the test
|
||||
const reason3 = 'my super bad reason 3'
|
||||
await commands[0].report({ videoId: video3Id, reason: reason3 })
|
||||
|
||||
const reason4 = 'my super bad reason 4'
|
||||
await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: reason4 })
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList()
|
||||
const abuses = body.data
|
||||
|
||||
const abuseVideo3 = body.data.find(a => a.video.id === video3Id)
|
||||
expect(abuseVideo3).to.not.be.undefined
|
||||
expect(abuseVideo3.video.countReports).to.equal(1, 'wrong reports count for video 3')
|
||||
expect(abuseVideo3.video.nthReport).to.equal(1, 'wrong report position in report list for video 3')
|
||||
expect(abuseVideo3.countReportsForReportee).to.equal(1, 'wrong reports count for reporter on video 3 abuse')
|
||||
expect(abuseVideo3.countReportsForReporter).to.equal(3, 'wrong reports count for reportee on video 3 abuse')
|
||||
|
||||
const abuseServer1 = abuses.find(a => a.video.id === servers[0].store.videoCreated.id)
|
||||
expect(abuseServer1.countReportsForReportee).to.equal(3, 'wrong reports count for reporter on video 1 abuse')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list predefined reasons as well as timestamps for the reported video', async function () {
|
||||
const reason5 = 'my super bad reason 5'
|
||||
const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
|
||||
const createRes = await commands[0].report({
|
||||
videoId: servers[0].store.videoCreated.id,
|
||||
reason: reason5,
|
||||
predefinedReasons: predefinedReasons5,
|
||||
startAt: 1,
|
||||
endAt: 5
|
||||
})
|
||||
|
||||
const body = await commands[0].getAdminList()
|
||||
|
||||
{
|
||||
const abuse = body.data.find(a => a.id === createRes.abuse.id)
|
||||
expect(abuse.reason).to.equals(reason5)
|
||||
expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, 'predefined reasons do not match the one reported')
|
||||
expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
|
||||
expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete the video abuse', async function () {
|
||||
await commands[1].delete({ abuseId: abuseServer2.id })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList()
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data.length).to.equal(1)
|
||||
expect(body.data[0].id).to.not.equal(abuseServer2.id)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList()
|
||||
expect(body.total).to.equal(6)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list and filter video abuses', async function () {
|
||||
async function list (query: Parameters<AbusesCommand['getAdminList']>[0]) {
|
||||
const body = await commands[0].getAdminList(query)
|
||||
|
||||
return body.data
|
||||
}
|
||||
|
||||
expect(await list({ id: 56 })).to.have.lengthOf(0)
|
||||
expect(await list({ id: 1 })).to.have.lengthOf(1)
|
||||
|
||||
expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
|
||||
expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
|
||||
|
||||
expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
|
||||
|
||||
expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
|
||||
expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
|
||||
|
||||
expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
|
||||
expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
|
||||
|
||||
expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
|
||||
expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
|
||||
|
||||
expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
|
||||
expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
|
||||
|
||||
expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
|
||||
expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
|
||||
|
||||
expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
|
||||
expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Comment abuses', function () {
|
||||
|
||||
async function getComment (server: PeerTubeServer, videoIdArg: number | string) {
|
||||
const videoId = typeof videoIdArg === 'string'
|
||||
? await server.videos.getId({ uuid: videoIdArg })
|
||||
: videoIdArg
|
||||
|
||||
const { data } = await server.comments.listThreads({ videoId })
|
||||
|
||||
return data[0]
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
servers[0].store.videoCreated = await servers[0].videos.quickUpload({ name: 'server 1' })
|
||||
servers[1].store.videoCreated = await servers[1].videos.quickUpload({ name: 'server 2' })
|
||||
|
||||
await servers[0].comments.createThread({ videoId: servers[0].store.videoCreated.id, text: 'comment server 1' })
|
||||
await servers[1].comments.createThread({ videoId: servers[1].store.videoCreated.id, text: 'comment server 2' })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should report abuse on a comment', async function () {
|
||||
this.timeout(15000)
|
||||
|
||||
const comment = await getComment(servers[0], servers[0].store.videoCreated.id)
|
||||
|
||||
const reason = 'it is a bad comment'
|
||||
await commands[0].report({ commentId: comment.id, reason })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () {
|
||||
{
|
||||
const comment = await getComment(servers[0], servers[0].store.videoCreated.id)
|
||||
const body = await commands[0].getAdminList({ filter: 'comment' })
|
||||
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data).to.have.lengthOf(1)
|
||||
|
||||
const abuse = body.data[0]
|
||||
expect(abuse.reason).to.equal('it is a bad comment')
|
||||
|
||||
expect(abuse.reporterAccount.name).to.equal('root')
|
||||
expect(abuse.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse.video).to.be.null
|
||||
|
||||
expect(abuse.comment.deleted).to.be.false
|
||||
expect(abuse.comment.id).to.equal(comment.id)
|
||||
expect(abuse.comment.text).to.equal(comment.text)
|
||||
expect(abuse.comment.video.name).to.equal('server 1')
|
||||
expect(abuse.comment.video.id).to.equal(servers[0].store.videoCreated.id)
|
||||
expect(abuse.comment.video.uuid).to.equal(servers[0].store.videoCreated.uuid)
|
||||
|
||||
expect(abuse.countReportsForReporter).to.equal(5)
|
||||
expect(abuse.countReportsForReportee).to.equal(5)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data.length).to.equal(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should report abuse on a remote comment', async function () {
|
||||
const comment = await getComment(servers[0], servers[1].store.videoCreated.uuid)
|
||||
|
||||
const reason = 'it is a really bad comment'
|
||||
await commands[0].report({ commentId: comment.id, reason })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
|
||||
const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.shortUUID)
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data.length).to.equal(2)
|
||||
|
||||
const abuse = body.data[0]
|
||||
expect(abuse.reason).to.equal('it is a bad comment')
|
||||
expect(abuse.countReportsForReporter).to.equal(6)
|
||||
expect(abuse.countReportsForReportee).to.equal(5)
|
||||
|
||||
const abuse2 = body.data[1]
|
||||
|
||||
expect(abuse2.reason).to.equal('it is a really bad comment')
|
||||
|
||||
expect(abuse2.reporterAccount.name).to.equal('root')
|
||||
expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse2.video).to.be.null
|
||||
|
||||
expect(abuse2.comment.deleted).to.be.false
|
||||
expect(abuse2.comment.id).to.equal(commentServer2.id)
|
||||
expect(abuse2.comment.text).to.equal(commentServer2.text)
|
||||
expect(abuse2.comment.video.name).to.equal('server 2')
|
||||
expect(abuse2.comment.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
|
||||
|
||||
expect(abuse2.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuse2.state.label).to.equal('Pending')
|
||||
|
||||
expect(abuse2.moderationComment).to.be.null
|
||||
|
||||
expect(abuse2.countReportsForReporter).to.equal(6)
|
||||
expect(abuse2.countReportsForReportee).to.equal(2)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data.length).to.equal(1)
|
||||
|
||||
abuseServer2 = body.data[0]
|
||||
expect(abuseServer2.reason).to.equal('it is a really bad comment')
|
||||
expect(abuseServer2.reporterAccount.name).to.equal('root')
|
||||
expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuseServer2.state.label).to.equal('Pending')
|
||||
|
||||
expect(abuseServer2.moderationComment).to.be.null
|
||||
|
||||
expect(abuseServer2.countReportsForReporter).to.equal(1)
|
||||
expect(abuseServer2.countReportsForReportee).to.equal(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should keep the comment abuse when deleting the comment', async function () {
|
||||
const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.uuid)
|
||||
|
||||
await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const body = await commands[0].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data).to.have.lengthOf(2)
|
||||
|
||||
const abuse = body.data.find(a => a.comment?.id === commentServer2.id)
|
||||
expect(abuse).to.not.be.undefined
|
||||
|
||||
expect(abuse.comment.text).to.be.empty
|
||||
expect(abuse.comment.video.name).to.equal('server 2')
|
||||
expect(abuse.comment.deleted).to.be.true
|
||||
})
|
||||
|
||||
it('Should delete the comment abuse', async function () {
|
||||
await commands[1].delete({ abuseId: abuseServer2.id })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data.length).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list and filter video abuses', async function () {
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'foo' })
|
||||
expect(body.total).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'ot' })
|
||||
expect(body.total).to.equal(2)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: 'createdAt' })
|
||||
expect(body.data).to.have.lengthOf(1)
|
||||
expect(body.data[0].comment.text).to.be.empty
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: '-createdAt' })
|
||||
expect(body.data).to.have.lengthOf(1)
|
||||
expect(body.data[0].comment.text).to.equal('comment server 1')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Account abuses', function () {
|
||||
|
||||
function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) {
|
||||
return server.accounts.get({ accountName: targetName + '@' + targetServer.host })
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
await servers[0].users.create({ username: 'user_1', password: 'donald' })
|
||||
|
||||
const token = await servers[1].users.generateUserAndToken('user_2')
|
||||
await servers[1].videos.upload({ token, attributes: { name: 'super video' } })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should report abuse on an account', async function () {
|
||||
this.timeout(15000)
|
||||
|
||||
const account = await getAccountFromServer(servers[0], 'user_1', servers[0])
|
||||
|
||||
const reason = 'it is a bad account'
|
||||
await commands[0].report({ accountId: account.id, reason })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 1 account abuse on server 1 and 0 on server 2', async function () {
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'account' })
|
||||
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data).to.have.lengthOf(1)
|
||||
|
||||
const abuse = body.data[0]
|
||||
expect(abuse.reason).to.equal('it is a bad account')
|
||||
|
||||
expect(abuse.reporterAccount.name).to.equal('root')
|
||||
expect(abuse.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse.video).to.be.null
|
||||
expect(abuse.comment).to.be.null
|
||||
|
||||
expect(abuse.flaggedAccount.name).to.equal('user_1')
|
||||
expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList({ filter: 'comment' })
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data.length).to.equal(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should report abuse on a remote account', async function () {
|
||||
const account = await getAccountFromServer(servers[0], 'user_2', servers[1])
|
||||
|
||||
const reason = 'it is a really bad account'
|
||||
await commands[0].report({ accountId: account.id, reason })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'account' })
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data.length).to.equal(2)
|
||||
|
||||
const abuse: AdminAbuse = body.data[0]
|
||||
expect(abuse.reason).to.equal('it is a bad account')
|
||||
|
||||
const abuse2: AdminAbuse = body.data[1]
|
||||
expect(abuse2.reason).to.equal('it is a really bad account')
|
||||
|
||||
expect(abuse2.reporterAccount.name).to.equal('root')
|
||||
expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuse2.video).to.be.null
|
||||
expect(abuse2.comment).to.be.null
|
||||
|
||||
expect(abuse2.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuse2.state.label).to.equal('Pending')
|
||||
|
||||
expect(abuse2.moderationComment).to.be.null
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList({ filter: 'account' })
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data.length).to.equal(1)
|
||||
|
||||
abuseServer2 = body.data[0]
|
||||
|
||||
expect(abuseServer2.reason).to.equal('it is a really bad account')
|
||||
|
||||
expect(abuseServer2.reporterAccount.name).to.equal('root')
|
||||
expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
|
||||
|
||||
expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
|
||||
expect(abuseServer2.state.label).to.equal('Pending')
|
||||
|
||||
expect(abuseServer2.moderationComment).to.be.null
|
||||
}
|
||||
})
|
||||
|
||||
it('Should keep the account abuse when deleting the account', async function () {
|
||||
const account = await getAccountFromServer(servers[1], 'user_2', servers[1])
|
||||
await servers[1].users.remove({ userId: account.userId })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const body = await commands[0].getAdminList({ filter: 'account' })
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data).to.have.lengthOf(2)
|
||||
|
||||
const abuse = body.data.find(a => a.reason === 'it is a really bad account')
|
||||
expect(abuse).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('Should delete the account abuse', async function () {
|
||||
await commands[1].delete({ abuseId: abuseServer2.id })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const body = await commands[1].getAdminList({ filter: 'account' })
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data.length).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getAdminList({ filter: 'account' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
abuseServer1 = body.data[0]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Common actions on abuses', function () {
|
||||
|
||||
it('Should update the state of an abuse', async function () {
|
||||
await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } })
|
||||
|
||||
const body = await commands[0].getAdminList({ id: abuseServer1.id })
|
||||
expect(body.data[0].state.id).to.equal(AbuseState.REJECTED)
|
||||
})
|
||||
|
||||
it('Should add a moderation comment', async function () {
|
||||
await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.ACCEPTED, moderationComment: 'Valid' } })
|
||||
|
||||
const body = await commands[0].getAdminList({ id: abuseServer1.id })
|
||||
expect(body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
|
||||
expect(body.data[0].moderationComment).to.equal('Valid')
|
||||
})
|
||||
})
|
||||
|
||||
describe('My abuses', async function () {
|
||||
let abuseId1: number
|
||||
let userAccessToken: string
|
||||
|
||||
before(async function () {
|
||||
userAccessToken = await servers[0].users.generateUserAndToken('user_42')
|
||||
|
||||
await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: 'user reason 1' })
|
||||
|
||||
const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid })
|
||||
await commands[0].report({ token: userAccessToken, videoId, reason: 'user reason 2' })
|
||||
})
|
||||
|
||||
it('Should correctly list my abuses', async function () {
|
||||
{
|
||||
const body = await commands[0].getUserList({ token: userAccessToken, start: 0, count: 5, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const abuses = body.data
|
||||
expect(abuses[0].reason).to.equal('user reason 1')
|
||||
expect(abuses[1].reason).to.equal('user reason 2')
|
||||
|
||||
abuseId1 = abuses[0].id
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const abuses: UserAbuse[] = body.data
|
||||
expect(abuses[0].reason).to.equal('user reason 2')
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: '-createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const abuses: UserAbuse[] = body.data
|
||||
expect(abuses[0].reason).to.equal('user reason 1')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly filter my abuses by id', async function () {
|
||||
const body = await commands[0].getUserList({ token: userAccessToken, id: abuseId1 })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
const abuses: UserAbuse[] = body.data
|
||||
expect(abuses[0].reason).to.equal('user reason 1')
|
||||
})
|
||||
|
||||
it('Should correctly filter my abuses by search', async function () {
|
||||
const body = await commands[0].getUserList({ token: userAccessToken, search: 'server 2' })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
const abuses: UserAbuse[] = body.data
|
||||
expect(abuses[0].reason).to.equal('user reason 2')
|
||||
})
|
||||
|
||||
it('Should correctly filter my abuses by state', async function () {
|
||||
await commands[0].update({ abuseId: abuseId1, body: { state: AbuseState.REJECTED } })
|
||||
|
||||
const body = await commands[0].getUserList({ token: userAccessToken, state: AbuseState.REJECTED })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
const abuses: UserAbuse[] = body.data
|
||||
expect(abuses[0].reason).to.equal('user reason 1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Abuse messages', async function () {
|
||||
let abuseId: number
|
||||
let userToken: string
|
||||
let abuseMessageUserId: number
|
||||
let abuseMessageModerationId: number
|
||||
|
||||
before(async function () {
|
||||
userToken = await servers[0].users.generateUserAndToken('user_43')
|
||||
|
||||
const body = await commands[0].report({ token: userToken, videoId: servers[0].store.videoCreated.id, reason: 'user 43 reason 1' })
|
||||
abuseId = body.abuse.id
|
||||
})
|
||||
|
||||
it('Should create some messages on the abuse', async function () {
|
||||
await commands[0].addMessage({ token: userToken, abuseId, message: 'message 1' })
|
||||
await commands[0].addMessage({ abuseId, message: 'message 2' })
|
||||
await commands[0].addMessage({ abuseId, message: 'message 3' })
|
||||
await commands[0].addMessage({ token: userToken, abuseId, message: 'message 4' })
|
||||
})
|
||||
|
||||
it('Should have the correct messages count when listing abuses', async function () {
|
||||
const results = await Promise.all([
|
||||
commands[0].getAdminList({ start: 0, count: 50 }),
|
||||
commands[0].getUserList({ token: userToken, start: 0, count: 50 })
|
||||
])
|
||||
|
||||
for (const body of results) {
|
||||
const abuses = body.data
|
||||
const abuse = abuses.find(a => a.id === abuseId)
|
||||
expect(abuse.countMessages).to.equal(4)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly list messages of this abuse', async function () {
|
||||
const results = await Promise.all([
|
||||
commands[0].listMessages({ abuseId }),
|
||||
commands[0].listMessages({ token: userToken, abuseId })
|
||||
])
|
||||
|
||||
for (const body of results) {
|
||||
expect(body.total).to.equal(4)
|
||||
|
||||
const abuseMessages: AbuseMessage[] = body.data
|
||||
|
||||
expect(abuseMessages[0].message).to.equal('message 1')
|
||||
expect(abuseMessages[0].byModerator).to.be.false
|
||||
expect(abuseMessages[0].account.name).to.equal('user_43')
|
||||
|
||||
abuseMessageUserId = abuseMessages[0].id
|
||||
|
||||
expect(abuseMessages[1].message).to.equal('message 2')
|
||||
expect(abuseMessages[1].byModerator).to.be.true
|
||||
expect(abuseMessages[1].account.name).to.equal('root')
|
||||
|
||||
expect(abuseMessages[2].message).to.equal('message 3')
|
||||
expect(abuseMessages[2].byModerator).to.be.true
|
||||
expect(abuseMessages[2].account.name).to.equal('root')
|
||||
abuseMessageModerationId = abuseMessages[2].id
|
||||
|
||||
expect(abuseMessages[3].message).to.equal('message 4')
|
||||
expect(abuseMessages[3].byModerator).to.be.false
|
||||
expect(abuseMessages[3].account.name).to.equal('user_43')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete messages', async function () {
|
||||
await commands[0].deleteMessage({ abuseId, messageId: abuseMessageModerationId })
|
||||
await commands[0].deleteMessage({ token: userToken, abuseId, messageId: abuseMessageUserId })
|
||||
|
||||
const results = await Promise.all([
|
||||
commands[0].listMessages({ abuseId }),
|
||||
commands[0].listMessages({ token: userToken, abuseId })
|
||||
])
|
||||
|
||||
for (const body of results) {
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const abuseMessages: AbuseMessage[] = body.data
|
||||
expect(abuseMessages[0].message).to.equal('message 2')
|
||||
expect(abuseMessages[1].message).to.equal('message 4')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,489 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests, createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test automatic tags', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let videoUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
await setDefaultAccountAvatar(servers)
|
||||
|
||||
await servers[1].config.enableLive({ allowReplay: false })
|
||||
|
||||
await doubleFollow(servers[0], servers[1]);
|
||||
|
||||
({ uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'video' }))
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
describe('Automatic tags on comments', function () {
|
||||
|
||||
describe('Built in external link auto tag', function () {
|
||||
|
||||
it('Should not assign external-link automatic tag with no URL inside the comment', async function () {
|
||||
const tests = [
|
||||
'my super comment',
|
||||
'toto.azfazfe',
|
||||
'Hello. Hi friends'
|
||||
]
|
||||
|
||||
for (const toTest of tests) {
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: toTest })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listForAdmin()
|
||||
|
||||
for (const comment of data) {
|
||||
expect(comment.automaticTags, `"${comment.text}" has an automatic tag`).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].comments.deleteAllComments({ videoUUID })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not assign external-link automatic tag if the URL is an internal link', async function () {
|
||||
const tests = [
|
||||
`Hi ${servers[0].url}`
|
||||
]
|
||||
|
||||
for (const toTest of tests) {
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: toTest })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
// Server 1
|
||||
{
|
||||
const { data } = await servers[0].comments.listForAdmin()
|
||||
|
||||
for (const comment of data) {
|
||||
expect(comment.automaticTags, `"${comment.text}" has an automatic tag`).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Server 2
|
||||
{
|
||||
const { data } = await servers[1].comments.listForAdmin()
|
||||
|
||||
for (const comment of data) {
|
||||
expect(comment.automaticTags, `"${comment.text}" hasn't an automatic tag`).to.have.lengthOf(1)
|
||||
expect(comment.automaticTags[0]).to.equal('external-link')
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].comments.deleteAllComments({ videoUUID })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should assign external-link automatic tag', async function () {
|
||||
const tests = [
|
||||
'example.com',
|
||||
'Hi example.com'
|
||||
]
|
||||
|
||||
for (const toTest of tests) {
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: toTest })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listForAdmin()
|
||||
|
||||
for (const comment of data) {
|
||||
expect(comment.automaticTags).to.have.lengthOf(1)
|
||||
expect(comment.automaticTags[0]).to.equal('external-link')
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].comments.deleteAllComments({ videoUUID })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With watched words', function () {
|
||||
let accountListId: number
|
||||
|
||||
it('Should create watched words list and automatically assign an automatic tag', async function () {
|
||||
// Account list
|
||||
{
|
||||
await servers[0].watchedWordsLists.createList({ listName: 'list 1', words: [ 'word 1', 'word 2' ], accountName: 'root' })
|
||||
|
||||
const { watchedWordsList } = await servers[0].watchedWordsLists.createList({
|
||||
listName: 'list 2',
|
||||
words: [ 'nemo' ],
|
||||
accountName: 'root'
|
||||
})
|
||||
accountListId = watchedWordsList.id
|
||||
}
|
||||
|
||||
// Server list
|
||||
{
|
||||
await servers[0].watchedWordsLists.createList({ listName: 'server 2', words: [ 'word 2' ] })
|
||||
}
|
||||
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'hi captain' })
|
||||
await servers[0].comments.addReplyToLastThread({ text: 'hi captain nemo' })
|
||||
await servers[1].comments.createThread({ videoId: videoUUID, text: 'hi captain nemo word 2 example.com' })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// Server comments list must not include account personal watched words
|
||||
{
|
||||
const { data } = await servers[0].comments.listForAdmin()
|
||||
const c = (text: string) => data.find(c => c.text === text)
|
||||
|
||||
expect(c('hi captain').automaticTags).to.have.lengthOf(0)
|
||||
expect(c('hi captain nemo').automaticTags).to.have.lengthOf(0)
|
||||
expect(c('hi captain nemo word 2 example.com').automaticTags).to.have.members([ 'server 2', 'external-link' ])
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos()
|
||||
const c = (text: string) => data.find(c => c.text === text)
|
||||
|
||||
expect(c('hi captain').automaticTags).to.have.lengthOf(0)
|
||||
expect(c('hi captain nemo').automaticTags).to.have.members([ 'list 2' ])
|
||||
expect(c('hi captain nemo word 2 example.com').automaticTags).to.have.members([ 'list 1', 'list 2', 'external-link' ])
|
||||
}
|
||||
})
|
||||
|
||||
it('Should update watched words list and assign auto tag with new words', async function () {
|
||||
// No tags
|
||||
{
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'my nautilus' })
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos()
|
||||
expect(data.find(c => c.text === 'my nautilus').automaticTags).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
await servers[0].watchedWordsLists.updateList({
|
||||
accountName: 'root',
|
||||
listId: accountListId,
|
||||
words: [ 'nautilus' ],
|
||||
listName: 'list 3'
|
||||
})
|
||||
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'captain nemo' })
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'my nautilus 2' })
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'word 1' })
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos()
|
||||
// Previous comment still have the same automatic tags
|
||||
expect(data.find(c => c.text === 'my nautilus').automaticTags).to.have.lengthOf(0)
|
||||
|
||||
expect(data.find(c => c.text === 'captain nemo').automaticTags).to.have.lengthOf(0)
|
||||
expect(data.find(c => c.text === 'my nautilus 2').automaticTags).to.have.members([ 'list 3' ])
|
||||
expect(data.find(c => c.text === 'word 1').automaticTags).to.have.members([ 'list 1' ])
|
||||
}
|
||||
})
|
||||
|
||||
it('Should delete watched words list and so not assign auto tags anymore', async function () {
|
||||
await servers[0].watchedWordsLists.deleteList({ accountName: 'root', listId: accountListId })
|
||||
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'my nautilus 3' })
|
||||
await servers[0].comments.createThread({ videoId: videoUUID, text: 'word 2' })
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos()
|
||||
expect(data.find(c => c.text === 'my nautilus 3').automaticTags).to.have.lengthOf(0)
|
||||
expect(data.find(c => c.text === 'word 2').automaticTags).to.have.members([ 'list 1' ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Searching comments with specific tags', function () {
|
||||
|
||||
it('Should search in "comments on my videos" comments with specific automatic tags', async function () {
|
||||
{
|
||||
const { total, data } = await servers[0].comments.listCommentsOnMyVideos({ autoTagOneOf: [ 'unknown' ] })
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
for (const autoTagOneOf of [ [ 'list 1' ], [ 'list 1', 'unknown' ] ]) {
|
||||
const { total, data } = await servers[0].comments.listCommentsOnMyVideos({ autoTagOneOf })
|
||||
|
||||
expect(total).to.equal(3)
|
||||
|
||||
expect(data.map(c => c.text)).to.have.members([
|
||||
'hi captain nemo word 2 example.com',
|
||||
'word 1',
|
||||
'word 2'
|
||||
])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should search in admin comments with specific automatic tags', async function () {
|
||||
{
|
||||
const { total, data } = await servers[0].comments.listForAdmin({ autoTagOneOf: [ 'list 1' ] })
|
||||
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[0].comments.listForAdmin({ autoTagOneOf: [ 'external-link' ] })
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].text).to.equal('hi captain nemo word 2 example.com')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Automatic tags on videos', function () {
|
||||
|
||||
before(async function () {
|
||||
await servers[0].videos.removeAll()
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
describe('Built in external link auto tag', function () {
|
||||
|
||||
it('Should not assign external-link automatic tag with no URL inside the video', async function () {
|
||||
const tests = [
|
||||
'my super video',
|
||||
'toto.azfazfe',
|
||||
'Hello. Hi friends'
|
||||
]
|
||||
|
||||
for (const toTest of tests) {
|
||||
await servers[0].videos.upload({ attributes: { name: toTest, description: toTest } })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.videos.listAllForAdmin()
|
||||
|
||||
for (const video of data) {
|
||||
expect(video.automaticTags, `"${video.name}" has an automatic tag`).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].videos.removeAll()
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not assign external-link automatic tag if the URL is an internal link', async function () {
|
||||
const tests = [
|
||||
`Hi ${servers[0].url}`
|
||||
]
|
||||
|
||||
for (const toTest of tests) {
|
||||
await servers[0].videos.upload({ attributes: { name: toTest, description: toTest } })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
// Server 1
|
||||
{
|
||||
const { data } = await servers[0].videos.listAllForAdmin()
|
||||
|
||||
for (const video of data) {
|
||||
expect(video.automaticTags, `"${video.name}" has an automatic tag`).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Server 2
|
||||
{
|
||||
const { data } = await servers[1].videos.listAllForAdmin()
|
||||
|
||||
for (const video of data) {
|
||||
expect(video.automaticTags, `"${video.name}" hasn't an automatic tag`).to.have.lengthOf(1)
|
||||
expect(video.automaticTags[0]).to.equal('external-link')
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].videos.removeAll()
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should assign external-link automatic tag', async function () {
|
||||
const tests = [
|
||||
'example.com',
|
||||
'Hi example.com'
|
||||
]
|
||||
|
||||
for (const toTest of tests) {
|
||||
await servers[0].videos.upload({ attributes: { name: toTest, description: toTest } })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.videos.listAllForAdmin()
|
||||
|
||||
for (const video of data) {
|
||||
expect(video.automaticTags).to.have.lengthOf(1)
|
||||
expect(video.automaticTags[0]).to.equal('external-link')
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].videos.removeAll()
|
||||
await waitJobs(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With watched words', function () {
|
||||
let serverListId: number
|
||||
let liveUUID: string
|
||||
|
||||
it('Should create watched words list and automatically assign an automatic tag', async function () {
|
||||
// Server list
|
||||
{
|
||||
await servers[0].watchedWordsLists.createList({
|
||||
listName: 'donald list',
|
||||
words: [ 'riri', 'fifi', 'loulou' ]
|
||||
})
|
||||
|
||||
const { watchedWordsList } = await servers[0].watchedWordsLists.createList({
|
||||
listName: 'mickey list',
|
||||
words: [ 'dingo', 'pluto' ]
|
||||
})
|
||||
serverListId = watchedWordsList.id
|
||||
}
|
||||
|
||||
// Account list
|
||||
{
|
||||
await servers[0].watchedWordsLists.createList({ listName: 'picsou list', words: [ 'goldie' ], accountName: 'root' })
|
||||
}
|
||||
|
||||
await servers[0].videos.upload({ attributes: { name: 'my dear goldie', description: 'hi riri and fifi' } })
|
||||
await servers[0].videoImports.importVideo({
|
||||
attributes: {
|
||||
targetUrl: FIXTURE_URLS.goodVideo,
|
||||
channelId: servers[0].store.channel.id,
|
||||
name: 'import video',
|
||||
description: 'pluto dog'
|
||||
}
|
||||
})
|
||||
const { uuid } = await servers[1].live.create({
|
||||
fields: {
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'live loulou',
|
||||
description: 'dingo and minnie'
|
||||
}
|
||||
})
|
||||
liveUUID = uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// Server videos list must not include account personal watched words
|
||||
{
|
||||
const { data } = await servers[0].videos.listAllForAdmin()
|
||||
const v = (name: string) => data.find(c => c.name === name)
|
||||
|
||||
expect(v('my dear goldie').automaticTags).to.have.members([ 'donald list' ])
|
||||
expect(v('import video').automaticTags).to.have.members([ 'mickey list' ])
|
||||
expect(v('live loulou').automaticTags).to.have.members([ 'donald list', 'mickey list' ])
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await servers[0].videos.listMyVideos()
|
||||
|
||||
for (const video of data) {
|
||||
expect(video.automaticTags).to.not.exist
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should update watched words list and assign auto tag on update', async function () {
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'hi minnie' })
|
||||
|
||||
{
|
||||
const { data } = await servers[0].videos.listAllForAdmin()
|
||||
expect(data.find(v => v.name === 'hi minnie').automaticTags).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
await servers[0].watchedWordsLists.updateList({
|
||||
listId: serverListId,
|
||||
words: [ 'Minnie' ],
|
||||
listName: 'mickey list v2'
|
||||
})
|
||||
|
||||
await servers[0].videos.update({ id: uuid, attributes: { name: 'hi minnie v2' } })
|
||||
|
||||
const { data } = await servers[0].videos.listAllForAdmin()
|
||||
expect(data.find(v => v.name === 'hi minnie v2').automaticTags).to.have.members([ 'mickey list v2' ])
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not update remote video if name/description has not changed', async function () {
|
||||
await servers[1].videos.update({
|
||||
id: liveUUID,
|
||||
attributes: {
|
||||
channelId: servers[0].store.channel.id,
|
||||
tags: [ 'super tag' ]
|
||||
}
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].videos.listAllForAdmin()
|
||||
expect(data.find(v => v.name === 'live loulou').automaticTags).to.have.members([ 'donald list', 'mickey list' ])
|
||||
})
|
||||
|
||||
it('Should update remote video if name/description has changed', async function () {
|
||||
await servers[1].videos.update({
|
||||
id: liveUUID,
|
||||
attributes: { name: 'live loulou v2' }
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].videos.listAllForAdmin()
|
||||
expect(data.find(v => v.name === 'live loulou v2').automaticTags).to.have.members([ 'donald list', 'mickey list v2' ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Searching videos with specific tags', function () {
|
||||
|
||||
it('Should search in admin videos with specific automatic tags', async function () {
|
||||
{
|
||||
const { total, data } = await servers[0].videos.listAllForAdmin({ autoTagOneOf: [ 'picsou list' ] })
|
||||
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[0].videos.listAllForAdmin({ autoTagOneOf: [ 'mickey list v2' ] })
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
expect(data.map(d => d.name)).to.have.members([ 'hi minnie v2', 'live loulou v2' ])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,231 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { UserNotificationType, UserNotificationType_Type } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function checkNotifications (server: PeerTubeServer, token: string, expected: UserNotificationType_Type[]) {
|
||||
const { data } = await server.notifications.list({ token, start: 0, count: 10, unread: true })
|
||||
expect(data).to.have.lengthOf(expected.length)
|
||||
|
||||
for (const type of expected) {
|
||||
expect(data.find(n => n.type === type)).to.exist
|
||||
}
|
||||
}
|
||||
|
||||
describe('Test blocklist notifications', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let videoUUID: string
|
||||
|
||||
let userToken1: string
|
||||
let userToken2: string
|
||||
let remoteUserToken: string
|
||||
|
||||
async function resetState () {
|
||||
try {
|
||||
await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user1_channel@' + servers[0].host })
|
||||
await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user2_channel@' + servers[0].host })
|
||||
} catch {}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].notifications.markAsReadAll({ token: userToken1 })
|
||||
await servers[0].notifications.markAsReadAll({ token: userToken2 })
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video' } })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
{
|
||||
await servers[1].comments.createThread({
|
||||
token: remoteUserToken,
|
||||
videoId: videoUUID,
|
||||
text: '@user2@' + servers[0].host + ' hello'
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user1_channel@' + servers[0].host })
|
||||
await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user2_channel@' + servers[0].host })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
{
|
||||
const user = { username: 'user1', password: 'password' }
|
||||
await servers[0].users.create({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
videoQuota: -1,
|
||||
videoQuotaDaily: -1
|
||||
})
|
||||
|
||||
userToken1 = await servers[0].login.getAccessToken(user)
|
||||
await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } })
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'user2', password: 'password' }
|
||||
await servers[0].users.create({ username: user.username, password: user.password })
|
||||
|
||||
userToken2 = await servers[0].login.getAccessToken(user)
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'user3', password: 'password' }
|
||||
await servers[1].users.create({ username: user.username, password: user.password })
|
||||
|
||||
remoteUserToken = await servers[1].login.getAccessToken(user)
|
||||
}
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
describe('User blocks another user', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await resetState()
|
||||
})
|
||||
|
||||
it('Should have appropriate notifications', async function () {
|
||||
const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
|
||||
await checkNotifications(servers[0], userToken1, notifs)
|
||||
})
|
||||
|
||||
it('Should block an account', async function () {
|
||||
await servers[0].blocklist.addToMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not have notifications from this account', async function () {
|
||||
await checkNotifications(servers[0], userToken1, [])
|
||||
})
|
||||
|
||||
it('Should have notifications of this account on user 2', async function () {
|
||||
const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
|
||||
|
||||
await checkNotifications(servers[0], userToken2, notifs)
|
||||
|
||||
await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host })
|
||||
})
|
||||
})
|
||||
|
||||
describe('User blocks another server', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await resetState()
|
||||
})
|
||||
|
||||
it('Should have appropriate notifications', async function () {
|
||||
const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
|
||||
await checkNotifications(servers[0], userToken1, notifs)
|
||||
})
|
||||
|
||||
it('Should block an account', async function () {
|
||||
await servers[0].blocklist.addToMyBlocklist({ token: userToken1, server: servers[1].host })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not have notifications from this account', async function () {
|
||||
await checkNotifications(servers[0], userToken1, [])
|
||||
})
|
||||
|
||||
it('Should have notifications of this account on user 2', async function () {
|
||||
const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
|
||||
|
||||
await checkNotifications(servers[0], userToken2, notifs)
|
||||
|
||||
await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, server: servers[1].host })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Server blocks a user', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await resetState()
|
||||
})
|
||||
|
||||
it('Should have appropriate notifications', async function () {
|
||||
{
|
||||
const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
|
||||
await checkNotifications(servers[0], userToken1, notifs)
|
||||
}
|
||||
|
||||
{
|
||||
const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
|
||||
await checkNotifications(servers[0], userToken2, notifs)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should block an account', async function () {
|
||||
await servers[0].blocklist.addToServerBlocklist({ account: 'user3@' + servers[1].host })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not have notifications from this account', async function () {
|
||||
await checkNotifications(servers[0], userToken1, [])
|
||||
await checkNotifications(servers[0], userToken2, [])
|
||||
|
||||
await servers[0].blocklist.removeFromServerBlocklist({ account: 'user3@' + servers[1].host })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Server blocks a server', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await resetState()
|
||||
})
|
||||
|
||||
it('Should have appropriate notifications', async function () {
|
||||
{
|
||||
const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
|
||||
await checkNotifications(servers[0], userToken1, notifs)
|
||||
}
|
||||
|
||||
{
|
||||
const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
|
||||
await checkNotifications(servers[0], userToken2, notifs)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should block an account', async function () {
|
||||
await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not have notifications from this account', async function () {
|
||||
await checkNotifications(servers[0], userToken1, [])
|
||||
await checkNotifications(servers[0], userToken2, [])
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,902 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { UserNotificationType } from '@peertube/peertube-models'
|
||||
import {
|
||||
BlocklistCommand,
|
||||
cleanupTests,
|
||||
CommentsCommand,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function checkAllVideos (server: PeerTubeServer, token: string) {
|
||||
{
|
||||
const { data } = await server.videos.listWithToken({ token })
|
||||
expect(data).to.have.lengthOf(5)
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await server.videos.list()
|
||||
expect(data).to.have.lengthOf(5)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAllComments (server: PeerTubeServer, token: string, videoUUID: string) {
|
||||
const { data } = await server.comments.listThreads({ videoId: videoUUID, start: 0, count: 25, sort: '-createdAt', token })
|
||||
|
||||
const threads = data.filter(t => t.isDeleted === false)
|
||||
expect(threads).to.have.lengthOf(2)
|
||||
|
||||
for (const thread of threads) {
|
||||
const tree = await server.comments.getThread({ videoId: videoUUID, threadId: thread.id, token })
|
||||
expect(tree.children).to.have.lengthOf(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCommentNotification (
|
||||
mainServer: PeerTubeServer,
|
||||
comment: { server: PeerTubeServer, token: string, videoUUID: string, text: string },
|
||||
check: 'presence' | 'absence'
|
||||
) {
|
||||
const command = comment.server.comments
|
||||
|
||||
const { threadId, createdAt } = await command.createThread({ token: comment.token, videoId: comment.videoUUID, text: comment.text })
|
||||
|
||||
await waitJobs([ mainServer, comment.server ])
|
||||
|
||||
const { data } = await mainServer.notifications.list({ start: 0, count: 30 })
|
||||
const commentNotifications = data.filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt)
|
||||
|
||||
if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1)
|
||||
else expect(commentNotifications).to.have.lengthOf(0)
|
||||
|
||||
await command.delete({ token: comment.token, videoId: comment.videoUUID, commentId: threadId })
|
||||
|
||||
await waitJobs([ mainServer, comment.server ])
|
||||
}
|
||||
|
||||
describe('Test blocklist', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let videoUUID1: string
|
||||
let videoUUID2: string
|
||||
let videoUUID3: string
|
||||
let userToken1: string
|
||||
let userModeratorToken: string
|
||||
let userToken2: string
|
||||
|
||||
let command: BlocklistCommand
|
||||
let commentsCommand: CommentsCommand[]
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(3)
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultAccountAvatar(servers)
|
||||
|
||||
command = servers[0].blocklist
|
||||
commentsCommand = servers.map(s => s.comments)
|
||||
|
||||
{
|
||||
const user = { username: 'user1', password: 'password' }
|
||||
await servers[0].users.create({ username: user.username, password: user.password })
|
||||
|
||||
userToken1 = await servers[0].login.getAccessToken(user)
|
||||
await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } })
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'moderator', password: 'password' }
|
||||
await servers[0].users.create({ username: user.username, password: user.password })
|
||||
|
||||
userModeratorToken = await servers[0].login.getAccessToken(user)
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'user2', password: 'password' }
|
||||
await servers[1].users.create({ username: user.username, password: user.password })
|
||||
|
||||
userToken2 = await servers[1].login.getAccessToken(user)
|
||||
await servers[1].videos.upload({ token: userToken2, attributes: { name: 'video user 2' } })
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } })
|
||||
videoUUID1 = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } })
|
||||
videoUUID2 = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } })
|
||||
videoUUID3 = uuid
|
||||
}
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
await doubleFollow(servers[0], servers[2])
|
||||
|
||||
{
|
||||
const created = await commentsCommand[0].createThread({ videoId: videoUUID1, text: 'comment root 1' })
|
||||
const reply = await commentsCommand[0].addReply({
|
||||
token: userToken1,
|
||||
videoId: videoUUID1,
|
||||
toCommentId: created.id,
|
||||
text: 'comment user 1'
|
||||
})
|
||||
await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: reply.id, text: 'comment root 1' })
|
||||
}
|
||||
|
||||
{
|
||||
const created = await commentsCommand[0].createThread({ token: userToken1, videoId: videoUUID1, text: 'comment user 1' })
|
||||
await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: created.id, text: 'comment root 1' })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
describe('User blocklist', function () {
|
||||
|
||||
describe('When managing account blocklist', function () {
|
||||
it('Should list all videos', function () {
|
||||
return checkAllVideos(servers[0], servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should list the comments', function () {
|
||||
return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
|
||||
})
|
||||
|
||||
it('Should block a remote account', async function () {
|
||||
await command.addToMyBlocklist({ account: 'user2@' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should hide its videos', async function () {
|
||||
const { data } = await servers[0].videos.listWithToken()
|
||||
|
||||
expect(data).to.have.lengthOf(4)
|
||||
|
||||
const v = data.find(v => v.name === 'video user 2')
|
||||
expect(v).to.be.undefined
|
||||
})
|
||||
|
||||
it('Should block a local account', async function () {
|
||||
await command.addToMyBlocklist({ account: 'user1' })
|
||||
})
|
||||
|
||||
it('Should hide its videos', async function () {
|
||||
const { data } = await servers[0].videos.listWithToken()
|
||||
|
||||
expect(data).to.have.lengthOf(3)
|
||||
|
||||
const v = data.find(v => v.name === 'video user 1')
|
||||
expect(v).to.be.undefined
|
||||
})
|
||||
|
||||
it('Should hide its comments', async function () {
|
||||
const { data } = await commentsCommand[0].listThreads({
|
||||
token: servers[0].accessToken,
|
||||
videoId: videoUUID1,
|
||||
start: 0,
|
||||
count: 25,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].totalReplies).to.equal(1)
|
||||
|
||||
const t = data.find(t => t.text === 'comment user 1')
|
||||
expect(t).to.be.undefined
|
||||
|
||||
for (const thread of data) {
|
||||
const tree = await commentsCommand[0].getThread({
|
||||
videoId: videoUUID1,
|
||||
threadId: thread.id,
|
||||
token: servers[0].accessToken
|
||||
})
|
||||
expect(tree.children).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have notifications from blocked accounts', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[0],
|
||||
token: userToken1,
|
||||
videoUUID: videoUUID2,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list all the videos with another user', async function () {
|
||||
return checkAllVideos(servers[0], userToken1)
|
||||
})
|
||||
|
||||
it('Should list blocked accounts', async function () {
|
||||
{
|
||||
const body = await command.listMyAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const block = body.data[0]
|
||||
expect(block.byAccount.displayName).to.equal('root')
|
||||
expect(block.byAccount.name).to.equal('root')
|
||||
expect(block.blockedAccount.displayName).to.equal('user2')
|
||||
expect(block.blockedAccount.name).to.equal('user2')
|
||||
expect(block.blockedAccount.host).to.equal('' + servers[1].host)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await command.listMyAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const block = body.data[0]
|
||||
expect(block.byAccount.displayName).to.equal('root')
|
||||
expect(block.byAccount.name).to.equal('root')
|
||||
expect(block.blockedAccount.displayName).to.equal('user1')
|
||||
expect(block.blockedAccount.name).to.equal('user1')
|
||||
expect(block.blockedAccount.host).to.equal('' + servers[0].host)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should search blocked accounts', async function () {
|
||||
const body = await command.listMyAccountBlocklist({ start: 0, count: 10, search: 'user2' })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
expect(body.data[0].blockedAccount.name).to.equal('user2')
|
||||
})
|
||||
|
||||
it('Should get blocked status', async function () {
|
||||
const remoteHandle = 'user2@' + servers[1].host
|
||||
const localHandle = 'user1@' + servers[0].host
|
||||
const unknownHandle = 'user5@' + servers[0].host
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ accounts: [ remoteHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(1)
|
||||
expect(status.accounts[remoteHandle].blockedByUser).to.be.false
|
||||
expect(status.accounts[remoteHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(1)
|
||||
expect(status.accounts[remoteHandle].blockedByUser).to.be.true
|
||||
expect(status.accounts[remoteHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(3)
|
||||
|
||||
for (const handle of [ localHandle, remoteHandle ]) {
|
||||
expect(status.accounts[handle].blockedByUser).to.be.true
|
||||
expect(status.accounts[handle].blockedByServer).to.be.false
|
||||
}
|
||||
|
||||
expect(status.accounts[unknownHandle].blockedByUser).to.be.false
|
||||
expect(status.accounts[unknownHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not allow a remote blocked user to comment my videos', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
{
|
||||
await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID3, text: 'comment user 2' })
|
||||
await waitJobs(servers)
|
||||
|
||||
await commentsCommand[0].createThread({ token: servers[0].accessToken, videoId: videoUUID3, text: 'uploader' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const commentId = await commentsCommand[1].findCommentId({ videoId: videoUUID3, text: 'uploader' })
|
||||
const message = 'reply by user 2'
|
||||
const reply = await commentsCommand[1].addReply({ token: userToken2, videoId: videoUUID3, toCommentId: commentId, text: message })
|
||||
await commentsCommand[1].addReply({ videoId: videoUUID3, toCommentId: reply.id, text: 'another reply' })
|
||||
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
// Server 2 has all the comments
|
||||
{
|
||||
const { data } = await commentsCommand[1].listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
|
||||
|
||||
expect(data).to.have.lengthOf(2)
|
||||
expect(data[0].text).to.equal('uploader')
|
||||
expect(data[1].text).to.equal('comment user 2')
|
||||
|
||||
const tree = await commentsCommand[1].getThread({ videoId: videoUUID3, threadId: data[0].id })
|
||||
expect(tree.children).to.have.lengthOf(1)
|
||||
expect(tree.children[0].comment.text).to.equal('reply by user 2')
|
||||
expect(tree.children[0].children).to.have.lengthOf(1)
|
||||
expect(tree.children[0].children[0].comment.text).to.equal('another reply')
|
||||
}
|
||||
|
||||
// Server 1 and 3 should only have uploader comments
|
||||
for (const server of [ servers[0], servers[2] ]) {
|
||||
const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].text).to.equal('uploader')
|
||||
|
||||
const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id })
|
||||
|
||||
if (server.serverNumber === 1) expect(tree.children).to.have.lengthOf(0)
|
||||
else expect(tree.children).to.have.lengthOf(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote account', async function () {
|
||||
await command.removeFromMyBlocklist({ account: 'user2@' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should display its videos', async function () {
|
||||
const { data } = await servers[0].videos.listWithToken()
|
||||
expect(data).to.have.lengthOf(4)
|
||||
|
||||
const v = data.find(v => v.name === 'video user 2')
|
||||
expect(v).not.to.be.undefined
|
||||
})
|
||||
|
||||
it('Should display its comments on my video', async function () {
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
|
||||
|
||||
// Server 3 should not have 2 comment threads, because server 1 did not forward the server 2 comment
|
||||
if (server.serverNumber === 3) {
|
||||
expect(data).to.have.lengthOf(1)
|
||||
continue
|
||||
}
|
||||
|
||||
expect(data).to.have.lengthOf(2)
|
||||
expect(data[0].text).to.equal('uploader')
|
||||
expect(data[1].text).to.equal('comment user 2')
|
||||
|
||||
const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id })
|
||||
expect(tree.children).to.have.lengthOf(1)
|
||||
expect(tree.children[0].comment.text).to.equal('reply by user 2')
|
||||
expect(tree.children[0].children).to.have.lengthOf(1)
|
||||
expect(tree.children[0].children[0].comment.text).to.equal('another reply')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the local account', async function () {
|
||||
await command.removeFromMyBlocklist({ account: 'user1' })
|
||||
})
|
||||
|
||||
it('Should display its comments', function () {
|
||||
return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
|
||||
})
|
||||
|
||||
it('Should have a notification from a non blocked account', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[0],
|
||||
token: userToken1,
|
||||
videoUUID: videoUUID2,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When managing server blocklist', function () {
|
||||
|
||||
it('Should list all videos', function () {
|
||||
return checkAllVideos(servers[0], servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should list the comments', function () {
|
||||
return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
|
||||
})
|
||||
|
||||
it('Should block a remote server', async function () {
|
||||
await command.addToMyBlocklist({ server: '' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should hide its videos', async function () {
|
||||
const { data } = await servers[0].videos.listWithToken()
|
||||
|
||||
expect(data).to.have.lengthOf(3)
|
||||
|
||||
const v1 = data.find(v => v.name === 'video user 2')
|
||||
const v2 = data.find(v => v.name === 'video server 2')
|
||||
|
||||
expect(v1).to.be.undefined
|
||||
expect(v2).to.be.undefined
|
||||
})
|
||||
|
||||
it('Should list all the videos with another user', async function () {
|
||||
return checkAllVideos(servers[0], userToken1)
|
||||
})
|
||||
|
||||
it('Should hide its comments', async function () {
|
||||
const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
|
||||
|
||||
await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id })
|
||||
})
|
||||
|
||||
it('Should not have notifications from blocked server', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[1],
|
||||
token: userToken2,
|
||||
videoUUID: videoUUID1,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list blocked servers', async function () {
|
||||
const body = await command.listMyServerBlocklist({ start: 0, count: 1, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
const block = body.data[0]
|
||||
expect(block.byAccount.displayName).to.equal('root')
|
||||
expect(block.byAccount.name).to.equal('root')
|
||||
expect(block.blockedServer.host).to.equal('' + servers[1].host)
|
||||
})
|
||||
|
||||
it('Should search blocked servers', async function () {
|
||||
const body = await command.listMyServerBlocklist({ start: 0, count: 10, search: servers[1].host })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
expect(body.data[0].blockedServer.host).to.equal(servers[1].host)
|
||||
})
|
||||
|
||||
it('Should get blocklist status', async function () {
|
||||
const blockedServer = servers[1].host
|
||||
const notBlockedServer = 'example.com'
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(0)
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(2)
|
||||
expect(status.hosts[blockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[blockedServer].blockedByServer).to.be.false
|
||||
|
||||
expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
|
||||
}
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(0)
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(2)
|
||||
expect(status.hosts[blockedServer].blockedByUser).to.be.true
|
||||
expect(status.hosts[blockedServer].blockedByServer).to.be.false
|
||||
|
||||
expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote server', async function () {
|
||||
await command.removeFromMyBlocklist({ server: '' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should display its videos', function () {
|
||||
return checkAllVideos(servers[0], servers[0].accessToken)
|
||||
})
|
||||
|
||||
it('Should display its comments', function () {
|
||||
return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
|
||||
})
|
||||
|
||||
it('Should have notification from unblocked server', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[1],
|
||||
token: userToken2,
|
||||
videoUUID: videoUUID1,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Server blocklist', function () {
|
||||
|
||||
describe('When managing account blocklist', function () {
|
||||
it('Should list all videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllVideos(servers[0], token)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list the comments', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllComments(servers[0], token, videoUUID1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should block a remote account', async function () {
|
||||
await command.addToServerBlocklist({ account: 'user2@' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should hide its videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
const { data } = await servers[0].videos.listWithToken({ token })
|
||||
|
||||
expect(data).to.have.lengthOf(4)
|
||||
|
||||
const v = data.find(v => v.name === 'video user 2')
|
||||
expect(v).to.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('Should block a local account', async function () {
|
||||
await command.addToServerBlocklist({ account: 'user1' })
|
||||
})
|
||||
|
||||
it('Should hide its videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
const { data } = await servers[0].videos.listWithToken({ token })
|
||||
|
||||
expect(data).to.have.lengthOf(3)
|
||||
|
||||
const v = data.find(v => v.name === 'video user 1')
|
||||
expect(v).to.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('Should hide its comments', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
const { data } = await commentsCommand[0].listThreads({ videoId: videoUUID1, count: 20, sort: '-createdAt', token })
|
||||
const threads = data.filter(t => t.isDeleted === false)
|
||||
|
||||
expect(threads).to.have.lengthOf(1)
|
||||
expect(threads[0].totalReplies).to.equal(1)
|
||||
|
||||
const t = threads.find(t => t.text === 'comment user 1')
|
||||
expect(t).to.be.undefined
|
||||
|
||||
for (const thread of threads) {
|
||||
const tree = await commentsCommand[0].getThread({ videoId: videoUUID1, threadId: thread.id, token })
|
||||
expect(tree.children).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have notification from blocked accounts by instance', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[1],
|
||||
token: userToken2,
|
||||
videoUUID: videoUUID1,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list blocked accounts', async function () {
|
||||
{
|
||||
const body = await command.listServerAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const block = body.data[0]
|
||||
expect(block.byAccount.displayName).to.equal('peertube')
|
||||
expect(block.byAccount.name).to.equal('peertube')
|
||||
expect(block.blockedAccount.displayName).to.equal('user2')
|
||||
expect(block.blockedAccount.name).to.equal('user2')
|
||||
expect(block.blockedAccount.host).to.equal('' + servers[1].host)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await command.listServerAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const block = body.data[0]
|
||||
expect(block.byAccount.displayName).to.equal('peertube')
|
||||
expect(block.byAccount.name).to.equal('peertube')
|
||||
expect(block.blockedAccount.displayName).to.equal('user1')
|
||||
expect(block.blockedAccount.name).to.equal('user1')
|
||||
expect(block.blockedAccount.host).to.equal('' + servers[0].host)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should search blocked accounts', async function () {
|
||||
const body = await command.listServerAccountBlocklist({ start: 0, count: 10, search: 'user2' })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
expect(body.data[0].blockedAccount.name).to.equal('user2')
|
||||
})
|
||||
|
||||
it('Should get blocked status', async function () {
|
||||
const remoteHandle = 'user2@' + servers[1].host
|
||||
const localHandle = 'user1@' + servers[0].host
|
||||
const unknownHandle = 'user5@' + servers[0].host
|
||||
|
||||
for (const token of [ undefined, servers[0].accessToken ]) {
|
||||
const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(3)
|
||||
|
||||
for (const handle of [ localHandle, remoteHandle ]) {
|
||||
expect(status.accounts[handle].blockedByUser).to.be.false
|
||||
expect(status.accounts[handle].blockedByServer).to.be.true
|
||||
}
|
||||
|
||||
expect(status.accounts[unknownHandle].blockedByUser).to.be.false
|
||||
expect(status.accounts[unknownHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote account', async function () {
|
||||
await command.removeFromServerBlocklist({ account: 'user2@' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should display its videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
const { data } = await servers[0].videos.listWithToken({ token })
|
||||
expect(data).to.have.lengthOf(4)
|
||||
|
||||
const v = data.find(v => v.name === 'video user 2')
|
||||
expect(v).not.to.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the local account', async function () {
|
||||
await command.removeFromServerBlocklist({ account: 'user1' })
|
||||
})
|
||||
|
||||
it('Should display its comments', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllComments(servers[0], token, videoUUID1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have notifications from unblocked accounts', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'displayed comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[1],
|
||||
token: userToken2,
|
||||
videoUUID: videoUUID1,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When managing server blocklist', function () {
|
||||
|
||||
it('Should list all videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllVideos(servers[0], token)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list the comments', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllComments(servers[0], token, videoUUID1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should block a remote server', async function () {
|
||||
await command.addToServerBlocklist({ server: '' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should hide its videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
const requests = [
|
||||
servers[0].videos.list(),
|
||||
servers[0].videos.listWithToken({ token })
|
||||
]
|
||||
|
||||
for (const req of requests) {
|
||||
const { data } = await req
|
||||
expect(data).to.have.lengthOf(3)
|
||||
|
||||
const v1 = data.find(v => v.name === 'video user 2')
|
||||
const v2 = data.find(v => v.name === 'video server 2')
|
||||
|
||||
expect(v1).to.be.undefined
|
||||
expect(v2).to.be.undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should hide its comments', async function () {
|
||||
const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
|
||||
|
||||
await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id })
|
||||
})
|
||||
|
||||
it('Should not have notification from blocked instances by instance', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[1],
|
||||
token: userToken2,
|
||||
videoUUID: videoUUID1,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'absence')
|
||||
}
|
||||
|
||||
{
|
||||
const now = new Date()
|
||||
await servers[1].follows.unfollow({ target: servers[0] })
|
||||
await waitJobs(servers)
|
||||
await servers[1].follows.follow({ hosts: [ servers[0].host ] })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].notifications.list({ start: 0, count: 30 })
|
||||
const commentNotifications = data.filter(n => {
|
||||
return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString()
|
||||
})
|
||||
|
||||
expect(commentNotifications).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list blocked servers', async function () {
|
||||
const body = await command.listServerServerBlocklist({ start: 0, count: 1, sort: 'createdAt' })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
const block = body.data[0]
|
||||
expect(block.byAccount.displayName).to.equal('peertube')
|
||||
expect(block.byAccount.name).to.equal('peertube')
|
||||
expect(block.blockedServer.host).to.equal('' + servers[1].host)
|
||||
})
|
||||
|
||||
it('Should search blocked servers', async function () {
|
||||
const body = await command.listServerServerBlocklist({ start: 0, count: 10, search: servers[1].host })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
expect(body.data[0].blockedServer.host).to.equal(servers[1].host)
|
||||
})
|
||||
|
||||
it('Should get blocklist status', async function () {
|
||||
const blockedServer = servers[1].host
|
||||
const notBlockedServer = 'example.com'
|
||||
|
||||
for (const token of [ undefined, servers[0].accessToken ]) {
|
||||
const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(0)
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(2)
|
||||
expect(status.hosts[blockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[blockedServer].blockedByServer).to.be.true
|
||||
|
||||
expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote server', async function () {
|
||||
await command.removeFromServerBlocklist({ server: '' + servers[1].host })
|
||||
})
|
||||
|
||||
it('Should list all videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllVideos(servers[0], token)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list the comments', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllComments(servers[0], token, videoUUID1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have notification from unblocked instances', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
{
|
||||
const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
|
||||
{
|
||||
const comment = {
|
||||
server: servers[1],
|
||||
token: userToken2,
|
||||
videoUUID: videoUUID1,
|
||||
text: 'hello @root@' + servers[0].host
|
||||
}
|
||||
await checkCommentNotification(servers[0], comment, 'presence')
|
||||
}
|
||||
|
||||
{
|
||||
const now = new Date()
|
||||
await servers[1].follows.unfollow({ target: servers[0] })
|
||||
await waitJobs(servers)
|
||||
await servers[1].follows.follow({ hosts: [ servers[0].host ] })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].notifications.list({ start: 0, count: 30 })
|
||||
const commentNotifications = data.filter(n => {
|
||||
return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString()
|
||||
})
|
||||
|
||||
expect(commentNotifications).to.have.lengthOf(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,552 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
ActivityApproveReply,
|
||||
ActivityPubOrderedCollection,
|
||||
HttpStatusCode,
|
||||
UserRole,
|
||||
VideoCommentObject,
|
||||
VideoCommentPolicy,
|
||||
VideoCommentPolicyType,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests, createMultipleServers,
|
||||
doubleFollow,
|
||||
makeActivityPubGetRequest, makeActivityPubRawRequest, setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expectStartWith } from '@tests/shared/checks.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test comments approval', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let userToken: string
|
||||
let anotherUserToken: string
|
||||
let moderatorToken: string
|
||||
|
||||
async function createVideo (commentsPolicy: VideoCommentPolicyType) {
|
||||
const { uuid } = await servers[0].videos.upload({
|
||||
token: userToken,
|
||||
attributes: {
|
||||
name: 'review policy: ' + commentsPolicy,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
commentsPolicy
|
||||
}
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(3)
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultAccountAvatar(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
await doubleFollow(servers[0], servers[2])
|
||||
await doubleFollow(servers[1], servers[2])
|
||||
|
||||
userToken = await servers[0].users.generateUserAndToken('user1')
|
||||
anotherUserToken = await servers[0].users.generateUserAndToken('user2')
|
||||
moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR)
|
||||
})
|
||||
|
||||
describe('On video with comments requiring approval', function () {
|
||||
let videoId: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
videoId = await createVideo(VideoCommentPolicy.REQUIRES_APPROVAL)
|
||||
})
|
||||
|
||||
it('Should create a local and remote comment that require approval', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await servers[0].comments.createThread({ text: 'local', videoId, token: anotherUserToken })
|
||||
await servers[1].comments.createThread({ text: 'remote', videoId })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
for (const c of data) {
|
||||
expect(c.heldForReview).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('Should display comments depending on the user', async function () {
|
||||
// Owner see the comments
|
||||
{
|
||||
const { data } = await servers[0].comments.listThreads({ videoId, token: userToken })
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
for (const c of data) {
|
||||
expect(c.heldForReview).to.be.true
|
||||
}
|
||||
}
|
||||
|
||||
// Anonymous doesn't see the comments
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listThreads({ videoId })
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
// Owner of the comment can see it
|
||||
{
|
||||
const { data } = await servers[1].comments.listThreads({ videoId, token: servers[1].accessToken })
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].heldForReview).to.be.true
|
||||
expect(data[0].text).to.equal('remote')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should create a local and remote reply and require approval', async function () {
|
||||
await servers[0].comments.addReplyToLastThread({ text: 'local reply', token: anotherUserToken })
|
||||
await servers[1].comments.addReplyToLastThread({ text: 'remote reply' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
expect(data).to.have.lengthOf(4)
|
||||
|
||||
for (const c of data) {
|
||||
expect(c.heldForReview).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('Should approve a thread comment', async function () {
|
||||
{
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
const commentId = data.find(c => c.text === 'remote').id
|
||||
await servers[0].comments.approve({ commentId, videoId, token: userToken })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
// Owner and moderators
|
||||
for (const token of [ userToken, moderatorToken ]) {
|
||||
const { data: threads } = await servers[0].comments.listThreads({ videoId, token })
|
||||
expect(threads).to.have.lengthOf(2)
|
||||
|
||||
for (const c of threads) {
|
||||
if (c.text === 'remote') expect(c.heldForReview).to.be.false
|
||||
else expect(c.heldForReview).to.be.true
|
||||
|
||||
const thread = await servers[0].comments.getThread({ videoId, threadId: c.id, token })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.heldForReview).to.equal(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Anonymous
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listThreads({ videoId })
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].heldForReview).to.be.false
|
||||
expect(data[0].text).to.equal('remote')
|
||||
|
||||
const thread = await server.comments.getThread({ videoId, threadId: data[0].id })
|
||||
expect(thread.children).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
// Owner of the comment can see it
|
||||
{
|
||||
const { data } = await servers[1].comments.listThreads({ videoId, token: servers[1].accessToken })
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].heldForReview).to.be.false
|
||||
expect(data[0].text).to.equal('remote')
|
||||
|
||||
const thread = await servers[1].comments.getThread({ videoId, threadId: data[0].id, token: servers[1].accessToken })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.heldForReview).to.equal(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should approve a reply comment', async function () {
|
||||
{
|
||||
const commentId = await servers[0].comments.findCommentId({ videoId, text: 'remote reply' })
|
||||
await servers[0].comments.approve({ commentId, videoId, token: userToken })
|
||||
await waitJobs(servers)
|
||||
|
||||
// Owner
|
||||
{
|
||||
const { data } = await servers[0].comments.listThreads({ videoId, token: userToken })
|
||||
expect(data.filter(c => c.heldForReview)).to.have.lengthOf(1)
|
||||
|
||||
const thread = await servers[0].comments.getThreadOf({ videoId, text: 'remote', token: userToken })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.text).to.equal('remote reply')
|
||||
expect(thread.children[0].comment.heldForReview).to.be.false
|
||||
}
|
||||
|
||||
// Other users
|
||||
for (const server of servers) {
|
||||
const thread = await server.comments.getThreadOf({ videoId, text: 'remote' })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.text).to.equal('remote reply')
|
||||
expect(thread.children[0].comment.heldForReview).to.be.false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list and filter on comments awaiting approval', async function () {
|
||||
{
|
||||
const { total, data } = await servers[0].comments.listCommentsOnMyVideos({ videoId, token: userToken })
|
||||
expect(total).to.equal(4)
|
||||
expect(data).to.have.lengthOf(4)
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[0].comments.listCommentsOnMyVideos({ videoId, token: userToken, isHeldForReview: true })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
expect(data.filter(c => c.heldForReview)).to.have.lengthOf(2)
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await servers[0].comments.listCommentsOnMyVideos({ videoId, token: userToken, isHeldForReview: false })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
expect(data.filter(c => !c.heldForReview)).to.have.lengthOf(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should approve a reply of a non approved reply', async function () {
|
||||
const threadId = await servers[0].comments.findCommentId({ videoId, text: 'local' })
|
||||
|
||||
const { id: replyId } = await servers[0].comments.addReply({
|
||||
videoId,
|
||||
toCommentId: await servers[0].comments.findCommentId({ videoId, text: 'local reply' }),
|
||||
text: 'local reply 2',
|
||||
token: anotherUserToken
|
||||
})
|
||||
|
||||
await servers[0].comments.approve({ commentId: replyId, videoId, token: userToken })
|
||||
await servers[0].comments.approve({ commentId: threadId, videoId, token: userToken })
|
||||
await waitJobs(servers)
|
||||
|
||||
// Owner
|
||||
{
|
||||
const { data } = await servers[0].comments.listThreads({ videoId, token: userToken })
|
||||
expect(data.filter(c => c.heldForReview)).to.have.lengthOf(0)
|
||||
|
||||
const thread = await servers[0].comments.getThreadOf({ videoId, text: 'local', token: userToken })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.text).to.equal('local reply')
|
||||
expect(thread.children[0].comment.heldForReview).to.be.true
|
||||
|
||||
expect(thread.children[0].children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].children[0].comment.text).to.equal('local reply 2')
|
||||
expect(thread.children[0].children[0].comment.heldForReview).to.be.false
|
||||
}
|
||||
|
||||
// Other users
|
||||
for (const server of servers) {
|
||||
const thread = await server.comments.getThreadOf({ videoId, text: 'local' })
|
||||
expect(thread.children).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have appropriate ActivityPub representation', async function () {
|
||||
const localNonApprovedId = await servers[0].comments.findCommentId({ text: 'local reply', videoId })
|
||||
const localApprovedId = await servers[0].comments.findCommentId({ text: 'local', videoId })
|
||||
const remoteApprovedId = await servers[0].comments.findCommentId({ text: 'remote', videoId })
|
||||
|
||||
{
|
||||
for (const page of [ 1, 2 ]) {
|
||||
const res = await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${videoId}/comments?page=${page}`)
|
||||
const { totalItems, orderedItems } = res.body as ActivityPubOrderedCollection<string>
|
||||
|
||||
expect(totalItems).to.equal(4)
|
||||
expect(orderedItems.some(url => url === `${servers[0].url}/videos/watch/${videoId}/comments/${localNonApprovedId}`)).to.be.false
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
await makeActivityPubGetRequest(
|
||||
servers[0].url,
|
||||
`/videos/watch/${videoId}/comments/${localNonApprovedId}`,
|
||||
HttpStatusCode.NOT_FOUND_404
|
||||
)
|
||||
|
||||
await makeActivityPubGetRequest(
|
||||
servers[0].url,
|
||||
`/videos/watch/${videoId}/comments/${localNonApprovedId}/approve-reply`,
|
||||
HttpStatusCode.NOT_FOUND_404
|
||||
)
|
||||
}
|
||||
|
||||
const toTest = [ { server: servers[0], commentId: localApprovedId }, { server: servers[1], commentId: remoteApprovedId } ]
|
||||
for (const { server, commentId } of toTest) {
|
||||
const res = await makeActivityPubGetRequest(server.url, `/videos/watch/${videoId}/comments/${commentId}`)
|
||||
const { replyApproval } = res.body as VideoCommentObject
|
||||
|
||||
expectStartWith(replyApproval, `${servers[0].url}/videos/watch/${videoId}/comments/`)
|
||||
const res2 = await makeActivityPubRawRequest(replyApproval, HttpStatusCode.OK_200)
|
||||
|
||||
const object = res2.body as ActivityApproveReply
|
||||
expect(object.type).to.equal('ApproveReply')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should remove an approved/non-approved comments', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
{
|
||||
const commentId = await servers[1].comments.findCommentId({ videoId, text: 'local' })
|
||||
await servers[1].comments.addReply({ videoId, toCommentId: commentId, text: 'remote reply on local' })
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
for (const text of [ 'remote', 'local reply', 'remote reply on local' ]) {
|
||||
const commentId = await servers[0].comments.findCommentId({ videoId, text })
|
||||
await servers[0].comments.delete({ videoId, commentId })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// Owner
|
||||
{
|
||||
const { data } = await servers[0].comments.listThreads({ videoId, token: userToken, sort: '-createdAt' })
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
{
|
||||
const remote = data[0]
|
||||
expect(remote.isDeleted).to.be.true
|
||||
|
||||
const thread = await servers[0].comments.getThread({ videoId, token: userToken, threadId: remote.id })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.text).to.equal('remote reply')
|
||||
}
|
||||
|
||||
{
|
||||
const local = data[1]
|
||||
expect(local.isDeleted).to.be.false
|
||||
|
||||
const thread = await servers[0].comments.getThread({ videoId, token: userToken, threadId: local.id })
|
||||
expect(thread.children).to.have.lengthOf(2)
|
||||
|
||||
{
|
||||
const localReply = thread.children[0]
|
||||
expect(localReply.comment.deletedAt).to.exist
|
||||
expect(localReply.comment.heldForReview).to.be.true
|
||||
expect(localReply.children).to.have.lengthOf(1)
|
||||
|
||||
expect(localReply.children).to.have.lengthOf(1)
|
||||
expect(localReply.children[0].comment.text).to.equal('local reply 2')
|
||||
expect(localReply.children[0].comment.heldForReview).to.be.false
|
||||
expect(localReply.children[0].children).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
expect(thread.children[1].comment.deletedAt).to.exist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other users
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listThreads({ videoId, sort: '-createdAt' })
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
{
|
||||
const remote = data[0]
|
||||
expect(remote.isDeleted).to.be.true
|
||||
|
||||
const thread = await server.comments.getThread({ videoId, threadId: remote.id })
|
||||
expect(thread.children).to.have.lengthOf(1)
|
||||
expect(thread.children[0].comment.text).to.equal('remote reply')
|
||||
}
|
||||
|
||||
{
|
||||
const local = data[1]
|
||||
expect(local.isDeleted).to.be.false
|
||||
|
||||
const thread = await server.comments.getThread({ videoId, threadId: local.id })
|
||||
// Anonymous users cannot see the thread because the delete comment was held for review
|
||||
expect(thread.children).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not require review for video uploader, admins and moderators', async function () {
|
||||
for (const token of [ userToken, moderatorToken, servers[0].accessToken ]) {
|
||||
await servers[0].comments.createThread({ videoId, text: 'right', token })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.comments.listThreads({ videoId, sort: '-createdAt' })
|
||||
expect(data.filter(c => c.text === 'right')).to.have.lengthOf(3)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('On video with comments with some tags requiring approval', function () {
|
||||
let videoId: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
videoId = await createVideo(VideoCommentPolicy.ENABLED)
|
||||
})
|
||||
|
||||
it('Should only have built-in auto tag policies and no policies set', async function () {
|
||||
const { review } = await servers[0].autoTags.getCommentPolicies({ accountName: 'user1', token: userToken })
|
||||
expect(review).to.have.lengthOf(0)
|
||||
|
||||
const { available } = await servers[0].autoTags.getAccountAvailable({ accountName: 'user1', token: userToken })
|
||||
expect(available.map(a => a.name)).to.deep.equal([ 'external-link' ])
|
||||
})
|
||||
|
||||
it('Should add watched words and so available tag policies', async function () {
|
||||
await servers[0].watchedWordsLists.createList({
|
||||
token: userToken,
|
||||
listName: 'forbidden-list',
|
||||
words: [ 'forbidden' ],
|
||||
accountName: 'user1'
|
||||
})
|
||||
|
||||
await servers[0].watchedWordsLists.createList({
|
||||
token: userToken,
|
||||
listName: 'allowed-list',
|
||||
words: [ 'allowed' ],
|
||||
accountName: 'user1'
|
||||
})
|
||||
|
||||
const { review } = await servers[0].autoTags.getCommentPolicies({ accountName: 'user1', token: userToken })
|
||||
expect(review).to.have.lengthOf(0)
|
||||
|
||||
const { available } = await servers[0].autoTags.getAccountAvailable({ accountName: 'user1', token: userToken })
|
||||
expect(available.map(a => a.name)).to.have.deep.members([ 'external-link', 'forbidden-list', 'allowed-list' ])
|
||||
})
|
||||
|
||||
it('Should update policies', async function () {
|
||||
await servers[0].autoTags.updateCommentPolicies({
|
||||
accountName: 'user1',
|
||||
review: [ 'external-link', 'forbidden-list' ],
|
||||
token: userToken
|
||||
})
|
||||
|
||||
const { review } = await servers[0].autoTags.getCommentPolicies({ accountName: 'user1', token: userToken })
|
||||
expect(review).to.have.deep.members([ 'external-link', 'forbidden-list' ])
|
||||
})
|
||||
|
||||
it('Should publish a comment without approval', async function () {
|
||||
const threadText = '1 - framasoft and allowed'
|
||||
const replyText = '1 - frama and allowed'
|
||||
|
||||
await servers[0].comments.createThread({ token: anotherUserToken, videoId, text: threadText })
|
||||
await waitJobs(servers)
|
||||
|
||||
const commentId = await servers[1].comments.findCommentId({ videoId, text: threadText })
|
||||
await servers[1].comments.addReply({ text: replyText, videoId, toCommentId: commentId })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
const t = data.find(c => c.text === threadText)
|
||||
const r = data.find(c => c.text === replyText)
|
||||
|
||||
expect(t.automaticTags).to.have.members([ 'allowed-list' ])
|
||||
expect(t.heldForReview).to.be.false
|
||||
|
||||
expect(r.automaticTags).to.have.members([ 'allowed-list' ])
|
||||
expect(r.heldForReview).to.be.false
|
||||
})
|
||||
|
||||
it('Should publish a comment with approval', async function () {
|
||||
const threadText = '2 - framasoft.org and allowed'
|
||||
const replyText = '2 - https://framasoft.org and forbidden'
|
||||
|
||||
await servers[1].comments.createThread({ videoId, text: threadText })
|
||||
await waitJobs(servers)
|
||||
|
||||
const commentId = await servers[0].comments.findCommentId({ videoId, text: threadText })
|
||||
await servers[0].comments.addReply({ token: anotherUserToken, text: replyText, videoId, toCommentId: commentId })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
const t = data.find(c => c.text === threadText)
|
||||
const r = data.find(c => c.text === replyText)
|
||||
|
||||
expect(t.automaticTags).to.have.members([ 'external-link', 'allowed-list' ])
|
||||
expect(t.heldForReview).to.be.true
|
||||
|
||||
expect(r.automaticTags).to.have.members([ 'external-link', 'forbidden-list' ])
|
||||
expect(r.heldForReview).to.be.true
|
||||
})
|
||||
|
||||
it('Should update policies and not update previously tags set', async function () {
|
||||
await servers[0].autoTags.updateCommentPolicies({ accountName: 'user1', review: [ 'forbidden-list' ], token: userToken })
|
||||
|
||||
const { review } = await servers[0].autoTags.getCommentPolicies({ accountName: 'user1', token: userToken })
|
||||
expect(review).to.have.deep.members([ 'forbidden-list' ])
|
||||
|
||||
const { available } = await servers[0].autoTags.getAccountAvailable({ accountName: 'user1', token: userToken })
|
||||
expect(available.map(a => a.name)).to.have.deep.members([ 'external-link', 'forbidden-list', 'allowed-list' ])
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ videoId, token: userToken })
|
||||
expect(data.filter(c => c.heldForReview)).to.have.lengthOf(2)
|
||||
})
|
||||
|
||||
it('Should publish a comment with and without approval base on the new policies', async function () {
|
||||
const threadText = '3 - framasoft.org and allowed'
|
||||
const replyText = '3 - forbidden'
|
||||
|
||||
await servers[0].comments.createThread({ token: anotherUserToken, videoId, text: threadText })
|
||||
await servers[0].comments.addReplyToLastThread({ token: anotherUserToken, text: replyText })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
const t = data.find(c => c.text === threadText)
|
||||
const r = data.find(c => c.text === replyText)
|
||||
|
||||
expect(t.automaticTags).to.have.members([ 'external-link', 'allowed-list' ])
|
||||
expect(t.heldForReview).to.be.false
|
||||
|
||||
expect(r.automaticTags).to.have.members([ 'forbidden-list' ])
|
||||
expect(r.heldForReview).to.be.true
|
||||
})
|
||||
|
||||
it('Should not require approval for a moderator but it should have the tag set', async function () {
|
||||
await servers[0].comments.createThread({ token: moderatorToken, videoId, text: 'forbidden' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listCommentsOnMyVideos({ token: userToken })
|
||||
const t = data.find(c => c.text === 'forbidden')
|
||||
|
||||
expect(t.automaticTags).to.have.members([ 'forbidden-list' ])
|
||||
expect(t.heldForReview).to.be.false
|
||||
})
|
||||
|
||||
it('Should not have threads waiting for approval before approbation for anonymous users on server 1 and 3', async function () {
|
||||
for (const server of [ servers[0], servers[2] ]) {
|
||||
const { data } = await server.comments.listThreads({ videoId })
|
||||
expect(data).to.have.lengthOf(3)
|
||||
|
||||
expect(data.some(c => c.text === '2 - framasoft.org and allowed')).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should see threads waiting for approval before approbation for anonymous users on server 2', async function () {
|
||||
const { data } = await servers[1].comments.listThreads({ videoId })
|
||||
expect(data).to.have.lengthOf(4)
|
||||
|
||||
expect(data.some(c => c.text === '2 - framasoft.org and allowed')).to.be.true
|
||||
expect(data.some(c => c.text === '2 - https://framasoft.org and forbidden')).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './abuses.js'
|
||||
export * from './automatic-tags.js'
|
||||
export * from './blocklist-notification.js'
|
||||
export * from './blocklist.js'
|
||||
export * from './video-blacklist.js'
|
||||
export * from './watched-words.js'
|
||||
@@ -0,0 +1,408 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { sortObjectComparator } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
|
||||
import {
|
||||
BlacklistCommand,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow, makeActivityPubGetRequest, PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultChannelAvatar,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video blacklist', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let videoId: number
|
||||
let command: BlacklistCommand
|
||||
|
||||
async function blacklistVideosOnServer (server: PeerTubeServer) {
|
||||
const { data } = await server.videos.list()
|
||||
|
||||
for (const video of data) {
|
||||
await server.blacklist.add({ videoId: video.id, reason: 'super reason' })
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
// Run servers
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
await setDefaultChannelAvatar(servers[0])
|
||||
|
||||
// Upload 2 videos on server 2
|
||||
await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } })
|
||||
await servers[1].videos.upload({ attributes: { name: 'My 2nd video', description: 'A video on server 2' } })
|
||||
|
||||
// Wait videos propagation, server 2 has transcoding enabled
|
||||
await waitJobs(servers)
|
||||
|
||||
command = servers[0].blacklist
|
||||
|
||||
// Blacklist the two videos on server 1
|
||||
await blacklistVideosOnServer(servers[0])
|
||||
})
|
||||
|
||||
describe('When listing/searching videos', function () {
|
||||
|
||||
it('Should not have the video blacklisted in videos list/search on server 1', async function () {
|
||||
{
|
||||
const { total, data } = await servers[0].videos.list()
|
||||
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.be.an('array')
|
||||
expect(data.length).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await servers[0].search.searchVideos({ search: 'video' })
|
||||
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.be.an('array')
|
||||
expect(body.data.length).to.equal(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have the blacklisted video in videos list/search on server 2', async function () {
|
||||
{
|
||||
const { total, data } = await servers[1].videos.list()
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.be.an('array')
|
||||
expect(data.length).to.equal(2)
|
||||
}
|
||||
|
||||
{
|
||||
const body = await servers[1].search.searchVideos({ search: 'video' })
|
||||
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data).to.be.an('array')
|
||||
expect(body.data.length).to.equal(2)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing manually blacklisted videos', function () {
|
||||
it('Should display all the blacklisted videos', async function () {
|
||||
const body = await command.list()
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(2)
|
||||
|
||||
for (const blacklistedVideo of blacklistedVideos) {
|
||||
expect(blacklistedVideo.reason).to.equal('super reason')
|
||||
videoId = blacklistedVideo.video.id
|
||||
}
|
||||
})
|
||||
|
||||
it('Should display all the blacklisted videos when applying manual type filter', async function () {
|
||||
const body = await command.list({ type: VideoBlacklistType.MANUAL })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('Should display nothing when applying automatic type filter', async function () {
|
||||
const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
|
||||
expect(body.total).to.equal(0)
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should get the correct sort when sorting by descending id', async function () {
|
||||
const body = await command.list({ sort: '-id' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(2)
|
||||
|
||||
const result = [ ...body.data ].sort(sortObjectComparator('id', 'desc'))
|
||||
expect(blacklistedVideos).to.deep.equal(result)
|
||||
})
|
||||
|
||||
it('Should get the correct sort when sorting by descending video name', async function () {
|
||||
const body = await command.list({ sort: '-name' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(2)
|
||||
|
||||
const result = [ ...body.data ].sort(sortObjectComparator('name', 'desc'))
|
||||
expect(blacklistedVideos).to.deep.equal(result)
|
||||
})
|
||||
|
||||
it('Should get the correct sort when sorting by ascending creation date', async function () {
|
||||
const body = await command.list({ sort: 'createdAt' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
expect(blacklistedVideos).to.be.an('array')
|
||||
expect(blacklistedVideos.length).to.equal(2)
|
||||
|
||||
const result = [ ...body.data ].sort(sortObjectComparator('createdAt', 'asc'))
|
||||
expect(blacklistedVideos).to.deep.equal(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating blacklisted videos', function () {
|
||||
it('Should change the reason', async function () {
|
||||
await command.update({ videoId, reason: 'my super reason updated' })
|
||||
|
||||
const body = await command.list({ sort: '-name' })
|
||||
const video = body.data.find(b => b.video.id === videoId)
|
||||
|
||||
expect(video.reason).to.equal('my super reason updated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When listing my videos', function () {
|
||||
it('Should display blacklisted videos', async function () {
|
||||
await blacklistVideosOnServer(servers[1])
|
||||
|
||||
const { total, data } = await servers[1].videos.listMyVideos()
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
for (const video of data) {
|
||||
expect(video.blacklisted).to.be.true
|
||||
expect(video.blacklistedReason).to.equal('super reason')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When removing a blacklisted video', function () {
|
||||
let videoToRemove: VideoBlacklist
|
||||
let blacklist = []
|
||||
|
||||
it('Should not have any video in videos list on server 1', async function () {
|
||||
const { total, data } = await servers[0].videos.list()
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.be.an('array')
|
||||
expect(data.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should remove a video from the blacklist on server 1', async function () {
|
||||
// Get one video in the blacklist
|
||||
const body = await command.list({ sort: '-name' })
|
||||
videoToRemove = body.data[0]
|
||||
blacklist = body.data.slice(1)
|
||||
|
||||
// Remove it
|
||||
await command.remove({ videoId: videoToRemove.video.id })
|
||||
})
|
||||
|
||||
it('Should have the ex-blacklisted video in videos list on server 1', async function () {
|
||||
const { total, data } = await servers[0].videos.list()
|
||||
expect(total).to.equal(1)
|
||||
|
||||
expect(data).to.be.an('array')
|
||||
expect(data.length).to.equal(1)
|
||||
|
||||
expect(data[0].name).to.equal(videoToRemove.video.name)
|
||||
expect(data[0].id).to.equal(videoToRemove.video.id)
|
||||
})
|
||||
|
||||
it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () {
|
||||
const body = await command.list({ sort: '-name' })
|
||||
expect(body.total).to.equal(1)
|
||||
|
||||
const videos = body.data
|
||||
expect(videos).to.be.an('array')
|
||||
expect(videos.length).to.equal(1)
|
||||
expect(videos).to.deep.equal(blacklist)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When blacklisting local videos', function () {
|
||||
let video3UUID: string
|
||||
let video4UUID: string
|
||||
|
||||
before(async function () {
|
||||
{
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 3' } })
|
||||
video3UUID = uuid
|
||||
}
|
||||
{
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 4' } })
|
||||
video4UUID = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should blacklist video 3 and keep it federated', async function () {
|
||||
await command.add({ videoId: video3UUID, reason: 'super reason', unfederate: false })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const { data } = await servers[0].videos.list()
|
||||
expect(data.find(v => v.uuid === video3UUID)).to.be.undefined
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await servers[1].videos.list()
|
||||
expect(data.find(v => v.uuid === video3UUID)).to.not.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unfederate the video', async function () {
|
||||
await command.add({ videoId: video4UUID, reason: 'super reason', unfederate: true })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.videos.list()
|
||||
expect(data.find(v => v.uuid === video4UUID)).to.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have the video unfederated even after an Update AP message', async function () {
|
||||
await servers[0].videos.update({ id: video4UUID, attributes: { description: 'super description' } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.videos.list()
|
||||
expect(data.find(v => v.uuid === video4UUID)).to.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have the correct video blacklist unfederate attribute', async function () {
|
||||
const body = await command.list({ sort: 'createdAt' })
|
||||
|
||||
const blacklistedVideos = body.data
|
||||
const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID)
|
||||
const video4Blacklisted = blacklistedVideos.find(b => b.video.uuid === video4UUID)
|
||||
|
||||
expect(video3Blacklisted.unfederated).to.be.false
|
||||
expect(video4Blacklisted.unfederated).to.be.true
|
||||
})
|
||||
|
||||
it('Should not have AP comments/announces/likes/dislikes', async function () {
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/comments`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/announces`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/likes`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/dislikes`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
})
|
||||
|
||||
it('Should remove the video from blacklist and refederate the video', async function () {
|
||||
await command.remove({ videoId: video4UUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.videos.list()
|
||||
expect(data.find(v => v.uuid === video4UUID)).to.not.be.undefined
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('When auto blacklist videos', function () {
|
||||
let userWithoutFlag: string
|
||||
let userWithFlag: string
|
||||
let channelOfUserWithoutFlag: number
|
||||
|
||||
before(async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await servers[0].config.enableAutoBlacklist()
|
||||
|
||||
{
|
||||
const user = { username: 'user_without_flag', password: 'password' }
|
||||
await servers[0].users.create({
|
||||
username: user.username,
|
||||
adminFlags: UserAdminFlag.NONE,
|
||||
password: user.password,
|
||||
role: UserRole.USER
|
||||
})
|
||||
|
||||
userWithoutFlag = await servers[0].login.getAccessToken(user)
|
||||
|
||||
const { videoChannels } = await servers[0].users.getMyInfo({ token: userWithoutFlag })
|
||||
channelOfUserWithoutFlag = videoChannels[0].id
|
||||
}
|
||||
|
||||
{
|
||||
const user = { username: 'user_with_flag', password: 'password' }
|
||||
await servers[0].users.create({
|
||||
username: user.username,
|
||||
adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST,
|
||||
password: user.password,
|
||||
role: UserRole.USER
|
||||
})
|
||||
|
||||
userWithFlag = await servers[0].login.getAccessToken(user)
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should auto blacklist a video on upload', async function () {
|
||||
await servers[0].videos.upload({ token: userWithoutFlag, attributes: { name: 'blacklisted' } })
|
||||
|
||||
const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data[0].video.name).to.equal('blacklisted')
|
||||
})
|
||||
|
||||
it('Should auto blacklist a video on URL import', async function () {
|
||||
this.timeout(15000)
|
||||
|
||||
const attributes = {
|
||||
targetUrl: FIXTURE_URLS.goodVideo,
|
||||
name: 'URL import',
|
||||
channelId: channelOfUserWithoutFlag
|
||||
}
|
||||
await servers[0].videoImports.importVideo({ token: userWithoutFlag, attributes })
|
||||
|
||||
const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data[1].video.name).to.equal('URL import')
|
||||
})
|
||||
|
||||
it('Should auto blacklist a video on torrent import', async function () {
|
||||
const attributes = {
|
||||
magnetUri: FIXTURE_URLS.magnet,
|
||||
name: 'Torrent import',
|
||||
channelId: channelOfUserWithoutFlag
|
||||
}
|
||||
await servers[0].videoImports.importVideo({ token: userWithoutFlag, attributes })
|
||||
|
||||
const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
|
||||
expect(body.total).to.equal(3)
|
||||
expect(body.data[2].video.name).to.equal('Torrent import')
|
||||
})
|
||||
|
||||
it('Should not auto blacklist a video on upload if the user has the bypass blacklist flag', async function () {
|
||||
await servers[0].videos.upload({ token: userWithFlag, attributes: { name: 'not blacklisted' } })
|
||||
|
||||
const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
|
||||
expect(body.total).to.equal(3)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,189 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test watched words', function () {
|
||||
let server: PeerTubeServer
|
||||
let userToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultAccountAvatar([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
})
|
||||
|
||||
function runTests (mode: 'server' | 'account') {
|
||||
let listId: number
|
||||
let accountName: string
|
||||
let token: string
|
||||
|
||||
before(() => {
|
||||
accountName = mode === 'server'
|
||||
? undefined
|
||||
: 'user1'
|
||||
|
||||
token = mode === 'server'
|
||||
? server.accessToken
|
||||
: userToken
|
||||
})
|
||||
|
||||
it('Should list empty watched words', async function () {
|
||||
const { data, total } = await server.watchedWordsLists.listWordsLists({ token, accountName })
|
||||
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should add watched words lists', async function () {
|
||||
{
|
||||
const { watchedWordsList } = await server.watchedWordsLists.createList({
|
||||
token,
|
||||
listName: 'list user one',
|
||||
words: [ 'word1' ],
|
||||
accountName
|
||||
})
|
||||
|
||||
listId = watchedWordsList.id
|
||||
}
|
||||
|
||||
{
|
||||
await server.watchedWordsLists.createList({
|
||||
token,
|
||||
listName: 'list user two',
|
||||
words: [ 'word2', 'word3' ],
|
||||
accountName
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === 'account') {
|
||||
await server.watchedWordsLists.createList({
|
||||
listName: 'list one',
|
||||
words: [ 'word4', 'word5' ],
|
||||
accountName: 'root'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('Should list watched words', async function () {
|
||||
if (mode === 'account') {
|
||||
const { data, total } = await server.watchedWordsLists.listWordsLists({ accountName: 'root' })
|
||||
|
||||
expect(total).to.equal(1)
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].id).to.exist
|
||||
expect(data[0].createdAt).to.exist
|
||||
expect(data[0].updatedAt).to.exist
|
||||
expect(data[0].listName).to.equal('list one')
|
||||
expect(data[0].words).to.deep.equal([ 'word4', 'word5' ])
|
||||
}
|
||||
|
||||
// With sort, start, count
|
||||
{
|
||||
const { data, total } = await server.watchedWordsLists.listWordsLists({
|
||||
token,
|
||||
accountName,
|
||||
sort: 'createdAt'
|
||||
})
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
expect(data[0].listName).to.equal('list user one')
|
||||
expect(data[0].words).to.deep.equal([ 'word1' ])
|
||||
|
||||
expect(data[1].listName).to.equal('list user two')
|
||||
expect(data[1].words).to.deep.equal([ 'word2', 'word3' ])
|
||||
}
|
||||
|
||||
{
|
||||
const { data, total } = await server.watchedWordsLists.listWordsLists({
|
||||
token,
|
||||
accountName,
|
||||
sort: '-listName'
|
||||
})
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
expect(data[0].listName).to.equal('list user two')
|
||||
expect(data[1].listName).to.equal('list user one')
|
||||
}
|
||||
|
||||
{
|
||||
const { data, total } = await server.watchedWordsLists.listWordsLists({
|
||||
accountName,
|
||||
token,
|
||||
sort: '-listName',
|
||||
start: 1,
|
||||
count: 1
|
||||
})
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
expect(data[0].listName).to.equal('list user one')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should update watched words lists', async function () {
|
||||
await server.watchedWordsLists.updateList({
|
||||
listId,
|
||||
token,
|
||||
accountName,
|
||||
words: [ 'updated-word1', 'updated-word2' ]
|
||||
})
|
||||
|
||||
await server.watchedWordsLists.updateList({
|
||||
listId,
|
||||
token,
|
||||
accountName,
|
||||
listName: 'updated-list'
|
||||
})
|
||||
|
||||
const { data } = await server.watchedWordsLists.listWordsLists({ token, accountName })
|
||||
const list = data.find(l => l.id === listId)
|
||||
|
||||
expect(list.listName).to.equal('updated-list')
|
||||
expect(list.words).to.deep.equal([ 'updated-word1', 'updated-word2' ])
|
||||
})
|
||||
|
||||
it('Should delete watched words lists', async function () {
|
||||
await server.watchedWordsLists.deleteList({
|
||||
listId,
|
||||
token,
|
||||
accountName
|
||||
})
|
||||
|
||||
const { total, data } = await server.watchedWordsLists.listWordsLists({ token, accountName })
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const list = data.find(l => l.id === listId)
|
||||
expect(list).to.not.exist
|
||||
})
|
||||
}
|
||||
|
||||
describe('Managing account watched words', function () {
|
||||
runTests('account')
|
||||
})
|
||||
|
||||
describe('Managing instance watched words', function () {
|
||||
runTests('server')
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,154 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js'
|
||||
import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
|
||||
describe('Test admin notifications', function () {
|
||||
let server: PeerTubeServer
|
||||
let sqlCommand: SQLCommand
|
||||
let userNotifications: UserNotification[] = []
|
||||
let adminNotifications: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
let baseParams: CheckerBaseParams
|
||||
let joinPeerTubeServer: MockJoinPeerTubeVersions
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
joinPeerTubeServer = new MockJoinPeerTubeVersions()
|
||||
const port = await joinPeerTubeServer.initialize()
|
||||
|
||||
const config = {
|
||||
peertube: {
|
||||
check_latest_version: {
|
||||
enabled: true,
|
||||
url: `http://127.0.0.1:${port}/versions.json`
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
index: {
|
||||
enabled: true,
|
||||
check_latest_versions_interval: '3 seconds'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await prepareNotificationsTest(1, config)
|
||||
emails = res.emails
|
||||
server = res.servers[0]
|
||||
|
||||
userNotifications = res.userNotifications
|
||||
adminNotifications = res.adminNotifications
|
||||
|
||||
baseParams = {
|
||||
server,
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: server.accessToken
|
||||
}
|
||||
|
||||
await server.plugins.install({ npmName: 'peertube-plugin-hello-world' })
|
||||
await server.plugins.install({ npmName: 'peertube-theme-background-red' })
|
||||
|
||||
sqlCommand = new SQLCommand(server)
|
||||
})
|
||||
|
||||
describe('Latest PeerTube version notification', function () {
|
||||
|
||||
it('Should not send a notification to admins if there is no new version', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
joinPeerTubeServer.setLatestVersion('1.4.2')
|
||||
|
||||
await wait(4500)
|
||||
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to admins on new version', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
joinPeerTubeServer.setLatestVersion('15.4.2')
|
||||
|
||||
await wait(4500)
|
||||
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send the same notification to admins', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await wait(4500)
|
||||
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('Should not have sent a notification to users', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should send a new notification after a new release', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
joinPeerTubeServer.setLatestVersion('15.4.3')
|
||||
|
||||
await wait(4500)
|
||||
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' })
|
||||
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Latest plugin version notification', function () {
|
||||
|
||||
it('Should not send a notification to admins if there is no new plugin version', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await wait(6000)
|
||||
await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to admins on new plugin version', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await sqlCommand.setPluginVersion('hello-world', '0.0.1')
|
||||
await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1')
|
||||
await wait(6000)
|
||||
|
||||
await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send the same notification to admins', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await wait(6000)
|
||||
|
||||
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('Should not have sent a notification to users', async function () {
|
||||
expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should send a new notification after a new plugin release', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await sqlCommand.setPluginVersion('hello-world', '0.0.1')
|
||||
await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1')
|
||||
await wait(6000)
|
||||
|
||||
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await sqlCommand.cleanup()
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { UserNotification } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import {
|
||||
CheckerBaseParams,
|
||||
checkMyVideoTranscriptionGenerated,
|
||||
prepareNotificationsTest
|
||||
} from '@tests/shared/notifications.js'
|
||||
import { join } from 'path'
|
||||
|
||||
describe('Test caption notifications', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
|
||||
let userNotifications: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
let userAccessToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const res = await prepareNotificationsTest(1)
|
||||
emails = res.emails
|
||||
userAccessToken = res.userAccessToken
|
||||
servers = res.servers
|
||||
userNotifications = res.userNotifications
|
||||
})
|
||||
|
||||
describe('Transcription of my video generated is published', function () {
|
||||
const language = { id: 'en', label: 'English' }
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userAccessToken
|
||||
}
|
||||
})
|
||||
|
||||
async function uploadAndWait () {
|
||||
const { uuid } = await servers[0].videos.upload({
|
||||
token: userAccessToken,
|
||||
attributes: {
|
||||
name: 'video',
|
||||
fixture: join('transcription', 'videos', 'the_last_man_on_earth.mp4'),
|
||||
language: undefined
|
||||
}
|
||||
})
|
||||
await waitJobs(servers)
|
||||
|
||||
return servers[0].videos.get({ id: uuid })
|
||||
}
|
||||
|
||||
it('Should not send a notification if transcription is not enabled', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const { name, shortUUID } = await uploadAndWait()
|
||||
|
||||
await checkMyVideoTranscriptionGenerated({ ...baseParams, videoName: name, shortUUID, language, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a notification transcription is enabled', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await servers[0].config.enableTranscription()
|
||||
|
||||
const { name, shortUUID } = await uploadAndWait()
|
||||
|
||||
await checkMyVideoTranscriptionGenerated({ ...baseParams, videoName: name, shortUUID, language, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,399 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { UserNotification, UserNotificationType, VideoCommentPolicy } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { CheckerBaseParams, checkCommentMention, checkNewCommentOnMyVideo, prepareNotificationsTest } from '@tests/shared/notifications.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test comments notifications', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let userToken: string
|
||||
let userToken2: string
|
||||
let userNotifications: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
|
||||
const commentText = '**hello** <a href="https://joinpeertube.org">world</a>, <h1>what do you think about peertube?</h1>'
|
||||
const expectedHtml = '<strong>hello</strong> <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">world</a>' +
|
||||
', </p>what do you think about peertube?'
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const res = await prepareNotificationsTest(2)
|
||||
emails = res.emails
|
||||
userToken = res.userAccessToken
|
||||
servers = res.servers
|
||||
userNotifications = res.userNotifications
|
||||
|
||||
userToken2 = await servers[0].users.generateUserAndToken('user2')
|
||||
await setDefaultAccountAvatar(servers[0], userToken2)
|
||||
})
|
||||
|
||||
describe('Comment on my video notifications', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not send a new comment notification after a comment on another video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
const commentId = created.id
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a new comment notification if I comment my own video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
|
||||
const created = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: 'comment' })
|
||||
const commentId = created.id
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a new comment notification if the account is muted', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
|
||||
const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
const commentId = created.id
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
|
||||
|
||||
await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' })
|
||||
})
|
||||
|
||||
it('Should send a new comment notification after a local comment on my video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
|
||||
const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
const commentId = created.id
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new comment notification after a remote comment on my video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[1].comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listThreads({ videoId: uuid })
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const commentId = data[0].id
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new comment notification after a local reply on my video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
|
||||
const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
|
||||
const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new comment notification after a remote reply on my video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const created = await servers[1].comments.createThread({ videoId: uuid, text: 'comment' })
|
||||
const threadId = created.id
|
||||
await servers[1].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listThreads({ videoId: uuid })
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const threadId = data[0].id
|
||||
const tree = await servers[0].comments.getThread({ videoId: uuid, threadId })
|
||||
|
||||
expect(tree.children).to.have.lengthOf(1)
|
||||
const commentId = tree.children[0].comment.id
|
||||
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new comment notification of a comment that requires approval', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { id: videoId, uuid, shortUUID } = await servers[0].videos.upload({
|
||||
token: userToken,
|
||||
attributes: { name: 'super video', commentsPolicy: VideoCommentPolicy.REQUIRES_APPROVAL }
|
||||
})
|
||||
await waitJobs(servers)
|
||||
|
||||
let localCommentId: number
|
||||
{
|
||||
const created = await servers[0].comments.createThread({ videoId: uuid, text: 'local approval', token: userToken2 })
|
||||
const commentId = localCommentId = created.id
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence', approval: true })
|
||||
}
|
||||
|
||||
{
|
||||
await servers[1].comments.createThread({ videoId: uuid, text: 'remote approval' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const commentId = await servers[0].comments.findCommentId({ token: userToken, videoId, text: 'remote approval' })
|
||||
|
||||
await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence', approval: true })
|
||||
}
|
||||
|
||||
// It should not re-notify on approval
|
||||
{
|
||||
await servers[0].comments.approve({ token: userToken, commentId: localCommentId, videoId: shortUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
const notifications = baseParams.socketNotifications
|
||||
.filter(n => n.type === UserNotificationType.NEW_COMMENT_ON_MY_VIDEO && n.comment?.video?.shortUUID === shortUUID)
|
||||
|
||||
expect(notifications).to.have.lengthOf(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should convert markdown in comment to html', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } })
|
||||
|
||||
await servers[0].comments.createThread({ videoId: uuid, text: commentText })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const latestEmail = emails[emails.length - 1]
|
||||
expect(latestEmail['html']).to.contain(expectedHtml)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mention notifications', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
const byAccountDisplayName = 'super root name'
|
||||
|
||||
before(async function () {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken
|
||||
}
|
||||
|
||||
await servers[0].users.updateMe({ displayName: 'super root name' })
|
||||
await servers[1].users.updateMe({ displayName: 'super root 2 name' })
|
||||
})
|
||||
|
||||
it('Should not send a new mention comment notification if I mention the video owner', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
|
||||
|
||||
const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a new mention comment notification if I mention myself', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
const { id: commentId } = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: '@user_1 hello' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a new mention notification if the account is muted', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
|
||||
|
||||
await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' })
|
||||
})
|
||||
|
||||
it('Should not send a new mention notification if the remote account mention a local account', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
await waitJobs(servers)
|
||||
const { id: threadId } = await servers[1].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const byAccountDisplayName = 'super root 2 name'
|
||||
await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a new mention notification after local comments', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hellotext: 1' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'presence' })
|
||||
|
||||
const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'hello 2 @user_1' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new mention notification after remote comments', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const text1 = `hello @user_1@${servers[0].host} 1`
|
||||
const { id: server2ThreadId } = await servers[1].comments.createThread({ videoId: uuid, text: text1 })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[0].comments.listThreads({ videoId: uuid })
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const byAccountDisplayName = 'super root 2 name'
|
||||
const threadId = data[0].id
|
||||
await checkCommentMention({ ...baseParams, shortUUID, commentId: threadId, threadId, byAccountDisplayName, checkType: 'presence' })
|
||||
|
||||
const text2 = `@user_1@${servers[0].host} hello 2 @root@${servers[0].host}`
|
||||
await servers[1].comments.addReply({ videoId: uuid, toCommentId: server2ThreadId, text: text2 })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const tree = await servers[0].comments.getThread({ videoId: uuid, threadId })
|
||||
|
||||
expect(tree.children).to.have.lengthOf(1)
|
||||
const commentId = tree.children[0].comment.id
|
||||
|
||||
await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send a new mention notification before approval', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { id: videoId, uuid, shortUUID } = await servers[0].videos.upload({
|
||||
attributes: { name: 'super video', commentsPolicy: VideoCommentPolicy.REQUIRES_APPROVAL }
|
||||
})
|
||||
await waitJobs(servers)
|
||||
|
||||
const localText = '@user_1 local approval'
|
||||
{
|
||||
const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: localText, token: userToken2 })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkCommentMention({
|
||||
...baseParams,
|
||||
shortUUID,
|
||||
threadId,
|
||||
commentId: threadId,
|
||||
byAccountDisplayName: 'user2',
|
||||
checkType: 'absence'
|
||||
})
|
||||
}
|
||||
|
||||
const remoteText = `@user_1@${servers[0].host} remote approval`
|
||||
{
|
||||
await servers[1].comments.createThread({ videoId: uuid, text: remoteText })
|
||||
await waitJobs(servers)
|
||||
|
||||
const threadId = await servers[0].comments.findCommentId({ token: userToken, videoId, text: remoteText })
|
||||
const byAccountDisplayName = 'super root 2 name'
|
||||
await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'absence' })
|
||||
}
|
||||
|
||||
// It should notify on approval
|
||||
{
|
||||
const toTest = [
|
||||
{ text: localText, byAccountDisplayName: 'user2' },
|
||||
{ text: remoteText, byAccountDisplayName: 'super root 2 name' }
|
||||
]
|
||||
|
||||
for (const { text, byAccountDisplayName } of toTest) {
|
||||
const localCommentId = await servers[0].comments.findCommentId({ token: userToken, videoId, text })
|
||||
|
||||
await servers[0].comments.approve({ commentId: localCommentId, videoId: shortUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkCommentMention({
|
||||
...baseParams,
|
||||
shortUUID,
|
||||
threadId: localCommentId,
|
||||
commentId: localCommentId,
|
||||
byAccountDisplayName,
|
||||
checkType: 'presence'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should convert markdown in comment to html', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
|
||||
|
||||
const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello 1' })
|
||||
|
||||
await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: '@user_1 ' + commentText })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const latestEmail = emails[emails.length - 1]
|
||||
expect(latestEmail['html']).to.contain(expectedHtml)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import './admin-notifications.js'
|
||||
import './captions-notifications.js'
|
||||
import './comments-notifications.js'
|
||||
import './moderation-notifications.js'
|
||||
import './notifications-api.js'
|
||||
import './registrations-notifications.js'
|
||||
import './user-notifications.js'
|
||||
@@ -0,0 +1,590 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { AbuseState, UserNotification, UserRole, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js'
|
||||
import {
|
||||
prepareNotificationsTest,
|
||||
CheckerBaseParams,
|
||||
checkNewVideoAbuseForModerators,
|
||||
checkNewCommentAbuseForModerators,
|
||||
checkNewAccountAbuseForModerators,
|
||||
checkAbuseStateChange,
|
||||
checkNewAbuseMessage,
|
||||
checkNewBlacklistOnMyVideo,
|
||||
checkNewInstanceFollower,
|
||||
checkAutoInstanceFollowing,
|
||||
checkVideoAutoBlacklistForModerators,
|
||||
checkMyVideoIsPublished,
|
||||
checkNewVideoFromSubscription
|
||||
} from '@tests/shared/notifications.js'
|
||||
|
||||
describe('Test moderation notifications', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let userToken1: string
|
||||
let userToken2: string
|
||||
|
||||
let userNotifications: UserNotification[] = []
|
||||
let adminNotifications: UserNotification[] = []
|
||||
let adminNotificationsServer2: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const res = await prepareNotificationsTest(3)
|
||||
emails = res.emails
|
||||
userToken1 = res.userAccessToken
|
||||
servers = res.servers
|
||||
userNotifications = res.userNotifications
|
||||
adminNotifications = res.adminNotifications
|
||||
adminNotificationsServer2 = res.adminNotificationsServer2
|
||||
|
||||
userToken2 = await servers[1].users.generateUserAndToken('user2', UserRole.USER)
|
||||
})
|
||||
|
||||
describe('Abuse for moderators notification', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: servers[0].accessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not send a notification to moderators on local abuse reported by an admin', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
await servers[0].abuses.report({ videoId: video.id, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators on local video abuse', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators on remote video abuse', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const videoId = await servers[1].videos.getId({ uuid: video.uuid })
|
||||
await servers[1].abuses.report({ token: userToken2, videoId, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators on local comment abuse', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
const comment = await servers[0].comments.createThread({
|
||||
token: userToken1,
|
||||
videoId: video.id,
|
||||
text: 'comment abuse ' + buildUUID()
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].abuses.report({ token: userToken1, commentId: comment.id, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators on remote comment abuse', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
await servers[0].comments.createThread({
|
||||
token: userToken1,
|
||||
videoId: video.id,
|
||||
text: 'comment abuse ' + buildUUID()
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const { data } = await servers[1].comments.listThreads({ videoId: video.uuid })
|
||||
const commentId = data[0].id
|
||||
await servers[1].abuses.report({ token: userToken2, commentId, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators on local account abuse', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const username = 'user' + new Date().getTime()
|
||||
const { account } = await servers[0].users.create({ username, password: 'donald' })
|
||||
const accountId = account.id
|
||||
|
||||
await servers[0].abuses.report({ token: userToken1, accountId, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators on remote account abuse', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const username = 'user' + new Date().getTime()
|
||||
const tmpToken = await servers[0].users.generateUserAndToken(username)
|
||||
await servers[0].videos.upload({ token: tmpToken, attributes: { name: 'super video' } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const account = await servers[1].accounts.get({ accountName: username + '@' + servers[0].host })
|
||||
await servers[1].abuses.report({ token: userToken2, accountId: account.id, reason: 'super reason' })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Abuse state change notification', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
let abuseId: number
|
||||
|
||||
before(async function () {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken1
|
||||
}
|
||||
|
||||
const name = 'abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
|
||||
abuseId = body.abuse.id
|
||||
})
|
||||
|
||||
it('Should send a notification to reporter if the abuse has been accepted', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await servers[0].abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.ACCEPTED, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to reporter if the abuse has been rejected', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await servers[0].abuses.update({ abuseId, body: { state: AbuseState.REJECTED } })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.REJECTED, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('New abuse message notification', function () {
|
||||
let baseParamsUser: CheckerBaseParams
|
||||
let baseParamsAdmin: CheckerBaseParams
|
||||
let abuseId: number
|
||||
let abuseId2: number
|
||||
|
||||
before(async function () {
|
||||
baseParamsUser = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken1
|
||||
}
|
||||
|
||||
baseParamsAdmin = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: servers[0].accessToken
|
||||
}
|
||||
|
||||
const name = 'abuse ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
{
|
||||
const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
|
||||
abuseId = body.abuse.id
|
||||
}
|
||||
|
||||
{
|
||||
const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason 2' })
|
||||
abuseId2 = body.abuse.id
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification to reporter on new message', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const message = 'my super message to users'
|
||||
await servers[0].abuses.addMessage({ abuseId, message })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewAbuseMessage({ ...baseParamsUser, abuseId, message, toEmail: 'user_1@example.com', checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send a notification to the admin if sent by the admin', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const message = 'my super message that should not be sent to the admin'
|
||||
await servers[0].abuses.addMessage({ abuseId, message })
|
||||
await waitJobs(servers)
|
||||
|
||||
const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
|
||||
await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId, message, toEmail, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a notification to moderators', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const message = 'my super message to moderators'
|
||||
await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message })
|
||||
await waitJobs(servers)
|
||||
|
||||
const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
|
||||
await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId: abuseId2, message, toEmail, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send a notification to reporter if sent by the reporter', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const message = 'my super message that should not be sent to reporter'
|
||||
await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message })
|
||||
await waitJobs(servers)
|
||||
|
||||
const toEmail = 'user_1@example.com'
|
||||
await checkNewAbuseMessage({ ...baseParamsUser, abuseId: abuseId2, message, toEmail, checkType: 'absence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video blacklist on my video', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken1
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification to video owner on blacklist', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
await servers[0].blacklist.add({ videoId: uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'blacklist' })
|
||||
})
|
||||
|
||||
it('Should send a notification to video owner on unblacklist', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
const name = 'video for abuse ' + buildUUID()
|
||||
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
|
||||
|
||||
await servers[0].blacklist.add({ videoId: uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].blacklist.remove({ videoId: uuid })
|
||||
await waitJobs(servers)
|
||||
|
||||
await wait(500)
|
||||
await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('New instance follows', function () {
|
||||
const instanceIndexServer = new MockInstancesIndex()
|
||||
let config: any
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(async function () {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: servers[0].accessToken
|
||||
}
|
||||
|
||||
const port = await instanceIndexServer.initialize()
|
||||
instanceIndexServer.addInstance(servers[1].host)
|
||||
|
||||
config = {
|
||||
followings: {
|
||||
instance: {
|
||||
autoFollowIndex: {
|
||||
indexUrl: `http://127.0.0.1:${port}/api/v1/instances/hosts`,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification only to admin when there is a new instance follower', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[2].follows.follow({ hosts: [ servers[0].url ] })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewInstanceFollower({ ...baseParams, followerHost: servers[2].host, checkType: 'presence' })
|
||||
|
||||
const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
|
||||
await checkNewInstanceFollower({ ...baseParams, ...userOverride, followerHost: servers[2].host, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a notification on auto follow back', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
await servers[2].follows.unfollow({ target: servers[0] })
|
||||
await waitJobs(servers)
|
||||
|
||||
const config = {
|
||||
followings: {
|
||||
instance: {
|
||||
autoFollowBack: { enabled: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
await servers[0].config.updateExistingConfig({ newConfig: config })
|
||||
|
||||
await servers[2].follows.follow({ hosts: [ servers[0].url ] })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const followerHost = servers[0].host
|
||||
const followingHost = servers[2].host
|
||||
await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
|
||||
|
||||
const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
|
||||
await checkAutoInstanceFollowing({ ...baseParams, ...userOverride, followerHost, followingHost, checkType: 'absence' })
|
||||
|
||||
config.followings.instance.autoFollowBack.enabled = false
|
||||
await servers[0].config.updateExistingConfig({ newConfig: config })
|
||||
await servers[0].follows.unfollow({ target: servers[2] })
|
||||
await servers[2].follows.unfollow({ target: servers[0] })
|
||||
})
|
||||
|
||||
it('Should send a notification on auto instances index follow', async function () {
|
||||
this.timeout(30000)
|
||||
await servers[0].follows.unfollow({ target: servers[1] })
|
||||
|
||||
await servers[0].config.updateExistingConfig({ newConfig: config })
|
||||
|
||||
await wait(5000)
|
||||
await waitJobs(servers)
|
||||
|
||||
const followerHost = servers[0].host
|
||||
const followingHost = servers[1].host
|
||||
await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
|
||||
|
||||
config.followings.instance.autoFollowIndex.enabled = false
|
||||
await servers[0].config.updateExistingConfig({ newConfig: config })
|
||||
await servers[0].follows.unfollow({ target: servers[1] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video-related notifications when video auto-blacklist is enabled', function () {
|
||||
let userBaseParams: CheckerBaseParams
|
||||
let adminBaseParamsServer1: CheckerBaseParams
|
||||
let adminBaseParamsServer2: CheckerBaseParams
|
||||
let uuid: string
|
||||
let shortUUID: string
|
||||
let videoName: string
|
||||
|
||||
before(async function () {
|
||||
|
||||
adminBaseParamsServer1 = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: servers[0].accessToken
|
||||
}
|
||||
|
||||
adminBaseParamsServer2 = {
|
||||
server: servers[1],
|
||||
emails,
|
||||
socketNotifications: adminNotificationsServer2,
|
||||
token: servers[1].accessToken
|
||||
}
|
||||
|
||||
userBaseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken1
|
||||
}
|
||||
|
||||
await servers[0].config.enableAutoBlacklist()
|
||||
|
||||
await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
|
||||
await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
|
||||
})
|
||||
|
||||
it('Should send notification to moderators on new video with auto-blacklist', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
videoName = 'video with auto-blacklist ' + buildUUID()
|
||||
const video = await servers[0].videos.upload({ token: userToken1, attributes: { name: videoName } })
|
||||
shortUUID = video.shortUUID
|
||||
uuid = video.uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send video publish notification if auto-blacklisted', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await checkMyVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a local user subscription notification if auto-blacklisted', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send video published and unblacklist after video unblacklisted', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await servers[0].blacklist.remove({ videoId: uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// FIXME: Can't test as two notifications sent to same user and util only checks last one
|
||||
// One notification might be better anyways
|
||||
// await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist')
|
||||
// await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence')
|
||||
})
|
||||
|
||||
it('Should send a local user subscription notification after removed from blacklist', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a remote user subscription notification after removed from blacklist', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const updateAt = new Date(new Date().getTime() + 1000000)
|
||||
|
||||
const name = 'video with auto-blacklist and future schedule ' + buildUUID()
|
||||
|
||||
const attributes = {
|
||||
name,
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
|
||||
const { shortUUID, uuid } = await servers[0].videos.upload({ token: userToken1, attributes })
|
||||
|
||||
await servers[0].blacklist.remove({ videoId: uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewBlacklistOnMyVideo({ ...userBaseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' })
|
||||
|
||||
// FIXME: Can't test absence as two notifications sent to same user and util only checks last one
|
||||
// One notification might be better anyways
|
||||
// await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
|
||||
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' })
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
// In 2 seconds
|
||||
const updateAt = new Date(new Date().getTime() + 2000)
|
||||
|
||||
const name = 'video with schedule done and still auto-blacklisted ' + buildUUID()
|
||||
|
||||
const attributes = {
|
||||
name,
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
|
||||
const { shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes })
|
||||
|
||||
await wait(6000)
|
||||
await checkMyVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' })
|
||||
await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const name = 'video without auto-blacklist ' + buildUUID()
|
||||
|
||||
// admin with blacklist right will not be auto-blacklisted
|
||||
const { shortUUID } = await servers[0].videos.upload({ attributes: { name } })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName: name, checkType: 'absence' })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
|
||||
await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,206 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { UserNotification, UserNotificationSettingValue } from '@peertube/peertube-models'
|
||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import {
|
||||
prepareNotificationsTest,
|
||||
CheckerBaseParams,
|
||||
getAllNotificationsSettings,
|
||||
checkNewVideoFromSubscription
|
||||
} from '@tests/shared/notifications.js'
|
||||
|
||||
describe('Test notifications API', function () {
|
||||
let server: PeerTubeServer
|
||||
let userNotifications: UserNotification[] = []
|
||||
let userToken: string
|
||||
let emails: object[] = []
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const res = await prepareNotificationsTest(1)
|
||||
emails = res.emails
|
||||
userToken = res.userAccessToken
|
||||
userNotifications = res.userNotifications
|
||||
server = res.servers[0]
|
||||
|
||||
await server.subscriptions.add({ token: userToken, targetUri: 'root_channel@' + server.host })
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await server.videos.randomUpload({ wait: false })
|
||||
}
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
describe('Notification list & count', function () {
|
||||
|
||||
it('Should correctly list notifications', async function () {
|
||||
const { data, total } = await server.notifications.list({ token: userToken, start: 0, count: 2 })
|
||||
|
||||
expect(data).to.have.lengthOf(2)
|
||||
expect(total).to.equal(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mark as read', function () {
|
||||
|
||||
it('Should mark as read some notifications', async function () {
|
||||
const { data } = await server.notifications.list({ token: userToken, start: 2, count: 3 })
|
||||
const ids = data.map(n => n.id)
|
||||
|
||||
await server.notifications.markAsRead({ token: userToken, ids })
|
||||
})
|
||||
|
||||
it('Should have the notifications marked as read', async function () {
|
||||
const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10 })
|
||||
|
||||
expect(data[0].read).to.be.false
|
||||
expect(data[1].read).to.be.false
|
||||
expect(data[2].read).to.be.true
|
||||
expect(data[3].read).to.be.true
|
||||
expect(data[4].read).to.be.true
|
||||
expect(data[5].read).to.be.false
|
||||
})
|
||||
|
||||
it('Should only list read notifications', async function () {
|
||||
const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: false })
|
||||
|
||||
for (const notification of data) {
|
||||
expect(notification.read).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('Should only list unread notifications', async function () {
|
||||
const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true })
|
||||
|
||||
for (const notification of data) {
|
||||
expect(notification.read).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should mark as read all notifications', async function () {
|
||||
await server.notifications.markAsReadAll({ token: userToken })
|
||||
|
||||
const body = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true })
|
||||
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Notification settings', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server,
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have notifications', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
await server.notifications.updateMySettings({
|
||||
token: userToken,
|
||||
settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.NONE }
|
||||
})
|
||||
|
||||
{
|
||||
const info = await server.users.getMyInfo({ token: userToken })
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE)
|
||||
}
|
||||
|
||||
const { name, shortUUID } = await server.videos.randomUpload()
|
||||
|
||||
const check = { web: true, mail: true }
|
||||
await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should only have web notifications', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await server.notifications.updateMySettings({
|
||||
token: userToken,
|
||||
settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.WEB }
|
||||
})
|
||||
|
||||
{
|
||||
const info = await server.users.getMyInfo({ token: userToken })
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB)
|
||||
}
|
||||
|
||||
const { name, shortUUID } = await server.videos.randomUpload()
|
||||
|
||||
{
|
||||
const check = { mail: true, web: false }
|
||||
await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
|
||||
}
|
||||
|
||||
{
|
||||
const check = { mail: false, web: true }
|
||||
await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should only have mail notifications', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await server.notifications.updateMySettings({
|
||||
token: userToken,
|
||||
settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.EMAIL }
|
||||
})
|
||||
|
||||
{
|
||||
const info = await server.users.getMyInfo({ token: userToken })
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL)
|
||||
}
|
||||
|
||||
const { name, shortUUID } = await server.videos.randomUpload()
|
||||
|
||||
{
|
||||
const check = { mail: false, web: true }
|
||||
await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
|
||||
}
|
||||
|
||||
{
|
||||
const check = { mail: true, web: false }
|
||||
await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have email and web notifications', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await server.notifications.updateMySettings({
|
||||
token: userToken,
|
||||
settings: {
|
||||
...getAllNotificationsSettings(),
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
const info = await server.users.getMyInfo({ token: userToken })
|
||||
expect(info.notificationSettings.newVideoFromSubscription).to.equal(
|
||||
UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
)
|
||||
}
|
||||
|
||||
const { name, shortUUID } = await server.videos.randomUpload()
|
||||
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { UserNotification } from '@peertube/peertube-models'
|
||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { CheckerBaseParams, prepareNotificationsTest, checkUserRegistered, checkRegistrationRequest } from '@tests/shared/notifications.js'
|
||||
|
||||
describe('Test registrations notifications', function () {
|
||||
let server: PeerTubeServer
|
||||
let userToken1: string
|
||||
|
||||
let userNotifications: UserNotification[] = []
|
||||
let adminNotifications: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const res = await prepareNotificationsTest(1)
|
||||
|
||||
server = res.servers[0]
|
||||
emails = res.emails
|
||||
userToken1 = res.userAccessToken
|
||||
adminNotifications = res.adminNotifications
|
||||
userNotifications = res.userNotifications
|
||||
|
||||
baseParams = {
|
||||
server,
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: server.accessToken
|
||||
}
|
||||
})
|
||||
|
||||
describe('New direct registration for moderators', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableSignup(false)
|
||||
})
|
||||
|
||||
it('Should send a notification only to moderators when a user registers on the instance', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
await server.registrations.register({ username: 'user_10' })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
await checkUserRegistered({ ...baseParams, username: 'user_10', checkType: 'presence' })
|
||||
|
||||
const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
|
||||
await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_10', checkType: 'absence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('New registration request for moderators', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableSignup(true)
|
||||
})
|
||||
|
||||
it('Should send a notification on new registration request', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const registrationReason = 'my reason'
|
||||
await server.registrations.requestRegistration({ username: 'user_11', registrationReason })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
await checkRegistrationRequest({ ...baseParams, username: 'user_11', registrationReason, checkType: 'presence' })
|
||||
|
||||
const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
|
||||
await checkRegistrationRequest({ ...baseParams, ...userOverride, username: 'user_11', registrationReason, checkType: 'absence' })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,647 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { UserNotification, UserNotificationType, VideoPrivacy, VideoStudioTask } from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import {
|
||||
prepareNotificationsTest,
|
||||
CheckerBaseParams,
|
||||
checkNewVideoFromSubscription,
|
||||
checkMyVideoIsPublished,
|
||||
checkVideoStudioEditionIsFinished,
|
||||
checkMyVideoImportIsFinished,
|
||||
checkNewActorFollow,
|
||||
checkNewLiveFromSubscription,
|
||||
waitUntilNotification
|
||||
} from '@tests/shared/notifications.js'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
|
||||
|
||||
describe('Test user notifications', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let userAccessToken: string
|
||||
|
||||
let userNotifications: UserNotification[] = []
|
||||
let adminNotifications: UserNotification[] = []
|
||||
let adminNotificationsServer2: UserNotification[] = []
|
||||
let emails: object[] = []
|
||||
|
||||
let channelId: number
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const res = await prepareNotificationsTest(3)
|
||||
emails = res.emails
|
||||
userAccessToken = res.userAccessToken
|
||||
servers = res.servers
|
||||
userNotifications = res.userNotifications
|
||||
adminNotifications = res.adminNotifications
|
||||
adminNotificationsServer2 = res.adminNotificationsServer2
|
||||
channelId = res.channelId
|
||||
})
|
||||
|
||||
describe('New video from my subscription notification', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userAccessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not send notifications if the user does not follow the video publisher', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
await uploadRandomVideoOnServers(servers, 1)
|
||||
|
||||
const notification = await servers[0].notifications.getLatest({ token: userAccessToken })
|
||||
expect(notification).to.be.undefined
|
||||
|
||||
expect(emails).to.have.lengthOf(0)
|
||||
expect(userNotifications).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should send a new video notification if the user follows the local video publisher', async function () {
|
||||
await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[0].host })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new video notification from a remote account', async function () {
|
||||
this.timeout(150000) // Server 2 has transcoding enabled
|
||||
|
||||
await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[1].host })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new video notification on a scheduled publication', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
// In 2 seconds
|
||||
const updateAt = new Date(new Date().getTime() + 2000)
|
||||
|
||||
const data = {
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
|
||||
|
||||
await wait(6000)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new video notification on a remote scheduled publication', async function () {
|
||||
this.timeout(100000)
|
||||
|
||||
// In 2 seconds
|
||||
const updateAt = new Date(new Date().getTime() + 2000)
|
||||
|
||||
const data = {
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
|
||||
await waitJobs(servers)
|
||||
|
||||
await wait(6000)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send a notification before the video is published', async function () {
|
||||
this.timeout(150000)
|
||||
|
||||
const updateAt = new Date(new Date().getTime() + 1000000)
|
||||
|
||||
const data = {
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
|
||||
|
||||
await wait(6000)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a new video notification when a video becomes public', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const data = { privacy: VideoPrivacy.PRIVATE }
|
||||
const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
|
||||
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
|
||||
await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a new video notification when a remote video becomes public', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const data = { privacy: VideoPrivacy.PRIVATE }
|
||||
const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
|
||||
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
|
||||
await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send a new video notification when a video becomes unlisted', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const data = { privacy: VideoPrivacy.PRIVATE }
|
||||
const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
|
||||
|
||||
await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a new video notification when a remote video becomes unlisted', async function () {
|
||||
this.timeout(100000)
|
||||
|
||||
const data = { privacy: VideoPrivacy.PRIVATE }
|
||||
const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
|
||||
|
||||
await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a new video notification after a video import', async function () {
|
||||
this.timeout(100000)
|
||||
|
||||
const name = 'video import ' + buildUUID()
|
||||
|
||||
const attributes = {
|
||||
name,
|
||||
channelId,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
targetUrl: FIXTURE_URLS.goodVideo
|
||||
}
|
||||
const { video } = await servers[0].videoImports.importVideo({ attributes })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('New live from my subscription notification', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
async function createAndStreamLive (server: PeerTubeServer) {
|
||||
const name = 'video live ' + buildUUID()
|
||||
|
||||
const streamDate = new Date()
|
||||
const { video } = await server.live.quickCreate({ name, permanentLive: true, saveReplay: false })
|
||||
await waitJobs(servers)
|
||||
|
||||
const ffmpegCommand = await server.live.sendRTMPStreamInVideo({ videoId: video.uuid })
|
||||
|
||||
return { name, video, ffmpegCommand, streamDate }
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userAccessToken
|
||||
}
|
||||
|
||||
await servers[0].config.enableLive({ allowReplay: false })
|
||||
})
|
||||
|
||||
it('Should not send a notification when a live is created', async function () {
|
||||
this.timeout(100000)
|
||||
|
||||
const name = 'video live ' + buildUUID()
|
||||
|
||||
const { video } = await servers[0].live.quickCreate({ name, permanentLive: true, saveReplay: false })
|
||||
await waitJobs(servers)
|
||||
await checkNewLiveFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should send a local notification when streaming in the live', async function () {
|
||||
this.timeout(100000)
|
||||
|
||||
const { name, video, ffmpegCommand, streamDate } = await createAndStreamLive(servers[0])
|
||||
|
||||
await waitUntilNotification({
|
||||
server: servers[0],
|
||||
token: userAccessToken,
|
||||
notificationType: UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION,
|
||||
fromDate: streamDate
|
||||
})
|
||||
|
||||
await checkNewLiveFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should send a remote notification when streaming in the live ', async function () {
|
||||
this.timeout(100000)
|
||||
|
||||
const { name, video, ffmpegCommand, streamDate } = await createAndStreamLive(servers[1])
|
||||
|
||||
await waitUntilNotification({
|
||||
server: servers[0],
|
||||
token: userAccessToken,
|
||||
notificationType: UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION,
|
||||
fromDate: streamDate
|
||||
})
|
||||
await checkNewLiveFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await waitJobs(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('My video is published', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[1],
|
||||
emails,
|
||||
socketNotifications: adminNotificationsServer2,
|
||||
token: servers[1].accessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not send a notification if transcoding is not enabled', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
|
||||
it('Should not send a notification if the wait transcoding is false', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false })
|
||||
await waitJobs(servers)
|
||||
|
||||
const notification = await servers[0].notifications.getLatest({ token: userAccessToken })
|
||||
if (notification) {
|
||||
expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification even if the video is not transcoded in other resolutions', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification with a transcoded video', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification when an imported video is transcoded', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const name = 'video import ' + buildUUID()
|
||||
|
||||
const attributes = {
|
||||
name,
|
||||
channelId,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
targetUrl: FIXTURE_URLS.goodVideo,
|
||||
waitTranscoding: true
|
||||
}
|
||||
const { video } = await servers[1].videoImports.importVideo({ attributes })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification when the scheduled update has been proceeded', async function () {
|
||||
this.timeout(140000)
|
||||
|
||||
// In 2 seconds
|
||||
const updateAt = new Date(new Date().getTime() + 2000)
|
||||
|
||||
const data = {
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
|
||||
|
||||
await wait(6000)
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should not send a notification before the video is published', async function () {
|
||||
this.timeout(150000)
|
||||
|
||||
const updateAt = new Date(new Date().getTime() + 1000000)
|
||||
|
||||
const data = {
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
scheduleUpdate: {
|
||||
updateAt: updateAt.toISOString(),
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
}
|
||||
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
|
||||
|
||||
await wait(6000)
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('My live replay is published', function () {
|
||||
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[1],
|
||||
emails,
|
||||
socketNotifications: adminNotificationsServer2,
|
||||
token: servers[1].accessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification is a live replay of a non permanent live is published', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { shortUUID } = await servers[1].live.create({
|
||||
fields: {
|
||||
name: 'non permanent live',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: servers[1].store.channel.id,
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
permanentLive: false
|
||||
}
|
||||
})
|
||||
|
||||
const ffmpegCommand = await servers[1].live.sendRTMPStreamInVideo({ videoId: shortUUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[1].live.waitUntilPublished({ videoId: shortUUID })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
await servers[1].live.waitUntilReplacedByReplay({ videoId: shortUUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: 'non permanent live', shortUUID, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification is a live replay of a permanent live is published', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { shortUUID } = await servers[1].live.create({
|
||||
fields: {
|
||||
name: 'permanent live',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: servers[1].store.channel.id,
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
permanentLive: true
|
||||
}
|
||||
})
|
||||
|
||||
const ffmpegCommand = await servers[1].live.sendRTMPStreamInVideo({ videoId: shortUUID })
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[1].live.waitUntilPublished({ videoId: shortUUID })
|
||||
|
||||
const liveDetails = await servers[1].videos.get({ id: shortUUID })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await servers[1].live.waitUntilWaiting({ videoId: shortUUID })
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await findExternalSavedVideo(servers[1], liveDetails)
|
||||
expect(video).to.exist
|
||||
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: video.name, shortUUID: video.shortUUID, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video studio', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[1],
|
||||
emails,
|
||||
socketNotifications: adminNotificationsServer2,
|
||||
token: servers[1].accessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification after studio edition', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const { name, shortUUID, id } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
|
||||
|
||||
await waitJobs(servers)
|
||||
await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
|
||||
const tasks: VideoStudioTask[] = [
|
||||
{
|
||||
name: 'cut',
|
||||
options: {
|
||||
start: 0,
|
||||
end: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
await servers[1].videoStudio.createEditionTasks({ videoId: id, tasks })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideoStudioEditionIsFinished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('My video is imported', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: adminNotifications,
|
||||
token: servers[0].accessToken
|
||||
}
|
||||
})
|
||||
|
||||
it('Should send a notification when the video import failed', async function () {
|
||||
this.timeout(70000)
|
||||
|
||||
const name = 'video import ' + buildUUID()
|
||||
|
||||
const attributes = {
|
||||
name,
|
||||
channelId,
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
targetUrl: FIXTURE_URLS.badVideo
|
||||
}
|
||||
const { video: { shortUUID } } = await servers[0].videoImports.importVideo({ attributes })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const url = FIXTURE_URLS.badVideo
|
||||
await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: false, checkType: 'presence' })
|
||||
})
|
||||
|
||||
it('Should send a notification when the video import succeeded', async function () {
|
||||
this.timeout(70000)
|
||||
|
||||
const name = 'video import ' + buildUUID()
|
||||
|
||||
const attributes = {
|
||||
name,
|
||||
channelId,
|
||||
privacy: VideoPrivacy.PRIVATE,
|
||||
targetUrl: FIXTURE_URLS.goodVideo
|
||||
}
|
||||
const { video: { shortUUID } } = await servers[0].videoImports.importVideo({ attributes })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
const url = FIXTURE_URLS.goodVideo
|
||||
await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: true, checkType: 'presence' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('New actor follow', function () {
|
||||
let baseParams: CheckerBaseParams
|
||||
const myChannelName = 'super channel name'
|
||||
const myUserName = 'super user name'
|
||||
|
||||
before(async function () {
|
||||
baseParams = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
socketNotifications: userNotifications,
|
||||
token: userAccessToken
|
||||
}
|
||||
|
||||
await servers[0].users.updateMe({ displayName: 'super root name' })
|
||||
|
||||
await servers[0].users.updateMe({
|
||||
token: userAccessToken,
|
||||
displayName: myUserName
|
||||
})
|
||||
|
||||
await servers[1].users.updateMe({ displayName: 'super root 2 name' })
|
||||
|
||||
await servers[0].channels.update({
|
||||
token: userAccessToken,
|
||||
channelName: 'user_1_channel',
|
||||
attributes: { displayName: myChannelName }
|
||||
})
|
||||
})
|
||||
|
||||
it('Should notify when a local channel is following one of our channel', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewActorFollow({
|
||||
...baseParams,
|
||||
followType: 'channel',
|
||||
followerName: 'root',
|
||||
followerDisplayName: 'super root name',
|
||||
followingDisplayName: myChannelName,
|
||||
checkType: 'presence'
|
||||
})
|
||||
|
||||
await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
|
||||
})
|
||||
|
||||
it('Should notify when a remote channel is following one of our channel', async function () {
|
||||
this.timeout(50000)
|
||||
|
||||
await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkNewActorFollow({
|
||||
...baseParams,
|
||||
followType: 'channel',
|
||||
followerName: 'root',
|
||||
followerDisplayName: 'super root 2 name',
|
||||
followingDisplayName: myChannelName,
|
||||
checkType: 'presence'
|
||||
})
|
||||
|
||||
await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
|
||||
})
|
||||
|
||||
// PeerTube does not support account -> account follows
|
||||
// it('Should notify when a local account is following one of our channel', async function () {
|
||||
// this.timeout(50000)
|
||||
//
|
||||
// await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@' + servers[0].host)
|
||||
//
|
||||
// await waitJobs(servers)
|
||||
//
|
||||
// await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence')
|
||||
// })
|
||||
|
||||
// it('Should notify when a remote account is following one of our channel', async function () {
|
||||
// this.timeout(50000)
|
||||
//
|
||||
// await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@' + servers[0].host)
|
||||
//
|
||||
// await waitJobs(servers)
|
||||
//
|
||||
// await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence')
|
||||
// })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './live.js'
|
||||
export * from './video-imports.js'
|
||||
export * from './video-static-file-privacy.js'
|
||||
export * from './videos.js'
|
||||
@@ -0,0 +1,357 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
findExternalSavedVideo,
|
||||
makeRawRequest,
|
||||
ObjectStorageCommand,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs,
|
||||
waitUntilLivePublishedOnAllServers,
|
||||
waitUntilLiveReplacedByReplayOnAllServers,
|
||||
waitUntilLiveWaitingOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expectStartWith } from '@tests/shared/checks.js'
|
||||
import { testLiveVideoResolutions } from '@tests/shared/live.js'
|
||||
import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
|
||||
async function createLive (server: PeerTubeServer, permanent: boolean) {
|
||||
const attributes: LiveVideoCreate = {
|
||||
channelId: server.store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
name: 'my super live',
|
||||
saveReplay: true,
|
||||
replaySettings: { privacy: VideoPrivacy.PUBLIC },
|
||||
permanentLive: permanent
|
||||
}
|
||||
|
||||
const { uuid } = await server.live.create({ fields: attributes })
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
async function checkFilesExist (options: {
|
||||
servers: PeerTubeServer[]
|
||||
videoUUID: string
|
||||
numberOfFiles: number
|
||||
objectStorage: ObjectStorageCommand
|
||||
}) {
|
||||
const { servers, videoUUID, numberOfFiles, objectStorage } = options
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const files = video.streamingPlaylists[0].files
|
||||
expect(files).to.have.lengthOf(numberOfFiles)
|
||||
|
||||
for (const file of files) {
|
||||
expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFilesCleanup (options: {
|
||||
server: PeerTubeServer
|
||||
videoUUID: string
|
||||
resolutions: number[]
|
||||
objectStorage: ObjectStorageCommand
|
||||
}) {
|
||||
const { server, videoUUID, resolutions, objectStorage } = options
|
||||
|
||||
const resolutionFiles = resolutions.map((_value, i) => `${i}.m3u8`)
|
||||
|
||||
for (const playlistName of [ 'master.m3u8' ].concat(resolutionFiles)) {
|
||||
await server.live.getPlaylistFile({
|
||||
videoUUID,
|
||||
playlistName,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404,
|
||||
objectStorage
|
||||
})
|
||||
}
|
||||
|
||||
await server.live.getSegmentFile({
|
||||
videoUUID,
|
||||
playlistNumber: 0,
|
||||
segment: 0,
|
||||
objectStorage,
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
}
|
||||
|
||||
describe('Object storage for lives', function () {
|
||||
if (areMockObjectStorageTestsDisabled()) return
|
||||
|
||||
let servers: PeerTubeServer[]
|
||||
let sqlCommandServer1: SQLCommand
|
||||
const objectStorage = new ObjectStorageCommand()
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await objectStorage.prepareDefaultMockBuckets()
|
||||
servers = await createMultipleServers(2, objectStorage.getDefaultMockConfig())
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableTranscoding()
|
||||
|
||||
sqlCommandServer1 = new SQLCommand(servers[0])
|
||||
})
|
||||
|
||||
describe('Without live transcoding', function () {
|
||||
let videoUUID: string
|
||||
|
||||
before(async function () {
|
||||
await servers[0].config.enableLive({ transcoding: false })
|
||||
|
||||
videoUUID = await createLive(servers[0], false)
|
||||
})
|
||||
|
||||
it('Should create a live and publish it on object storage', async function () {
|
||||
this.timeout(220000)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, videoUUID)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId: videoUUID,
|
||||
resolutions: [ 720 ],
|
||||
transcoded: false,
|
||||
objectStorage
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have saved the replay on object storage', async function () {
|
||||
this.timeout(220000)
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkFilesExist({ servers, videoUUID, numberOfFiles: 1, objectStorage })
|
||||
})
|
||||
|
||||
it('Should have cleaned up live files from object storage', async function () {
|
||||
await checkFilesCleanup({ server: servers[0], videoUUID, resolutions: [ 720 ], objectStorage })
|
||||
})
|
||||
})
|
||||
|
||||
describe('With live transcoding', function () {
|
||||
const resolutions = [ 720, 480, 360, 240, 144 ]
|
||||
|
||||
before(async function () {
|
||||
await servers[0].config.enableLive({ transcoding: true })
|
||||
})
|
||||
|
||||
describe('Normal replay', function () {
|
||||
let videoUUIDNonPermanent: string
|
||||
|
||||
before(async function () {
|
||||
videoUUIDNonPermanent = await createLive(servers[0], false)
|
||||
})
|
||||
|
||||
it('Should create a live and publish it on object storage', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDNonPermanent })
|
||||
await waitUntilLivePublishedOnAllServers(servers, videoUUIDNonPermanent)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId: videoUUIDNonPermanent,
|
||||
resolutions,
|
||||
transcoded: true,
|
||||
objectStorage
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have saved the replay on object storage', async function () {
|
||||
this.timeout(220000)
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUIDNonPermanent)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkFilesExist({ servers, videoUUID: videoUUIDNonPermanent, numberOfFiles: 5, objectStorage })
|
||||
})
|
||||
|
||||
it('Should have cleaned up live files from object storage', async function () {
|
||||
await checkFilesCleanup({ server: servers[0], videoUUID: videoUUIDNonPermanent, resolutions, objectStorage })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Permanent replay', function () {
|
||||
let videoUUIDPermanent: string
|
||||
|
||||
before(async function () {
|
||||
videoUUIDPermanent = await createLive(servers[0], true)
|
||||
})
|
||||
|
||||
it('Should create a live and publish it on object storage', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent })
|
||||
await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId: videoUUIDPermanent,
|
||||
resolutions,
|
||||
transcoded: true,
|
||||
objectStorage
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have saved the replay on object storage', async function () {
|
||||
this.timeout(220000)
|
||||
|
||||
await waitUntilLiveWaitingOnAllServers(servers, videoUUIDPermanent)
|
||||
await waitJobs(servers)
|
||||
|
||||
const videoLiveDetails = await servers[0].videos.get({ id: videoUUIDPermanent })
|
||||
const replay = await findExternalSavedVideo(servers[0], videoLiveDetails)
|
||||
|
||||
await checkFilesExist({ servers, videoUUID: replay.uuid, numberOfFiles: 5, objectStorage })
|
||||
})
|
||||
|
||||
it('Should have cleaned up live files from object storage', async function () {
|
||||
await checkFilesCleanup({ server: servers[0], videoUUID: videoUUIDPermanent, resolutions, objectStorage })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('With object storage base url', function () {
|
||||
const mockObjectStorageProxy = new MockObjectStorageProxy()
|
||||
let baseMockUrl: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const port = await mockObjectStorageProxy.initialize()
|
||||
const bucketName = objectStorage.getMockStreamingPlaylistsBucketName()
|
||||
baseMockUrl = `http://127.0.0.1:${port}/${bucketName}`
|
||||
|
||||
await objectStorage.prepareDefaultMockBuckets()
|
||||
|
||||
const config = {
|
||||
object_storage: {
|
||||
enabled: true,
|
||||
endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(),
|
||||
region: ObjectStorageCommand.getMockRegion(),
|
||||
|
||||
credentials: ObjectStorageCommand.getMockCredentialsConfig(),
|
||||
|
||||
streaming_playlists: {
|
||||
bucket_name: bucketName,
|
||||
prefix: '',
|
||||
base_url: baseMockUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await servers[0].kill()
|
||||
await servers[0].run(config)
|
||||
|
||||
await servers[0].config.enableLive({ transcoding: true, resolutions: 'min' })
|
||||
})
|
||||
|
||||
it('Should publish a live and replace the base url', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const videoUUIDPermanent = await createLive(servers[0], true)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent })
|
||||
await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId: videoUUIDPermanent,
|
||||
resolutions: [ 720 ],
|
||||
transcoded: true,
|
||||
objectStorage,
|
||||
objectStorageBaseUrl: baseMockUrl
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With live stream to object storage disabled', function () {
|
||||
let videoUUID: string
|
||||
|
||||
before(async function () {
|
||||
await servers[0].kill()
|
||||
await servers[0].run(objectStorage.getDefaultMockConfig({ storeLiveStreams: false }))
|
||||
await servers[0].config.enableLive({ transcoding: false })
|
||||
|
||||
videoUUID = await createLive(servers[0], false)
|
||||
})
|
||||
|
||||
it('Should create a live and keep it on file system', async function () {
|
||||
this.timeout(220000)
|
||||
|
||||
const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
|
||||
await waitUntilLivePublishedOnAllServers(servers, videoUUID)
|
||||
|
||||
await testLiveVideoResolutions({
|
||||
originServer: servers[0],
|
||||
sqlCommand: sqlCommandServer1,
|
||||
servers,
|
||||
liveVideoId: videoUUID,
|
||||
resolutions: [ 720 ],
|
||||
transcoded: false,
|
||||
objectStorage: undefined
|
||||
})
|
||||
|
||||
// Should not have files on object storage
|
||||
await checkFilesCleanup({ server: servers[0], videoUUID, resolutions: [ 720 ], objectStorage })
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have saved the replay on object storage', async function () {
|
||||
this.timeout(220000)
|
||||
|
||||
await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUID)
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkFilesExist({ servers, videoUUID, numberOfFiles: 1, objectStorage })
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await sqlCommandServer1.cleanup()
|
||||
await objectStorage.cleanupMock()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { expectStartWith } from '@tests/shared/checks.js'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeRawRequest,
|
||||
ObjectStorageCommand,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function importVideo (server: PeerTubeServer) {
|
||||
const attributes = {
|
||||
name: 'import 2',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId: server.store.channel.id,
|
||||
targetUrl: FIXTURE_URLS.goodVideo720
|
||||
}
|
||||
|
||||
const { video: { uuid } } = await server.videoImports.importVideo({ attributes })
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
describe('Object storage for video import', function () {
|
||||
if (areMockObjectStorageTestsDisabled()) return
|
||||
|
||||
let server: PeerTubeServer
|
||||
const objectStorage = new ObjectStorageCommand()
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await objectStorage.prepareDefaultMockBuckets()
|
||||
|
||||
server = await createSingleServer(1, objectStorage.getDefaultMockConfig())
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableVideoImports()
|
||||
})
|
||||
|
||||
describe('Without transcoding', async function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.disableTranscoding()
|
||||
})
|
||||
|
||||
it('Should import a video and have sent it to object storage', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const uuid = await importVideo(server)
|
||||
await waitJobs(server)
|
||||
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
|
||||
const fileUrl = video.files[0].fileUrl
|
||||
expectStartWith(fileUrl, objectStorage.getMockWebVideosBaseUrl())
|
||||
|
||||
await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('With transcoding', async function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableTranscoding()
|
||||
})
|
||||
|
||||
it('Should import a video and have sent it to object storage', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const uuid = await importVideo(server)
|
||||
await waitJobs(server)
|
||||
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
expect(video.files).to.have.lengthOf(5)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists[0].files).to.have.lengthOf(5)
|
||||
|
||||
for (const file of video.files) {
|
||||
expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
for (const file of video.streamingPlaylists[0].files) {
|
||||
expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await objectStorage.cleanupMock()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,584 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { basename } from 'path'
|
||||
import { getAllFiles, getHLS } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { areScalewayObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
findExternalSavedVideo,
|
||||
makeRawRequest,
|
||||
ObjectStorageCommand,
|
||||
PeerTubeServer,
|
||||
sendRTMPStream,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expectStartWith } from '@tests/shared/checks.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
|
||||
|
||||
function extractFilenameFromUrl (url: string) {
|
||||
const parts = basename(url).split(':')
|
||||
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
describe('Object storage for video static file privacy', function () {
|
||||
// We need real world object storage to check ACL
|
||||
if (areScalewayObjectStorageTestsDisabled()) return
|
||||
|
||||
let server: PeerTubeServer
|
||||
let sqlCommand: SQLCommand
|
||||
let userToken: string
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkPrivateVODFiles (uuid: string) {
|
||||
const video = await server.videos.getWithToken({ id: uuid })
|
||||
|
||||
for (const file of video.files) {
|
||||
expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/web-videos/private/')
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
for (const file of getAllFiles(video)) {
|
||||
const internalFileUrl = await sqlCommand.getInternalFileUrl(file.id)
|
||||
expectStartWith(internalFileUrl, ObjectStorageCommand.getScalewayBaseUrl())
|
||||
|
||||
const { text } = await makeRawRequest({
|
||||
url: internalFileUrl,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
expect(text).to.contain('Unsupported Authorization Type')
|
||||
}
|
||||
|
||||
const hls = getHLS(video)
|
||||
|
||||
if (hls) {
|
||||
for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
|
||||
expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
|
||||
}
|
||||
|
||||
await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
for (const file of hls.files) {
|
||||
expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPublicVODFiles (uuid: string) {
|
||||
const video = await server.videos.getWithToken({ id: uuid })
|
||||
|
||||
for (const file of getAllFiles(video)) {
|
||||
expectStartWith(file.fileUrl, ObjectStorageCommand.getScalewayBaseUrl())
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
const hls = getHLS(video)
|
||||
|
||||
if (hls) {
|
||||
expectStartWith(hls.playlistUrl, ObjectStorageCommand.getScalewayBaseUrl())
|
||||
expectStartWith(hls.segmentsSha256Url, ObjectStorageCommand.getScalewayBaseUrl())
|
||||
|
||||
await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
server = await createSingleServer(1, ObjectStorageCommand.getDefaultScalewayConfig({ serverNumber: 1 }))
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableMinimumTranscoding()
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user1')
|
||||
|
||||
sqlCommand = new SQLCommand(server)
|
||||
})
|
||||
|
||||
describe('VOD', function () {
|
||||
let privateVideoUUID: string
|
||||
let publicVideoUUID: string
|
||||
let passwordProtectedVideoUUID: string
|
||||
let userPrivateVideoUUID: string
|
||||
|
||||
const correctPassword = 'my super password'
|
||||
const correctPasswordHeader = { 'x-peertube-video-password': correctPassword }
|
||||
const incorrectPasswordHeader = { 'x-peertube-video-password': correctPassword + 'toto' }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getSampleFileUrls (videoId: string) {
|
||||
const video = await server.videos.getWithToken({ id: videoId })
|
||||
|
||||
return {
|
||||
webVideoFile: video.files[0].fileUrl,
|
||||
hlsFile: getHLS(video).files[0].fileUrl
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('Should upload a private video and have appropriate object storage ACL', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
|
||||
privateVideoUUID = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'user video', token: userToken, privacy: VideoPrivacy.PRIVATE })
|
||||
userPrivateVideoUUID = uuid
|
||||
}
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
await checkPrivateVODFiles(privateVideoUUID)
|
||||
})
|
||||
|
||||
it('Should upload a password protected video and have appropriate object storage ACL', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
{
|
||||
const { uuid } = await server.videos.quickUpload({
|
||||
name: 'video',
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
videoPasswords: [ correctPassword ]
|
||||
})
|
||||
passwordProtectedVideoUUID = uuid
|
||||
}
|
||||
await waitJobs([ server ])
|
||||
|
||||
await checkPrivateVODFiles(passwordProtectedVideoUUID)
|
||||
})
|
||||
|
||||
it('Should upload a public video and have appropriate object storage ACL', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED })
|
||||
await waitJobs([ server ])
|
||||
|
||||
publicVideoUUID = uuid
|
||||
|
||||
await checkPublicVODFiles(publicVideoUUID)
|
||||
})
|
||||
|
||||
it('Should not get files without appropriate OAuth token', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID)
|
||||
|
||||
await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should not get files without appropriate password or appropriate OAuth token', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID)
|
||||
|
||||
await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({
|
||||
url: webVideoFile,
|
||||
token: null,
|
||||
headers: incorrectPasswordHeader,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({
|
||||
url: webVideoFile,
|
||||
token: null,
|
||||
headers: correctPasswordHeader,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({
|
||||
url: hlsFile,
|
||||
token: null,
|
||||
headers: incorrectPasswordHeader,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({
|
||||
url: hlsFile,
|
||||
token: null,
|
||||
headers: correctPasswordHeader,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not get HLS file of another video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const privateVideo = await server.videos.getWithToken({ id: privateVideoUUID })
|
||||
const hlsFilename = basename(getHLS(privateVideo).files[0].fileUrl)
|
||||
|
||||
const badUrl = server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + userPrivateVideoUUID + '/' + hlsFilename
|
||||
const goodUrl = server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + privateVideoUUID + '/' + hlsFilename
|
||||
|
||||
await makeRawRequest({ url: badUrl, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
|
||||
it('Should correctly check OAuth, video file token of private video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID })
|
||||
const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID })
|
||||
|
||||
const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID)
|
||||
|
||||
for (const url of [ webVideoFile, hlsFile ]) {
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly check OAuth, video file token or video password of password protected video', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID })
|
||||
const goodVideoFileToken = await server.videoToken.getVideoFileToken({
|
||||
videoId: passwordProtectedVideoUUID,
|
||||
videoPassword: correctPassword
|
||||
})
|
||||
|
||||
const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID)
|
||||
|
||||
for (const url of [ hlsFile, webVideoFile ]) {
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({
|
||||
url,
|
||||
headers: incorrectPasswordHeader,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
await makeRawRequest({ url, headers: correctPasswordHeader, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should reinject video file token', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID })
|
||||
|
||||
await checkVideoFileTokenReinjection({
|
||||
server,
|
||||
videoUUID: privateVideoUUID,
|
||||
videoFileToken,
|
||||
resolutions: [ 240, 720 ],
|
||||
isLive: false
|
||||
})
|
||||
})
|
||||
|
||||
it('Should update public video to private', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.videos.update({ id: publicVideoUUID, attributes: { privacy: VideoPrivacy.INTERNAL } })
|
||||
|
||||
await checkPrivateVODFiles(publicVideoUUID)
|
||||
})
|
||||
|
||||
it('Should update private video to public', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.videos.update({ id: publicVideoUUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
|
||||
await checkPublicVODFiles(publicVideoUUID)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Live', function () {
|
||||
let normalLiveId: string
|
||||
let normalLive: LiveVideo
|
||||
|
||||
let permanentLiveId: string
|
||||
let permanentLive: LiveVideo
|
||||
|
||||
let passwordProtectedLiveId: string
|
||||
let passwordProtectedLive: LiveVideo
|
||||
|
||||
const correctPassword = 'my super password'
|
||||
|
||||
let unrelatedFileToken: string
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkLiveFiles (live: LiveVideo, liveId: string, videoPassword?: string) {
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
await server.live.waitUntilPublished({ videoId: liveId })
|
||||
|
||||
const video = videoPassword
|
||||
? await server.videos.getWithPassword({ id: liveId, password: videoPassword })
|
||||
: await server.videos.getWithToken({ id: liveId })
|
||||
|
||||
const fileToken = videoPassword
|
||||
? await server.videoToken.getVideoFileToken({ token: null, videoId: video.uuid, videoPassword })
|
||||
: await server.videoToken.getVideoFileToken({ videoId: video.uuid })
|
||||
|
||||
const hls = video.streamingPlaylists[0]
|
||||
|
||||
for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
|
||||
expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
|
||||
|
||||
await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
|
||||
if (videoPassword) {
|
||||
await makeRawRequest({
|
||||
url,
|
||||
headers: { 'x-peertube-video-password': videoPassword },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await makeRawRequest({
|
||||
url,
|
||||
headers: { 'x-peertube-video-password': 'incorrectPassword' },
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
}
|
||||
|
||||
async function checkReplay (replay: VideoDetails) {
|
||||
const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid })
|
||||
|
||||
const hls = replay.streamingPlaylists[0]
|
||||
expect(hls.files).to.not.have.lengthOf(0)
|
||||
|
||||
for (const file of hls.files) {
|
||||
await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({
|
||||
url: file.fileUrl,
|
||||
query: { videoFileToken: unrelatedFileToken },
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
|
||||
for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
|
||||
expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
|
||||
|
||||
await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableMinimumTranscoding()
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'another video' })
|
||||
unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
|
||||
|
||||
await server.config.enableLive({
|
||||
allowReplay: true,
|
||||
transcoding: true,
|
||||
resolutions: 'min'
|
||||
})
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: true,
|
||||
permanentLive: false,
|
||||
privacy: VideoPrivacy.PRIVATE
|
||||
})
|
||||
normalLiveId = video.uuid
|
||||
normalLive = live
|
||||
}
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: true,
|
||||
permanentLive: true,
|
||||
privacy: VideoPrivacy.PRIVATE
|
||||
})
|
||||
permanentLiveId = video.uuid
|
||||
permanentLive = live
|
||||
}
|
||||
|
||||
{
|
||||
const { video, live } = await server.live.quickCreate({
|
||||
saveReplay: false,
|
||||
permanentLive: false,
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
videoPasswords: [ correctPassword ]
|
||||
})
|
||||
passwordProtectedLiveId = video.uuid
|
||||
passwordProtectedLive = live
|
||||
}
|
||||
})
|
||||
|
||||
it('Should create a private normal live and have a private static path', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await checkLiveFiles(normalLive, normalLiveId)
|
||||
})
|
||||
|
||||
it('Should create a private permanent live and have a private static path', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await checkLiveFiles(permanentLive, permanentLiveId)
|
||||
})
|
||||
|
||||
it('Should create a password protected live and have a private static path', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await checkLiveFiles(passwordProtectedLive, passwordProtectedLiveId, correctPassword)
|
||||
})
|
||||
|
||||
it('Should reinject video file token in permanent live', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey })
|
||||
await server.live.waitUntilPublished({ videoId: permanentLiveId })
|
||||
|
||||
const video = await server.videos.getWithToken({ id: permanentLiveId })
|
||||
const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid })
|
||||
|
||||
await checkVideoFileTokenReinjection({
|
||||
server,
|
||||
videoUUID: permanentLiveId,
|
||||
videoFileToken,
|
||||
resolutions: [ 720 ],
|
||||
isLive: true
|
||||
})
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
|
||||
it('Should have created a replay of the normal live with a private static path', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId })
|
||||
|
||||
const replay = await server.videos.getWithToken({ id: normalLiveId })
|
||||
await checkReplay(replay)
|
||||
})
|
||||
|
||||
it('Should have created a replay of the permanent live with a private static path', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await server.live.waitUntilWaiting({ videoId: permanentLiveId })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const live = await server.videos.getWithToken({ id: permanentLiveId })
|
||||
const replayFromList = await findExternalSavedVideo(server, live)
|
||||
const replay = await server.videos.getWithToken({ id: replayFromList.id })
|
||||
|
||||
await checkReplay(replay)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With private files proxy disabled and public ACL for private files', function () {
|
||||
let videoUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await server.kill()
|
||||
|
||||
const config = ObjectStorageCommand.getDefaultScalewayConfig({
|
||||
serverNumber: 1,
|
||||
enablePrivateProxy: false,
|
||||
privateACL: 'public-read'
|
||||
})
|
||||
await server.run(config)
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
it('Should display object storage path for a private video and be able to access them', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await checkPublicVODFiles(videoUUID)
|
||||
})
|
||||
|
||||
it('Should not be able to access object storage proxy', async function () {
|
||||
const privateVideo = await server.videos.getWithToken({ id: videoUUID })
|
||||
const webVideoFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl)
|
||||
const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl)
|
||||
|
||||
await makeRawRequest({
|
||||
url: server.url + '/object-storage-proxy/web-videos/private/' + webVideoFilename,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeRawRequest({
|
||||
url: server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + videoUUID + '/' + hlsFilename,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const { data } = await server.videos.listAllForAdmin()
|
||||
|
||||
for (const v of data) {
|
||||
await server.videos.remove({ id: v.uuid })
|
||||
}
|
||||
|
||||
for (const v of data) {
|
||||
await server.servers.waitUntilLog('Removed files of video ' + v.url)
|
||||
}
|
||||
|
||||
await sqlCommand.cleanup()
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,434 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import bytes from 'bytes'
|
||||
import { expect } from 'chai'
|
||||
import { stat } from 'fs/promises'
|
||||
import merge from 'lodash-es/merge.js'
|
||||
import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models'
|
||||
import { areMockObjectStorageTestsDisabled, sha1 } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
createSingleServer,
|
||||
doubleFollow,
|
||||
killallServers,
|
||||
makeRawRequest,
|
||||
ObjectStorageCommand,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expectStartWith, expectLogDoesNotContain } from '@tests/shared/checks.js'
|
||||
import { checkTmpIsEmpty } from '@tests/shared/directories.js'
|
||||
import { generateHighBitrateVideo } from '@tests/shared/generate.js'
|
||||
import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
|
||||
|
||||
async function checkFiles (options: {
|
||||
server: PeerTubeServer
|
||||
originServer: PeerTubeServer
|
||||
originSQLCommand: SQLCommand
|
||||
|
||||
video: VideoDetails
|
||||
|
||||
baseMockUrl?: string
|
||||
|
||||
playlistBucket: string
|
||||
playlistPrefix?: string
|
||||
|
||||
webVideoBucket: string
|
||||
webVideoPrefix?: string
|
||||
}) {
|
||||
const {
|
||||
server,
|
||||
originServer,
|
||||
originSQLCommand,
|
||||
video,
|
||||
playlistBucket,
|
||||
webVideoBucket,
|
||||
baseMockUrl,
|
||||
playlistPrefix,
|
||||
webVideoPrefix
|
||||
} = options
|
||||
|
||||
let allFiles = video.files
|
||||
|
||||
for (const file of video.files) {
|
||||
const baseUrl = baseMockUrl
|
||||
? `${baseMockUrl}/${webVideoBucket}/`
|
||||
: `http://${webVideoBucket}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||
|
||||
const prefix = webVideoPrefix || ''
|
||||
const start = baseUrl + prefix
|
||||
|
||||
expectStartWith(file.fileUrl, start)
|
||||
|
||||
const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 })
|
||||
const location = res.headers['location']
|
||||
expectStartWith(location, start)
|
||||
|
||||
await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
|
||||
const hls = video.streamingPlaylists[0]
|
||||
|
||||
if (hls) {
|
||||
allFiles = allFiles.concat(hls.files)
|
||||
|
||||
const baseUrl = baseMockUrl
|
||||
? `${baseMockUrl}/${playlistBucket}/`
|
||||
: `http://${playlistBucket}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||
|
||||
const prefix = playlistPrefix || ''
|
||||
const start = baseUrl + prefix
|
||||
|
||||
expectStartWith(hls.playlistUrl, start)
|
||||
expectStartWith(hls.segmentsSha256Url, start)
|
||||
|
||||
await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const resSha = await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(JSON.stringify(resSha.body)).to.not.throw
|
||||
|
||||
let i = 0
|
||||
for (const file of hls.files) {
|
||||
expectStartWith(file.fileUrl, start)
|
||||
|
||||
const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 })
|
||||
const location = res.headers['location']
|
||||
expectStartWith(location, start)
|
||||
|
||||
await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
if (originServer.internalServerNumber === server.internalServerNumber) {
|
||||
const infohash = sha1(`${2 + hls.playlistUrl}+V${i}`)
|
||||
const dbInfohashes = await originSQLCommand.getPlaylistInfohash(hls.id)
|
||||
|
||||
expect(dbInfohashes).to.include(infohash)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of allFiles) {
|
||||
await checkWebTorrentWorks(file.magnetUri)
|
||||
|
||||
const res = await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(res.body).to.have.length.above(100)
|
||||
}
|
||||
|
||||
return allFiles.map(f => f.fileUrl)
|
||||
}
|
||||
|
||||
function runTestSuite (options: {
|
||||
fixture?: string
|
||||
|
||||
maxUploadPart?: string
|
||||
|
||||
playlistBucket: string
|
||||
playlistPrefix?: string
|
||||
|
||||
webVideoBucket: string
|
||||
webVideoPrefix?: string
|
||||
|
||||
useMockBaseUrl?: boolean
|
||||
}) {
|
||||
const mockObjectStorageProxy = new MockObjectStorageProxy()
|
||||
const { fixture } = options
|
||||
let baseMockUrl: string
|
||||
|
||||
let servers: PeerTubeServer[]
|
||||
let sqlCommands: SQLCommand[] = []
|
||||
const objectStorage = new ObjectStorageCommand()
|
||||
|
||||
let keptUrls: string[] = []
|
||||
|
||||
const uuidsToDelete: string[] = []
|
||||
let deletedUrls: string[] = []
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
const port = await mockObjectStorageProxy.initialize()
|
||||
baseMockUrl = options.useMockBaseUrl
|
||||
? `http://127.0.0.1:${port}`
|
||||
: undefined
|
||||
|
||||
await objectStorage.createMockBucket(options.playlistBucket)
|
||||
await objectStorage.createMockBucket(options.webVideoBucket)
|
||||
|
||||
const config = {
|
||||
object_storage: {
|
||||
enabled: true,
|
||||
endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(),
|
||||
region: ObjectStorageCommand.getMockRegion(),
|
||||
|
||||
credentials: ObjectStorageCommand.getMockCredentialsConfig(),
|
||||
|
||||
max_upload_part: options.maxUploadPart || '5MB',
|
||||
|
||||
streaming_playlists: {
|
||||
bucket_name: options.playlistBucket,
|
||||
prefix: options.playlistPrefix,
|
||||
base_url: baseMockUrl
|
||||
? `${baseMockUrl}/${options.playlistBucket}`
|
||||
: undefined
|
||||
},
|
||||
|
||||
web_videos: {
|
||||
bucket_name: options.webVideoBucket,
|
||||
prefix: options.webVideoPrefix,
|
||||
base_url: baseMockUrl
|
||||
? `${baseMockUrl}/${options.webVideoBucket}`
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
servers = await createMultipleServers(2, config)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
for (const server of servers) {
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video to keep' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const files = await server.videos.listFiles({ id: uuid })
|
||||
keptUrls = keptUrls.concat(files.map(f => f.fileUrl))
|
||||
}
|
||||
|
||||
sqlCommands = servers.map(s => new SQLCommand(s))
|
||||
})
|
||||
|
||||
it('Should upload a video and move it to the object storage without transcoding', async function () {
|
||||
this.timeout(40000)
|
||||
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1', fixture })
|
||||
uuidsToDelete.push(uuid)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
|
||||
|
||||
deletedUrls = deletedUrls.concat(files)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should upload a video and move it to the object storage with transcoding', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { uuid } = await servers[1].videos.quickUpload({ name: 'video 2', fixture })
|
||||
uuidsToDelete.push(uuid)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
|
||||
|
||||
deletedUrls = deletedUrls.concat(files)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fetch correctly all the files', async function () {
|
||||
for (const url of deletedUrls.concat(keptUrls)) {
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly delete the files', async function () {
|
||||
await servers[0].videos.remove({ id: uuidsToDelete[0] })
|
||||
await servers[1].videos.remove({ id: uuidsToDelete[1] })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const url of deletedUrls) {
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have kept other files', async function () {
|
||||
for (const url of keptUrls) {
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have an empty tmp directory', async function () {
|
||||
for (const server of servers) {
|
||||
await checkTmpIsEmpty(server)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have downloaded files from object storage', async function () {
|
||||
for (const server of servers) {
|
||||
await expectLogDoesNotContain(server, 'from object storage')
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await mockObjectStorageProxy.terminate()
|
||||
await objectStorage.cleanupMock()
|
||||
|
||||
for (const sqlCommand of sqlCommands) {
|
||||
await sqlCommand.cleanup()
|
||||
}
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
}
|
||||
|
||||
describe('Object storage for videos', function () {
|
||||
if (areMockObjectStorageTestsDisabled()) return
|
||||
|
||||
const objectStorage = new ObjectStorageCommand()
|
||||
|
||||
describe('Test config', function () {
|
||||
let server: PeerTubeServer
|
||||
|
||||
const baseConfig = objectStorage.getDefaultMockConfig()
|
||||
|
||||
const badCredentials = {
|
||||
access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secret_access_key: 'aJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
|
||||
}
|
||||
|
||||
it('Should fail with same bucket names without prefix', function (done) {
|
||||
const config = merge({}, baseConfig, {
|
||||
object_storage: {
|
||||
streaming_playlists: {
|
||||
bucket_name: 'aaa'
|
||||
},
|
||||
|
||||
web_videos: {
|
||||
bucket_name: 'aaa'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createSingleServer(1, config)
|
||||
.then(() => done(new Error('Did not throw')))
|
||||
.catch(() => done())
|
||||
})
|
||||
|
||||
it('Should fail with bad credentials', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await objectStorage.prepareDefaultMockBuckets()
|
||||
|
||||
const config = merge({}, baseConfig, {
|
||||
object_storage: {
|
||||
credentials: badCredentials
|
||||
}
|
||||
})
|
||||
|
||||
server = await createSingleServer(1, config)
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
|
||||
await waitJobs([ server ], { skipDelayed: true })
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
expectStartWith(video.files[0].fileUrl, server.url)
|
||||
|
||||
await killallServers([ server ])
|
||||
})
|
||||
|
||||
it('Should succeed with credentials from env', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await objectStorage.prepareDefaultMockBuckets()
|
||||
|
||||
const config = merge({}, baseConfig, {
|
||||
object_storage: {
|
||||
credentials: {
|
||||
access_key_id: '',
|
||||
secret_access_key: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const goodCredentials = ObjectStorageCommand.getMockCredentialsConfig()
|
||||
|
||||
server = await createSingleServer(1, config, {
|
||||
env: {
|
||||
AWS_ACCESS_KEY_ID: goodCredentials.access_key_id,
|
||||
AWS_SECRET_ACCESS_KEY: goodCredentials.secret_access_key
|
||||
}
|
||||
})
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({ name: 'video' })
|
||||
|
||||
await waitJobs([ server ], { skipDelayed: true })
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await objectStorage.cleanupMock()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test simple object storage', function () {
|
||||
runTestSuite({
|
||||
playlistBucket: objectStorage.getMockBucketName('streaming-playlists'),
|
||||
webVideoBucket: objectStorage.getMockBucketName('web-videos')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test object storage with prefix', function () {
|
||||
runTestSuite({
|
||||
playlistBucket: objectStorage.getMockBucketName('mybucket'),
|
||||
webVideoBucket: objectStorage.getMockBucketName('mybucket'),
|
||||
|
||||
playlistPrefix: 'streaming-playlists_',
|
||||
webVideoPrefix: 'webvideo_'
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test object storage with prefix and base URL', function () {
|
||||
runTestSuite({
|
||||
playlistBucket: objectStorage.getMockBucketName('mybucket'),
|
||||
webVideoBucket: objectStorage.getMockBucketName('mybucket'),
|
||||
|
||||
playlistPrefix: 'streaming-playlists/',
|
||||
webVideoPrefix: 'webvideo/',
|
||||
|
||||
useMockBaseUrl: true
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test object storage with file bigger than upload part', function () {
|
||||
let fixture: string
|
||||
const maxUploadPart = '5MB'
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
fixture = await generateHighBitrateVideo()
|
||||
|
||||
const { size } = await stat(fixture)
|
||||
|
||||
if (bytes.parse(maxUploadPart) > size) {
|
||||
throw Error(`Fixture file is too small (${size}) to make sense for this test.`)
|
||||
}
|
||||
})
|
||||
|
||||
runTestSuite({
|
||||
maxUploadPart,
|
||||
playlistBucket: objectStorage.getMockBucketName('streaming-playlists'),
|
||||
webVideoBucket: objectStorage.getMockBucketName('web-videos'),
|
||||
fixture
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
import './redundancy-constraints.js'
|
||||
import './redundancy.js'
|
||||
import './manage-redundancy.js'
|
||||
@@ -0,0 +1,324 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
RedundancyCommand,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { VideoPrivacy, VideoRedundanciesTarget } from '@peertube/peertube-models'
|
||||
|
||||
describe('Test manage videos redundancy', function () {
|
||||
const targets: VideoRedundanciesTarget[] = [ 'my-videos', 'remote-videos' ]
|
||||
|
||||
let servers: PeerTubeServer[]
|
||||
let video1Server2UUID: string
|
||||
let video2Server2UUID: string
|
||||
let redundanciesToRemove: number[] = []
|
||||
|
||||
let commands: RedundancyCommand[]
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const config = {
|
||||
transcoding: {
|
||||
hls: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
redundancy: {
|
||||
videos: {
|
||||
check_interval: '1 second',
|
||||
strategies: [
|
||||
{
|
||||
strategy: 'recently-added',
|
||||
min_lifetime: '1 hour',
|
||||
size: '10MB',
|
||||
min_views: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
servers = await createMultipleServers(3, config)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
commands = servers.map(s => s.redundancy)
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
|
||||
video1Server2UUID = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2' } })
|
||||
video2Server2UUID = uuid
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
await doubleFollow(servers[0], servers[2])
|
||||
await commands[0].updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not have redundancies on server 3', async function () {
|
||||
for (const target of targets) {
|
||||
const body = await commands[2].listVideos({ target })
|
||||
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly list followings by redundancy', async function () {
|
||||
const body = await servers[0].follows.getFollowings({ sort: '-redundancyAllowed' })
|
||||
|
||||
expect(body.total).to.equal(2)
|
||||
expect(body.data).to.have.lengthOf(2)
|
||||
|
||||
expect(body.data[0].following.host).to.equal(servers[1].host)
|
||||
expect(body.data[1].following.host).to.equal(servers[2].host)
|
||||
})
|
||||
|
||||
it('Should not have "remote-videos" redundancies on server 2', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 10)
|
||||
await waitJobs(servers)
|
||||
|
||||
const body = await commands[1].listVideos({ target: 'remote-videos' })
|
||||
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should have "my-videos" redundancies on server 2', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const body = await commands[1].listVideos({ target: 'my-videos' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const videos = body.data
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
||||
const videos1 = videos.find(v => v.uuid === video1Server2UUID)
|
||||
const videos2 = videos.find(v => v.uuid === video2Server2UUID)
|
||||
|
||||
expect(videos1.name).to.equal('video 1 server 2')
|
||||
expect(videos2.name).to.equal('video 2 server 2')
|
||||
|
||||
expect(videos1.redundancies.files).to.have.lengthOf(4)
|
||||
expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists)
|
||||
|
||||
for (const r of redundancies) {
|
||||
expect(r.strategy).to.be.null
|
||||
expect(r.fileUrl).to.exist
|
||||
expect(r.createdAt).to.exist
|
||||
expect(r.updatedAt).to.exist
|
||||
expect(r.expiresOn).to.exist
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have "my-videos" redundancies on server 1', async function () {
|
||||
const body = await commands[0].listVideos({ target: 'my-videos' })
|
||||
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it('Should have "remote-videos" redundancies on server 1', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const body = await commands[0].listVideos({ target: 'remote-videos' })
|
||||
expect(body.total).to.equal(2)
|
||||
|
||||
const videos = body.data
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
||||
const videos1 = videos.find(v => v.uuid === video1Server2UUID)
|
||||
const videos2 = videos.find(v => v.uuid === video2Server2UUID)
|
||||
|
||||
expect(videos1.name).to.equal('video 1 server 2')
|
||||
expect(videos2.name).to.equal('video 2 server 2')
|
||||
|
||||
expect(videos1.redundancies.files).to.have.lengthOf(4)
|
||||
expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists)
|
||||
|
||||
for (const r of redundancies) {
|
||||
expect(r.strategy).to.equal('recently-added')
|
||||
expect(r.fileUrl).to.exist
|
||||
expect(r.createdAt).to.exist
|
||||
expect(r.updatedAt).to.exist
|
||||
expect(r.expiresOn).to.exist
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly paginate and sort results', async function () {
|
||||
{
|
||||
const body = await commands[0].listVideos({
|
||||
target: 'remote-videos',
|
||||
sort: 'name',
|
||||
start: 0,
|
||||
count: 2
|
||||
})
|
||||
|
||||
const videos = body.data
|
||||
expect(videos[0].name).to.equal('video 1 server 2')
|
||||
expect(videos[1].name).to.equal('video 2 server 2')
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].listVideos({
|
||||
target: 'remote-videos',
|
||||
sort: '-name',
|
||||
start: 0,
|
||||
count: 2
|
||||
})
|
||||
|
||||
const videos = body.data
|
||||
expect(videos[0].name).to.equal('video 2 server 2')
|
||||
expect(videos[1].name).to.equal('video 1 server 2')
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].listVideos({
|
||||
target: 'remote-videos',
|
||||
sort: '-name',
|
||||
start: 1,
|
||||
count: 1
|
||||
})
|
||||
|
||||
expect(body.data[0].name).to.equal('video 1 server 2')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should manually add a redundancy and list it', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const uuid = (await servers[1].videos.quickUpload({ name: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid
|
||||
await waitJobs(servers)
|
||||
const videoId = await servers[0].videos.getId({ uuid })
|
||||
|
||||
await commands[0].addVideo({ videoId })
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 15)
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const body = await commands[0].listVideos({
|
||||
target: 'remote-videos',
|
||||
sort: '-name',
|
||||
start: 0,
|
||||
count: 5
|
||||
})
|
||||
|
||||
const video = body.data[0]
|
||||
|
||||
expect(video.name).to.equal('video 3 server 2')
|
||||
expect(video.redundancies.files).to.have.lengthOf(4)
|
||||
expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
|
||||
|
||||
for (const r of redundancies) {
|
||||
redundanciesToRemove.push(r.id)
|
||||
|
||||
expect(r.strategy).to.equal('manual')
|
||||
expect(r.fileUrl).to.exist
|
||||
expect(r.createdAt).to.exist
|
||||
expect(r.updatedAt).to.exist
|
||||
expect(r.expiresOn).to.be.null
|
||||
}
|
||||
}
|
||||
|
||||
const body = await commands[1].listVideos({
|
||||
target: 'my-videos',
|
||||
sort: '-name',
|
||||
start: 0,
|
||||
count: 5
|
||||
})
|
||||
|
||||
const video = body.data[0]
|
||||
expect(video.name).to.equal('video 3 server 2')
|
||||
expect(video.redundancies.files).to.have.lengthOf(4)
|
||||
expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
|
||||
|
||||
for (const r of redundancies) {
|
||||
expect(r.strategy).to.be.null
|
||||
expect(r.fileUrl).to.exist
|
||||
expect(r.createdAt).to.exist
|
||||
expect(r.updatedAt).to.exist
|
||||
expect(r.expiresOn).to.be.null
|
||||
}
|
||||
})
|
||||
|
||||
it('Should manually remove a redundancy and remove it from the list', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
for (const redundancyId of redundanciesToRemove) {
|
||||
await commands[0].removeVideo({ redundancyId })
|
||||
}
|
||||
|
||||
{
|
||||
const body = await commands[0].listVideos({
|
||||
target: 'remote-videos',
|
||||
sort: '-name',
|
||||
start: 0,
|
||||
count: 5
|
||||
})
|
||||
|
||||
const videos = body.data
|
||||
|
||||
expect(videos).to.have.lengthOf(2)
|
||||
|
||||
const video = videos[0]
|
||||
expect(video.name).to.equal('video 2 server 2')
|
||||
expect(video.redundancies.files).to.have.lengthOf(4)
|
||||
expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
|
||||
|
||||
redundanciesToRemove = redundancies.map(r => r.id)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should remove another (auto) redundancy', async function () {
|
||||
for (const redundancyId of redundanciesToRemove) {
|
||||
await commands[0].removeVideo({ redundancyId })
|
||||
}
|
||||
|
||||
const body = await commands[0].listVideos({
|
||||
target: 'remote-videos',
|
||||
sort: '-name',
|
||||
start: 0,
|
||||
count: 5
|
||||
})
|
||||
|
||||
const videos = body.data
|
||||
expect(videos).to.have.lengthOf(1)
|
||||
expect(videos[0].name).to.equal('video 1 server 2')
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,191 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
killallServers,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test redundancy constraints', function () {
|
||||
let remoteServer: PeerTubeServer
|
||||
let localServer: PeerTubeServer
|
||||
let servers: PeerTubeServer[]
|
||||
|
||||
const remoteServerConfig = {
|
||||
redundancy: {
|
||||
videos: {
|
||||
check_interval: '1 second',
|
||||
strategies: [
|
||||
{
|
||||
strategy: 'recently-added',
|
||||
min_lifetime: '1 hour',
|
||||
size: '100MB',
|
||||
min_views: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadWrapper (videoName: string) {
|
||||
// Wait for transcoding
|
||||
const { id } = await localServer.videos.upload({ attributes: { name: 'to transcode', privacy: VideoPrivacy.PRIVATE } })
|
||||
await waitJobs([ localServer ])
|
||||
|
||||
// Update video to schedule a federation
|
||||
await localServer.videos.update({ id, attributes: { name: videoName, privacy: VideoPrivacy.PUBLIC } })
|
||||
}
|
||||
|
||||
async function getTotalRedundanciesLocalServer () {
|
||||
const body = await localServer.redundancy.listVideos({ target: 'my-videos' })
|
||||
|
||||
return body.total
|
||||
}
|
||||
|
||||
async function getTotalRedundanciesRemoteServer () {
|
||||
const body = await remoteServer.redundancy.listVideos({ target: 'remote-videos' })
|
||||
|
||||
return body.total
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
{
|
||||
remoteServer = await createSingleServer(1, remoteServerConfig)
|
||||
}
|
||||
|
||||
{
|
||||
const config = {
|
||||
remote_redundancy: {
|
||||
videos: {
|
||||
accept_from: 'nobody'
|
||||
}
|
||||
}
|
||||
}
|
||||
localServer = await createSingleServer(2, config)
|
||||
}
|
||||
|
||||
servers = [ remoteServer, localServer ]
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
await localServer.videos.upload({ attributes: { name: 'video 1 server 2' } })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await remoteServer.follows.follow({ hosts: [ localServer.url ] })
|
||||
await waitJobs(servers)
|
||||
await remoteServer.redundancy.updateRedundancy({ host: localServer.host, redundancyAllowed: true })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have redundancy on server 1 but not on server 2 with a nobody filter', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await remoteServer.servers.waitUntilLog('Duplicated ', 5)
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesRemoteServer()
|
||||
expect(total).to.equal(1)
|
||||
}
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesLocalServer()
|
||||
expect(total).to.equal(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have redundancy on server 1 and on server 2 with an anybody filter', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const config = {
|
||||
remote_redundancy: {
|
||||
videos: {
|
||||
accept_from: 'anybody'
|
||||
}
|
||||
}
|
||||
}
|
||||
await killallServers([ localServer ])
|
||||
await localServer.run(config)
|
||||
|
||||
await uploadWrapper('video 2 server 2')
|
||||
|
||||
await remoteServer.servers.waitUntilLog('Duplicated ', 10)
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesRemoteServer()
|
||||
expect(total).to.equal(2)
|
||||
}
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesLocalServer()
|
||||
expect(total).to.equal(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have redundancy on server 1 but not on server 2 with a followings filter', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const config = {
|
||||
remote_redundancy: {
|
||||
videos: {
|
||||
accept_from: 'followings'
|
||||
}
|
||||
}
|
||||
}
|
||||
await killallServers([ localServer ])
|
||||
await localServer.run(config)
|
||||
|
||||
await uploadWrapper('video 3 server 2')
|
||||
|
||||
await remoteServer.servers.waitUntilLog('Duplicated ', 15)
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesRemoteServer()
|
||||
expect(total).to.equal(3)
|
||||
}
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesLocalServer()
|
||||
expect(total).to.equal(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have redundancy on server 1 and on server 2 with followings filter now server 2 follows server 1', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
await localServer.follows.follow({ hosts: [ remoteServer.url ] })
|
||||
await waitJobs(servers)
|
||||
|
||||
await uploadWrapper('video 4 server 2')
|
||||
await remoteServer.servers.waitUntilLog('Duplicated ', 20)
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesRemoteServer()
|
||||
expect(total).to.equal(4)
|
||||
}
|
||||
|
||||
{
|
||||
const total = await getTotalRedundanciesLocalServer()
|
||||
expect(total).to.equal(2)
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,743 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoDetails,
|
||||
VideoFile,
|
||||
VideoPrivacy,
|
||||
VideoRedundancyStrategy,
|
||||
VideoRedundancyStrategyWithManual
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
killallServers,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkSegmentHash } from '@tests/shared/streaming-playlists.js'
|
||||
import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js'
|
||||
import { magnetUriDecode } from '@tests/shared/webtorrent.js'
|
||||
|
||||
let servers: PeerTubeServer[] = []
|
||||
let video1Server2: VideoDetails
|
||||
|
||||
async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) {
|
||||
const parsed = await magnetUriDecode(file.magnetUri)
|
||||
|
||||
for (const ws of baseWebseeds) {
|
||||
const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`)
|
||||
expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
|
||||
}
|
||||
|
||||
expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
|
||||
|
||||
for (const url of parsed.urlList) {
|
||||
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
}
|
||||
|
||||
async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebVideo = true) {
|
||||
const strategies: any[] = []
|
||||
|
||||
if (strategy !== null) {
|
||||
strategies.push(
|
||||
{
|
||||
min_lifetime: '1 hour',
|
||||
strategy,
|
||||
size: '400KB',
|
||||
|
||||
...additionalParams
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const config = {
|
||||
transcoding: {
|
||||
web_videos: {
|
||||
enabled: withWebVideo
|
||||
},
|
||||
hls: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
redundancy: {
|
||||
videos: {
|
||||
check_interval: '5 seconds',
|
||||
strategies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
servers = await createMultipleServers(3, config)
|
||||
|
||||
// Get the access tokens
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
{
|
||||
const { id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
|
||||
video1Server2 = await servers[1].videos.get({ id })
|
||||
|
||||
await servers[1].views.simulateView({ id })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
// Server 1 and server 2 follow each other
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
// Server 1 and server 3 follow each other
|
||||
await doubleFollow(servers[0], servers[2])
|
||||
// Server 2 and server 3 follow each other
|
||||
await doubleFollow(servers[1], servers[2])
|
||||
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
async function ensureSameFilenames (videoUUID: string) {
|
||||
let webVideoFilenames: string[]
|
||||
let hlsFilenames: string[]
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.getWithToken({ id: videoUUID })
|
||||
|
||||
// Ensure we use the same filenames that the origin
|
||||
|
||||
const localWebVideoFilenames = video.files.map(f => basename(f.fileUrl)).sort()
|
||||
const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort()
|
||||
|
||||
if (webVideoFilenames) expect(webVideoFilenames).to.deep.equal(localWebVideoFilenames)
|
||||
else webVideoFilenames = localWebVideoFilenames
|
||||
|
||||
if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames)
|
||||
else hlsFilenames = localHLSFilenames
|
||||
}
|
||||
|
||||
return { webVideoFilenames, hlsFilenames }
|
||||
}
|
||||
|
||||
async function check1WebSeed (videoUUID?: string) {
|
||||
if (!videoUUID) videoUUID = video1Server2.uuid
|
||||
|
||||
const webseeds = [
|
||||
`${servers[1].url}/static/web-videos/`
|
||||
]
|
||||
|
||||
for (const server of servers) {
|
||||
// With token to avoid issues with video follow constraints
|
||||
const video = await server.videos.getWithToken({ id: videoUUID })
|
||||
|
||||
for (const f of video.files) {
|
||||
await checkMagnetWebseeds(f, webseeds, server)
|
||||
}
|
||||
}
|
||||
|
||||
await ensureSameFilenames(videoUUID)
|
||||
}
|
||||
|
||||
async function check2Webseeds (videoUUID?: string) {
|
||||
if (!videoUUID) videoUUID = video1Server2.uuid
|
||||
|
||||
const webseeds = [
|
||||
`${servers[0].url}/static/redundancy/`,
|
||||
`${servers[1].url}/static/web-videos/`
|
||||
]
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
for (const file of video.files) {
|
||||
await checkMagnetWebseeds(file, webseeds, server)
|
||||
}
|
||||
}
|
||||
|
||||
const { webVideoFilenames } = await ensureSameFilenames(videoUUID)
|
||||
|
||||
const directories = [
|
||||
servers[0].getDirectoryPath('redundancy'),
|
||||
servers[1].getDirectoryPath('web-videos')
|
||||
]
|
||||
|
||||
for (const directory of directories) {
|
||||
const files = await readdir(directory)
|
||||
expect(files).to.have.length.at.least(4)
|
||||
|
||||
// Ensure we files exist on disk
|
||||
expect(files.find(f => webVideoFilenames.includes(f))).to.exist
|
||||
}
|
||||
}
|
||||
|
||||
async function check0PlaylistRedundancies (videoUUID?: string) {
|
||||
if (!videoUUID) videoUUID = video1Server2.uuid
|
||||
|
||||
for (const server of servers) {
|
||||
// With token to avoid issues with video follow constraints
|
||||
const video = await server.videos.getWithToken({ id: videoUUID })
|
||||
|
||||
expect(video.streamingPlaylists).to.be.an('array')
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
await ensureSameFilenames(videoUUID)
|
||||
}
|
||||
|
||||
async function check1PlaylistRedundancies (videoUUID?: string) {
|
||||
if (!videoUUID) videoUUID = video1Server2.uuid
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
|
||||
|
||||
const redundancy = video.streamingPlaylists[0].redundancies[0]
|
||||
|
||||
expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
|
||||
}
|
||||
|
||||
const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls/' + videoUUID
|
||||
const baseUrlSegment = servers[0].url + '/static/redundancy/hls/' + videoUUID
|
||||
|
||||
const video = await servers[0].videos.get({ id: videoUUID })
|
||||
const hlsPlaylist = video.streamingPlaylists[0]
|
||||
|
||||
for (const resolution of [ 240, 360, 480, 720 ]) {
|
||||
await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist })
|
||||
}
|
||||
|
||||
const { hlsFilenames } = await ensureSameFilenames(videoUUID)
|
||||
|
||||
const directories = [
|
||||
servers[0].getDirectoryPath('redundancy/hls'),
|
||||
servers[1].getDirectoryPath('streaming-playlists/hls')
|
||||
]
|
||||
|
||||
for (const directory of directories) {
|
||||
const files = await readdir(join(directory, videoUUID))
|
||||
expect(files).to.have.length.at.least(4)
|
||||
|
||||
// Ensure we files exist on disk
|
||||
expect(files.find(f => hlsFilenames.includes(f))).to.exist
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
|
||||
let totalSize: number = null
|
||||
let statsLength = 1
|
||||
|
||||
if (strategy !== 'manual') {
|
||||
totalSize = 409600
|
||||
statsLength = 2
|
||||
}
|
||||
|
||||
const data = await servers[0].stats.get()
|
||||
expect(data.videosRedundancy).to.have.lengthOf(statsLength)
|
||||
|
||||
const stat = data.videosRedundancy[0]
|
||||
expect(stat.strategy).to.equal(strategy)
|
||||
expect(stat.totalSize).to.equal(totalSize)
|
||||
|
||||
return stat
|
||||
}
|
||||
|
||||
async function checkStatsWith1Redundancy (strategy: VideoRedundancyStrategyWithManual, onlyHls = false) {
|
||||
const stat = await checkStatsGlobal(strategy)
|
||||
|
||||
expect(stat.totalUsed).to.be.at.least(1).and.below(409601)
|
||||
expect(stat.totalVideoFiles).to.equal(onlyHls ? 4 : 8)
|
||||
expect(stat.totalVideos).to.equal(1)
|
||||
}
|
||||
|
||||
async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWithManual) {
|
||||
const stat = await checkStatsGlobal(strategy)
|
||||
|
||||
expect(stat.totalUsed).to.equal(0)
|
||||
expect(stat.totalVideoFiles).to.equal(0)
|
||||
expect(stat.totalVideos).to.equal(0)
|
||||
}
|
||||
|
||||
async function findServerFollows () {
|
||||
const body = await servers[0].follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
|
||||
const follows = body.data
|
||||
const server2 = follows.find(f => f.following.host === `${servers[1].host}`)
|
||||
const server3 = follows.find(f => f.following.host === `${servers[2].host}`)
|
||||
|
||||
return { server2, server3 }
|
||||
}
|
||||
|
||||
async function enableRedundancyOnServer1 () {
|
||||
await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
|
||||
|
||||
const { server2, server3 } = await findServerFollows()
|
||||
|
||||
expect(server3).to.not.be.undefined
|
||||
expect(server3.following.hostRedundancyAllowed).to.be.false
|
||||
|
||||
expect(server2).to.not.be.undefined
|
||||
expect(server2.following.hostRedundancyAllowed).to.be.true
|
||||
}
|
||||
|
||||
async function disableRedundancyOnServer1 () {
|
||||
await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: false })
|
||||
|
||||
const { server2, server3 } = await findServerFollows()
|
||||
|
||||
expect(server3).to.not.be.undefined
|
||||
expect(server3.following.hostRedundancyAllowed).to.be.false
|
||||
|
||||
expect(server2).to.not.be.undefined
|
||||
expect(server2.following.hostRedundancyAllowed).to.be.false
|
||||
}
|
||||
|
||||
describe('Test videos redundancy', function () {
|
||||
|
||||
describe('With most-views strategy', function () {
|
||||
const strategy = 'most-views'
|
||||
|
||||
before(function () {
|
||||
this.timeout(240000)
|
||||
|
||||
return createServers(strategy)
|
||||
})
|
||||
|
||||
it('Should have 1 webseed on the first video', async function () {
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
await checkStatsWithoutRedundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should enable redundancy on server 1', function () {
|
||||
return enableRedundancyOnServer1()
|
||||
})
|
||||
|
||||
it('Should have 2 webseeds on the first video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 5)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check2Webseeds()
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await disableRedundancyOnServer1()
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(5000)
|
||||
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
|
||||
await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
return cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With trending strategy', function () {
|
||||
const strategy = 'trending'
|
||||
|
||||
before(function () {
|
||||
this.timeout(240000)
|
||||
|
||||
return createServers(strategy)
|
||||
})
|
||||
|
||||
it('Should have 1 webseed on the first video', async function () {
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
await checkStatsWithoutRedundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should enable redundancy on server 1', function () {
|
||||
return enableRedundancyOnServer1()
|
||||
})
|
||||
|
||||
it('Should have 2 webseeds on the first video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 5)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check2Webseeds()
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should unfollow server 3 and keep duplicated videos', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await servers[0].follows.unfollow({ target: servers[2] })
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(5000)
|
||||
|
||||
await check2Webseeds()
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should unfollow server 2 and remove duplicated videos', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await servers[0].follows.unfollow({ target: servers[1] })
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(5000)
|
||||
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
|
||||
await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With recently added strategy', function () {
|
||||
const strategy = 'recently-added'
|
||||
|
||||
before(function () {
|
||||
this.timeout(240000)
|
||||
|
||||
return createServers(strategy, { min_views: 3 })
|
||||
})
|
||||
|
||||
it('Should have 1 webseed on the first video', async function () {
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
await checkStatsWithoutRedundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should enable redundancy on server 1', function () {
|
||||
return enableRedundancyOnServer1()
|
||||
})
|
||||
|
||||
it('Should still have 1 webseed on the first video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(15000)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
await checkStatsWithoutRedundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should view 2 times the first video to have > min_views config', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await servers[0].views.simulateView({ id: video1Server2.uuid })
|
||||
await servers[2].views.simulateView({ id: video1Server2.uuid })
|
||||
|
||||
await wait(10000)
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have 2 webseeds on the first video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 5)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check2Webseeds()
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should remove the video and the redundancy files', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await saveVideoInServers(servers, video1Server2.uuid)
|
||||
await servers[1].videos.remove({ id: video1Server2.uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With only HLS files', function () {
|
||||
const strategy = 'recently-added'
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await createServers(strategy, { min_views: 3 }, false)
|
||||
})
|
||||
|
||||
it('Should have 0 playlist redundancy on the first video', async function () {
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
})
|
||||
|
||||
it('Should enable redundancy on server 1', function () {
|
||||
return enableRedundancyOnServer1()
|
||||
})
|
||||
|
||||
it('Should still have 0 redundancy on the first video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(15000)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check0PlaylistRedundancies()
|
||||
await checkStatsWithoutRedundancy(strategy)
|
||||
})
|
||||
|
||||
it('Should have 1 redundancy on the first video', async function () {
|
||||
this.timeout(160000)
|
||||
|
||||
await servers[0].views.simulateView({ id: video1Server2.uuid })
|
||||
await servers[2].views.simulateView({ id: video1Server2.uuid })
|
||||
|
||||
await wait(10000)
|
||||
await waitJobs(servers)
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 1)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy(strategy, true)
|
||||
})
|
||||
|
||||
it('Should remove the video and the redundancy files', async function () {
|
||||
this.timeout(20000)
|
||||
|
||||
await saveVideoInServers(servers, video1Server2.uuid)
|
||||
await servers[1].videos.remove({ id: video1Server2.uuid })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With manual strategy', function () {
|
||||
before(function () {
|
||||
this.timeout(240000)
|
||||
|
||||
return createServers(null)
|
||||
})
|
||||
|
||||
it('Should have 1 webseed on the first video', async function () {
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
await checkStatsWithoutRedundancy('manual')
|
||||
})
|
||||
|
||||
it('Should create a redundancy on first video', async function () {
|
||||
await servers[0].redundancy.addVideo({ videoId: video1Server2.id })
|
||||
})
|
||||
|
||||
it('Should have 2 webseeds on the first video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 5)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check2Webseeds()
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy('manual')
|
||||
})
|
||||
|
||||
it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
const body = await servers[0].redundancy.listVideos({ target: 'remote-videos' })
|
||||
|
||||
const videos = body.data
|
||||
expect(videos).to.have.lengthOf(1)
|
||||
|
||||
const video = videos[0]
|
||||
|
||||
for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
|
||||
await servers[0].redundancy.removeVideo({ redundancyId: r.id })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
await wait(5000)
|
||||
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
|
||||
await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test expiration', function () {
|
||||
const strategy = 'recently-added'
|
||||
|
||||
async function checkContains (servers: PeerTubeServer[], str: string) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: video1Server2.uuid })
|
||||
|
||||
for (const f of video.files) {
|
||||
expect(f.magnetUri).to.contain(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNotContains (servers: PeerTubeServer[], str: string) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: video1Server2.uuid })
|
||||
|
||||
for (const f of video.files) {
|
||||
expect(f.magnetUri).to.not.contain(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
|
||||
|
||||
await enableRedundancyOnServer1()
|
||||
})
|
||||
|
||||
it('Should still have 2 webseeds after 10 seconds', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await wait(10000)
|
||||
|
||||
try {
|
||||
await checkContains(servers, 'http%3A%2F%2F' + servers[0].hostname + '%3A' + servers[0].port)
|
||||
} catch {
|
||||
// Maybe a server deleted a redundancy in the scheduler
|
||||
await wait(2000)
|
||||
|
||||
await checkContains(servers, 'http%3A%2F%2F' + servers[0].hostname + '%3A' + servers[0].port)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should stop server 1 and expire video redundancy', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await killallServers([ servers[0] ])
|
||||
|
||||
await wait(15000)
|
||||
|
||||
await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2F' + servers[0].port + '%3A' + servers[0].port)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test file replacement', function () {
|
||||
let video2Server2UUID: string
|
||||
const strategy = 'recently-added'
|
||||
|
||||
before(async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
|
||||
|
||||
await enableRedundancyOnServer1()
|
||||
|
||||
await waitJobs(servers)
|
||||
await servers[0].servers.waitUntilLog('Duplicated ', 5)
|
||||
await waitJobs(servers)
|
||||
|
||||
await check2Webseeds()
|
||||
await check1PlaylistRedundancies()
|
||||
await checkStatsWith1Redundancy(strategy)
|
||||
|
||||
const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2', privacy: VideoPrivacy.PRIVATE } })
|
||||
video2Server2UUID = uuid
|
||||
|
||||
// Wait transcoding before federation
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[1].videos.update({ id: video2Server2UUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
})
|
||||
|
||||
it('Should cache video 2 webseeds on the first video', async function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
let checked = false
|
||||
|
||||
while (checked === false) {
|
||||
await wait(1000)
|
||||
|
||||
try {
|
||||
await check1WebSeed()
|
||||
await check0PlaylistRedundancies()
|
||||
|
||||
await check2Webseeds(video2Server2UUID)
|
||||
await check1PlaylistRedundancies(video2Server2UUID)
|
||||
|
||||
checked = true
|
||||
} catch {
|
||||
checked = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should disable strategy and remove redundancies', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await killallServers([ servers[0] ])
|
||||
await servers[0].run({
|
||||
redundancy: {
|
||||
videos: {
|
||||
check_interval: '1 second',
|
||||
strategies: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './runner-common.js'
|
||||
export * from './runner-live-transcoding.js'
|
||||
export * from './runner-socket.js'
|
||||
export * from './runner-studio-transcoding.js'
|
||||
export * from './runner-transcription.js'
|
||||
export * from './runner-vod-transcoding.js'
|
||||
@@ -0,0 +1,744 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
Runner,
|
||||
RunnerJob,
|
||||
RunnerJobAdmin,
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
RunnerJobVODWebVideoTranscodingPayload,
|
||||
RunnerRegistrationToken
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test runner common actions', function () {
|
||||
let server: PeerTubeServer
|
||||
let registrationToken: string
|
||||
let runnerToken: string
|
||||
let jobMaxPriority: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
server = await createSingleServer(1, {
|
||||
remote_runners: {
|
||||
stalled_jobs: {
|
||||
vod: '5 seconds'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableTranscoding({ hls: true, webVideo: true })
|
||||
await server.config.enableRemoteTranscoding()
|
||||
})
|
||||
|
||||
describe('Managing runner registration tokens', function () {
|
||||
let base: RunnerRegistrationToken[]
|
||||
let registrationTokenToDelete: RunnerRegistrationToken
|
||||
|
||||
it('Should have a default registration token', async function () {
|
||||
const { total, data } = await server.runnerRegistrationTokens.list()
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
|
||||
const token = data[0]
|
||||
expect(token.id).to.exist
|
||||
expect(token.createdAt).to.exist
|
||||
expect(token.updatedAt).to.exist
|
||||
expect(token.registeredRunnersCount).to.equal(0)
|
||||
expect(token.registrationToken).to.exist
|
||||
})
|
||||
|
||||
it('Should create other registration tokens', async function () {
|
||||
await server.runnerRegistrationTokens.generate()
|
||||
await server.runnerRegistrationTokens.generate()
|
||||
|
||||
const { total, data } = await server.runnerRegistrationTokens.list()
|
||||
expect(total).to.equal(3)
|
||||
expect(data).to.have.lengthOf(3)
|
||||
})
|
||||
|
||||
it('Should list registration tokens', async function () {
|
||||
{
|
||||
const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
|
||||
expect(total).to.equal(3)
|
||||
expect(data).to.have.lengthOf(3)
|
||||
expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt))
|
||||
expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt))
|
||||
|
||||
base = data
|
||||
|
||||
registrationTokenToDelete = data[0]
|
||||
registrationToken = data[1].registrationToken
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 })
|
||||
expect(total).to.equal(3)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].registrationToken).to.equal(base[0].registrationToken)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have appropriate registeredRunnersCount for registration tokens', async function () {
|
||||
await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken })
|
||||
await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken })
|
||||
|
||||
const { data } = await server.runnerRegistrationTokens.list()
|
||||
|
||||
for (const d of data) {
|
||||
if (d.registrationToken === registrationTokenToDelete.registrationToken) {
|
||||
expect(d.registeredRunnersCount).to.equal(2)
|
||||
} else {
|
||||
expect(d.registeredRunnersCount).to.equal(0)
|
||||
}
|
||||
}
|
||||
|
||||
const { data: runners } = await server.runners.list()
|
||||
expect(runners).to.have.lengthOf(2)
|
||||
})
|
||||
|
||||
it('Should delete a registration token', async function () {
|
||||
await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id })
|
||||
|
||||
const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
for (const d of data) {
|
||||
expect(d.registeredRunnersCount).to.equal(0)
|
||||
expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have removed runners of this registration token', async function () {
|
||||
const { data: runners } = await server.runners.list()
|
||||
expect(runners).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Managing runners', function () {
|
||||
let toDelete: Runner
|
||||
|
||||
it('Should not have runners available', async function () {
|
||||
const { total, data } = await server.runners.list()
|
||||
|
||||
expect(data).to.have.lengthOf(0)
|
||||
expect(total).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should register runners', async function () {
|
||||
const now = new Date()
|
||||
|
||||
const result = await server.runners.register({
|
||||
name: 'runner 1',
|
||||
description: 'my super runner 1',
|
||||
registrationToken
|
||||
})
|
||||
expect(result.runnerToken).to.exist
|
||||
runnerToken = result.runnerToken
|
||||
|
||||
await server.runners.register({
|
||||
name: 'runner 2',
|
||||
registrationToken
|
||||
})
|
||||
|
||||
const { total, data } = await server.runners.list({ sort: 'createdAt' })
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
|
||||
for (const d of data) {
|
||||
expect(d.id).to.exist
|
||||
expect(d.createdAt).to.exist
|
||||
expect(d.updatedAt).to.exist
|
||||
expect(new Date(d.createdAt)).to.be.above(now)
|
||||
expect(new Date(d.updatedAt)).to.be.above(now)
|
||||
expect(new Date(d.lastContact)).to.be.above(now)
|
||||
expect(d.ip).to.exist
|
||||
}
|
||||
|
||||
expect(data[0].name).to.equal('runner 1')
|
||||
expect(data[0].description).to.equal('my super runner 1')
|
||||
|
||||
expect(data[1].name).to.equal('runner 2')
|
||||
expect(data[1].description).to.be.null
|
||||
|
||||
toDelete = data[1]
|
||||
})
|
||||
|
||||
it('Should list runners', async function () {
|
||||
const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 })
|
||||
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].name).to.equal('runner 1')
|
||||
})
|
||||
|
||||
it('Should delete a runner', async function () {
|
||||
await server.runners.delete({ id: toDelete.id })
|
||||
|
||||
const { total, data } = await server.runners.list()
|
||||
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].name).to.equal('runner 1')
|
||||
})
|
||||
|
||||
it('Should unregister a runner', async function () {
|
||||
const registered = await server.runners.autoRegisterRunner()
|
||||
|
||||
{
|
||||
const { total, data } = await server.runners.list()
|
||||
expect(total).to.equal(2)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
}
|
||||
|
||||
await server.runners.unregister({ runnerToken: registered })
|
||||
|
||||
{
|
||||
const { total, data } = await server.runners.list()
|
||||
expect(total).to.equal(1)
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].name).to.equal('runner 1')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Managing runner jobs', function () {
|
||||
let jobUUID: string
|
||||
let jobToken: string
|
||||
let lastRunnerContact: Date
|
||||
let failedJob: RunnerJob
|
||||
|
||||
async function checkMainJobState (
|
||||
mainJobState: RunnerJobStateType,
|
||||
otherJobStates: RunnerJobStateType[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ]
|
||||
) {
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
for (const job of data) {
|
||||
if (job.uuid === jobUUID) {
|
||||
expect(job.state.id).to.equal(mainJobState)
|
||||
} else {
|
||||
expect(otherJobStates).to.include(job.state.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMainJob () {
|
||||
return server.runnerJobs.getJob({ uuid: jobUUID })
|
||||
}
|
||||
|
||||
describe('List jobs', function () {
|
||||
|
||||
it('Should not have jobs', async function () {
|
||||
const { total, data } = await server.runnerJobs.list()
|
||||
|
||||
expect(data).to.have.lengthOf(0)
|
||||
expect(total).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should upload a video and have available jobs', async function () {
|
||||
await server.videos.quickUpload({ name: 'to transcode' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { total, data } = await server.runnerJobs.list()
|
||||
|
||||
expect(data).to.have.lengthOf(10)
|
||||
expect(total).to.equal(10)
|
||||
|
||||
for (const job of data) {
|
||||
expect(job.startedAt).to.not.exist
|
||||
expect(job.finishedAt).to.not.exist
|
||||
expect(job.payload).to.exist
|
||||
expect(job.privatePayload).to.exist
|
||||
}
|
||||
|
||||
const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding')
|
||||
const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding')
|
||||
|
||||
expect(hlsJobs).to.have.lengthOf(5)
|
||||
expect(webVideoJobs).to.have.lengthOf(5)
|
||||
|
||||
const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING)
|
||||
const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB)
|
||||
|
||||
expect(pendingJobs).to.have.lengthOf(1)
|
||||
expect(waitingJobs).to.have.lengthOf(9)
|
||||
})
|
||||
|
||||
it('Should upload another video and list/sort jobs', async function () {
|
||||
await server.videos.quickUpload({ name: 'to transcode 2' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 })
|
||||
|
||||
expect(data).to.have.lengthOf(20)
|
||||
expect(total).to.equal(20)
|
||||
|
||||
jobUUID = data[16].uuid
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' })
|
||||
expect(total).to.equal(20)
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].uuid).to.equal(jobUUID)
|
||||
}
|
||||
|
||||
{
|
||||
let previousPriority = Infinity
|
||||
const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' })
|
||||
expect(total).to.equal(20)
|
||||
|
||||
for (const job of data) {
|
||||
expect(job.priority).to.be.at.most(previousPriority)
|
||||
previousPriority = job.priority
|
||||
|
||||
if (job.state.id === RunnerJobState.PENDING) {
|
||||
jobMaxPriority = job.uuid
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should search jobs', async function () {
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ search: jobUUID })
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(total).to.equal(1)
|
||||
|
||||
expect(data[0].uuid).to.equal(jobUUID)
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ search: 'toto' })
|
||||
|
||||
expect(data).to.have.lengthOf(0)
|
||||
expect(total).to.equal(0)
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ search: 'hls' })
|
||||
|
||||
expect(data).to.not.have.lengthOf(0)
|
||||
expect(total).to.not.equal(0)
|
||||
|
||||
for (const job of data) {
|
||||
expect(job.type).to.include('hls')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should filter jobs', async function () {
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] })
|
||||
|
||||
expect(data).to.not.have.lengthOf(0)
|
||||
expect(total).to.not.equal(0)
|
||||
|
||||
for (const job of data) {
|
||||
expect(job.state.label).to.equal('Waiting for parent job to finish')
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] })
|
||||
|
||||
expect(data).to.have.lengthOf(0)
|
||||
expect(total).to.equal(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accept/update/abort/process a job', function () {
|
||||
|
||||
it('Should request available jobs', async function () {
|
||||
lastRunnerContact = new Date()
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
|
||||
// Only optimize jobs are available
|
||||
expect(availableJobs).to.have.lengthOf(2)
|
||||
|
||||
for (const job of availableJobs) {
|
||||
expect(job.uuid).to.exist
|
||||
expect(job.payload.input).to.exist
|
||||
expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist
|
||||
|
||||
expect((job as RunnerJobAdmin).privatePayload).to.not.exist
|
||||
}
|
||||
|
||||
const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding')
|
||||
const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding')
|
||||
|
||||
expect(hlsJobs).to.have.lengthOf(0)
|
||||
expect(webVideoJobs).to.have.lengthOf(2)
|
||||
|
||||
jobUUID = webVideoJobs[0].uuid
|
||||
})
|
||||
|
||||
it('Should have sorted available jobs by priority', async function () {
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
|
||||
expect(availableJobs[0].uuid).to.equal(jobMaxPriority)
|
||||
})
|
||||
|
||||
it('Should have last runner contact updated', async function () {
|
||||
await wait(1000)
|
||||
|
||||
const { data } = await server.runners.list({ sort: 'createdAt' })
|
||||
expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact)
|
||||
})
|
||||
|
||||
it('Should accept a job', async function () {
|
||||
const startedAt = new Date()
|
||||
|
||||
const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => {
|
||||
expect(job.uuid).to.equal(jobUUID)
|
||||
|
||||
expect(job.type).to.equal('vod-web-video-transcoding')
|
||||
expect(job.state.label).to.equal('Processing')
|
||||
expect(job.state.id).to.equal(RunnerJobState.PROCESSING)
|
||||
|
||||
expect(job.runner).to.exist
|
||||
expect(job.runner.name).to.equal('runner 1')
|
||||
expect(job.runner.description).to.equal('my super runner 1')
|
||||
|
||||
expect(job.progress).to.be.null
|
||||
|
||||
expect(job.startedAt).to.exist
|
||||
expect(new Date(job.startedAt)).to.be.above(startedAt)
|
||||
|
||||
expect(job.finishedAt).to.not.exist
|
||||
|
||||
expect(job.failures).to.equal(0)
|
||||
|
||||
expect(job.payload).to.exist
|
||||
|
||||
if (fromAccept) {
|
||||
expect(job.jobToken).to.exist
|
||||
expect((job as RunnerJobAdmin).privatePayload).to.not.exist
|
||||
} else {
|
||||
expect(job.jobToken).to.not.exist
|
||||
expect((job as RunnerJobAdmin).privatePayload).to.exist
|
||||
}
|
||||
}
|
||||
|
||||
checkProcessingJob(job, true)
|
||||
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
const processingJob = data.find(j => j.uuid === jobUUID)
|
||||
checkProcessingJob(processingJob, false)
|
||||
|
||||
await checkMainJobState(RunnerJobState.PROCESSING)
|
||||
})
|
||||
|
||||
it('Should update a job', async function () {
|
||||
await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 })
|
||||
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
for (const job of data) {
|
||||
if (job.state.id === RunnerJobState.PROCESSING) {
|
||||
expect(job.progress).to.equal(53)
|
||||
} else {
|
||||
expect(job.progress).to.be.null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should abort a job', async function () {
|
||||
await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' })
|
||||
|
||||
await checkMainJobState(RunnerJobState.PENDING)
|
||||
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
for (const job of data) {
|
||||
expect(job.progress).to.be.null
|
||||
}
|
||||
})
|
||||
|
||||
it('Should accept the same job again and post a success', async function () {
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist
|
||||
|
||||
const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
await checkMainJobState(RunnerJobState.PROCESSING)
|
||||
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
for (const job of data) {
|
||||
expect(job.progress).to.be.null
|
||||
}
|
||||
|
||||
const payload = {
|
||||
videoFile: 'video_short.mp4'
|
||||
}
|
||||
|
||||
await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
})
|
||||
|
||||
it('Should not have available jobs anymore', async function () {
|
||||
await checkMainJobState(RunnerJobState.COMPLETED)
|
||||
|
||||
const job = await getMainJob()
|
||||
expect(job.finishedAt).to.exist
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error job', function () {
|
||||
|
||||
it('Should accept another job and post an error', async function () {
|
||||
await server.runnerJobs.cancelAllJobs()
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
jobUUID = availableJobs[0].uuid
|
||||
|
||||
const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
|
||||
})
|
||||
|
||||
it('Should have job failures increased', async function () {
|
||||
const job = await getMainJob()
|
||||
expect(job.state.id).to.equal(RunnerJobState.PENDING)
|
||||
expect(job.failures).to.equal(1)
|
||||
expect(job.error).to.be.null
|
||||
expect(job.progress).to.be.null
|
||||
expect(job.finishedAt).to.not.exist
|
||||
})
|
||||
|
||||
it('Should error a job when job attempts is too big', async function () {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i })
|
||||
}
|
||||
|
||||
const job = await getMainJob()
|
||||
expect(job.failures).to.equal(5)
|
||||
expect(job.state.id).to.equal(RunnerJobState.ERRORED)
|
||||
expect(job.state.label).to.equal('Errored')
|
||||
expect(job.error).to.equal('Error 3')
|
||||
expect(job.progress).to.be.null
|
||||
expect(job.finishedAt).to.exist
|
||||
|
||||
failedJob = job
|
||||
})
|
||||
|
||||
it('Should have failed children jobs too', async function () {
|
||||
const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' })
|
||||
|
||||
const children = data.filter(j => j.parent?.uuid === failedJob.uuid)
|
||||
expect(children).to.have.lengthOf(9)
|
||||
|
||||
for (const child of children) {
|
||||
expect(child.parent.uuid).to.equal(failedJob.uuid)
|
||||
expect(child.parent.type).to.equal(failedJob.type)
|
||||
expect(child.parent.state.id).to.equal(failedJob.state.id)
|
||||
expect(child.parent.state.label).to.equal(failedJob.state.label)
|
||||
|
||||
expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED)
|
||||
expect(child.state.label).to.equal('Parent job failed')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cancel', function () {
|
||||
|
||||
it('Should cancel a pending job', async function () {
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
{
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
|
||||
jobUUID = pendingJob.uuid
|
||||
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID })
|
||||
}
|
||||
|
||||
{
|
||||
const job = await getMainJob()
|
||||
expect(job.state.id).to.equal(RunnerJobState.CANCELLED)
|
||||
expect(job.state.label).to.equal('Cancelled')
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
const children = data.filter(j => j.parent?.uuid === jobUUID)
|
||||
expect(children).to.have.lengthOf(9)
|
||||
|
||||
for (const child of children) {
|
||||
expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should cancel an already accepted job and skip success/error', async function () {
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
jobUUID = availableJobs[0].uuid
|
||||
|
||||
const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
await server.runnerJobs.cancelByAdmin({ jobUUID })
|
||||
|
||||
await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Remove', function () {
|
||||
|
||||
it('Should remove a pending job', async function () {
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
{
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
|
||||
jobUUID = pendingJob.uuid
|
||||
|
||||
await server.runnerJobs.deleteByAdmin({ jobUUID })
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
|
||||
|
||||
const parent = data.find(j => j.uuid === jobUUID)
|
||||
expect(parent).to.not.exist
|
||||
|
||||
const children = data.filter(j => j.parent?.uuid === jobUUID)
|
||||
expect(children).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Stalled jobs', function () {
|
||||
|
||||
it('Should abort stalled jobs', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken })
|
||||
const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken })
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await wait(2000)
|
||||
|
||||
await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid })
|
||||
}
|
||||
|
||||
const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid })
|
||||
const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid })
|
||||
|
||||
expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING)
|
||||
expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rate limit', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.kill()
|
||||
|
||||
await server.run({
|
||||
rates_limit: {
|
||||
api: {
|
||||
max: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should rate limit an unknown runner, but not a registered one', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.videos.quickUpload({ name: 'video' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken })
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try {
|
||||
await server.runnerJobs.request({ runnerToken })
|
||||
await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Invalid
|
||||
{
|
||||
await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
|
||||
await server.runnerJobs.update({
|
||||
runnerToken: 'toto',
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
|
||||
})
|
||||
}
|
||||
|
||||
// Not provided
|
||||
{
|
||||
await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
|
||||
await server.runnerJobs.update({
|
||||
runnerToken: undefined,
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
|
||||
})
|
||||
}
|
||||
|
||||
// Registered
|
||||
{
|
||||
await server.runnerJobs.request({ runnerToken })
|
||||
await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,332 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { FfmpegCommand } from 'fluent-ffmpeg'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveRTMPHLSTranscodingUpdatePayload,
|
||||
LiveVideo,
|
||||
LiveVideoError,
|
||||
LiveVideoErrorType,
|
||||
RunnerJob,
|
||||
RunnerJobLiveRTMPHLSTranscodingPayload,
|
||||
Video,
|
||||
VideoPrivacy,
|
||||
VideoState
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
sendRTMPStream,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
stopFfmpeg,
|
||||
testFfmpegStreamError,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test runner live transcoding', function () {
|
||||
let server: PeerTubeServer
|
||||
let runnerToken: string
|
||||
let baseUrl: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableRemoteTranscoding()
|
||||
await server.config.enableTranscoding()
|
||||
runnerToken = await server.runners.autoRegisterRunner()
|
||||
|
||||
baseUrl = server.url + '/static/streaming-playlists/hls'
|
||||
})
|
||||
|
||||
describe('Without transcoding enabled', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableLive({
|
||||
allowReplay: false,
|
||||
resolutions: 'min',
|
||||
transcoding: false
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not have available jobs', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { live, video } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
|
||||
|
||||
const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
await server.live.waitUntilPublished({ videoId: video.id })
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.requestLive({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(0)
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With transcoding enabled on classic live', function () {
|
||||
let live: LiveVideo
|
||||
let video: Video
|
||||
let ffmpegCommand: FfmpegCommand
|
||||
let jobUUID: string
|
||||
let acceptedJob: RunnerJob & { jobToken: string }
|
||||
|
||||
async function testPlaylistFile (fixture: string, expected: string) {
|
||||
const text = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${fixture}` })
|
||||
expect(await readFile(buildAbsoluteFixturePath(expected), 'utf-8')).to.equal(text)
|
||||
|
||||
}
|
||||
|
||||
async function testTSFile (fixture: string, expected: string) {
|
||||
const { body } = await makeRawRequest({ url: `${baseUrl}/${video.uuid}/${fixture}`, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(await readFile(buildAbsoluteFixturePath(expected))).to.deep.equal(body)
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableLive({
|
||||
allowReplay: true,
|
||||
resolutions: 'max',
|
||||
transcoding: true
|
||||
})
|
||||
})
|
||||
|
||||
it('Should publish a a live and have available jobs', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const data = await server.live.quickCreate({ permanentLive: false, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
|
||||
live = data.live
|
||||
video = data.video
|
||||
|
||||
ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const job = await server.runnerJobs.requestLiveJob(runnerToken)
|
||||
jobUUID = job.uuid
|
||||
|
||||
expect(job.type).to.equal('live-rtmp-hls-transcoding')
|
||||
expect(job.payload.input.rtmpUrl).to.exist
|
||||
|
||||
expect(job.payload.output.toTranscode).to.have.lengthOf(5)
|
||||
|
||||
for (const { resolution, fps } of job.payload.output.toTranscode) {
|
||||
expect([ 720, 480, 360, 240, 144 ]).to.contain(resolution)
|
||||
|
||||
expect(fps).to.be.above(25)
|
||||
expect(fps).to.be.below(70)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should update the live with a new chunk', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const { job } = await server.runnerJobs.accept<RunnerJobLiveRTMPHLSTranscodingPayload>({ jobUUID, runnerToken })
|
||||
acceptedJob = job
|
||||
|
||||
{
|
||||
const payload: LiveRTMPHLSTranscodingUpdatePayload = {
|
||||
masterPlaylistFile: 'live/master.m3u8',
|
||||
resolutionPlaylistFile: 'live/0.m3u8',
|
||||
resolutionPlaylistFilename: '0.m3u8',
|
||||
type: 'add-chunk',
|
||||
videoChunkFile: 'live/0-000067.ts',
|
||||
videoChunkFilename: '0-000067.ts'
|
||||
}
|
||||
await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload, progress: 50 })
|
||||
|
||||
const updatedJob = await server.runnerJobs.getJob({ uuid: job.uuid })
|
||||
expect(updatedJob.progress).to.equal(50)
|
||||
}
|
||||
|
||||
{
|
||||
const payload: LiveRTMPHLSTranscodingUpdatePayload = {
|
||||
resolutionPlaylistFile: 'live/1.m3u8',
|
||||
resolutionPlaylistFilename: '1.m3u8',
|
||||
type: 'add-chunk',
|
||||
videoChunkFile: 'live/1-000068.ts',
|
||||
videoChunkFilename: '1-000068.ts'
|
||||
}
|
||||
await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload })
|
||||
}
|
||||
|
||||
await wait(1000)
|
||||
|
||||
await testPlaylistFile('master.m3u8', 'live/master.m3u8')
|
||||
await testPlaylistFile('0.m3u8', 'live/0.m3u8')
|
||||
await testPlaylistFile('1.m3u8', 'live/1.m3u8')
|
||||
|
||||
await testTSFile('0-000067.ts', 'live/0-000067.ts')
|
||||
await testTSFile('1-000068.ts', 'live/1-000068.ts')
|
||||
})
|
||||
|
||||
it('Should replace existing m3u8 on update', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const payload: LiveRTMPHLSTranscodingUpdatePayload = {
|
||||
masterPlaylistFile: 'live/1.m3u8',
|
||||
resolutionPlaylistFilename: '0.m3u8',
|
||||
resolutionPlaylistFile: 'live/1.m3u8',
|
||||
type: 'add-chunk',
|
||||
videoChunkFile: 'live/1-000069.ts',
|
||||
videoChunkFilename: '1-000068.ts'
|
||||
}
|
||||
await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
|
||||
await wait(1000)
|
||||
|
||||
await testPlaylistFile('master.m3u8', 'live/1.m3u8')
|
||||
await testPlaylistFile('0.m3u8', 'live/1.m3u8')
|
||||
await testTSFile('1-000068.ts', 'live/1-000069.ts')
|
||||
})
|
||||
|
||||
it('Should update the live with removed chunks', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const payload: LiveRTMPHLSTranscodingUpdatePayload = {
|
||||
resolutionPlaylistFile: 'live/0.m3u8',
|
||||
resolutionPlaylistFilename: '0.m3u8',
|
||||
type: 'remove-chunk',
|
||||
videoChunkFilename: '1-000068.ts'
|
||||
}
|
||||
await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
|
||||
|
||||
await wait(1000)
|
||||
|
||||
await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/master.m3u8` })
|
||||
await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/0.m3u8` })
|
||||
await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/1.m3u8` })
|
||||
await makeRawRequest({ url: `${baseUrl}/${video.uuid}/0-000067.ts`, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: `${baseUrl}/${video.uuid}/1-000068.ts`, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should complete the live and save the replay', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
for (const segment of [ '0-000069.ts', '0-000070.ts' ]) {
|
||||
const payload: LiveRTMPHLSTranscodingUpdatePayload = {
|
||||
masterPlaylistFile: 'live/master.m3u8',
|
||||
resolutionPlaylistFilename: '0.m3u8',
|
||||
resolutionPlaylistFile: 'live/0.m3u8',
|
||||
type: 'add-chunk',
|
||||
videoChunkFile: 'live/' + segment,
|
||||
videoChunkFilename: segment
|
||||
}
|
||||
await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
|
||||
|
||||
await wait(1000)
|
||||
}
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
{
|
||||
const { state } = await server.videos.get({ id: video.uuid })
|
||||
expect(state.id).to.equal(VideoState.PUBLISHED)
|
||||
}
|
||||
|
||||
await stopFfmpeg(ffmpegCommand)
|
||||
|
||||
await server.runnerJobs.success({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload: {} })
|
||||
|
||||
await wait(1500)
|
||||
await waitJobs([ server ])
|
||||
|
||||
{
|
||||
const { state } = await server.videos.get({ id: video.uuid })
|
||||
expect(state.id).to.equal(VideoState.LIVE_ENDED)
|
||||
|
||||
const session = await server.live.findLatestSession({ videoId: video.uuid })
|
||||
expect(session.error).to.be.null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('With transcoding enabled on cancelled/aborted/errored live', function () {
|
||||
let live: LiveVideo
|
||||
let video: Video
|
||||
let ffmpegCommand: FfmpegCommand
|
||||
|
||||
async function prepare () {
|
||||
ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
|
||||
await server.runnerJobs.requestLiveJob(runnerToken)
|
||||
|
||||
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
async function checkSessionError (error: LiveVideoErrorType) {
|
||||
await wait(1500)
|
||||
await waitJobs([ server ])
|
||||
|
||||
const session = await server.live.findLatestSession({ videoId: video.uuid })
|
||||
expect(session.error).to.equal(error)
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableLive({
|
||||
allowReplay: true,
|
||||
resolutions: 'max',
|
||||
transcoding: true
|
||||
})
|
||||
|
||||
const data = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
|
||||
live = data.live
|
||||
video = data.video
|
||||
})
|
||||
|
||||
it('Should abort a running live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const job = await prepare()
|
||||
|
||||
await Promise.all([
|
||||
server.runnerJobs.abort({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, reason: 'abort' }),
|
||||
testFfmpegStreamError(ffmpegCommand, true)
|
||||
])
|
||||
|
||||
// Abort is not supported
|
||||
await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
|
||||
})
|
||||
|
||||
it('Should cancel a running live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const job = await prepare()
|
||||
|
||||
await Promise.all([
|
||||
server.runnerJobs.cancelByAdmin({ jobUUID: job.uuid }),
|
||||
testFfmpegStreamError(ffmpegCommand, true)
|
||||
])
|
||||
|
||||
await checkSessionError(LiveVideoError.RUNNER_JOB_CANCEL)
|
||||
})
|
||||
|
||||
it('Should error a running live', async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
const job = await prepare()
|
||||
|
||||
await Promise.all([
|
||||
server.runnerJobs.error({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, message: 'error' }),
|
||||
testFfmpegStreamError(ffmpegCommand, true)
|
||||
])
|
||||
|
||||
await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test runner socket', function () {
|
||||
let server: PeerTubeServer
|
||||
let runnerToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
await server.config.enableTranscoding({ hls: true, webVideo: true })
|
||||
await server.config.enableRemoteTranscoding()
|
||||
runnerToken = await server.runners.autoRegisterRunner()
|
||||
})
|
||||
|
||||
it('Should throw an error without runner token', function (done) {
|
||||
const localSocket = server.socketIO.getRunnersSocket({ runnerToken: null })
|
||||
localSocket.on('connect_error', err => {
|
||||
expect(err.message).to.contain('No runner token provided')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('Should throw an error with a bad runner token', function (done) {
|
||||
const localSocket = server.socketIO.getRunnersSocket({ runnerToken: 'ergag' })
|
||||
localSocket.on('connect_error', err => {
|
||||
expect(err.message).to.contain('Invalid runner token')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('Should not send ping if there is no available jobs', async function () {
|
||||
let pings = 0
|
||||
const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
|
||||
localSocket.on('available-jobs', () => pings++)
|
||||
|
||||
expect(pings).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should send a ping on available job', async function () {
|
||||
let pings = 0
|
||||
const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
|
||||
localSocket.on('available-jobs', () => pings++)
|
||||
|
||||
await server.videos.quickUpload({ name: 'video1' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (pings !== 1) {
|
||||
await wait(500)
|
||||
}
|
||||
|
||||
await server.videos.quickUpload({ name: 'video2' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while ((pings as number) !== 2) {
|
||||
await wait(500)
|
||||
}
|
||||
|
||||
await server.runnerJobs.cancelAllJobs()
|
||||
})
|
||||
|
||||
it('Should send a ping when a child is ready', async function () {
|
||||
let pings = 0
|
||||
const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
|
||||
localSocket.on('available-jobs', () => pings++)
|
||||
|
||||
await server.videos.quickUpload({ name: 'video3' })
|
||||
await waitJobs([ server ])
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (pings !== 1) {
|
||||
await wait(500)
|
||||
}
|
||||
|
||||
await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
|
||||
await waitJobs([ server ])
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while ((pings as number) !== 2) {
|
||||
await wait(500)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not send a ping if the ended job does not have a child', async function () {
|
||||
let pings = 0
|
||||
const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
|
||||
localSocket.on('available-jobs', () => pings++)
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken })
|
||||
const job = availableJobs.find(j => j.type === 'vod-web-video-transcoding')
|
||||
await server.runnerJobs.autoProcessWebVideoJob(runnerToken, job.uuid)
|
||||
|
||||
// Wait for debounce
|
||||
await wait(1000)
|
||||
await waitJobs([ server ])
|
||||
|
||||
expect(pings).to.equal(0)
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,169 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
RunnerJobStudioTranscodingPayload,
|
||||
VideoStudioTranscodingSuccess,
|
||||
VideoState,
|
||||
VideoStudioTask,
|
||||
VideoStudioTaskIntro
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
VideoStudioCommand,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkVideoDuration } from '@tests/shared/checks.js'
|
||||
import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js'
|
||||
|
||||
describe('Test runner video studio transcoding', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let runnerToken: string
|
||||
let videoUUID: string
|
||||
let jobUUID: string
|
||||
|
||||
async function renewStudio (tasks: VideoStudioTask[] = VideoStudioCommand.getComplexTask()) {
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(1)
|
||||
|
||||
jobUUID = availableJobs[0].uuid
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
|
||||
await servers[0].config.enableStudio()
|
||||
await servers[0].config.enableRemoteStudio()
|
||||
|
||||
runnerToken = await servers[0].runners.autoRegisterRunner()
|
||||
})
|
||||
|
||||
it('Should error a studio transcoding job', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await renewStudio()
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
|
||||
const jobToken = job.jobToken
|
||||
|
||||
await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
|
||||
}
|
||||
|
||||
const video = await servers[0].videos.get({ id: videoUUID })
|
||||
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||
|
||||
await checkPersistentTmpIsEmpty(servers[0])
|
||||
})
|
||||
|
||||
it('Should cancel a transcoding job', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await renewStudio()
|
||||
|
||||
await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
|
||||
|
||||
const video = await servers[0].videos.get({ id: videoUUID })
|
||||
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||
|
||||
await checkPersistentTmpIsEmpty(servers[0])
|
||||
})
|
||||
|
||||
it('Should execute a remote studio job', async function () {
|
||||
this.timeout(240_000)
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
name: 'add-outro' as 'add-outro',
|
||||
options: {
|
||||
file: 'video_short.webm'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add-watermark' as 'add-watermark',
|
||||
options: {
|
||||
file: 'custom-thumbnail.png'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add-intro' as 'add-intro',
|
||||
options: {
|
||||
file: 'video_very_short_240p.mp4'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
await renewStudio(tasks)
|
||||
|
||||
for (const server of servers) {
|
||||
await checkVideoDuration(server, videoUUID, 5)
|
||||
}
|
||||
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobStudioTranscodingPayload>({ runnerToken, jobUUID })
|
||||
const jobToken = job.jobToken
|
||||
|
||||
expect(job.type === 'video-studio-transcoding')
|
||||
expect(job.payload.input.videoFileUrl).to.exist
|
||||
|
||||
// Check video input file
|
||||
{
|
||||
await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
}
|
||||
|
||||
// Check task files
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
const task = tasks[i]
|
||||
const payloadTask = job.payload.tasks[i]
|
||||
|
||||
expect(payloadTask.name).to.equal(task.name)
|
||||
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath(task.options.file))
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({
|
||||
url: (payloadTask as VideoStudioTaskIntro).options.file as string,
|
||||
jobToken,
|
||||
runnerToken
|
||||
})
|
||||
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
}
|
||||
|
||||
const payload: VideoStudioTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' }
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
await checkVideoDuration(server, videoUUID, 2)
|
||||
}
|
||||
|
||||
await checkPersistentTmpIsEmpty(servers[0])
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
RunnerJobTranscriptionPayload,
|
||||
TranscriptionSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test runner transcription', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let runnerToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableTranscription({ remote: true })
|
||||
runnerToken = await servers[0].runners.autoRegisterRunner()
|
||||
})
|
||||
|
||||
async function upload () {
|
||||
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video', language: undefined } })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(1)
|
||||
|
||||
const jobUUID = availableJobs[0].uuid
|
||||
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobTranscriptionPayload>({ runnerToken, jobUUID })
|
||||
return { uuid, job }
|
||||
}
|
||||
|
||||
it('Should execute a remote transcription job', async function () {
|
||||
this.timeout(240_000)
|
||||
|
||||
const { uuid, job } = await upload()
|
||||
|
||||
expect(job.type === 'video-transcription')
|
||||
expect(job.payload.input.videoFileUrl).to.exist
|
||||
|
||||
// Check video input file
|
||||
{
|
||||
await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken: job.jobToken, runnerToken })
|
||||
}
|
||||
|
||||
const payload: TranscriptionSuccess = {
|
||||
inputLanguage: 'ar',
|
||||
vttFile: 'subtitle-good1.vtt'
|
||||
}
|
||||
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
expect(video.language.id).to.equal('ar')
|
||||
|
||||
const captions = await server.captions.list({ videoId: uuid })
|
||||
expect(captions)
|
||||
}
|
||||
|
||||
await checkPersistentTmpIsEmpty(servers[0])
|
||||
})
|
||||
|
||||
it('Should not assign caption/language with an unknown inputLanguage', async function () {
|
||||
this.timeout(240_000)
|
||||
|
||||
const { uuid, job } = await upload()
|
||||
|
||||
const payload: TranscriptionSuccess = {
|
||||
inputLanguage: 'toto',
|
||||
vttFile: 'subtitle-good1.vtt'
|
||||
}
|
||||
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
expect(video.language.id).to.be.null
|
||||
|
||||
const { total, data } = await server.captions.list({ videoId: uuid })
|
||||
expect(total).to.equal(0)
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should error a transcription job and decrease the job count', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { job, uuid } = await upload()
|
||||
await servers[0].runnerJobs.error({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, message: 'Error' })
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const { job: { jobToken } } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID: job.uuid })
|
||||
|
||||
await servers[0].runnerJobs.error({ runnerToken, jobUUID: job.uuid, jobToken, message: 'Error' })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await servers[0].captions.runGenerate({ videoId: uuid })
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,545 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobVODAudioMergeTranscodingPayload,
|
||||
RunnerJobVODHLSTranscodingPayload,
|
||||
RunnerJobVODPayload,
|
||||
RunnerJobVODWebVideoTranscodingPayload,
|
||||
VideoState,
|
||||
VODAudioMergeTranscodingSuccess,
|
||||
VODHLSTranscodingSuccess,
|
||||
VODWebVideoTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
makeGetRequest,
|
||||
makeRawRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function processAllJobs (server: PeerTubeServer, runnerToken: string) {
|
||||
do {
|
||||
const { availableJobs } = await server.runnerJobs.requestVOD({ runnerToken })
|
||||
if (availableJobs.length === 0) break
|
||||
|
||||
const { job } = await server.runnerJobs.accept<RunnerJobVODPayload>({ runnerToken, jobUUID: availableJobs[0].uuid })
|
||||
|
||||
const payload: RunnerJobSuccessPayload = {
|
||||
videoFile: `video_short_${job.payload.output.resolution}p.mp4`,
|
||||
resolutionPlaylistFile: `video_short_${job.payload.output.resolution}p.m3u8`
|
||||
}
|
||||
await server.runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
|
||||
} while (true)
|
||||
|
||||
await waitJobs([ server ])
|
||||
}
|
||||
|
||||
describe('Test runner VOD transcoding', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
let runnerToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120_000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].config.enableRemoteTranscoding()
|
||||
runnerToken = await servers[0].runners.autoRegisterRunner()
|
||||
})
|
||||
|
||||
describe('Without transcoding', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].config.disableTranscoding()
|
||||
await servers[0].videos.quickUpload({ name: 'video' })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should not have available jobs', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('With classic transcoding enabled', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
|
||||
})
|
||||
|
||||
it('Should error a transcoding job', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].runnerJobs.cancelAllJobs()
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
|
||||
const jobUUID = availableJobs[0].uuid
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
|
||||
const jobToken = job.jobToken
|
||||
|
||||
await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
|
||||
}
|
||||
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
|
||||
})
|
||||
|
||||
it('Should cancel a transcoding job', async function () {
|
||||
await servers[0].runnerJobs.cancelAllJobs()
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||
await waitJobs(servers)
|
||||
|
||||
const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
|
||||
const jobUUID = availableJobs[0].uuid
|
||||
|
||||
await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
|
||||
|
||||
const video = await servers[0].videos.get({ id: uuid })
|
||||
expect(video.state.id).to.equal(VideoState.PUBLISHED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Web video transcoding only', function () {
|
||||
let videoUUID: string
|
||||
let jobToken: string
|
||||
let jobUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].runnerJobs.cancelAllJobs()
|
||||
await servers[0].config.enableTranscoding({ hls: false, webVideo: true })
|
||||
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have jobs available for remote runners', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(1)
|
||||
|
||||
jobUUID = availableJobs[0].uuid
|
||||
})
|
||||
|
||||
it('Should have a valid first transcoding job', async function () {
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
expect(job.type === 'vod-web-video-transcoding')
|
||||
expect(job.payload.input.videoFileUrl).to.exist
|
||||
expect(job.payload.output.resolution).to.equal(720)
|
||||
expect(job.payload.output.fps).to.equal(25)
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
|
||||
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
})
|
||||
|
||||
it('Should transcode the max video resolution and send it back to the server', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const payload: VODWebVideoTranscodingSuccess = {
|
||||
videoFile: 'video_short.mp4'
|
||||
}
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the video updated', async function () {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
|
||||
const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have 4 lower resolution to transcode', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(4)
|
||||
|
||||
for (const resolution of [ 480, 360, 240, 144 ]) {
|
||||
const job = availableJobs.find(j => j.payload.output.resolution === resolution)
|
||||
expect(job).to.exist
|
||||
expect(job.type).to.equal('vod-web-video-transcoding')
|
||||
|
||||
if (resolution === 240) jobUUID = job.uuid
|
||||
}
|
||||
})
|
||||
|
||||
it('Should process one of these transcoding jobs', async function () {
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
||||
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
|
||||
const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${job.payload.output.resolution}p.mp4` }
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
})
|
||||
|
||||
it('Should process all other jobs', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(3)
|
||||
|
||||
for (const resolution of [ 480, 360, 144 ]) {
|
||||
const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
|
||||
expect(availableJob).to.exist
|
||||
jobUUID = availableJob.uuid
|
||||
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
|
||||
const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the video updated', async function () {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
expect(video.files).to.have.lengthOf(5)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
|
||||
const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
|
||||
|
||||
for (const file of video.files) {
|
||||
await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have available jobs anymore', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('HLS transcoding only', function () {
|
||||
let videoUUID: string
|
||||
let jobToken: string
|
||||
let jobUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
|
||||
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should run the optimize job', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
|
||||
})
|
||||
|
||||
it('Should have 5 HLS resolution to transcode', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(5)
|
||||
|
||||
for (const resolution of [ 720, 480, 360, 240, 144 ]) {
|
||||
const job = availableJobs.find(j => j.payload.output.resolution === resolution)
|
||||
expect(job).to.exist
|
||||
expect(job.type).to.equal('vod-hls-transcoding')
|
||||
|
||||
if (resolution === 480) jobUUID = job.uuid
|
||||
}
|
||||
})
|
||||
|
||||
it('Should process one of these transcoding jobs', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
|
||||
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
|
||||
const payload: VODHLSTranscodingSuccess = {
|
||||
videoFile: 'video_short_480p.mp4',
|
||||
resolutionPlaylistFile: 'video_short_480p.m3u8'
|
||||
}
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the video updated', async function () {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const hls = video.streamingPlaylists[0]
|
||||
expect(hls.files).to.have.lengthOf(1)
|
||||
|
||||
await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should process all other jobs', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(4)
|
||||
|
||||
let maxQualityFile = 'video_short.mp4'
|
||||
|
||||
for (const resolution of [ 720, 360, 240, 144 ]) {
|
||||
const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
|
||||
expect(availableJob).to.exist
|
||||
jobUUID = availableJob.uuid
|
||||
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
|
||||
const payload: VODHLSTranscodingSuccess = {
|
||||
videoFile: `video_short_${resolution}p.mp4`,
|
||||
resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
|
||||
}
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
if (resolution === 720) {
|
||||
maxQualityFile = 'video_short_720p.mp4'
|
||||
}
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the video updated', async function () {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(video.files).to.have.lengthOf(0)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const hls = video.streamingPlaylists[0]
|
||||
expect(hls.files).to.have.lengthOf(5)
|
||||
|
||||
await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have available jobs anymore', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Web video and HLS transcoding', function () {
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
|
||||
|
||||
await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should process the first optimize job', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
|
||||
})
|
||||
|
||||
it('Should have 9 jobs to process', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
|
||||
expect(availableJobs).to.have.lengthOf(9)
|
||||
|
||||
const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
|
||||
const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
|
||||
|
||||
expect(webVideoJobs).to.have.lengthOf(4)
|
||||
expect(hlsJobs).to.have.lengthOf(5)
|
||||
})
|
||||
|
||||
it('Should process all available jobs', async function () {
|
||||
await processAllJobs(servers[0], runnerToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Audio merge transcoding', function () {
|
||||
let videoUUID: string
|
||||
let jobToken: string
|
||||
let jobUUID: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
|
||||
|
||||
const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
|
||||
const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
|
||||
videoUUID = uuid
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have an audio merge transcoding job', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(1)
|
||||
|
||||
expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
|
||||
|
||||
jobUUID = availableJobs[0].uuid
|
||||
})
|
||||
|
||||
it('Should have a valid remote audio merge transcoding job', async function () {
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
expect(job.type === 'vod-audio-merge-transcoding')
|
||||
expect(job.payload.input.audioFileUrl).to.exist
|
||||
expect(job.payload.input.previewFileUrl).to.exist
|
||||
expect(job.payload.output.resolution).to.equal(480)
|
||||
|
||||
{
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
}
|
||||
|
||||
{
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
|
||||
|
||||
const video = await servers[0].videos.get({ id: videoUUID })
|
||||
const { body: inputFile } = await makeGetRequest({
|
||||
url: servers[0].url,
|
||||
path: video.previewPath,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should merge the audio', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the video updated', async function () {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
|
||||
const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')))
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have 7 lower resolutions to transcode', async function () {
|
||||
const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
|
||||
expect(availableJobs).to.have.lengthOf(7)
|
||||
|
||||
for (const resolution of [ 360, 240, 144 ]) {
|
||||
const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution)
|
||||
expect(jobs).to.have.lengthOf(2)
|
||||
}
|
||||
|
||||
jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
|
||||
})
|
||||
|
||||
it('Should process one other job', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
|
||||
jobToken = job.jobToken
|
||||
|
||||
const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
|
||||
const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
|
||||
expect(body).to.deep.equal(inputFile)
|
||||
|
||||
const payload: VODHLSTranscodingSuccess = {
|
||||
videoFile: `video_short_480p.mp4`,
|
||||
resolutionPlaylistFile: `video_short_480p.m3u8`
|
||||
}
|
||||
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
await waitJobs(servers)
|
||||
})
|
||||
|
||||
it('Should have the video updated', async function () {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(video.files).to.have.lengthOf(1)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const hls = video.streamingPlaylists[0]
|
||||
expect(hls.files).to.have.lengthOf(1)
|
||||
|
||||
await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should process all available jobs', async function () {
|
||||
await processAllJobs(servers[0], runnerToken)
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
変更されたファイルが多すぎるため,一部のファイルは表示されません さらに表示
新しい課題から参照
ユーザをブロックする