From f97916d2f71e671f596e1d320762b8db53d4efc8 Mon Sep 17 00:00:00 2001 From: Matthew Holloway Date: Wed, 13 Nov 2024 16:24:44 +0000 Subject: [PATCH] Move to TypeScript: Upload, StreamSource (#734) * browser StreamSource TS * TS upload and options * Remove whitespace * Fix type of default options * Only pass strings to `HttpRequest#setHeader` (#709) * setHeader value arg should be string * Explicit casting of header values, number to string * Fix syntax * Fix lint --------- Co-Authored-By: Marius Kleidl * Fix error emission Co-Authored-By: Marius Kleidl * Allows `bytesTotal` to be null for progress events Co-Authored-By: Marius Kleidl --------- Co-authored-by: Matthew Holloway Co-authored-by: Marius Kleidl Co-authored-by: dragan_d_dragon --- lib/browser/sources/StreamSource.ts | 26 ++++---- lib/options.ts | 8 ++- lib/upload.ts | 92 ++++++++++++++++++++++------- test/spec/test-browser-specific.cjs | 16 ++--- test/spec/test-common.cjs | 12 ++-- test/spec/test-node-specific.cjs | 10 ++-- test/spec/test-parallel-uploads.cjs | 14 ++--- 7 files changed, 114 insertions(+), 64 deletions(-) diff --git a/lib/browser/sources/StreamSource.ts b/lib/browser/sources/StreamSource.ts index 3c47e192..ab805cdf 100644 --- a/lib/browser/sources/StreamSource.ts +++ b/lib/browser/sources/StreamSource.ts @@ -1,8 +1,8 @@ import type { FileSource } from '../../options.js' -function len(blobOrArray): number { +function len(blobOrArray: StreamSource['_buffer']): number { if (blobOrArray === undefined) return 0 - if (blobOrArray.size !== undefined) return blobOrArray.size + if (blobOrArray instanceof Blob) return blobOrArray.size return blobOrArray.length } @@ -10,28 +10,26 @@ function len(blobOrArray): number { Typed arrays and blobs don't have a concat method. This function helps StreamSource accumulate data to reach chunkSize. */ -function concat(a, b) { - if (a.concat) { - // Is `a` an Array? - return a.concat(b) +function concat(a: T, b: T): T { + if (Array.isArray(a) && Array.isArray(b)) { + return a.concat(b) as T } - if (a instanceof Blob) { - return new Blob([a, b], { type: a.type }) + if (a instanceof Blob && b instanceof Blob) { + return new Blob([a, b], { type: a.type }) as T } - if (a.set) { - // Is `a` a typed array? - const c = new a.constructor(a.length + b.length) + if (a instanceof Uint8Array && b instanceof Uint8Array) { + const c = new Uint8Array(a.length + b.length) c.set(a) c.set(b, a.length) - return c + return c as T } throw new Error('Unknown data type') } export default class StreamSource implements FileSource { - _reader: Pick + _reader: Pick, 'read'> - _buffer: Blob | undefined + _buffer: Blob | Uint8Array | number[] | undefined // _bufferOffset defines at which position the content of _buffer (if it is set) // is located in the view of the entire stream. It does not mean at which offset diff --git a/lib/options.ts b/lib/options.ts index a14002ee..cbf67739 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -39,8 +39,10 @@ export interface UploadOptions { fingerprint: (file: UploadInput, options: UploadOptions) => Promise uploadSize: number | null - onProgress: ((bytesSent: number, bytesTotal: number) => void) | null - onChunkComplete: ((chunkSize: number, bytesAccepted: number, bytesTotal: number) => void) | null + onProgress: ((bytesSent: number, bytesTotal: number | null) => void) | null + onChunkComplete: + | ((chunkSize: number, bytesAccepted: number, bytesTotal: number | null) => void) + | null onSuccess: (() => void) | null onError: ((error: Error | DetailedError) => void) | null onShouldRetry: @@ -67,7 +69,7 @@ export interface UploadOptions { fileReader: FileReader httpStack: HttpStack - protocol: string + protocol: typeof PROTOCOL_TUS_V1 | typeof PROTOCOL_IETF_DRAFT_03 } export interface UrlStorage { diff --git a/lib/upload.ts b/lib/upload.ts index 613154fa..becb216e 100644 --- a/lib/upload.ts +++ b/lib/upload.ts @@ -49,7 +49,7 @@ export const defaultOptions = { fileReader: null, httpStack: null, - protocol: PROTOCOL_TUS_V1, + protocol: PROTOCOL_TUS_V1 as UploadOptions['protocol'], } export default class BaseUpload { @@ -269,6 +269,11 @@ export default class BaseUpload { ? this._parallelUploadUrls.length : this.options.parallelUploads + if (this._size === null) { + this._emitError(new Error('tus: Expected _size to be set')) + return + } + // The input file will be split into multiple slices which are uploaded in separate // requests. Here we get the start and end position for the slices. const partsBoundaries = @@ -316,9 +321,13 @@ export default class BaseUpload { onError: reject, // Based in the progress for this partial upload, calculate the progress // for the entire final upload. - onProgress: (newPartProgress) => { + onProgress: (newPartProgress: number) => { totalProgress = totalProgress - lastPartProgress + newPartProgress lastPartProgress = newPartProgress + if (totalSize === null) { + this._emitError(new Error('tus: Expected totalSize to be set')) + return + } this._emitProgress(totalProgress, totalSize) }, // Wait until every partial upload has an upload URL, so we can add @@ -350,6 +359,10 @@ export default class BaseUpload { // creating the final upload. Promise.all(uploads) .then(() => { + if (this.options.endpoint === null) { + this._emitError(new Error('tus: Expected options.endpoint to be set')) + return + } req = this._openRequest('POST', this.options.endpoint) // @ts-expect-error We know that _parallelUploadUrls is defined req.setHeader('Upload-Concat', `final;${this._parallelUploadUrls.join(' ')}`) @@ -363,6 +376,10 @@ export default class BaseUpload { return this._sendRequest(req) }) .then((res) => { + if (res === undefined) { + this._emitError(new Error('tus: Expected res to be defined')) + return + } if (!inStatusCategory(res.getStatus(), 200)) { this._emitHttpError(req, res, 'tus: unexpected response while creating upload') return @@ -374,6 +391,11 @@ export default class BaseUpload { return } + if (this.options.endpoint === null) { + this._emitError(new Error('tus: Expeced endpoint to be defined.')) + return + } + this.url = resolveUrl(this.options.endpoint, location) log(`Created upload at ${this.url}`) @@ -517,10 +539,10 @@ export default class BaseUpload { * data may not have been accepted by the server yet. * * @param {number} bytesSent Number of bytes sent to the server. - * @param {number} bytesTotal Total number of bytes to be sent to the server. + * @param {number|null} bytesTotal Total number of bytes to be sent to the server. * @api private */ - _emitProgress(bytesSent, bytesTotal) { + _emitProgress(bytesSent: number, bytesTotal: number | null): void { if (typeof this.options.onProgress === 'function') { this.options.onProgress(bytesSent, bytesTotal) } @@ -532,10 +554,10 @@ export default class BaseUpload { * @param {number} chunkSize Size of the chunk that was accepted by the server. * @param {number} bytesAccepted Total number of bytes that have been * accepted by the server. - * @param {number} bytesTotal Total number of bytes to be sent to the server. + * @param {number|null} bytesTotal Total number of bytes to be sent to the server. * @api private */ - _emitChunkComplete(chunkSize, bytesAccepted, bytesTotal) { + _emitChunkComplete(chunkSize: number, bytesAccepted: number, bytesTotal: number | null): void { if (typeof this.options.onChunkComplete === 'function') { this.options.onChunkComplete(chunkSize, bytesAccepted, bytesTotal) } @@ -557,9 +579,12 @@ export default class BaseUpload { const req = this._openRequest('POST', this.options.endpoint) if (this.options.uploadLengthDeferred) { - req.setHeader('Upload-Defer-Length', 1) + req.setHeader('Upload-Defer-Length', '1') } else { - req.setHeader('Upload-Length', this._size) + if (this._size === null) { + this._emitError(new Error('tus: expected _size to be set')) + } + req.setHeader('Upload-Length', `${this._size}`) } // Add metadata if values have been added @@ -568,7 +593,7 @@ export default class BaseUpload { req.setHeader('Upload-Metadata', metadata) } - let promise: Promise + let promise: Promise if (this.options.uploadDataDuringCreation && !this.options.uploadLengthDeferred) { this._offset = 0 promise = this._addChunkToRequest(req) @@ -581,6 +606,11 @@ export default class BaseUpload { promise .then((res) => { + if (res === undefined) { + this._emitError(new Error('tus: Expected res to be set')) + return + } + if (!inStatusCategory(res.getStatus(), 200)) { this._emitHttpError(req, res, 'tus: unexpected response while creating upload') return @@ -592,6 +622,11 @@ export default class BaseUpload { return } + if (this.options.endpoint === null) { + this._emitError(new Error('tus: Expected options.endpoint to be set')) + return + } + this.url = resolveUrl(this.options.endpoint, location) log(`Created upload at ${this.url}`) @@ -628,6 +663,10 @@ export default class BaseUpload { * @api private */ _resumeUpload() { + if (this.url === null) { + this._emitError(new Error('tus: Expected url to be set')) + return + } const req = this._openRequest('HEAD', this.url) const promise = this._sendRequest(req) @@ -728,6 +767,10 @@ export default class BaseUpload { let req: HttpRequest + if (this.url === null) { + this._emitError(new Error('tus: Expected url to be set')) + return + } // Some browser and servers may not support the PATCH method. For those // cases, you can tell tus-js-client to use a POST request with the // X-HTTP-Method-Override header for simulating a PATCH request. @@ -743,6 +786,10 @@ export default class BaseUpload { promise .then((res) => { + if (res === undefined) { + this._emitError(new Error('tus: Expected res to be defined')) + return + } if (!inStatusCategory(res.getStatus(), 200)) { this._emitHttpError(req, res, 'tus: unexpected response while uploading chunk') return @@ -797,7 +844,7 @@ export default class BaseUpload { // upload size and can tell the tus server. if (this.options.uploadLengthDeferred && done) { this._size = this._offset + valueSize - req.setHeader('Upload-Length', this._size) + req.setHeader('Upload-Length', `${this._size}`) } // The specified uploadSize might not match the actual amount of data that a source @@ -859,7 +906,7 @@ export default class BaseUpload { * * @api private */ - _openRequest(method, url) { + _openRequest(method: string, url: string) { const req = openRequest(method, url, this.options) this._req = req return req @@ -928,7 +975,7 @@ export default class BaseUpload { } } -function encodeMetadata(metadata) { +function encodeMetadata(metadata: Record) { return Object.entries(metadata) .map(([key, value]) => `${key} ${Base64.encode(String(value))}`) .join(',') @@ -940,7 +987,7 @@ function encodeMetadata(metadata) { * * @api private */ -function inStatusCategory(status, category) { +function inStatusCategory(status: number, category: 100 | 200 | 300 | 400 | 500): boolean { return status >= category && status < category + 100 } @@ -951,7 +998,7 @@ function inStatusCategory(status, category) { * * @api private */ -function openRequest(method, url, options) { +function openRequest(method: string, url: string, options: UploadOptions) { const req = options.httpStack.createRequest(method, url) if (options.protocol === PROTOCOL_IETF_DRAFT_03) { @@ -1023,7 +1070,7 @@ function isOnline(): boolean { * * @api private */ -function shouldRetry(err, retryAttempt, options) { +function shouldRetry(err: Error | DetailedError, retryAttempt: number, options: UploadOptions) { // We only attempt a retry if // - retryDelays option is set // - we didn't exceed the maxium number of retries, yet, and @@ -1031,10 +1078,11 @@ function shouldRetry(err, retryAttempt, options) { // - the error is server error (i.e. not a status 4xx except a 409 or 423) or // a onShouldRetry is specified and returns true // - the browser does not indicate that we are offline + const isNetworkError = 'originalRequest' in err && err.originalRequest != null if ( options.retryDelays == null || retryAttempt >= options.retryDelays.length || - err.originalRequest == null + !isNetworkError ) { return false } @@ -1051,7 +1099,7 @@ function shouldRetry(err, retryAttempt, options) { * @param {DetailedError} err * @returns {boolean} */ -function defaultOnShouldRetry(err) { +function defaultOnShouldRetry(err: DetailedError): boolean { const status = err.originalResponse ? err.originalResponse.getStatus() : 0 return (!inStatusCategory(status, 400) || status === 409 || status === 423) && isOnline() } @@ -1062,22 +1110,24 @@ function defaultOnShouldRetry(err) { * header with the value /upload/abc, the resolved URL will be: * http://example.com/upload/abc */ -function resolveUrl(origin, link) { +function resolveUrl(origin: string, link: string): string { return new URL(link, origin).toString() } +type Part = { start: number; end: number } + /** * Calculate the start and end positions for the parts if an upload * is split into multiple parallel requests. * * @param {number} totalSize The byte size of the upload, which will be split. * @param {number} partCount The number in how many parts the upload will be split. - * @return {object[]} + * @return {Part[]} * @api private */ -function splitSizeIntoParts(totalSize, partCount) { +function splitSizeIntoParts(totalSize: number, partCount: number): Part[] { const partSize = Math.floor(totalSize / partCount) - const parts: { start: number; end: number }[] = [] + const parts: Part[] = [] for (let i = 0; i < partCount; i++) { parts.push({ diff --git a/test/spec/test-browser-specific.cjs b/test/spec/test-browser-specific.cjs index 3bb9e430..af940bbb 100644 --- a/test/spec/test-browser-specific.cjs +++ b/test/spec/test-browser-specific.cjs @@ -293,7 +293,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Upload-Length']).toBe(undefined) - expect(req.requestHeaders['Upload-Defer-Length']).toBe(1) + expect(req.requestHeaders['Upload-Defer-Length']).toBe('1') req.respondWith({ status: 201, @@ -323,7 +323,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads/blargh') expect(req.method).toBe('PATCH') expect(req.requestHeaders['Upload-Offset']).toBe('11') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream') expect(req.body).toBe(null) @@ -371,7 +371,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Upload-Length']).toBe(undefined) - expect(req.requestHeaders['Upload-Defer-Length']).toBe(1) + expect(req.requestHeaders['Upload-Defer-Length']).toBe('1') req.respondWith({ status: 201, @@ -416,7 +416,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads/blargh') expect(req.method).toBe('PATCH') expect(req.requestHeaders['Upload-Offset']).toBe('11') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream') expect(req.body).toBe(null) @@ -481,7 +481,7 @@ describe('tus', () => { req = await testStack.nextRequest() expect(req.url).toBe('http://tus.io/files/foo') expect(req.method).toBe('PATCH') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') req.respondWith({ status: 204, @@ -553,7 +553,7 @@ describe('tus', () => { req = await testStack.nextRequest() expect(req.url).toBe('http://tus.io/files/foo') expect(req.method).toBe('PATCH') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') req.respondWith({ status: 204, @@ -647,7 +647,7 @@ describe('tus', () => { req = await testStack.nextRequest() expect(req.url).toBe('http://tus.io/files/foo') expect(req.method).toBe('PATCH') - expect(req.requestHeaders['Upload-Length']).toBe(18) + expect(req.requestHeaders['Upload-Length']).toBe('18') req.respondWith({ status: 204, @@ -767,7 +767,7 @@ describe('tus', () => { req = await testStack.nextRequest() expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') req.respondWith({ status: 201, diff --git a/test/spec/test-common.cjs b/test/spec/test-common.cjs index 63191c5b..66b0ee2c 100644 --- a/test/spec/test-common.cjs +++ b/test/spec/test-common.cjs @@ -57,7 +57,7 @@ describe('tus', () => { expect(req.method).toBe('POST') expect(req.requestHeaders.Custom).toBe('blargh') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') expect(req.requestHeaders['Upload-Metadata']).toBe( 'foo aGVsbG8=,bar d29ybGQ=,nonlatin c8WCb8WEY2U=,number MTAw', ) @@ -119,7 +119,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') // The upload URL should be cleared when tus-js.client tries to create a new upload. expect(upload.url).toBe(null) @@ -147,7 +147,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream') expect(req.body.size).toBe(11) @@ -191,7 +191,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream') expect(req.body.size).toBe(6) @@ -394,7 +394,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') req.respondWith({ status: 201, @@ -492,7 +492,7 @@ describe('tus', () => { expect(req.url).toBe('http://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(0) + expect(req.requestHeaders['Upload-Length']).toBe('0') req.respondWith({ status: 201, diff --git a/test/spec/test-node-specific.cjs b/test/spec/test-node-specific.cjs index 80251c3d..7b3d5d69 100644 --- a/test/spec/test-node-specific.cjs +++ b/test/spec/test-node-specific.cjs @@ -178,7 +178,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(5) + expect(req.requestHeaders['Upload-Length']).toBe('5') expect(req.requestHeaders['Upload-Concat']).toBe('partial') req.respondWith({ @@ -207,7 +207,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(6) + expect(req.requestHeaders['Upload-Length']).toBe('6') expect(req.requestHeaders['Upload-Concat']).toBe('partial') req.respondWith({ @@ -530,9 +530,9 @@ async function expectHelloWorldUpload(input, options) { expect(req.method).toBe('POST') if (options.uploadLengthDeferred) { expect(req.requestHeaders['Upload-Length']).toBe(undefined) - expect(req.requestHeaders['Upload-Defer-Length']).toBe(1) + expect(req.requestHeaders['Upload-Defer-Length']).toBe('1') } else { - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') expect(req.requestHeaders['Upload-Defer-Length']).toBe(undefined) } @@ -562,7 +562,7 @@ async function expectHelloWorldUpload(input, options) { expect(req.requestHeaders['Upload-Offset']).toBe('7') if (options.uploadLengthDeferred) { - expect(req.requestHeaders['Upload-Length']).toBe(11) + expect(req.requestHeaders['Upload-Length']).toBe('11') } expect(await getBodySize(req.body)).toBe(4) diff --git a/test/spec/test-parallel-uploads.cjs b/test/spec/test-parallel-uploads.cjs index 17879420..922fd79e 100644 --- a/test/spec/test-parallel-uploads.cjs +++ b/test/spec/test-parallel-uploads.cjs @@ -92,7 +92,7 @@ describe('tus', () => { expect(req.method).toBe('POST') expect(req.requestHeaders.Custom).toBe('blargh') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(5) + expect(req.requestHeaders['Upload-Length']).toBe('5') expect(req.requestHeaders['Upload-Concat']).toBe('partial') expect(req.requestHeaders['Upload-Metadata']).toBeUndefined() @@ -108,7 +108,7 @@ describe('tus', () => { expect(req.method).toBe('POST') expect(req.requestHeaders.Custom).toBe('blargh') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(6) + expect(req.requestHeaders['Upload-Length']).toBe('6') expect(req.requestHeaders['Upload-Concat']).toBe('partial') expect(req.requestHeaders['Upload-Metadata']).toBeUndefined() @@ -230,7 +230,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(1) + expect(req.requestHeaders['Upload-Length']).toBe('1') expect(req.requestHeaders['Upload-Concat']).toBe('partial') req.respondWith({ @@ -244,7 +244,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(10) + expect(req.requestHeaders['Upload-Length']).toBe('10') expect(req.requestHeaders['Upload-Concat']).toBe('partial') req.respondWith({ @@ -339,7 +339,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(5) + expect(req.requestHeaders['Upload-Length']).toBe('5') req.respondWith({ status: 500, @@ -464,7 +464,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(5) + expect(req.requestHeaders['Upload-Length']).toBe('5') expect(req.requestHeaders['Upload-Concat']).toBe('partial') expect(req.requestHeaders['Upload-Metadata']).toBeUndefined() @@ -479,7 +479,7 @@ describe('tus', () => { expect(req.url).toBe('https://tus.io/uploads') expect(req.method).toBe('POST') expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0') - expect(req.requestHeaders['Upload-Length']).toBe(6) + expect(req.requestHeaders['Upload-Length']).toBe('6') expect(req.requestHeaders['Upload-Concat']).toBe('partial') expect(req.requestHeaders['Upload-Metadata']).toBeUndefined()