]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/videos/resumable-upload.ts
Cleanup tests imports
[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
86347717 3import { expect } from 'chai'
f6d6e7f8 4import { pathExists, readdir, stat } from 'fs-extra'
5import { join } from 'path'
c55e3d72 6import { buildAbsoluteFixturePath } from '@shared/core-utils'
33ac85bf 7import { sha1 } from '@shared/extra-utils'
4c7e60bc 8import { HttpStatusCode, VideoPrivacy } from '@shared/models'
c55e3d72 9import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, setDefaultVideoChannel } from '@shared/server-commands'
f6d6e7f8 10
f6d6e7f8 11// Most classic resumable upload tests are done in other test suites
12
13describe('Test resumable upload', function () {
14 const defaultFixture = 'video_short.mp4'
254d3579 15 let server: PeerTubeServer
f6d6e7f8 16 let rootId: number
020d3d3d
C
17 let userAccessToken: string
18 let userChannelId: number
f6d6e7f8 19
20 async function buildSize (fixture: string, size?: number) {
21 if (size !== undefined) return size
22
23 const baseFixture = buildAbsoluteFixturePath(fixture)
24 return (await stat(baseFixture)).size
25 }
26
020d3d3d
C
27 async function prepareUpload (options: {
28 channelId?: number
29 token?: string
30 size?: number
31 originalName?: string
32 lastModified?: number
33 } = {}) {
34 const { token, originalName, lastModified } = options
35
36 const size = await buildSize(defaultFixture, options.size)
f6d6e7f8 37
38 const attributes = {
39 name: 'video',
020d3d3d 40 channelId: options.channelId ?? server.store.channel.id,
f6d6e7f8 41 privacy: VideoPrivacy.PUBLIC,
42 fixture: defaultFixture
43 }
44
45 const mimetype = 'video/mp4'
46
020d3d3d 47 const res = await server.videos.prepareResumableUpload({ token, attributes, size, mimetype, originalName, lastModified })
f6d6e7f8 48
49 return res.header['location'].split('?')[1]
50 }
51
52 async function sendChunks (options: {
020d3d3d 53 token?: string
f6d6e7f8 54 pathUploadId: string
55 size?: number
56 expectedStatus?: HttpStatusCode
57 contentLength?: number
58 contentRange?: string
59 contentRangeBuilder?: (start: number, chunk: any) => string
33ac85bf 60 digestBuilder?: (chunk: any) => string
f6d6e7f8 61 }) {
33ac85bf 62 const { token, pathUploadId, expectedStatus, contentLength, contentRangeBuilder, digestBuilder } = options
f6d6e7f8 63
64 const size = await buildSize(defaultFixture, options.size)
65 const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
66
89d241a7 67 return server.videos.sendResumableChunks({
020d3d3d 68 token,
f6d6e7f8 69 pathUploadId,
70 videoFilePath: absoluteFilePath,
71 size,
72 contentLength,
73 contentRangeBuilder,
33ac85bf 74 digestBuilder,
d23dd9fb 75 expectedStatus
f6d6e7f8 76 })
77 }
78
79 async function checkFileSize (uploadIdArg: string, expectedSize: number | null) {
80 const uploadId = uploadIdArg.replace(/^upload_id=/, '')
81
82 const subPath = join('tmp', 'resumable-uploads', uploadId)
89d241a7 83 const filePath = server.servers.buildDirectory(subPath)
f6d6e7f8 84 const exists = await pathExists(filePath)
85
86 if (expectedSize === null) {
87 expect(exists).to.be.false
88 return
89 }
90
91 expect(exists).to.be.true
92
93 expect((await stat(filePath)).size).to.equal(expectedSize)
94 }
95
96 async function countResumableUploads () {
97 const subPath = join('tmp', 'resumable-uploads')
89d241a7 98 const filePath = server.servers.buildDirectory(subPath)
f6d6e7f8 99
100 const files = await readdir(filePath)
101 return files.length
102 }
103
104 before(async function () {
105 this.timeout(30000)
106
254d3579 107 server = await createSingleServer(1)
83903cb6
C
108 await setAccessTokensToServers([ server ])
109 await setDefaultVideoChannel([ server ])
f6d6e7f8 110
89d241a7 111 const body = await server.users.getMyInfo()
7926c5f9 112 rootId = body.id
f6d6e7f8 113
020d3d3d
C
114 {
115 userAccessToken = await server.users.generateUserAndToken('user1')
116 const { videoChannels } = await server.users.getMyInfo({ token: userAccessToken })
117 userChannelId = videoChannels[0].id
118 }
119
89d241a7 120 await server.users.update({ userId: rootId, videoQuota: 10_000_000 })
f6d6e7f8 121 })
122
123 describe('Directory cleaning', function () {
124
33ac85bf
C
125 // FIXME: https://github.com/kukhariev/node-uploadx/pull/524/files#r852989382
126 // it('Should correctly delete files after an upload', async function () {
127 // const uploadId = await prepareUpload()
128 // await sendChunks({ pathUploadId: uploadId })
129 // await server.videos.endResumableUpload({ pathUploadId: uploadId })
f6d6e7f8 130
33ac85bf
C
131 // expect(await countResumableUploads()).to.equal(0)
132 // })
f6d6e7f8 133
134 it('Should not delete files after an unfinished upload', async function () {
135 await prepareUpload()
136
137 expect(await countResumableUploads()).to.equal(2)
138 })
139
140 it('Should not delete recent uploads', async function () {
89d241a7 141 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
f6d6e7f8 142
143 expect(await countResumableUploads()).to.equal(2)
144 })
145
146 it('Should delete old uploads', async function () {
89d241a7 147 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
f6d6e7f8 148
149 expect(await countResumableUploads()).to.equal(0)
150 })
151 })
152
153 describe('Resumable upload and chunks', function () {
154
155 it('Should accept the same amount of chunks', async function () {
156 const uploadId = await prepareUpload()
157 await sendChunks({ pathUploadId: uploadId })
158
159 await checkFileSize(uploadId, null)
160 })
161
162 it('Should not accept more chunks than expected', async function () {
020d3d3d 163 const uploadId = await prepareUpload({ size: 100 })
f6d6e7f8 164
165 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
166 await checkFileSize(uploadId, 0)
167 })
168
169 it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
020d3d3d 170 const uploadId = await prepareUpload({ size: 1500 })
f6d6e7f8 171
fea11cf2
C
172 // Content length check seems to have changed in v16
173 if (process.version.startsWith('v16')) {
174 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentLength: 1000 })
175 await checkFileSize(uploadId, 1000)
176 } else {
177 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
178 await checkFileSize(uploadId, 0)
179 }
f6d6e7f8 180 })
181
182 it('Should not accept more chunks than expected with an invalid content length', async function () {
020d3d3d 183 const uploadId = await prepareUpload({ size: 500 })
f6d6e7f8 184
185 const size = 1000
186
764b1a14
C
187 // Content length check seems to have changed in v16
188 const expectedStatus = process.version.startsWith('v16')
189 ? HttpStatusCode.CONFLICT_409
190 : HttpStatusCode.BAD_REQUEST_400
191
83903cb6 192 const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}`
764b1a14 193 await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size })
f6d6e7f8 194 await checkFileSize(uploadId, 0)
195 })
276250f0
RK
196
197 it('Should be able to accept 2 PUT requests', async function () {
198 const uploadId = await prepareUpload()
199
200 const result1 = await sendChunks({ pathUploadId: uploadId })
201 const result2 = await sendChunks({ pathUploadId: uploadId })
202
203 expect(result1.body.video.uuid).to.exist
204 expect(result1.body.video.uuid).to.equal(result2.body.video.uuid)
205
206 expect(result1.headers['x-resumable-upload-cached']).to.not.exist
207 expect(result2.headers['x-resumable-upload-cached']).to.equal('true')
208
209 await checkFileSize(uploadId, null)
210 })
020d3d3d
C
211
212 it('Should not have the same upload id with 2 different users', async function () {
213 const originalName = 'toto.mp4'
214 const lastModified = new Date().getTime()
215
216 const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
217 const uploadId2 = await prepareUpload({ originalName, lastModified, channelId: userChannelId, token: userAccessToken })
218
219 expect(uploadId1).to.not.equal(uploadId2)
220 })
221
222 it('Should have the same upload id with the same user', async function () {
223 const originalName = 'toto.mp4'
224 const lastModified = new Date().getTime()
225
226 const uploadId1 = await prepareUpload({ originalName, lastModified })
227 const uploadId2 = await prepareUpload({ originalName, lastModified })
228
229 expect(uploadId1).to.equal(uploadId2)
230 })
231
232 it('Should not cache a request with 2 different users', async function () {
233 const originalName = 'toto.mp4'
234 const lastModified = new Date().getTime()
235
236 const uploadId = await prepareUpload({ originalName, lastModified, token: server.accessToken })
237
238 await sendChunks({ pathUploadId: uploadId, token: server.accessToken })
239 await sendChunks({ pathUploadId: uploadId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
240 })
241
242 it('Should not cache a request after a delete', async function () {
243 const originalName = 'toto.mp4'
244 const lastModified = new Date().getTime()
245 const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
246
247 await sendChunks({ pathUploadId: uploadId1 })
248 await server.videos.endResumableUpload({ pathUploadId: uploadId1 })
249
250 const uploadId2 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
251 expect(uploadId1).to.equal(uploadId2)
252
253 const result2 = await sendChunks({ pathUploadId: uploadId1 })
254 expect(result2.headers['x-resumable-upload-cached']).to.not.exist
255 })
33ac85bf
C
256
257 it('Should refuse an invalid digest', async function () {
258 const uploadId = await prepareUpload({ token: server.accessToken })
259
260 await sendChunks({
261 pathUploadId: uploadId,
262 token: server.accessToken,
263 digestBuilder: () => 'sha=' + 'a'.repeat(40),
264 expectedStatus: 460
265 })
266 })
267
268 it('Should accept an appropriate digest', async function () {
269 const uploadId = await prepareUpload({ token: server.accessToken })
270
271 await sendChunks({
272 pathUploadId: uploadId,
273 token: server.accessToken,
274 digestBuilder: (chunk: Buffer) => {
275 return 'sha1=' + sha1(chunk, 'base64')
276 }
277 })
278 })
f6d6e7f8 279 })
280
06c27593 281 after(async function () {
83903cb6 282 await cleanupTests([ server ])
06c27593 283 })
f6d6e7f8 284})