]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/videos/resumable-upload.ts
Use an object to represent a server
[github/Chocobozzz/PeerTube.git] / server / tests / api / videos / resumable-upload.ts
CommitLineData
f6d6e7f8 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { pathExists, readdir, stat } from 'fs-extra'
6import { join } from 'path'
7import { HttpStatusCode } from '@shared/core-utils'
8import {
9 buildAbsoluteFixturePath,
06c27593 10 cleanupTests,
254d3579
C
11 createSingleServer,
12 PeerTubeServer,
f6d6e7f8 13 setAccessTokensToServers,
7926c5f9 14 setDefaultVideoChannel
f6d6e7f8 15} from '@shared/extra-utils'
7926c5f9 16import { VideoPrivacy } from '@shared/models'
f6d6e7f8 17
18const expect = chai.expect
19
20// Most classic resumable upload tests are done in other test suites
21
22describe('Test resumable upload', function () {
23 const defaultFixture = 'video_short.mp4'
254d3579 24 let server: PeerTubeServer
f6d6e7f8 25 let rootId: number
26
27 async function buildSize (fixture: string, size?: number) {
28 if (size !== undefined) return size
29
30 const baseFixture = buildAbsoluteFixturePath(fixture)
31 return (await stat(baseFixture)).size
32 }
33
34 async function prepareUpload (sizeArg?: number) {
35 const size = await buildSize(defaultFixture, sizeArg)
36
37 const attributes = {
38 name: 'video',
89d241a7 39 channelId: server.store.channel.id,
f6d6e7f8 40 privacy: VideoPrivacy.PUBLIC,
41 fixture: defaultFixture
42 }
43
44 const mimetype = 'video/mp4'
45
89d241a7 46 const res = await server.videos.prepareResumableUpload({ attributes, size, mimetype })
f6d6e7f8 47
48 return res.header['location'].split('?')[1]
49 }
50
51 async function sendChunks (options: {
52 pathUploadId: string
53 size?: number
54 expectedStatus?: HttpStatusCode
55 contentLength?: number
56 contentRange?: string
57 contentRangeBuilder?: (start: number, chunk: any) => string
58 }) {
59 const { pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options
60
61 const size = await buildSize(defaultFixture, options.size)
62 const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
63
89d241a7 64 return server.videos.sendResumableChunks({
f6d6e7f8 65 pathUploadId,
66 videoFilePath: absoluteFilePath,
67 size,
68 contentLength,
69 contentRangeBuilder,
d23dd9fb 70 expectedStatus
f6d6e7f8 71 })
72 }
73
74 async function checkFileSize (uploadIdArg: string, expectedSize: number | null) {
75 const uploadId = uploadIdArg.replace(/^upload_id=/, '')
76
77 const subPath = join('tmp', 'resumable-uploads', uploadId)
89d241a7 78 const filePath = server.servers.buildDirectory(subPath)
f6d6e7f8 79 const exists = await pathExists(filePath)
80
81 if (expectedSize === null) {
82 expect(exists).to.be.false
83 return
84 }
85
86 expect(exists).to.be.true
87
88 expect((await stat(filePath)).size).to.equal(expectedSize)
89 }
90
91 async function countResumableUploads () {
92 const subPath = join('tmp', 'resumable-uploads')
89d241a7 93 const filePath = server.servers.buildDirectory(subPath)
f6d6e7f8 94
95 const files = await readdir(filePath)
96 return files.length
97 }
98
99 before(async function () {
100 this.timeout(30000)
101
254d3579 102 server = await createSingleServer(1)
f6d6e7f8 103 await setAccessTokensToServers([ server ])
104 await setDefaultVideoChannel([ server ])
105
89d241a7 106 const body = await server.users.getMyInfo()
7926c5f9 107 rootId = body.id
f6d6e7f8 108
89d241a7 109 await server.users.update({ userId: rootId, videoQuota: 10_000_000 })
f6d6e7f8 110 })
111
112 describe('Directory cleaning', function () {
113
114 it('Should correctly delete files after an upload', async function () {
115 const uploadId = await prepareUpload()
116 await sendChunks({ pathUploadId: uploadId })
117
118 expect(await countResumableUploads()).to.equal(0)
119 })
120
121 it('Should not delete files after an unfinished upload', async function () {
122 await prepareUpload()
123
124 expect(await countResumableUploads()).to.equal(2)
125 })
126
127 it('Should not delete recent uploads', async function () {
89d241a7 128 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
f6d6e7f8 129
130 expect(await countResumableUploads()).to.equal(2)
131 })
132
133 it('Should delete old uploads', async function () {
89d241a7 134 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
f6d6e7f8 135
136 expect(await countResumableUploads()).to.equal(0)
137 })
138 })
139
140 describe('Resumable upload and chunks', function () {
141
142 it('Should accept the same amount of chunks', async function () {
143 const uploadId = await prepareUpload()
144 await sendChunks({ pathUploadId: uploadId })
145
146 await checkFileSize(uploadId, null)
147 })
148
149 it('Should not accept more chunks than expected', async function () {
150 const size = 100
151 const uploadId = await prepareUpload(size)
152
153 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
154 await checkFileSize(uploadId, 0)
155 })
156
157 it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
158 const uploadId = await prepareUpload(1500)
159
160 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
161 await checkFileSize(uploadId, 0)
162 })
163
164 it('Should not accept more chunks than expected with an invalid content length', async function () {
165 const uploadId = await prepareUpload(500)
166
167 const size = 1000
168
169 const contentRangeBuilder = start => `bytes ${start}-${start + size - 1}/${size}`
170 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentRangeBuilder, contentLength: size })
171 await checkFileSize(uploadId, 0)
172 })
173 })
174
06c27593
C
175 after(async function () {
176 await cleanupTests([ server ])
177 })
f6d6e7f8 178})