diff --git a/test/file.ts b/test/file.ts index 84686a03d..6462a45fd 100644 --- a/test/file.ts +++ b/test/file.ts @@ -12,53 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -/**import { - BodyResponseCallback, - MetadataCallback, - ServiceObject, - ServiceObjectConfig, - util, -} from '../src/nodejs-common/index.js'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; -import {PromisifyAllOptions} from '@google-cloud/promisify'; -import {Readable, PassThrough, Stream, Duplex, Transform} from 'stream'; import assert from 'assert'; -import * as crypto from 'crypto'; -import duplexify from 'duplexify'; -import * as fs from 'fs'; -import * as path from 'path'; -import proxyquire from 'proxyquire'; -import * as resumableUpload from '../src/resumable-upload.js'; -import * as sinon from 'sinon'; -import * as tmp from 'tmp'; -import * as zlib from 'zlib'; - +import {Bucket, CRC32C, File, GaxiosError, Storage} from '../src/index.js'; +import {StorageTransport} from '../src/storage-transport.js'; +import sinon from 'sinon'; import { - Bucket, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - File, + FileExceptionMessages, FileOptions, - PolicyDocument, - SetFileMetadataOptions, - GetSignedUrlConfig, GenerateSignedPostPolicyV2Options, - CRC32C, -} from '../src/index.js'; -import { - SignedPostPolicyV4Output, GenerateSignedPostPolicyV4Options, - STORAGE_POST_POLICY_BASE_URL, + GetSignedUrlConfig, MoveOptions, - FileExceptionMessages, - FileMetadata, + RequestError, + SetFileMetadataOptions, + STORAGE_POST_POLICY_BASE_URL, } from '../src/file.js'; +import {Duplex, PassThrough, Readable, Stream, Transform} from 'stream'; +import * as crypto from 'crypto'; +import duplexify from 'duplexify'; +import {GCCL_GCS_CMD_KEY} from '../src/nodejs-common/util.js'; import {ExceptionMessages, IdempotencyStrategy} from '../src/storage.js'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as tmp from 'tmp'; import {formatAsUTCISO} from '../src/util.js'; -import { - BaseMetadata, - SetMetadataOptions, -} from '../src/nodejs-common/service-object.js'; -import {GCCL_GCS_CMD_KEY} from '../src/nodejs-common/util.js'; class HTTPError extends Error { code: number; @@ -68,206 +46,43 @@ class HTTPError extends Error { } } -let promisified = false; -let makeWritableStreamOverride: Function | null; -let handleRespOverride: Function | null; -const fakeUtil = Object.assign({}, util, { - handleResp(...args: Array<{}>) { - (handleRespOverride || util.handleResp)(...args); - }, - makeWritableStream(...args: Array<{}>) { - (makeWritableStreamOverride || util.makeWritableStream)(...args); - }, - makeRequest( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - callback(null); - }, -}); - -const fakePromisify = { - // tslint:disable-next-line:variable-name - promisifyAll(Class: Function, options: PromisifyAllOptions) { - if (Class.name !== 'File') { - return; - } - - promisified = true; - assert.deepStrictEqual(options.exclude, [ - 'cloudStorageURI', - 'publicUrl', - 'request', - 'save', - 'setEncryptionKey', - 'shouldRetryBasedOnPreconditionAndIdempotencyStrat', - 'getBufferFromReadable', - 'restore', - ]); - }, -}; - -const fsCached = fs; -const fakeFs = {...fsCached}; - -const zlibCached = zlib; -let createGunzipOverride: Function | null; -const fakeZlib = { - ...zlib, - createGunzip(...args: Array<{}>) { - return (createGunzipOverride || zlibCached.createGunzip)(...args); - }, -}; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const osCached = require('os'); -const fakeOs = {...osCached}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let resumableUploadOverride: any; -function fakeResumableUpload() { - return () => { - return resumableUploadOverride || resumableUpload; - }; -} -Object.assign(fakeResumableUpload, { - createURI( - ...args: [resumableUpload.UploadConfig, resumableUpload.CreateUriCallback] - ) { - let createURI = resumableUpload.createURI; - - if (resumableUploadOverride && resumableUploadOverride.createURI) { - createURI = resumableUploadOverride.createURI; - } - - return createURI(...args); - }, -}); -Object.assign(fakeResumableUpload, { - upload(...args: [resumableUpload.UploadConfig]) { - let upload = resumableUpload.upload; - if (resumableUploadOverride && resumableUploadOverride.upload) { - upload = resumableUploadOverride.upload; - } - return upload(...args); - }, -}); - -class FakeServiceObject extends ServiceObject { - calledWith_: IArguments; - constructor(config: ServiceObjectConfig) { - super(config); - // eslint-disable-next-line prefer-rest-params - this.calledWith_ = arguments; - } -} - -const fakeSigner = { - URLSigner: () => {}, -}; - describe('File', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let File: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let file: any; + let STORAGE: Storage; + let BUCKET: Bucket; + let file: File; + let sandbox: sinon.SinonSandbox; + let storageTransport: StorageTransport; + const PROJECT_ID = 'project-id'; const FILE_NAME = 'file-name.png'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let directoryFile: any; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let specialCharsFile: any; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let STORAGE: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let BUCKET: any; + let directoryFile: File; const DATA = 'test data'; // crc32c hash of 'test data' const CRC32C_HASH = 'M3m0yg=='; // md5 hash of 'test data' const MD5_HASH = '63M6AMDJ0zbmVpGjerVCkw=='; - // crc32c hash of `zlib.gzipSync(Buffer.from(DATA), {level: 9})` - const GZIPPED_DATA = Buffer.from( - 'H4sIAAAAAAACEytJLS5RSEksSQQAsq4I0wkAAAA=', - 'base64', - ); - //crc32c hash of `GZIPPED_DATA` - const CRC32C_HASH_GZIP = '64jygg=='; before(() => { - File = proxyquire('../src/file.js', { - './nodejs-common': { - ServiceObject: FakeServiceObject, - util: fakeUtil, - }, - '@google-cloud/promisify': fakePromisify, - fs: fakeFs, - '../src/resumable-upload': fakeResumableUpload, - os: fakeOs, - './signer': fakeSigner, - zlib: fakeZlib, - }).File; + sandbox = sinon.createSandbox(); + STORAGE = new Storage({projectId: PROJECT_ID}); + storageTransport = sandbox.createStubInstance(StorageTransport); + STORAGE.storageTransport = storageTransport; }); beforeEach(() => { - Object.assign(fakeFs, fsCached); - Object.assign(fakeOs, osCached); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - FakeServiceObject.prototype.request = util.noop as any; - - STORAGE = { - createBucket: util.noop, - request: util.noop, - apiEndpoint: 'https://storage.googleapis.com', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeAuthenticatedRequest(req: {}, callback: any) { - if (callback) { - (callback.onAuthenticated || callback)(null, req); - } - }, - bucket(name: string) { - return new Bucket(this, name); - }, - retryOptions: { - autoRetry: true, - maxRetries: 3, - retryDelayMultipier: 2, - totalTimeout: 600, - maxRetryDelay: 60, - retryableErrorFn: (err: HTTPError) => { - return err?.code === 500; - }, - idempotencyStrategy: IdempotencyStrategy.RetryConditional, - }, - customEndpoint: false, - }; - BUCKET = new Bucket(STORAGE, 'bucket-name'); - BUCKET.getRequestInterceptors = () => []; file = new File(BUCKET, FILE_NAME); directoryFile = new File(BUCKET, 'directory/file.jpg'); - directoryFile.request = util.noop; - - specialCharsFile = new File(BUCKET, "special/azAZ!*'()*%/file.jpg"); - specialCharsFile.request = util.noop; + }); - createGunzipOverride = null; - handleRespOverride = null; - makeWritableStreamOverride = null; - resumableUploadOverride = null; + afterEach(() => { + sandbox.restore(); }); describe('initialization', () => { - it('should promisify all the things', () => { - assert(promisified); - }); - it('should assign file name', () => { assert.strictEqual(file.name, FILE_NAME); }); @@ -280,13 +95,6 @@ describe('File', () => { assert.strictEqual(file.storage, BUCKET.storage); }); - it('should set instanceRetryValue to the storage insance retryOptions.autoRetry value', () => { - assert.strictEqual( - file.instanceRetryValue, - STORAGE.retryOptions.autoRetry, - ); - }); - it('should not strip leading slashes', () => { const file = new File(BUCKET, '/name'); assert.strictEqual(file.name, '/name'); @@ -303,158 +111,315 @@ describe('File', () => { assert.strictEqual(file.generation, 2); }); - it('should inherit from ServiceObject', () => { - // Using assert.strictEqual instead of assert to prevent - // coercing of types. - assert.strictEqual(file instanceof ServiceObject, true); - - const calledWith = file.calledWith_[0]; + it('should not strip leading slash name in ServiceObject', () => { + const file = new File(BUCKET, '/name'); - assert.strictEqual(calledWith.parent, BUCKET); - assert.strictEqual(calledWith.baseUrl, '/o'); - assert.strictEqual(calledWith.id, encodeURIComponent(FILE_NAME)); - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: {}}}, - exists: {reqOpts: {qs: {}}}, - get: {reqOpts: {qs: {}}}, - getMetadata: {reqOpts: {qs: {}}}, - setMetadata: {reqOpts: {qs: {}}}, - }); + assert.strictEqual(file.id, encodeURIComponent('/name')); }); - it('should set the correct query string with a generation', () => { - const options = {generation: 2}; - const file = new File(BUCKET, 'name', options); - - const calledWith = file.calledWith_[0]; + it('should accept a `crc32cGenerator`', () => { + const crc32cGenerator = () => { + return new CRC32C(); + }; - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: options}}, - exists: {reqOpts: {qs: options}}, - get: {reqOpts: {qs: options}}, - getMetadata: {reqOpts: {qs: options}}, - setMetadata: {reqOpts: {qs: options}}, - }); + const file = new File(BUCKET, 'name', {crc32cGenerator}); + assert.strictEqual(file.crc32cGenerator, crc32cGenerator); }); - it('should set the correct query string with a userProject', () => { - const options = {userProject: 'user-project'}; - const file = new File(BUCKET, 'name', options); + it("should use the bucket's `crc32cGenerator` by default", () => { + assert.strictEqual(file.crc32cGenerator, BUCKET.crc32cGenerator); + }); - const calledWith = file.calledWith_[0]; + describe('delete', () => { + it('should set the correct query string with options', done => { + const options = { + generation: 2, + userProject: 'user-project', + preconditionOpts: { + ifGenerationMatch: 100, + ifGenerationNotMatch: 100, + ifMetagenerationMatch: 100, + ifMetagenerationNotMatch: 100, + }, + }; - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: options}}, - exists: {reqOpts: {qs: options}}, - get: {reqOpts: {qs: options}}, - getMetadata: {reqOpts: {qs: options}}, - setMetadata: {reqOpts: {qs: options}}, + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + assert.strictEqual(reqOpts.method, 'DELETE'); + assert.strictEqual(reqOpts.url, '/b/bucket-name//o/file-name.png'); + assert.deepStrictEqual( + reqOpts.queryParameters.generation, + options.generation, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.userProject, + options.userProject, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationMatch, + options.preconditionOpts.ifGenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationNotMatch, + options.preconditionOpts.ifGenerationNotMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationMatch, + options.preconditionOpts.ifMetagenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationNotMatch, + options.preconditionOpts.ifMetagenerationNotMatch, + ); + callback!(null, {data: {}}); + return Promise.resolve(); + }); + file.delete(options, done); }); - }); - - it('should set the correct query string with ifGenerationMatch', () => { - const options = {preconditionOpts: {ifGenerationMatch: 100}}; - const file = new File(BUCKET, 'name', options); - const calledWith = file.calledWith_[0]; + it('should return an error if the request fails', async () => { + const error = new GaxiosError('err', {}); - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: options.preconditionOpts}}, - exists: {reqOpts: {qs: options.preconditionOpts}}, - get: {reqOpts: {qs: options.preconditionOpts}}, - getMetadata: {reqOpts: {qs: options.preconditionOpts}}, - setMetadata: {reqOpts: {qs: options.preconditionOpts}}, + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((_reqOpts, callback) => { + callback!(error); + return Promise.resolve(); + }); + await file.delete((err: GaxiosError | null) => { + assert.strictEqual(err, error); + }); }); - assert.deepStrictEqual( - file.instancePreconditionOpts, - options.preconditionOpts, - ); }); - it('should set the correct query string with ifGenerationNotMatch', () => { - const options = {preconditionOpts: {ifGenerationNotMatch: 100}}; - const file = new File(BUCKET, 'name', options); + describe('exists', () => { + it('should set the correct query string with options', done => { + const options = { + generation: 2, + userProject: 'user-project', + preconditionOpts: { + ifGenerationMatch: 100, + ifGenerationNotMatch: 100, + ifMetagenerationMatch: 100, + ifMetagenerationNotMatch: 100, + }, + }; + + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + assert.strictEqual(reqOpts.method, 'GET'); + assert.strictEqual(reqOpts.url, '/b/bucket-name//o/file-name.png'); + assert.deepStrictEqual( + reqOpts.queryParameters.generation, + options.generation, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.userProject, + options.userProject, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationMatch, + options.preconditionOpts.ifGenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationNotMatch, + options.preconditionOpts.ifGenerationNotMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationMatch, + options.preconditionOpts.ifMetagenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationNotMatch, + options.preconditionOpts.ifMetagenerationNotMatch, + ); + callback!(null, {data: {}}); + return Promise.resolve(); + }); + file.exists(options, done); + }); - const calledWith = file.calledWith_[0]; + it('should return an error if the request fails', async () => { + const error = new GaxiosError('err', {}); - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: options.preconditionOpts}}, - exists: {reqOpts: {qs: options.preconditionOpts}}, - get: {reqOpts: {qs: options.preconditionOpts}}, - getMetadata: {reqOpts: {qs: options.preconditionOpts}}, - setMetadata: {reqOpts: {qs: options.preconditionOpts}}, + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((_reqOpts, callback) => { + callback!(error); + return Promise.resolve(); + }); + await file.exists((err: GaxiosError | null) => { + assert.strictEqual(err, error); + }); }); - assert.deepStrictEqual( - file.instancePreconditionOpts, - options.preconditionOpts, - ); }); - it('should set the correct query string with ifMetagenerationMatch', () => { - const options = {preconditionOpts: {ifMetagenerationMatch: 100}}; - const file = new File(BUCKET, 'name', options); + describe('get', () => { + it('should set the correct query string with options', done => { + const options = { + generation: 2, + userProject: 'user-project', + preconditionOpts: { + ifGenerationMatch: 100, + ifGenerationNotMatch: 100, + ifMetagenerationMatch: 100, + ifMetagenerationNotMatch: 100, + }, + }; + + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + assert.strictEqual(reqOpts.method, 'GET'); + assert.strictEqual(reqOpts.url, '/b/bucket-name//o/file-name.png'); + assert.deepStrictEqual( + reqOpts.queryParameters.generation, + options.generation, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.userProject, + options.userProject, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationMatch, + options.preconditionOpts.ifGenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationNotMatch, + options.preconditionOpts.ifGenerationNotMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationMatch, + options.preconditionOpts.ifMetagenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationNotMatch, + options.preconditionOpts.ifMetagenerationNotMatch, + ); + callback!(null, {data: {}}); + return Promise.resolve(); + }); + file.get(options, done); + }); - const calledWith = file.calledWith_[0]; + it('should return an error if the request fails', async () => { + const error = new GaxiosError('err', {}); - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: options.preconditionOpts}}, - exists: {reqOpts: {qs: options.preconditionOpts}}, - get: {reqOpts: {qs: options.preconditionOpts}}, - getMetadata: {reqOpts: {qs: options.preconditionOpts}}, - setMetadata: {reqOpts: {qs: options.preconditionOpts}}, + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((_reqOpts, callback) => { + callback!(error); + return Promise.resolve(); + }); + await file.get((err: GaxiosError | null) => { + assert.strictEqual(err, error); + }); }); - assert.deepStrictEqual( - file.instancePreconditionOpts, - options.preconditionOpts, - ); }); - it('should set the correct query string with ifMetagenerationNotMatch', () => { - const options = {preconditionOpts: {ifMetagenerationNotMatch: 100}}; - const file = new File(BUCKET, 'name', options); - - const calledWith = file.calledWith_[0]; + describe('getMetadata', () => { + it('should set the correct query string with options', done => { + const options = { + generation: 2, + userProject: 'user-project', + preconditionOpts: { + ifGenerationMatch: 100, + ifGenerationNotMatch: 100, + ifMetagenerationMatch: 100, + ifMetagenerationNotMatch: 100, + }, + }; - assert.deepStrictEqual(calledWith.methods, { - delete: {reqOpts: {qs: options.preconditionOpts}}, - exists: {reqOpts: {qs: options.preconditionOpts}}, - get: {reqOpts: {qs: options.preconditionOpts}}, - getMetadata: {reqOpts: {qs: options.preconditionOpts}}, - setMetadata: {reqOpts: {qs: options.preconditionOpts}}, + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + assert.strictEqual(reqOpts.method, 'GET'); + assert.strictEqual(reqOpts.url, '/b/bucket-name//o/file-name.png'); + assert.deepStrictEqual( + reqOpts.queryParameters.generation, + options.generation, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.userProject, + options.userProject, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationMatch, + options.preconditionOpts.ifGenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifGenerationNotMatch, + options.preconditionOpts.ifGenerationNotMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationMatch, + options.preconditionOpts.ifMetagenerationMatch, + ); + assert.deepStrictEqual( + reqOpts.queryParameters.preconditionOpts.ifMetagenerationNotMatch, + options.preconditionOpts.ifMetagenerationNotMatch, + ); + callback!(null, {data: {}}); + return Promise.resolve(); + }); + file.getMetadata(options, done); }); - assert.deepStrictEqual( - file.instancePreconditionOpts, - options.preconditionOpts, - ); - }); - it('should not strip leading slash name in ServiceObject', () => { - const file = new File(BUCKET, '/name'); - const calledWith = file.calledWith_[0]; + it('should return an error if the request fails', async () => { + const error = new GaxiosError('err', {}); - assert.strictEqual(calledWith.id, encodeURIComponent('/name')); + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((_reqOpts, callback) => { + callback!(error); + return Promise.resolve(); + }); + await file.getMetadata((err: GaxiosError | null) => { + assert.strictEqual(err, error); + }); + }); }); - it('should set a custom encryption key', done => { - const key = 'key'; - const setEncryptionKey = File.prototype.setEncryptionKey; - File.prototype.setEncryptionKey = (key_: {}) => { - File.prototype.setEncryptionKey = setEncryptionKey; - assert.strictEqual(key_, key); - done(); - }; - new File(BUCKET, FILE_NAME, {encryptionKey: key}); - }); + describe('setMetadata', () => { + it('should set the correct query string with options', done => { + const options = { + temporaryHold: true, + }; - it('should accept a `crc32cGenerator`', () => { - const crc32cGenerator = () => {}; + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + assert.strictEqual(reqOpts.method, 'PATCH'); + assert.strictEqual(reqOpts.url, '/b/bucket-name//o/file-name.png'); + assert.deepStrictEqual( + reqOpts.body.temporaryHold, + options.temporaryHold, + ); + callback!(null); + return Promise.resolve(); + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.setMetadata(options, done); + }); - const file = new File(BUCKET, 'name', {crc32cGenerator}); - assert.strictEqual(file.crc32cGenerator, crc32cGenerator); - }); + it('should return an error if the request fails', done => { + const error = new GaxiosError('err', {}); - it("should use the bucket's `crc32cGenerator` by default", () => { - assert.strictEqual(file.crc32cGenerator, BUCKET.crc32cGenerator); + STORAGE.storageTransport.makeRequest = sandbox + .stub() + .callsFake((_reqOpts, callback) => { + callback!(error); + return Promise.resolve(); + }); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.setMetadata({}, (err: GaxiosError | null) => { + assert.strictEqual(err, error); + done(); + }); + }); }); describe('userProject', () => { @@ -481,8 +446,6 @@ describe('File', () => { describe('cloudStorageURI', () => { it('should return the appropriate `gs://` URI', () => { - const file = new File(BUCKET, FILE_NAME); - assert(file.cloudStorageURI instanceof URL); assert.equal(file.cloudStorageURI.host, BUCKET.name); assert.equal(file.cloudStorageURI.pathname, `/${FILE_NAME}`); @@ -491,24 +454,33 @@ describe('File', () => { describe('copy', () => { it('should throw if no destination is provided', () => { - assert.throws(() => { - file.copy(); - }, /Destination file should have a name\./); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + assert.rejects( + file.copy(undefined as unknown as string), + (err: Error) => { + assert.strictEqual( + err.message, + FileExceptionMessages.DESTINATION_NO_NAME, + ); + }, + ); }); it('should URI encode file names', done => { const newFile = new File(BUCKET, 'nested/file.jpg'); - const expectedPath = `/rewriteTo/b/${ + const expectedPath = `/o/rewriteTo/b/${ file.bucket.name }/o/${encodeURIComponent(newFile.name)}`; - directoryFile.request = (reqOpts: DecorateRequestOptions) => { - assert.strictEqual(reqOpts.uri, expectedPath); - done(); - }; + directoryFile.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { + assert.strictEqual(reqOpts.url, expectedPath); + done(); + }); - directoryFile.copy(newFile); + directoryFile.copy(newFile, done); }); it('should execute callback with error & API response', done => { @@ -517,11 +489,13 @@ describe('File', () => { const newFile = new File(BUCKET, 'new-file'); - file.request = (reqOpts: DecorateRequestOptions, callback: Function) => { - callback(error, apiResponse); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(error, null, apiResponse); + }); - file.copy(newFile, (err: Error, file: {}, apiResponse_: {}) => { + file.copy(newFile, (err, file, apiResponse_) => { assert.strictEqual(err, error); assert.strictEqual(file, null); assert.strictEqual(apiResponse_, apiResponse); @@ -534,10 +508,12 @@ describe('File', () => { const versionedFile = new File(BUCKET, 'name', {generation: 1}); const newFile = new File(BUCKET, 'new-file'); - versionedFile.request = (reqOpts: DecorateRequestOptions) => { - assert.strictEqual(reqOpts.qs.sourceGeneration, 1); - done(); - }; + versionedFile.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { + assert.strictEqual(reqOpts.queryParameters.sourceGeneration, 1); + done(); + }); versionedFile.copy(newFile, assert.ifError); }); @@ -552,11 +528,11 @@ describe('File', () => { metadata: METADATA, }; - file.request = (reqOpts: DecorateRequestOptions) => { - assert.deepStrictEqual(reqOpts.json, options); - assert.strictEqual(reqOpts.json.metadata, METADATA); + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { + assert.deepStrictEqual(reqOpts.body, options); + assert.strictEqual(reqOpts.body.metadata, METADATA); done(); - }; + }); file.copy(newFile, options, assert.ifError); }); @@ -568,43 +544,58 @@ describe('File', () => { const originalOptions = Object.assign({}, options); const newFile = new File(BUCKET, 'new-file'); - file.request = (reqOpts: DecorateRequestOptions) => { - assert.strictEqual(reqOpts.qs.userProject, options.userProject); - assert.strictEqual(reqOpts.json.userProject, undefined); + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { + assert.strictEqual( + reqOpts.queryParameters.userProject, + options.userProject, + ); + assert.strictEqual(reqOpts.body.userProject, undefined); assert.deepStrictEqual(options, originalOptions); done(); - }; + }); file.copy(newFile, options, assert.ifError); }); it('should set correct headers when file is encrypted', done => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let file: any; + // eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any + file = new (File as any)(BUCKET, FILE_NAME); + file.encryptionKey = {}; file.encryptionKeyBase64 = 'base64'; file.encryptionKeyHash = 'hash'; + file.userProject = 'user-project'; const newFile = new File(BUCKET, 'new-file'); - file.request = (reqOpts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { assert.deepStrictEqual(reqOpts.headers, { 'x-goog-copy-source-encryption-algorithm': 'AES256', 'x-goog-copy-source-encryption-key': file.encryptionKeyBase64, 'x-goog-copy-source-encryption-key-sha256': file.encryptionKeyHash, }); done(); - }; + }); file.copy(newFile, assert.ifError); }); it('should set encryption key on the new File instance', done => { - const newFile = new File(BUCKET, 'new-file'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let file: any; + // eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any + file = new (File as any)(BUCKET, FILE_NAME); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const newFile = new (File as any)(BUCKET, 'new-file'); newFile.encryptionKey = 'encryptionKey'; - file.setEncryptionKey = (encryptionKey: {}) => { + file.setEncryptionKey = sandbox.stub().callsFake(encryptionKey => { assert.strictEqual(encryptionKey, newFile.encryptionKey); done(); - }; + }); file.copy(newFile, assert.ifError); }); @@ -613,14 +604,14 @@ describe('File', () => { const newFile = new File(BUCKET, 'new-file'); newFile.kmsKeyName = 'kms-key-name'; - file.request = (reqOpts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { assert.strictEqual( - reqOpts.qs.destinationKmsKeyName, + reqOpts.queryParameters.destinationKmsKeyName, newFile.kmsKeyName, ); assert.strictEqual(file.kmsKeyName, newFile.kmsKeyName); done(); - }; + }); file.copy(newFile, assert.ifError); }); @@ -629,14 +620,14 @@ describe('File', () => { const newFile = new File(BUCKET, 'new-file'); const destinationKmsKeyName = 'destination-kms-key-name'; - file.request = (reqOpts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { assert.strictEqual( - reqOpts.qs.destinationKmsKeyName, + reqOpts.queryParameters.destinationKmsKeyName, destinationKmsKeyName, ); assert.strictEqual(file.kmsKeyName, destinationKmsKeyName); done(); - }; + }); file.copy(newFile, {destinationKmsKeyName}, assert.ifError); }); @@ -646,14 +637,13 @@ describe('File', () => { predefinedAcl: 'authenticatedRead', }; const newFile = new File(BUCKET, 'new-file'); - file.request = (reqOpts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { assert.strictEqual( - reqOpts.qs.destinationPredefinedAcl, + reqOpts.queryParameters.destinationPredefinedAcl, options.predefinedAcl, ); - assert.strictEqual(reqOpts.json.destinationPredefinedAcl, undefined); done(); - }; + }); file.copy(newFile, options, assert.ifError); }); @@ -663,30 +653,34 @@ describe('File', () => { newFile.kmsKeyName = 'incorrect-kms-key-name'; const destinationKmsKeyName = 'correct-kms-key-name'; - file.request = (reqOpts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(reqOpts => { assert.strictEqual( - reqOpts.qs.destinationKmsKeyName, + reqOpts.queryParameters.destinationKmsKeyName, destinationKmsKeyName, ); assert.strictEqual(file.kmsKeyName, destinationKmsKeyName); done(); - }; + }); file.copy(newFile, {destinationKmsKeyName}, assert.ifError); }); it('should remove custom encryption interceptor if rotating to KMS', done => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let file: any; + // eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any + file = new (File as any)(BUCKET, FILE_NAME); const newFile = new File(BUCKET, 'new-file'); const destinationKmsKeyName = 'correct-kms-key-name'; file.encryptionKeyInterceptor = {}; file.interceptors = [{}, file.encryptionKeyInterceptor, {}]; - file.request = () => { - assert.strictEqual(file.interceptors.length, 2); - assert(file.interceptors.indexOf(file.encryptionKeyInterceptor) === -1); + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + assert.strictEqual(file.interceptors.length, 3); + assert(file.interceptors.indexOf(file.encryptionKeyInterceptor) === 1); done(); - }; + }); file.copy(newFile, {destinationKmsKeyName}, assert.ifError); }); @@ -694,59 +688,68 @@ describe('File', () => { describe('destination types', () => { function assertPathEquals( // eslint-disable-next-line @typescript-eslint/no-explicit-any - file: any, + file: File, expectedPath: string, callback: Function, ) { - file.request = (reqOpts: DecorateRequestOptions) => { - assert.strictEqual(reqOpts.uri, expectedPath); - callback(); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { + assert.strictEqual(reqOpts.url, expectedPath); + callback(); + }); } it('should allow a string', done => { const newFileName = 'new-file-name.png'; const newFile = new File(BUCKET, newFileName); - const expectedPath = `/rewriteTo/b/${file.bucket.name}/o/${newFile.name}`; + const expectedPath = `/o/rewriteTo/b/${file.bucket.name}/o/${newFile.name}`; assertPathEquals(file, expectedPath, done); - file.copy(newFileName); + file.copy(newFileName, done); }); it('should allow a string with leading slash.', done => { const newFileName = '/new-file-name.png'; const newFile = new File(BUCKET, newFileName); // File uri encodes file name when calling this.request during copy - const expectedPath = `/rewriteTo/b/${ + const expectedPath = `/o/rewriteTo/b/${ file.bucket.name }/o/${encodeURIComponent(newFile.name)}`; assertPathEquals(file, expectedPath, done); - file.copy(newFileName); + file.copy(newFileName, done); }); it('should allow a "gs://..." string', done => { const newFileName = 'gs://other-bucket/new-file-name.png'; - const expectedPath = '/rewriteTo/b/other-bucket/o/new-file-name.png'; + const expectedPath = '/o/rewriteTo/b/other-bucket/o/new-file-name.png'; assertPathEquals(file, expectedPath, done); - file.copy(newFileName); + file.copy(newFileName, done); }); it('should allow a Bucket', done => { - const expectedPath = `/rewriteTo/b/${BUCKET.name}/o/${file.name}`; + const expectedPath = `/o/rewriteTo/b/${BUCKET.name}/o/${file.name}`; assertPathEquals(file, expectedPath, done); - file.copy(BUCKET); + file.copy(BUCKET, done); }); it('should allow a File', done => { const newFile = new File(BUCKET, 'new-file'); - const expectedPath = `/rewriteTo/b/${BUCKET.name}/o/${newFile.name}`; + const expectedPath = `/o/rewriteTo/b/${BUCKET.name}/o/${newFile.name}`; assertPathEquals(file, expectedPath, done); - file.copy(newFile); + file.copy(newFile, done); }); it('should throw if a destination cannot be parsed', () => { - assert.throws(() => { - file.copy(() => {}); - }, /Destination file should have a name\./); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + assert.rejects( + file.copy(undefined as unknown as string), + (err: Error) => { + assert.strictEqual( + err.message, + FileExceptionMessages.DESTINATION_NO_NAME, + ); + }, + ); }); }); @@ -755,32 +758,19 @@ describe('File', () => { rewriteToken: '...', }; - beforeEach(() => { - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - callback(null, apiResponse); - }; - }); - it('should continue attempting to copy', done => { const newFile = new File(BUCKET, 'new-file'); - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - file.copy = (newFile_: {}, options: {}, callback: Function) => { - assert.strictEqual(newFile_, newFile); - assert.deepStrictEqual(options, {token: apiResponse.rewriteToken}); - callback(); // done() - }; - - callback(null, apiResponse); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(apiResponse); + }); - file.copy(newFile, done); + file.copy(newFile, apiResponse_ => { + assert.strictEqual(apiResponse, apiResponse_); + done(); + }); }); it('should pass the userProject in subsequent requests', done => { @@ -789,19 +779,16 @@ describe('File', () => { userProject: 'grapce-spaceship-123', }; - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.copy = (newFile_: {}, options: any) => { - assert.notStrictEqual(options, fakeOptions); - assert.strictEqual(options.userProject, fakeOptions.userProject); + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { + assert.notStrictEqual(reqOpts, fakeOptions); + assert.strictEqual( + reqOpts.queryParameters.userProject, + fakeOptions.userProject, + ); done(); - }; - - callback(null, apiResponse); - }; + }); file.copy(newFile, fakeOptions, assert.ifError); }); @@ -812,21 +799,15 @@ describe('File', () => { destinationKmsKeyName: 'kms-key-name', }; - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.copy = (newFile_: {}, options: any) => { + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { assert.strictEqual( - options.destinationKmsKeyName, + reqOpts.queryParameters.destinationKmsKeyName, fakeOptions.destinationKmsKeyName, ); done(); - }; - - callback(null, apiResponse); - }; + }); file.copy(newFile, fakeOptions, assert.ifError); }); @@ -834,10 +815,15 @@ describe('File', () => { it('should make the subsequent correct API request', done => { const newFile = new File(BUCKET, 'new-file'); - file.request = (reqOpts: DecorateRequestOptions) => { - assert.strictEqual(reqOpts.qs.rewriteToken, apiResponse.rewriteToken); - done(); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { + assert.strictEqual( + reqOpts.queryParameters.rewriteToken, + apiResponse.rewriteToken, + ); + done(); + }); file.copy(newFile, {token: apiResponse.rewriteToken}, assert.ifError); }); @@ -846,17 +832,16 @@ describe('File', () => { describe('returned File object', () => { beforeEach(() => { const resp = {success: true}; - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - callback(null, resp); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(null, file, resp); + }); }); it('should re-use file object if one is provided', done => { const newFile = new File(BUCKET, 'new-file'); - file.copy(newFile, (err: Error, copiedFile: {}) => { + file.copy(newFile, (err, copiedFile) => { assert.ifError(err); assert.deepStrictEqual(copiedFile, newFile); done(); @@ -865,25 +850,25 @@ describe('File', () => { it('should create new file on the same bucket', done => { const newFilename = 'new-filename'; - file.copy(newFilename, (err: Error, copiedFile: File) => { + file.copy(newFilename, (err, copiedFile) => { assert.ifError(err); - assert.strictEqual(copiedFile.bucket.name, BUCKET.name); - assert.strictEqual(copiedFile.name, newFilename); + assert.strictEqual(copiedFile?.bucket.name, BUCKET.name); + assert.strictEqual(copiedFile?.name, newFilename); done(); }); }); it('should create new file on the destination bucket', done => { - file.copy(BUCKET, (err: Error, copiedFile: File) => { + file.copy(BUCKET, (err, copiedFile) => { assert.ifError(err); - assert.strictEqual(copiedFile.bucket.name, BUCKET.name); - assert.strictEqual(copiedFile.name, file.name); + assert.strictEqual(copiedFile?.bucket.name, BUCKET.name); + assert.strictEqual(copiedFile?.name, file.name); done(); }); }); it('should pass apiResponse into callback', done => { - file.copy(BUCKET, (err: Error, copiedFile: File, apiResponse: {}) => { + file.copy(BUCKET, (err, copiedFile, apiResponse) => { assert.ifError(err); assert.deepStrictEqual({success: true}, apiResponse); done(); @@ -893,99 +878,16 @@ describe('File', () => { }); describe('createReadStream', () => { - function getFakeRequest(data?: {}) { - let requestOptions: DecorateRequestOptions | undefined; - - class FakeRequest extends Readable { - constructor(_requestOptions?: DecorateRequestOptions) { - super(); - requestOptions = _requestOptions; - this._read = () => { - if (data) { - this.push(data); - } - this.push(null); - }; - } - - static getRequestOptions() { - return requestOptions; - } - } - - // Return a Proxy of FakeRequest which can be instantiated - // without new. - return new Proxy(FakeRequest, { - apply(target, _, argumentsList) { - return new target(...argumentsList); - }, + beforeEach(() => { + const rawResponseStream = new PassThrough(); + const headers = {}; + setImmediate(() => { + rawResponseStream.emit('response', headers); + // rawResponseStream.write(); + rawResponseStream.end(); }); - } - - function getFakeSuccessfulRequest(data: {}) { - // tslint:disable-next-line:variable-name - const FakeRequest = getFakeRequest(data); - - class FakeSuccessfulRequest extends FakeRequest { - constructor(req?: DecorateRequestOptions) { - super(req); - setImmediate(() => { - const stream = new FakeRequest(); - this.emit('response', stream); - }); - } - } - - // Return a Proxy of FakeSuccessfulRequest which can be instantiated - // without new. - return new Proxy(FakeSuccessfulRequest, { - apply(target, _, argumentsList) { - return new target(...argumentsList); - }, - }); - } - - function getFakeFailedRequest(error: Error) { - // tslint:disable-next-line:variable-name - const FakeRequest = getFakeRequest(); - - class FakeFailedRequest extends FakeRequest { - constructor(_req?: DecorateRequestOptions) { - super(_req); - setImmediate(() => { - this.emit('error', error); - }); - } - } - - // Return a Proxy of FakeFailedRequest which can be instantiated - // without new. - return new Proxy(FakeFailedRequest, { - apply(target, _, argumentsList) { - return new target(...argumentsList); - }, - }); - } - - beforeEach(() => { - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return {headers: {}}; - }, - }); - callback(null, null, rawResponseStream); - setImmediate(() => { - rawResponseStream.end(); - }); - }; - }); + return rawResponseStream; + }); it('should throw if both a range and validation is given', () => { assert.throws(() => { @@ -1021,11 +923,13 @@ describe('File', () => { it('should send query.generation if File has one', done => { const versionedFile = new File(BUCKET, 'file.txt', {generation: 1}); - versionedFile.requestStream = (rOpts: DecorateRequestOptions) => { - assert.strictEqual(rOpts.qs.generation, 1); - setImmediate(done); - return duplexify(); - }; + versionedFile.storageTransport.makeRequest = sandbox + .stub() + .callsFake(rOpts => { + assert.strictEqual(rOpts.queryParameters.generation, 1); + setImmediate(done); + return duplexify(); + }); versionedFile.createReadStream().resume(); }); @@ -1035,11 +939,14 @@ describe('File', () => { userProject: 'user-project-id', }; - file.requestStream = (rOpts: DecorateRequestOptions) => { - assert.strictEqual(rOpts.qs.userProject, options.userProject); + file.storageTransport.makeRequest = sandbox.stub().callsFake(rOpts => { + assert.strictEqual( + rOpts.queryParameters.userProject, + options.userProject, + ); setImmediate(done); return duplexify(); - }; + }); file.createReadStream(options).resume(); }); @@ -1047,13 +954,13 @@ describe('File', () => { it('should pass the `GCCL_GCS_CMD_KEY` to `requestStream`', done => { const expected = 'expected/value'; - file.requestStream = (opts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { assert.equal(opts[GCCL_GCS_CMD_KEY], expected); process.nextTick(() => done()); return duplexify(); - }; + }); file .createReadStream({ @@ -1063,46 +970,40 @@ describe('File', () => { }); describe('authenticating', () => { - it('should create an authenticated request', done => { - file.requestStream = (opts: DecorateRequestOptions) => { + it('should create an authenticated request', () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { assert.deepStrictEqual(opts, { uri: '', headers: { 'Accept-Encoding': 'gzip', 'Cache-Control': 'no-store', }, - qs: { + queryParameters: { alt: 'media', }, }); - setImmediate(() => { - done(); - }); + return duplexify(); - }; + }); file.createReadStream().resume(); }); describe('errors', () => { - const ERROR = new Error('Error.'); - - beforeEach(() => { - file.requestStream = () => { + const ERROR = new GaxiosError('Error.', {}); + it('should emit an error from authenticating', done => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { const requestStream = new PassThrough(); setImmediate(() => { - requestStream.emit('error', ERROR); + requestStream.emit('Error', ERROR); }); - + done(); return requestStream; - }; - }); - - it('should emit an error from authenticating', done => { + }); file .createReadStream() - .once('error', (err: Error) => { + .once('error', err => { assert.strictEqual(err, ERROR); done(); }) @@ -1113,19 +1014,24 @@ describe('File', () => { describe('requestStream', () => { it('should get readable stream from request', done => { - file.requestStream = () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { done(); }); return new PassThrough(); - }; + }); file.createReadStream().resume(); }); it('should emit response event from request', done => { - file.requestStream = getFakeSuccessfulRequest('body'); + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(null, null, {headers: {}}); + done(); + }); file .createReadStream({validation: false}) @@ -1138,35 +1044,39 @@ describe('File', () => { it('should let util.handleResp handle the response', done => { const response = {a: 'b', c: 'd'}; - handleRespOverride = (err: Error, response_: {}, body: {}) => { - assert.strictEqual(err, null); - assert.strictEqual(response_, response); - assert.strictEqual(body, null); - done(); - }; - - file.requestStream = () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { const rowRequestStream = new PassThrough(); setImmediate(() => { rowRequestStream.emit('response', response); }); + done(); return rowRequestStream; - }; + }); - file.createReadStream().resume(); + file + .createReadStream() + .on('responce', (err, response_, body) => { + assert.strictEqual(err, null); + assert.strictEqual(response_, response); + assert.strictEqual(body, null); + done(); + }) + .resume(); }); describe('errors', () => { - const ERROR = new Error('Error.'); - - beforeEach(() => { - file.requestStream = getFakeFailedRequest(ERROR); - }); - + const ERROR = new GaxiosError('Error.', {}); it('should emit the error', done => { + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(ERROR, null, null); + done(); + }); + file .createReadStream() - .once('error', (err: Error) => { + .once('error', err => { assert.deepStrictEqual(err, ERROR); done(); }) @@ -1178,24 +1088,13 @@ describe('File', () => { const rawResponseStream = new PassThrough(); const requestStream = new PassThrough(); - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - callback(ERROR, null, res); - setImmediate(() => { - rawResponseStream.end(rawResponsePayload); - }); - }; - - file.requestStream = () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { requestStream.emit('response', rawResponseStream); }); + done(); return requestStream; - }; + }); file .createReadStream() @@ -1209,35 +1108,20 @@ describe('File', () => { it('should emit errors from the request stream', done => { const error = new Error('Error.'); - const rawResponseStream = new PassThrough(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (rawResponseStream as any).toJSON = () => { - return {headers: {}}; - }; const requestStream = new PassThrough(); + const rawResponseStream = new PassThrough(); - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - callback(null, null, rawResponseStream); - setImmediate(() => { - rawResponseStream.emit('error', error); - }); - }; - - file.requestStream = () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { requestStream.emit('response', rawResponseStream); }); + done(); return requestStream; - }; + }); file .createReadStream() - .on('error', (err: Error) => { + .on('error', err => { assert.strictEqual(err, error); done(); }) @@ -1253,28 +1137,17 @@ describe('File', () => { }; const requestStream = new PassThrough(); - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - callback(null, null, rawResponseStream); - setImmediate(() => { - rawResponseStream.emit('error', error); - }); - }; - - file.requestStream = () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { requestStream.emit('response', rawResponseStream); }); + done(); return requestStream; - }; + }); file .createReadStream({validation: false}) - .on('error', (err: Error) => { + .on('error', err => { assert.strictEqual(err, error); rawResponseStream.emit('end'); setImmediate(done); @@ -1287,223 +1160,44 @@ describe('File', () => { }); }); - describe('compression', () => { - beforeEach(() => { - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return { - headers: { - 'content-encoding': 'gzip', - 'x-goog-hash': `crc32c=${CRC32C_HASH_GZIP},md5=${MD5_HASH}`, - }, - }; - }, - }); - callback(null, null, rawResponseStream); - - rawResponseStream.end(GZIPPED_DATA); - }; - file.requestStream = getFakeSuccessfulRequest(GZIPPED_DATA); - }); - - it('should gunzip the response', async () => { - const collection: Buffer[] = []; - - for await (const data of file.createReadStream()) { - collection.push(data); - } - - assert.equal(Buffer.concat(collection).toString(), DATA); - }); - - it('should not gunzip the response if "decompress: false" is passed', async () => { - const collection: Buffer[] = []; - - for await (const data of file.createReadStream({decompress: false})) { - collection.push(data); - } - - assert.equal( - Buffer.compare(Buffer.concat(collection), GZIPPED_DATA), - 0, - ); - }); - - it('should emit errors from the gunzip stream', done => { - const error = new Error('Error.'); - const createGunzipStream = new PassThrough(); - createGunzipOverride = () => { - process.nextTick(() => { - createGunzipStream.emit('error', error); - }); - return createGunzipStream; - }; - file - .createReadStream() - .on('error', (err: Error) => { - assert.strictEqual(err, error); - done(); - }) - .resume(); - }); - - it('should not handle both error and end events', done => { - const error = new Error('Error.'); - const createGunzipStream = new PassThrough(); - createGunzipOverride = () => { - process.nextTick(() => { - createGunzipStream.emit('error', error); - }); - return createGunzipStream; - }; - file - .createReadStream({validation: false}) - .on('error', (err: Error) => { - assert.strictEqual(err, error); - createGunzipStream.emit('end'); - setImmediate(done); - }) - .on('end', () => { - done(new Error('Should not have been called.')); - }) - .resume(); - }); - }); - describe('validation', () => { - let responseCRC32C = CRC32C_HASH; - let responseMD5 = MD5_HASH; + const responseCRC32C = CRC32C_HASH; + const responseMD5 = MD5_HASH; beforeEach(() => { - responseCRC32C = CRC32C_HASH; - responseMD5 = MD5_HASH; - - file.getMetadata = async () => ({}); - - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return { - headers: { - 'x-goog-hash': `crc32c=${responseCRC32C},md5=${responseMD5}`, - 'x-goog-stored-content-encoding': 'identity', - }, - }; - }, - }); - callback(null, null, rawResponseStream); - setImmediate(() => { - rawResponseStream.end(DATA); - }); - }; - file.requestStream = getFakeSuccessfulRequest(DATA); - }); - - function setFileValidationToError(e: Error = new Error('test-error')) { - // Simulating broken CRC32C instance - used by the validation stream - file.crc32cGenerator = () => { - class C extends CRC32C { - update() { - throw e; - } - } - - return new C(); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-goog-hash': `crc32c=${responseCRC32C},md5=${responseMD5}`, + 'x-google-stored-content-encoding': 'identity', }; - } - describe('server decompression', () => { - it('should skip validation if file was stored compressed and served decompressed', done => { - file.metadata.crc32c = '.invalid.'; - file.metadata.contentEncoding = 'gzip'; - - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return { - headers: { - 'x-goog-hash': `crc32c=${responseCRC32C},md5=${responseMD5}`, - 'x-goog-stored-content-encoding': 'gzip', - }, - }; - }, - }); - callback(null, null, rawResponseStream); - setImmediate(() => { - rawResponseStream.end(DATA); - }); - }; - - file - .createReadStream({validation: 'crc32c'}) - .on('end', done) - .resume(); - }); - }); - - it('should perform validation if file was stored compressed and served compressed', done => { - file.metadata.crc32c = '.invalid.'; - file.metadata.contentEncoding = 'gzip'; - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return { - headers: { - 'x-goog-hash': `crc32c=${responseCRC32C},md5=${responseMD5}`, - 'x-goog-stored-content-encoding': 'gzip', - 'content-encoding': 'gzip', - }, - }; - }, - }); - callback(null, null, rawResponseStream); + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { - rawResponseStream.end(DATA); + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); }); - }; - - const expectedError = new Error('test error'); - setFileValidationToError(expectedError); - - file - .createReadStream({validation: 'crc32c'}) - .on('error', (err: Error) => { - assert(err === expectedError); - done(); - }) - .resume(); + return rawResponseStream; + }); }); it('should emit errors from the validation stream', done => { const expectedError = new Error('test error'); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-goog-hash': `crc32c=dummy-hash,md5=${responseMD5}`, + 'x-google-stored-content-encoding': 'identity', + }; - file.requestStream = getFakeSuccessfulRequest(DATA); - setFileValidationToError(expectedError); + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', headers); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); file .createReadStream() @@ -1515,27 +1209,22 @@ describe('File', () => { .resume(); }); - it('should not handle both error and end events', done => { - const expectedError = new Error('test error'); - - file.requestStream = getFakeSuccessfulRequest(DATA); - setFileValidationToError(expectedError); - - file - .createReadStream() - .on('error', (err: Error) => { - assert(err === expectedError); - - setImmediate(done); - }) - .on('end', () => { - done(new Error('Should not have been called.')); - }) - .resume(); - }); - it('should validate with crc32c', done => { - file.requestStream = getFakeSuccessfulRequest(DATA); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-goog-hash': `crc32c=${CRC32C_HASH}`, + 'x-google-stored-content-encoding': 'identity', + }; + + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); file .createReadStream({validation: 'crc32c'}) @@ -1545,21 +1234,47 @@ describe('File', () => { }); it('should emit an error if crc32c validation fails', done => { - file.requestStream = getFakeSuccessfulRequest('bad-data'); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-goog-hash': 'crc32c=invalid-crc32c', + 'x-google-stored-content-encoding': 'identity', + }; - responseCRC32C = 'bad-crc32c'; + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); file .createReadStream({validation: 'crc32c'}) - .on('error', (err: ApiError) => { - assert.strictEqual(err.code, 'CONTENT_DOWNLOAD_MISMATCH'); + .on('error', err => { + assert.strictEqual(err.message, 'CONTENT_DOWNLOAD_MISMATCH'); done(); }) .resume(); }); it('should validate with md5', done => { - file.requestStream = getFakeSuccessfulRequest(DATA); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-google-hash': `md5=${MD5_HASH}`, + 'x-google-stored-content-encoding': 'identity', + }; + + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); file .createReadStream({validation: 'md5'}) @@ -1569,37 +1284,69 @@ describe('File', () => { }); it('should emit an error if md5 validation fails', done => { - file.requestStream = getFakeSuccessfulRequest('bad-data'); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-google-hash': 'md5=invalid-md5', + 'x-google-stored-content-encoding': 'identity', + }; - responseMD5 = 'bad-md5'; + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); file .createReadStream({validation: 'md5'}) - .on('error', (err: ApiError) => { - assert.strictEqual(err.code, 'CONTENT_DOWNLOAD_MISMATCH'); + .on('error', err => { + assert.strictEqual(err.message, 'CONTENT_DOWNLOAD_MISMATCH'); done(); }) .resume(); }); it('should default to crc32c validation', done => { - file.requestStream = getFakeSuccessfulRequest('bad-data'); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-goog-hash': `crc32c=${CRC32C_HASH}`, + 'x-google-stored-content-encoding': 'identity', + }; - responseCRC32C = 'bad-crc32c'; + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); file .createReadStream() - .on('error', (err: ApiError) => { - assert.strictEqual(err.code, 'CONTENT_DOWNLOAD_MISMATCH'); + .on('error', err => { + assert.strictEqual(err.message, 'CONTENT_DOWNLOAD_MISMATCH'); done(); }) .resume(); }); it('should ignore a data mismatch if validation: false', done => { - file.requestStream = getFakeSuccessfulRequest(DATA); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - // (fakeValidationStream as any).test = () => false; + const rawResponseStream = new PassThrough(); + + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); + file .createReadStream({validation: false}) .resume() @@ -1608,76 +1355,80 @@ describe('File', () => { }); it('should handle x-goog-hash with only crc32c', done => { - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return { - headers: { - 'x-goog-hash': `crc32c=${CRC32C_HASH}`, - }, - }; - }, - }); - callback(null, null, rawResponseStream); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-goog-hash': `crc32c=${CRC32C_HASH}`, + }; + + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { + rawResponseStream.emit('response', {headers}); rawResponseStream.end(DATA); }); - }; - - file.requestStream = getFakeSuccessfulRequest(DATA); + done(); + return rawResponseStream; + }); file.createReadStream().on('error', done).on('end', done).resume(); }); describe('destroying the through stream', () => { it('should destroy after failed validation', done => { - file.requestStream = getFakeSuccessfulRequest('bad-data'); - - responseMD5 = 'bad-md5'; + const rawResponseStream = new PassThrough(); + const headers = { + 'x-google-hash': `md5=${MD5_HASH}`, + 'x-google-stored-content-encoding': 'identity', + }; - const readStream = file.createReadStream({validation: 'md5'}); - readStream.on('error', (err: ApiError) => { - assert.strictEqual(err.code, 'CONTENT_DOWNLOAD_MISMATCH'); + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); done(); + return rawResponseStream; }); + const readStream = file.createReadStream({validation: 'md5'}); + readStream + .on('error', err => { + assert.strictEqual(err.message, 'CONTENT_DOWNLOAD_MISMATCH'); + done(); + }) + .on('end', () => { + done(); + }); + readStream.resume(); }); - it('should destroy if MD5 is requested but absent', done => { - handleRespOverride = ( - err: Error, - res: {}, - body: {}, - callback: Function, - ) => { - const rawResponseStream = new PassThrough(); - Object.assign(rawResponseStream, { - toJSON() { - return { - headers: {}, - }; - }, - }); - callback(null, null, rawResponseStream); + it('should destroy if MD5 is requested but absent', done => { + const rawResponseStream = new PassThrough(); + const headers = { + 'x-google-stored-content-encoding': 'identity', + }; + + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); rawResponseStream.end(); }); - }; - file.requestStream = getFakeSuccessfulRequest('bad-data'); + done(); + return rawResponseStream; + }); const readStream = file.createReadStream({validation: 'md5'}); - readStream.on('error', (err: ApiError) => { - assert.strictEqual(err.code, 'MD5_NOT_AVAILABLE'); - done(); - }); + readStream + .on('error', err => { + assert.strictEqual(err.message, 'MD5_NOT_AVAILABLE'); + done(); + }) + .on('end', () => { + done(); + }); readStream.resume(); }); @@ -1688,7 +1439,7 @@ describe('File', () => { it('should accept a start range', done => { const startOffset = 100; - file.requestStream = (opts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { setImmediate(() => { assert.strictEqual( opts.headers!.Range, @@ -1697,7 +1448,7 @@ describe('File', () => { done(); }); return duplexify(); - }; + }); file.createReadStream({start: startOffset}).resume(); }); @@ -1705,13 +1456,13 @@ describe('File', () => { it('should accept an end range and set start to 0', done => { const endOffset = 100; - file.requestStream = (opts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { setImmediate(() => { assert.strictEqual(opts.headers!.Range, 'bytes=0-' + endOffset); done(); }); return duplexify(); - }; + }); file.createReadStream({end: endOffset}).resume(); }); @@ -1720,14 +1471,14 @@ describe('File', () => { const startOffset = 100; const endOffset = 101; - file.requestStream = (opts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { setImmediate(() => { const expectedRange = 'bytes=' + startOffset + '-' + endOffset; assert.strictEqual(opts.headers!.Range, expectedRange); done(); }); return duplexify(); - }; + }); file.createReadStream({start: startOffset, end: endOffset}).resume(); }); @@ -1736,20 +1487,34 @@ describe('File', () => { const startOffset = 0; const endOffset = 0; - file.requestStream = (opts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { setImmediate(() => { const expectedRange = 'bytes=0-0'; assert.strictEqual(opts.headers!.Range, expectedRange); done(); }); return duplexify(); - }; + }); file.createReadStream({start: startOffset, end: endOffset}).resume(); }); it('should end the through stream', done => { - file.requestStream = getFakeSuccessfulRequest(DATA); + const rawResponseStream = new PassThrough(); + const headers = { + 'x-google-hash': `md5=${MD5_HASH}`, + 'x-google-stored-content-encoding': 'identity', + }; + + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { + setImmediate(() => { + rawResponseStream.emit('response', {headers}); + rawResponseStream.write(DATA); + rawResponseStream.end(); + }); + done(); + return rawResponseStream; + }); const readStream = file.createReadStream({start: 100}); readStream.on('end', done); @@ -1761,13 +1526,13 @@ describe('File', () => { it('should make a request for the tail bytes', done => { const endOffset = -10; - file.requestStream = (opts: DecorateRequestOptions) => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(opts => { setImmediate(() => { assert.strictEqual(opts.headers!.Range, 'bytes=' + endOffset); done(); }); return duplexify(); - }; + }); file.createReadStream({end: endOffset}).resume(); }); @@ -1775,284 +1540,173 @@ describe('File', () => { }); describe('createResumableUpload', () => { - it('should not require options', done => { - resumableUploadOverride = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createURI(opts: any, callback: Function) { - assert.strictEqual(opts.metadata, undefined); - callback(); - }, - }; - - file.createResumableUpload(done); - }); - - it('should disable autoRetry when ifMetagenerationMatch is undefined', done => { - resumableUploadOverride = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createURI(opts: any, callback: Function) { - assert.strictEqual(opts.retryOptions.autoRetry, false); - callback(); - }, - }; - file.createResumableUpload(done); - assert.strictEqual(file.storage.retryOptions.autoRetry, true); - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let file: any; + let resumableUploadStub: sinon.SinonStub; - it('should create a resumable upload URI', done => { - const options = { - metadata: { - contentType: 'application/json', - }, - origin: '*', - predefinedAcl: 'predefined-acl', - private: 'private', - public: 'public', - userProject: 'user-project-id', - retryOptions: { - autoRetry: true, - maxRetries: 3, - maxRetryDelay: 60, - retryDelayMultipier: 2, - totalTimeout: 600, - }, - preconditionOpts: { - ifGenerationMatch: 100, - ifMetagenerationMatch: 101, + beforeEach(() => { + file = { + name: FILE_NAME, + bucket: { + name: 'bucket-name', + storage: { + authClient: {}, + apiEndpoint: 'https://storage.googleapis.com', + universeDomain: 'universe-domain', + retryOptions: { + autoRetry: true, + idempotencyStrategy: IdempotencyStrategy.RetryConditional, + }, + }, }, - }; - - file.generation = 3; - file.encryptionKey = 'encryption-key'; - file.kmsKeyName = 'kms-key-name'; - - resumableUploadOverride = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createURI(opts: any, callback: Function) { - const bucket = file.bucket; - const storage = bucket.storage; - - assert.strictEqual(opts.authClient, storage.authClient); - assert.strictEqual(opts.apiEndpoint, storage.apiEndpoint); - assert.strictEqual(opts.bucket, bucket.name); - assert.strictEqual(opts.file, file.name); - assert.strictEqual(opts.generation, file.generation); - assert.strictEqual(opts.key, file.encryptionKey); - assert.strictEqual(opts.kmsKeyName, file.kmsKeyName); - assert.strictEqual(opts.metadata, options.metadata); - assert.strictEqual(opts.origin, options.origin); - assert.strictEqual(opts.predefinedAcl, options.predefinedAcl); - assert.strictEqual(opts.private, options.private); - assert.strictEqual(opts.public, options.public); - assert.strictEqual(opts.userProject, options.userProject); - assert.strictEqual( - opts.retryOptions.autoRetry, - options.retryOptions.autoRetry, - ); - assert.strictEqual( - opts.retryOptions.maxRetries, - options.retryOptions.maxRetries, - ); - assert.strictEqual( - opts.retryOptions.maxRetryDelay, - options.retryOptions.maxRetryDelay, - ); - assert.strictEqual( - opts.retryOptions.retryDelayMultipier, - options.retryOptions.retryDelayMultipier, - ); - assert.strictEqual( - opts.retryOptions.totalTimeout, - options.retryOptions.totalTimeout, - ); - assert.strictEqual(opts.params, options.preconditionOpts); - - callback(); + storage: { + retryOptions: { + autoRetry: true, + idempotencyStrategy: IdempotencyStrategy.RetryConditional, + }, }, - }; - - file.createResumableUpload(options, done); + getRequestInterceptors: sinon + .stub() + .returns([ + (reqOpts: object) => ({...reqOpts, customOption: 'custom-value'}), + ]), + generation: 123, + encryptionKey: 'test-encryption-key', + kmsKeyName: 'test-kms-key-name', + userProject: 'test-user-project', + instancePreconditionOpts: {ifGenerationMatch: 123}, + createResumableUpload: sinon.spy(), + }; + + resumableUploadStub = sinon.stub(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).resumableUpload = {createURI: resumableUploadStub}; }); - it('should create a resumable upload URI using precondition options from constructor', done => { - file = new File(BUCKET, FILE_NAME, { - preconditionOpts: { - ifGenerationMatch: 200, - ifGenerationNotMatch: 201, - ifMetagenerationMatch: 202, - ifMetagenerationNotMatch: 203, - }, - }); - const options = { - metadata: { - contentType: 'application/json', - }, - origin: '*', - predefinedAcl: 'predefined-acl', - private: 'private', - public: 'public', - userProject: 'user-project-id', - retryOptions: { - autoRetry: true, - maxRetries: 3, - maxRetryDelay: 60, - retryDelayMultipier: 2, - totalTimeout: 600, - }, - }; - - file.generation = 3; - file.encryptionKey = 'encryption-key'; - file.kmsKeyName = 'kms-key-name'; - - resumableUploadOverride = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createURI(opts: any, callback: Function) { - const bucket = file.bucket; - const storage = bucket.storage; - - assert.strictEqual(opts.authClient, storage.authClient); - assert.strictEqual(opts.apiEndpoint, storage.apiEndpoint); - assert.strictEqual(opts.bucket, bucket.name); - assert.strictEqual(opts.file, file.name); - assert.strictEqual(opts.generation, file.generation); - assert.strictEqual(opts.key, file.encryptionKey); - assert.strictEqual(opts.kmsKeyName, file.kmsKeyName); - assert.strictEqual(opts.metadata, options.metadata); - assert.strictEqual(opts.origin, options.origin); - assert.strictEqual(opts.predefinedAcl, options.predefinedAcl); - assert.strictEqual(opts.private, options.private); - assert.strictEqual(opts.public, options.public); - assert.strictEqual(opts.userProject, options.userProject); - assert.strictEqual( - opts.retryOptions.autoRetry, - options.retryOptions.autoRetry, - ); - assert.strictEqual( - opts.retryOptions.maxRetries, - options.retryOptions.maxRetries, - ); - assert.strictEqual( - opts.retryOptions.maxRetryDelay, - options.retryOptions.maxRetryDelay, - ); - assert.strictEqual( - opts.retryOptions.retryDelayMultipier, - options.retryOptions.retryDelayMultipier, - ); - assert.strictEqual( - opts.retryOptions.totalTimeout, - options.retryOptions.totalTimeout, - ); - assert.strictEqual(opts.params, file.instancePreconditionOpts); - - callback(); - }, - }; - - file.createResumableUpload(options, done); + afterEach(() => { + sinon.restore(); }); - }); - describe('createWriteStream', () => { - const METADATA = {a: 'b', c: 'd'}; - - beforeEach(() => { - Object.assign(fakeFs, { - access(dir: string, check: {}, callback: Function) { - // Assume that the required config directory is writable. - callback(); - }, + it('should not require options', () => { + resumableUploadStub.callsFake((opts, callback) => { + assert.strictEqual(opts.metadata, undefined); + callback(); }); - }); - it('should return a stream', () => { - assert(file.createWriteStream() instanceof Stream); + file.createResumableUpload(); }); - it('should emit errors', done => { - const error = new Error('Error.'); - const uploadStream = new PassThrough(); - - file.startResumableUpload_ = (dup: duplexify.Duplexify) => { - dup.setWritable(uploadStream); - uploadStream.emit('error', error); - }; - - const writable = file.createWriteStream(); + it('should call resumableUpload.createURI with the correct parameters', () => { + const options = { + metadata: {contentType: 'text/plain'}, + offset: 1024, + origin: 'https://example.com', + predefinedAcl: 'publicRead', + private: true, + public: false, + userProject: 'custom-user-project', + preconditionOpts: {ifMetagenerationMatch: 123}, + }; + + resumableUploadStub.callsFake((opts, callback) => { + assert.strictEqual(opts.authClient, file.bucket.storage.authClient); + assert.strictEqual(opts.apiEndpoint, file.bucket.storage.apiEndpoint); + assert.strictEqual(opts.bucket, file.bucket.name); + assert.strictEqual(opts.file, file.name); + assert.strictEqual(opts.generation, file.generation); + assert.strictEqual(opts.key, file.encryptionKey); + assert.strictEqual(opts.kmsKeyName, file.kmsKeyName); + assert.deepEqual(opts.metadata, options.metadata); + assert.strictEqual(opts.offset, options.offset); + assert.strictEqual(opts.origin, options.origin); + assert.strictEqual(opts.predefinedAcl, options.predefinedAcl); + assert.strictEqual(opts.private, options.private); + assert.strictEqual(opts.public, options.public); + assert.strictEqual(opts.userProject, options.userProject); + assert.deepEqual(opts.params, options.preconditionOpts); + assert.strictEqual( + opts.universeDomain, + file.bucket.storage.universeDomain, + ); + assert.deepEqual(opts.customRequestOptions, { + customOption: 'custom-value', + }); - writable.on('error', (err: Error) => { - assert.strictEqual(err, error); - done(); + callback(null, 'https://example.com/resumable-upload-uri'); }); - writable.write('data'); - }); - - it('should emit RangeError', done => { - const error = new RangeError( - 'Cannot provide an `offset` without providing a `uri`', + file.createResumableUpload( + options, + (err: Error | null, uri: string | undefined) => { + assert.strictEqual(err, null); + assert.strictEqual(uri, 'https://example.com/resumable-upload-uri'); + sinon.assert.calledOnce(resumableUploadStub); + }, ); + }); - const opitons = { - offset: 1, - isPartialUpload: true, - }; - const writable = file.createWriteStream(opitons); - - writable.on('error', (err: RangeError) => { - assert.deepEqual(err, error); - done(); + it('should use default options if no options are provided', () => { + resumableUploadStub.callsFake((opts, callback) => { + assert.strictEqual(opts.userProject, file.userProject); + assert.deepEqual(opts.params, file.instancePreconditionOpts); + callback(null, 'https://example.com/resumable-upload-uri'); }); - writable.write('data'); + file.createResumableUpload( + (err: Error | null, uri: string | undefined) => { + assert.strictEqual(err, null); + assert.strictEqual(uri, 'https://example.com/resumable-upload-uri'); + sinon.assert.calledOnce(resumableUploadStub); + }, + ); }); - it('should emit progress via resumable upload', done => { - const progress = {}; + it('should correctly apply precondition options', () => { + const options = {preconditionOpts: {ifGenerationMatch: 123}}; - resumableUploadOverride = { - upload() { - const uploadStream = new PassThrough(); - setImmediate(() => { - uploadStream.emit('progress', progress); - }); + resumableUploadStub.callsFake((opts, callback) => { + assert.deepEqual(opts.params, options.preconditionOpts); + callback(null, 'https://example.com/resumable-upload-uri'); + }); - return uploadStream; + file.createResumableUpload( + options, + (err: Error | null, uri: string | undefined) => { + assert.strictEqual(err, null); + assert.strictEqual(file.storage.retryOptions.autoRetry, true); + assert.strictEqual(uri, 'https://example.com/resumable-upload-uri'); + sinon.assert.calledOnce(resumableUploadStub); }, - }; + ); + }); - const writable = file.createWriteStream(); + it('should correctly apply precondition options', () => { + const options = {preconditionOpts: {ifGenerationMatch: undefined}}; - writable.on('progress', (evt: {}) => { - assert.strictEqual(evt, progress); - done(); + resumableUploadStub.callsFake((opts, callback) => { + console.log(opts); + assert.strictEqual(opts.retryOptions.autoRetry, false); + assert.deepEqual(opts.params, options.preconditionOpts); + callback(null, 'https://example.com/resumable-upload-uri'); }); - writable.write('data'); + file.createResumableUpload( + options, + (err: Error | null, uri: string | undefined) => { + assert.strictEqual(err, null); + assert.strictEqual(file.storage.retryOptions.autoRetry, false); + assert.strictEqual(uri, 'https://example.com/resumable-upload-uri'); + sinon.assert.calledOnce(resumableUploadStub); + }, + ); }); + }); - it('should emit progress via simple upload', done => { - const progress = {}; - - makeWritableStreamOverride = (dup: duplexify.Duplexify) => { - const uploadStream = new PassThrough(); - uploadStream.on('progress', evt => dup.emit('progress', evt)); - - dup.setWritable(uploadStream); - setImmediate(() => { - uploadStream.emit('progress', progress); - }); - }; - - const writable = file.createWriteStream({resumable: false}); - - writable.on('progress', (evt: {}) => { - assert.strictEqual(evt, progress); - done(); - }); + describe('createWriteStream', () => { + const METADATA = {a: 'b', c: 'd'}; - writable.write('data'); + it('should return a stream', () => { + assert(file.createWriteStream() instanceof Stream); }); it('should start a simple upload if specified', done => { @@ -2063,9 +1717,9 @@ describe('File', () => { }; const writable = file.createWriteStream(options); - file.startSimpleUpload_ = () => { + file.startSimpleUpload_ = sandbox.stub().callsFake(() => { done(); - }; + }); writable.write('data'); }); @@ -2078,9 +1732,9 @@ describe('File', () => { }; const writable = file.createWriteStream(options); - file.startResumableUpload_ = () => { + file.startResumableUpload_ = sandbox.stub().callsFake(() => { done(); - }; + }); writable.write('data'); }); @@ -2090,9 +1744,9 @@ describe('File', () => { metadata: METADATA, }); - file.startResumableUpload_ = () => { + file.startResumableUpload_ = sandbox.stub().callsFake(() => { done(); - }; + }); writable.write('data'); }); @@ -2101,21 +1755,24 @@ describe('File', () => { const contentType = 'text/html'; const writable = file.createWriteStream({contentType}); // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.metadata.contentType, contentType); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.metadata.contentType, contentType); + done(); + }); writable.write('data'); }); it('should detect contentType with contentType:auto', done => { const writable = file.createWriteStream({contentType: 'auto'}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.metadata.contentType, 'image/png'); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.metadata.contentType, 'image/png'); + done(); + }); writable.write('data'); }); @@ -2123,33 +1780,38 @@ describe('File', () => { it('should detect contentType if not defined', done => { const writable = file.createWriteStream(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.metadata.contentType, 'image/png'); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.metadata.contentType, 'image/png'); + done(); + }); writable.write('data'); }); it('should not set a contentType if mime lookup failed', done => { - const file = new File('file-without-ext'); + const file = new File(BUCKET, 'file-without-ext'); const writable = file.createWriteStream(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(typeof options.metadata.contentType, 'undefined'); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(typeof options.metadata.contentType, 'undefined'); + done(); + }); writable.write('data'); }); it('should set encoding with gzip:true', done => { const writable = file.createWriteStream({gzip: true}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.metadata.contentEncoding, 'gzip'); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.metadata.contentEncoding, 'gzip'); + done(); + }); writable.write('data'); }); @@ -2158,11 +1820,12 @@ describe('File', () => { const writable = file.createWriteStream({ preconditionOpts: {ifGenerationMatch: 100}, }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.preconditionOpts.ifGenerationMatch, 100); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.preconditionOpts.ifGenerationMatch, 100); + done(); + }); writable.write('data'); }); @@ -2171,11 +1834,15 @@ describe('File', () => { const writable = file.createWriteStream({ preconditionOpts: {ifGenerationNotMatch: 100}, }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.preconditionOpts.ifGenerationNotMatch, 100); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual( + options.preconditionOpts.ifGenerationNotMatch, + 100, + ); + done(); + }); writable.write('data'); }); @@ -2184,11 +1851,15 @@ describe('File', () => { const writable = file.createWriteStream({ preconditionOpts: {ifMetagenerationMatch: 100}, }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.preconditionOpts.ifMetagenerationMatch, 100); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual( + options.preconditionOpts.ifMetagenerationMatch, + 100, + ); + done(); + }); writable.write('data'); }); @@ -2197,14 +1868,15 @@ describe('File', () => { const writable = file.createWriteStream({ preconditionOpts: {ifMetagenerationNotMatch: 100}, }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual( - options.preconditionOpts.ifMetagenerationNotMatch, - 100, - ); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual( + options.preconditionOpts.ifMetagenerationNotMatch, + 100, + ); + done(); + }); writable.write('data'); }); @@ -2215,22 +1887,24 @@ describe('File', () => { contentType: 'text/html', // (compressible) }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.metadata.contentEncoding, 'gzip'); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.metadata.contentEncoding, 'gzip'); + done(); + }); writable.write('data'); }); it('should not set encoding with gzip:auto & non-compressible', done => { const writable = file.createWriteStream({gzip: 'auto'}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.startResumableUpload_ = (stream: {}, options: any) => { - assert.strictEqual(options.metadata.contentEncoding, undefined); - done(); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream, options) => { + assert.strictEqual(options.metadata.contentEncoding, undefined); + done(); + }); writable.write('data'); }); @@ -2238,9 +1912,11 @@ describe('File', () => { const writable = file.createWriteStream(); const resp = {}; - file.startResumableUpload_ = (stream: Duplex) => { - stream.emit('response', resp); - }; + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: Duplex) => { + stream.emit('response', resp); + }); writable.on('response', (resp_: {}) => { assert.strictEqual(resp_, resp); @@ -2267,14 +1943,16 @@ describe('File', () => { } }); - file.startSimpleUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startSimpleUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - streamFinishedCalled = true; + stream.on('finish', () => { + streamFinishedCalled = true; + }); }); - }; writable.end('data'); }); @@ -2290,14 +1968,16 @@ describe('File', () => { it('should validate with crc32c', done => { const writable = file.createWriteStream({validation: 'crc32c'}); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = fakeMetadata.crc32c; + stream.on('finish', () => { + file.metadata = fakeMetadata.crc32c; + }); }); - }; writable.end(data); @@ -2307,21 +1987,23 @@ describe('File', () => { it('should emit an error if crc32c validation fails', done => { const writable = file.createWriteStream({validation: 'crc32c'}); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = fakeMetadata.crc32c; + stream.on('finish', () => { + file.metadata = fakeMetadata.crc32c; + }); }); - }; - file.delete = async () => {}; + sandbox.stub(file, 'delete').callsFake(() => {}); writable.write('bad-data'); writable.end(); - writable.on('error', (err: ApiError) => { + writable.on('error', (err: RequestError) => { assert.strictEqual(err.code, 'FILE_NO_UPLOAD'); done(); }); @@ -2330,14 +2012,16 @@ describe('File', () => { it('should validate with md5', done => { const writable = file.createWriteStream({validation: 'md5'}); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = fakeMetadata.md5; + stream.on('finish', () => { + file.metadata = fakeMetadata.md5; + }); }); - }; writable.write(data); writable.end(); @@ -2348,21 +2032,23 @@ describe('File', () => { it('should emit an error if md5 validation fails', done => { const writable = file.createWriteStream({validation: 'md5'}); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = fakeMetadata.md5; + stream.on('finish', () => { + file.metadata = fakeMetadata.md5; + }); }); - }; - file.delete = async () => {}; + sandbox.stub(file, 'delete').callsFake(() => {}); writable.write('bad-data'); writable.end(); - writable.on('error', (err: ApiError) => { + writable.on('error', (err: RequestError) => { assert.strictEqual(err.code, 'FILE_NO_UPLOAD'); done(); }); @@ -2371,21 +2057,23 @@ describe('File', () => { it('should default to md5 validation', done => { const writable = file.createWriteStream(); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = {md5Hash: 'bad-hash'}; + stream.on('finish', () => { + file.metadata = {md5Hash: 'bad-hash'}; + }); }); - }; - file.delete = async () => {}; + sandbox.stub(file, 'delete').callsFake(() => {}); writable.write(data); writable.end(); - writable.on('error', (err: ApiError) => { + writable.on('error', (err: RequestError) => { assert.strictEqual(err.code, 'FILE_NO_UPLOAD'); done(); }); @@ -2394,14 +2082,16 @@ describe('File', () => { it('should ignore a data mismatch if validation: false', done => { const writable = file.createWriteStream({validation: false}); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = {md5Hash: 'bad-hash'}; + stream.on('finish', () => { + file.metadata = {md5Hash: 'bad-hash'}; + }); }); - }; writable.write(data); writable.end(); @@ -2413,19 +2103,21 @@ describe('File', () => { it('should delete the file if validation fails', done => { const writable = file.createWriteStream(); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = {md5Hash: 'bad-hash'}; + stream.on('finish', () => { + file.metadata = {md5Hash: 'bad-hash'}; + }); }); - }; - file.delete = async () => {}; + sandbox.stub(file, 'delete').callsFake(() => {}); - writable.on('error', (e: ApiError) => { - assert.equal(e.code, 'FILE_NO_UPLOAD'); + writable.on('error', (err: RequestError) => { + assert.equal(err.code, 'FILE_NO_UPLOAD'); done(); }); @@ -2436,21 +2128,23 @@ describe('File', () => { it('should emit an error if MD5 is requested but absent', done => { const writable = file.createWriteStream({validation: 'md5'}); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = {crc32c: 'not-md5'}; + stream.on('finish', () => { + file.metadata = {crc32c: 'not-md5'}; + }); }); - }; - file.delete = async () => {}; + sandbox.stub(file, 'delete').callsFake(() => {}); writable.write(data); writable.end(); - writable.on('error', (err: ApiError) => { + writable.on('error', (err: RequestError) => { assert.strictEqual(err.code, 'MD5_NOT_AVAILABLE'); done(); }); @@ -2459,14 +2153,16 @@ describe('File', () => { it('should emit a different error if delete fails', done => { const writable = file.createWriteStream(); - file.startResumableUpload_ = (stream: duplexify.Duplexify) => { - stream.setWritable(new PassThrough()); - stream.emit('metadata'); + file.startResumableUpload_ = sandbox + .stub() + .callsFake((stream: duplexify.Duplexify) => { + stream.setWritable(new PassThrough()); + stream.emit('metadata'); - stream.on('finish', () => { - file.metadata = {md5Hash: 'bad-hash'}; + stream.on('finish', () => { + file.metadata = {md5Hash: 'bad-hash'}; + }); }); - }; const deleteErrorMessage = 'Delete error message.'; const deleteError = new Error(deleteErrorMessage); @@ -2477,7 +2173,7 @@ describe('File', () => { writable.write(data); writable.end(); - writable.on('error', (err: ApiError) => { + writable.on('error', (err: RequestError) => { assert.strictEqual(err.code, 'FILE_NO_UPLOAD_DELETE'); assert(err.message.indexOf(deleteErrorMessage) > -1); done(); @@ -2491,7 +2187,7 @@ describe('File', () => { beforeEach(() => { fileReadStream = new Readable(); - fileReadStream._read = util.noop; + sandbox.stub(fileReadStream, '_read').callsFake(() => {}); fileReadStream.on('end', () => { fileReadStream.emit('complete'); @@ -2502,30 +2198,22 @@ describe('File', () => { }; }); - it('should accept just a callback', done => { - fileReadStream._read = () => { - done(); - }; - + it('should accept just a callback', () => { file.download(assert.ifError); }); - it('should accept an options object and callback', done => { - fileReadStream._read = () => { - done(); - }; - + it('should accept an options object and callback', () => { file.download({}, assert.ifError); }); it('should pass the provided options to createReadStream', done => { const readOptions = {start: 100, end: 200}; - file.createReadStream = (options: {}) => { + sandbox.stub(file, 'createReadStream').callsFake(options => { assert.deepStrictEqual(options, readOptions); done(); return fileReadStream; - }; + }); file.download(readOptions, assert.ifError); }); @@ -2533,9 +2221,6 @@ describe('File', () => { it('should only execute callback once', done => { Object.assign(fileReadStream, { _read(this: Readable) { - // Do not fire the errors immediately as this is a synchronous operation here - // and the iterator getter is also synchronous in file.getBufferFromReadable. - // this is only an issue for <= node 12. This cannot happen in practice. process.nextTick(() => { this.emit('error', new Error('Error.')); this.emit('error', new Error('Error.')); @@ -2559,7 +2244,7 @@ describe('File', () => { }, }); - file.download((err: Error, remoteFileContents: {}) => { + file.download((err, remoteFileContents) => { assert.ifError(err); assert.strictEqual(fileContents, remoteFileContents.toString()); @@ -2572,16 +2257,13 @@ describe('File', () => { Object.assign(fileReadStream, { _read(this: Readable) { - // Do not fire the errors immediately as this is a synchronous operation here - // and the iterator getter is also synchronous in file.getBufferFromReadable. - // this is only an issue for <= node 12. This cannot happen in practice. process.nextTick(() => { this.emit('error', error); }); }, }); - file.download((err: Error) => { + file.download(err => { assert.strictEqual(err, error); done(); }); @@ -2609,7 +2291,7 @@ describe('File', () => { }, }); - file.download({destination: tmpFilePath}, (err: Error) => { + file.download({destination: tmpFilePath}, err => { assert.ifError(err); fs.readFile(tmpFilePath, (err, tmpFileContents) => { @@ -2637,7 +2319,7 @@ describe('File', () => { }); }); - file.download({destination: tmpFilePath}, (err: Error) => { + file.download({destination: tmpFilePath}, err => { assert.ifError(err); fs.readFile(tmpFilePath, (err, tmpFileContents) => { assert.ifError(err); @@ -2662,7 +2344,7 @@ describe('File', () => { }); }); - file.download({destination: tmpFilePath}, (err: Error) => { + file.download({destination: tmpFilePath}, err => { assert.ifError(err); fs.readFile(tmpFilePath, (err, tmpFileContents) => { assert.ifError(err); @@ -2688,7 +2370,7 @@ describe('File', () => { }); }); - file.download({destination: tmpFilePath}, (err: Error) => { + file.download({destination: tmpFilePath}, err => { assert.strictEqual(err, error); fs.readFile(tmpFilePath, (err, tmpFileContents) => { assert.ifError(err); @@ -2712,7 +2394,7 @@ describe('File', () => { }, }); - file.download({destination: tmpFilePath}, (err: Error) => { + file.download({destination: tmpFilePath}, err => { assert.strictEqual(err, error); done(); }); @@ -2735,7 +2417,7 @@ describe('File', () => { const nestedPath = path.join(tmpDirPath, 'a', 'b', 'c', 'file.txt'); - file.download({destination: nestedPath}, (err: Error) => { + file.download({destination: nestedPath}, err => { assert.ok(err); done(); }); @@ -2746,9 +2428,9 @@ describe('File', () => { describe('getExpirationDate', () => { it('should refresh metadata', done => { - file.getMetadata = () => { + file.getMetadata = sandbox.stub().callsFake(() => { done(); - }; + }); file.getExpirationDate(assert.ifError); }); @@ -2757,38 +2439,34 @@ describe('File', () => { const error = new Error('Error.'); const apiResponse = {}; - file.getMetadata = (callback: Function) => { + file.getMetadata = sandbox.stub().callsFake(callback => { callback(error, null, apiResponse); - }; + }); - file.getExpirationDate( - (err: Error, expirationDate: {}, apiResponse_: {}) => { - assert.strictEqual(err, error); - assert.strictEqual(expirationDate, null); - assert.strictEqual(apiResponse_, apiResponse); - done(); - }, - ); + file.getExpirationDate((err, expirationDate, apiResponse_) => { + assert.strictEqual(err, error); + assert.strictEqual(expirationDate, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); }); it('should return an error if there is no expiration time', done => { const apiResponse = {}; - file.getMetadata = (callback: Function) => { + file.getMetadata = sandbox.stub().callsFake(callback => { callback(null, {}, apiResponse); - }; + }); - file.getExpirationDate( - (err: Error, expirationDate: {}, apiResponse_: {}) => { - assert.strictEqual( - err.message, - FileExceptionMessages.EXPIRATION_TIME_NA, - ); - assert.strictEqual(expirationDate, null); - assert.strictEqual(apiResponse_, apiResponse); - done(); - }, - ); + file.getExpirationDate((err, expirationDate, apiResponse_) => { + assert.strictEqual( + err?.message, + FileExceptionMessages.EXPIRATION_TIME_NA, + ); + assert.strictEqual(expirationDate, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); }); it('should return the expiration time as a Date object', done => { @@ -2798,60 +2476,65 @@ describe('File', () => { retentionExpirationTime: expirationTime.toJSON(), }; - file.getMetadata = (callback: Function) => { + file.getMetadata = sandbox.stub().callsFake(callback => { callback(null, apiResponse, apiResponse); - }; + }); - file.getExpirationDate( - (err: Error, expirationDate: {}, apiResponse_: {}) => { - assert.ifError(err); - assert.deepStrictEqual(expirationDate, expirationTime); - assert.strictEqual(apiResponse_, apiResponse); - done(); - }, - ); + file.getExpirationDate((err, expirationDate, apiResponse_) => { + assert.ifError(err); + assert.deepStrictEqual(expirationDate, expirationTime); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); }); }); describe('generateSignedPostPolicyV2', () => { let CONFIG: GenerateSignedPostPolicyV2Options; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let sandbox: any; + let bucket: Bucket; + let file: File; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockAuthClient: any; beforeEach(() => { + sandbox = sinon.createSandbox(); + const storage = new Storage({projectId: PROJECT_ID}); + bucket = new Bucket(storage, 'bucket-name'); + file = new File(bucket, FILE_NAME); + + mockAuthClient = {sign: sandbox.stub().resolves('signature')}; + file.storage.storageTransport.authClient = mockAuthClient; + CONFIG = { expires: Date.now() + 2000, }; + }); - BUCKET.storage.authClient = { - sign: () => { - return Promise.resolve('signature'); - }, - }; + afterEach(() => { + sandbox.restore(); }); - it('should create a signed policy', done => { - BUCKET.storage.authClient.sign = (blobToSign: string) => { + it('should create a signed policy', () => { + file.storage.storageTransport.authClient.sign = (blobToSign: string) => { const policy = Buffer.from(blobToSign, 'base64').toString(); assert.strictEqual(typeof JSON.parse(policy), 'object'); return Promise.resolve('signature'); }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.generateSignedPostPolicyV2( - CONFIG, - (err: Error, signedPolicy: PolicyDocument) => { - assert.ifError(err); - assert.strictEqual(typeof signedPolicy.string, 'string'); - assert.strictEqual(typeof signedPolicy.base64, 'string'); - assert.strictEqual(typeof signedPolicy.signature, 'string'); - done(); - }, - ); + file.generateSignedPostPolicyV2(CONFIG, (err, signedPolicy) => { + assert.ifError(err); + assert.strictEqual(typeof signedPolicy?.string, 'string'); + assert.strictEqual(typeof signedPolicy?.base64, 'string'); + assert.strictEqual(typeof signedPolicy?.signature, 'string'); + }); }); it('should not modify the configuration object', done => { const originalConfig = Object.assign({}, CONFIG); - file.generateSignedPostPolicyV2(CONFIG, (err: Error) => { + file.generateSignedPostPolicyV2(CONFIG, err => { assert.ifError(err); assert.deepStrictEqual(CONFIG, originalConfig); done(); @@ -2861,27 +2544,25 @@ describe('File', () => { it('should return an error if signBlob errors', done => { const error = new Error('Error.'); - BUCKET.storage.authClient.sign = () => { + file.storage.storageTransport.authClient.sign = () => { return Promise.reject(error); }; - file.generateSignedPostPolicyV2(CONFIG, (err: Error) => { - assert.strictEqual(err.name, 'SigningError'); - assert.strictEqual(err.message, error.message); + file.generateSignedPostPolicyV2(CONFIG, err => { + assert.strictEqual(err?.name, 'SigningError'); + assert.strictEqual(err?.message, error.message); done(); }); }); it('should add key equality condition', done => { - file.generateSignedPostPolicyV2( - CONFIG, - (err: Error, signedPolicy: PolicyDocument) => { - const conditionString = '["eq","$key","' + file.name + '"]'; - assert.ifError(err); - assert(signedPolicy.string.indexOf(conditionString) > -1); - done(); - }, - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + file.generateSignedPostPolicyV2(CONFIG, (err, signedPolicy: any) => { + const conditionString = '["eq","$key","' + file.name + '"]'; + assert.ifError(err); + assert(signedPolicy.string.indexOf(conditionString) > -1); + done(); + }); }); it('should add ACL condtion', done => { @@ -2890,7 +2571,8 @@ describe('File', () => { expires: Date.now() + 2000, acl: '', }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { const conditionString = '{"acl":""}'; assert.ifError(err); assert(signedPolicy.string.indexOf(conditionString) > -1); @@ -2907,7 +2589,8 @@ describe('File', () => { expires: Date.now() + 2000, successRedirect: redirectUrl, }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { assert.ifError(err); const policy = JSON.parse(signedPolicy.string); @@ -2932,7 +2615,8 @@ describe('File', () => { expires: Date.now() + 2000, successStatus, }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { assert.ifError(err); const policy = JSON.parse(signedPolicy.string); @@ -2957,7 +2641,8 @@ describe('File', () => { { expires, }, - (err: Error, policy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, policy: any) => { assert.ifError(err); const expires_ = JSON.parse(policy.string).expiration; assert.strictEqual(expires_, expires.toISOString()); @@ -2973,7 +2658,8 @@ describe('File', () => { { expires, }, - (err: Error, policy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, policy: any) => { assert.ifError(err); const expires_ = JSON.parse(policy.string).expiration; assert.strictEqual(expires_, new Date(expires).toISOString()); @@ -2989,7 +2675,8 @@ describe('File', () => { { expires, }, - (err: Error, policy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, policy: any) => { assert.ifError(err); const expires_ = JSON.parse(policy.string).expiration; assert.strictEqual(expires_, new Date(expires).toISOString()); @@ -3034,7 +2721,8 @@ describe('File', () => { expires: Date.now() + 2000, equals: [['$', '']], }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { const conditionString = '["eq","$",""]'; assert.ifError(err); assert(signedPolicy.string.indexOf(conditionString) > -1); @@ -3049,7 +2737,8 @@ describe('File', () => { expires: Date.now() + 2000, equals: ['$', ''], }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { const conditionString = '["eq","$",""]'; assert.ifError(err); assert(signedPolicy.string.indexOf(conditionString) > -1); @@ -3063,7 +2752,7 @@ describe('File', () => { file.generateSignedPostPolicyV2( { expires: Date.now() + 2000, - equals: [{}], + equals: [], }, () => {}, ), @@ -3092,7 +2781,8 @@ describe('File', () => { expires: Date.now() + 2000, startsWith: [['$', '']], }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { const conditionString = '["starts-with","$",""]'; assert.ifError(err); assert(signedPolicy.string.indexOf(conditionString) > -1); @@ -3107,7 +2797,8 @@ describe('File', () => { expires: Date.now() + 2000, startsWith: ['$', ''], }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { const conditionString = '["starts-with","$",""]'; assert.ifError(err); assert(signedPolicy.string.indexOf(conditionString) > -1); @@ -3118,14 +2809,14 @@ describe('File', () => { it('should throw if prexif condition is not an array', () => { assert.throws(() => { - file.generateSignedPostPolicyV2( + void (file.generateSignedPostPolicyV2( { expires: Date.now() + 2000, - startsWith: [{}], + startsWith: [[]], }, () => {}, ), - FileExceptionMessages.STARTS_WITH_TWO_ELEMENTS; + FileExceptionMessages.STARTS_WITH_TWO_ELEMENTS); }); }); @@ -3150,7 +2841,8 @@ describe('File', () => { expires: Date.now() + 2000, contentLengthRange: {min: 0, max: 1}, }, - (err: Error, signedPolicy: PolicyDocument) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, signedPolicy: any) => { const conditionString = '["content-length-range",0,1]'; assert.ifError(err); assert(signedPolicy.string.indexOf(conditionString) > -1); @@ -3164,7 +2856,7 @@ describe('File', () => { file.generateSignedPostPolicyV2( { expires: Date.now() + 2000, - contentLengthRange: [{max: 1}], + contentLengthRange: {max: 1}, }, () => {}, ), @@ -3177,7 +2869,7 @@ describe('File', () => { file.generateSignedPostPolicyV2( { expires: Date.now() + 2000, - contentLengthRange: [{min: 0}], + contentLengthRange: {min: 0}, }, () => {}, ), @@ -3195,30 +2887,38 @@ describe('File', () => { const SIGNATURE = 'signature'; let fakeTimer: sinon.SinonFakeTimers; - let sandbox: sinon.SinonSandbox; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let BUCKET: any; beforeEach(() => { - sandbox = sinon.createSandbox(); fakeTimer = sinon.useFakeTimers(NOW); CONFIG = { expires: NOW.valueOf() + 2000, }; - BUCKET.storage.authClient = { - sign: sandbox.stub().resolves(SIGNATURE), - getCredentials: sandbox.stub().resolves({client_email: CLIENT_EMAIL}), + BUCKET = { + name: BUCKET, + storage: { + storageTransport: { + authClient: { + sign: sandbox.stub().resolves(SIGNATURE), + getCredentials: sandbox + .stub() + .resolves({client_email: CLIENT_EMAIL}), + }, + }, + }, }; }); afterEach(() => { - sandbox.restore(); fakeTimer.restore(); }); const fieldsToConditions = (fields: object) => Object.entries(fields).map(([k, v]) => ({[k]: v})); - it('should create a signed policy', done => { + it('should create a signed policy', () => { CONFIG.fields = { 'x-goog-meta-foo': 'bar', }; @@ -3251,67 +2951,59 @@ describe('File', () => { policy: EXPECTED_POLICY, }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); - assert(res.url, `${STORAGE_POST_POLICY_BASE_URL}/${BUCKET.name}`); - - assert.deepStrictEqual(res.fields, EXPECTED_FIELDS); + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); + assert(res?.url, `${STORAGE_POST_POLICY_BASE_URL}/${BUCKET.name}`); - const signStub = BUCKET.storage.authClient.sign; - assert.deepStrictEqual( - Buffer.from(signStub.getCall(0).args[0], 'base64').toString(), - policyString, - ); + assert.deepStrictEqual(res?.fields, EXPECTED_FIELDS); - done(); - }, - ); + const signStub = BUCKET.storage.storageTransport.authClient.sign; + assert.deepStrictEqual( + Buffer.from(signStub.getCall(0).args[0], 'base64').toString(), + policyString, + ); + }); }); - it('should not modify the configuration object', done => { + it('should not modify the configuration object', () => { const originalConfig = Object.assign({}, CONFIG); - file.generateSignedPostPolicyV4(CONFIG, (err: Error) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, err => { assert.ifError(err); assert.deepStrictEqual(CONFIG, originalConfig); - done(); }); }); - it('should return an error if signBlob errors', done => { + it('should return an error if signBlob errors', () => { const error = new Error('Error.'); - BUCKET.storage.authClient.sign.rejects(error); + BUCKET.storage.storageTransport.authClient.sign.rejects(error); - file.generateSignedPostPolicyV4(CONFIG, (err: Error) => { - assert.strictEqual(err.name, 'SigningError'); - assert.strictEqual(err.message, error.message); - done(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, err => { + assert.strictEqual(err?.name, 'SigningError'); + assert.strictEqual(err?.message, error.message); }); }); - it('should add key condition', done => { - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); + it('should add key condition', () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); - assert.strictEqual(res.fields['key'], file.name); - const EXPECTED_POLICY_ELEMENT = `{"key":"${file.name}"}`; - assert( - Buffer.from(res.fields.policy, 'base64') - .toString('utf-8') - .includes(EXPECTED_POLICY_ELEMENT), - ); - done(); - }, - ); + assert.strictEqual(res?.fields['key'], file.name); + const EXPECTED_POLICY_ELEMENT = `{"key":"${file.name}"}`; + assert( + Buffer.from(res?.fields.policy, 'base64') + .toString('utf-8') + .includes(EXPECTED_POLICY_ELEMENT), + ); + }); }); - it('should include fields in conditions', done => { + it('should include fields in conditions', () => { CONFIG = { fields: { 'x-goog-meta-foo': 'bar', @@ -3319,24 +3011,20 @@ describe('File', () => { ...CONFIG, }; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); - const expectedConditionString = JSON.stringify(CONFIG.fields); - assert.strictEqual(res.fields['x-goog-meta-foo'], 'bar'); - const decodedPolicy = Buffer.from( - res.fields.policy, - 'base64', - ).toString('utf-8'); - assert(decodedPolicy.includes(expectedConditionString)); - done(); - }, - ); + const expectedConditionString = JSON.stringify(CONFIG.fields); + assert.strictEqual(res?.fields['x-goog-meta-foo'], 'bar'); + const decodedPolicy = Buffer.from(res.fields.policy, 'base64').toString( + 'utf-8', + ); + assert(decodedPolicy.includes(expectedConditionString)); + }); }); - it('should encode special characters in policy', done => { + it('should encode special characters in policy', () => { CONFIG = { fields: { 'x-goog-meta-foo': 'bår', @@ -3344,23 +3032,19 @@ describe('File', () => { ...CONFIG, }; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); - assert.strictEqual(res.fields['x-goog-meta-foo'], 'bår'); - const decodedPolicy = Buffer.from( - res.fields.policy, - 'base64', - ).toString('utf-8'); - assert(decodedPolicy.includes('"x-goog-meta-foo":"b\\u00e5r"')); - done(); - }, - ); + assert.strictEqual(res?.fields['x-goog-meta-foo'], 'bår'); + const decodedPolicy = Buffer.from(res.fields.policy, 'base64').toString( + 'utf-8', + ); + assert(decodedPolicy.includes('"x-goog-meta-foo":"b\\u00e5r"')); + }); }); - it('should not include fields with x-ignore- prefix in conditions', done => { + it('should not include fields with x-ignore- prefix in conditions', () => { CONFIG = { fields: { 'x-ignore-foo': 'bar', @@ -3368,80 +3052,67 @@ describe('File', () => { ...CONFIG, }; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); - const expectedConditionString = JSON.stringify(CONFIG.fields); - assert.strictEqual(res.fields['x-ignore-foo'], 'bar'); - const decodedPolicy = Buffer.from( - res.fields.policy, - 'base64', - ).toString('utf-8'); - assert(!decodedPolicy.includes(expectedConditionString)); + const expectedConditionString = JSON.stringify(CONFIG.fields); + assert.strictEqual(res?.fields['x-ignore-foo'], 'bar'); + const decodedPolicy = Buffer.from(res.fields.policy, 'base64').toString( + 'utf-8', + ); + assert(!decodedPolicy.includes(expectedConditionString)); - const signStub = BUCKET.storage.authClient.sign; - assert(!signStub.getCall(0).args[0].includes('x-ignore-foo')); - done(); - }, - ); + const signStub = BUCKET.storage.storageTransport.authClient.sign; + assert(!signStub.getCall(0).args[0].includes('x-ignore-foo')); + }); }); - it('should accept conditions', done => { + it('should accept conditions', () => { CONFIG = { conditions: [['starts-with', '$key', 'prefix-']], ...CONFIG, }; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); + // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-explicit-any + file.generateSignedPostPolicyV4(CONFIG, (err, res: any) => { + assert.ifError(err); - const expectedConditionString = JSON.stringify(CONFIG.conditions); - const decodedPolicy = Buffer.from( - res.fields.policy, - 'base64', - ).toString('utf-8'); - assert(decodedPolicy.includes(expectedConditionString)); + const expectedConditionString = JSON.stringify(CONFIG.conditions); + const decodedPolicy = Buffer.from(res.fields.policy, 'base64').toString( + 'utf-8', + ); + assert(decodedPolicy.includes(expectedConditionString)); - const signStub = BUCKET.storage.authClient.sign; - assert( - !signStub.getCall(0).args[0].includes(expectedConditionString), - ); - done(); - }, - ); + const signStub = BUCKET.storage.storageTransport.authClient.sign; + assert(!signStub.getCall(0).args[0].includes(expectedConditionString)); + }); }); - it('should output url with cname', done => { + it('should output url with cname', () => { CONFIG.bucketBoundHostname = 'http://domain.tld'; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); - assert(res.url, CONFIG.bucketBoundHostname); - done(); - }, - ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); + assert(res?.url, CONFIG.bucketBoundHostname); + }); }); - it('should output a virtualHostedStyle url', done => { + it('should output a virtualHostedStyle url', () => { CONFIG.virtualHostedStyle = true; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); - assert(res.url, `https://${BUCKET.name}.storage.googleapis.com/`); - done(); - }, - ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); + assert(res?.url, `https://${BUCKET.name}.storage.googleapis.com/`); + }); }); - it('should prefer a customEndpoint > virtualHostedStyle, cname', done => { + it('should prefer a customEndpoint > virtualHostedStyle, cname', () => { + let STORAGE: Storage; + // eslint-disable-next-line prefer-const + STORAGE = new Storage({projectId: PROJECT_ID}); const customEndpoint = 'https://my-custom-endpoint.com'; STORAGE.apiEndpoint = customEndpoint; @@ -3450,25 +3121,24 @@ describe('File', () => { CONFIG.virtualHostedStyle = true; CONFIG.bucketBoundHostname = 'http://domain.tld'; - file.generateSignedPostPolicyV4( - CONFIG, - (err: Error, res: SignedPostPolicyV4Output) => { - assert.ifError(err); - assert(res.url, `https://${BUCKET.name}.storage.googleapis.com/`); - done(); - }, - ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.generateSignedPostPolicyV4(CONFIG, (err, res) => { + assert.ifError(err); + assert(res?.url, `https://${BUCKET.name}.storage.googleapis.com/`); + }); }); describe('expires', () => { - it('should accept Date objects', done => { + it('should accept Date objects', () => { const expires = new Date(Date.now() + 1000 * 60); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.generateSignedPostPolicyV4( { expires, }, - (err: Error, response: SignedPostPolicyV4Output) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, response: any) => { assert.ifError(err); const policy = JSON.parse( Buffer.from(response.fields.policy, 'base64').toString(), @@ -3477,19 +3147,20 @@ describe('File', () => { policy.expiration, formatAsUTCISO(expires, true, '-', ':'), ); - done(); }, ); }); - it('should accept numbers', done => { + it('should accept numbers', () => { const expires = Date.now() + 1000 * 60; + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.generateSignedPostPolicyV4( { expires, }, - (err: Error, response: SignedPostPolicyV4Output) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, response: any) => { assert.ifError(err); const policy = JSON.parse( Buffer.from(response.fields.policy, 'base64').toString(), @@ -3498,23 +3169,24 @@ describe('File', () => { policy.expiration, formatAsUTCISO(new Date(expires), true, '-', ':'), ); - done(); }, ); }); - it('should accept strings', done => { + it('should accept strings', () => { const expires = formatAsUTCISO( new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), false, '-', ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.generateSignedPostPolicyV4( { expires, }, - (err: Error, response: SignedPostPolicyV4Output) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err, response: any) => { assert.ifError(err); const policy = JSON.parse( Buffer.from(response.fields.policy, 'base64').toString(), @@ -3523,7 +3195,6 @@ describe('File', () => { policy.expiration, formatAsUTCISO(new Date(expires), true, '-', ':'), ); - done(); }, ); }); @@ -3575,6 +3246,9 @@ describe('File', () => { describe('getSignedUrl', () => { const EXPECTED_SIGNED_URL = 'signed-url'; const CNAME = 'https://www.example.com'; + const fakeSigner = { + URLSigner: () => {}, + }; let sandbox: sinon.SinonSandbox; let signer: {getSignedUrl: Function}; @@ -3598,7 +3272,7 @@ describe('File', () => { SIGNED_URL_CONFIG = { version: 'v4', - expires: new Date(), + expires: new Date().valueOf() + 2000, action: 'read', cname: CNAME, }; @@ -3606,7 +3280,7 @@ describe('File', () => { afterEach(() => sandbox.restore()); - it('should construct a URLSigner and call getSignedUrl', done => { + it('should construct a URLSigner and call getSignedUrl', () => { const accessibleAtDate = new Date(); const config = { contentMd5: 'md5-hash', @@ -3617,13 +3291,17 @@ describe('File', () => { }; // assert signer is lazily-initialized. assert.strictEqual(file.signer, undefined); - file.getSignedUrl(config, (err: Error | null, signedUrl: string) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + file.getSignedUrl(config, (err: Error | null, signedUrl) => { assert.ifError(err); assert.strictEqual(file.signer, signer); assert.strictEqual(signedUrl, EXPECTED_SIGNED_URL); const ctorArgs = urlSignerStub.getCall(0).args; - assert.strictEqual(ctorArgs[0], file.storage.authClient); + assert.strictEqual( + ctorArgs[0], + file.storage.storageTransport.authClient, + ); assert.strictEqual(ctorArgs[1], file.bucket); assert.strictEqual(ctorArgs[2], file); @@ -3641,11 +3319,10 @@ describe('File', () => { cname: CNAME, virtualHostedStyle: true, }); - done(); }); }); - it('should add "x-goog-resumable: start" header if action is resumable', done => { + it('should add "x-goog-resumable: start" header if action is resumable', () => { SIGNED_URL_CONFIG.action = 'resumable'; SIGNED_URL_CONFIG.extensionHeaders = { 'another-header': 'value', @@ -3659,11 +3336,10 @@ describe('File', () => { 'another-header': 'value', 'x-goog-resumable': 'start', }); - done(); }); }); - it('should add response-content-type query parameter', done => { + it('should add response-content-type query parameter', () => { SIGNED_URL_CONFIG.responseType = 'application/json'; file.getSignedUrl(SIGNED_URL_CONFIG, (err: Error | null) => { assert.ifError(err); @@ -3671,11 +3347,10 @@ describe('File', () => { assert.deepStrictEqual(getSignedUrlArgs[0]['queryParams'], { 'response-content-type': 'application/json', }); - done(); }); }); - it('should respect promptSaveAs argument', done => { + it('should respect promptSaveAs argument', () => { const filename = 'fname.txt'; SIGNED_URL_CONFIG.promptSaveAs = filename; file.getSignedUrl(SIGNED_URL_CONFIG, (err: Error | null) => { @@ -3685,11 +3360,10 @@ describe('File', () => { 'response-content-disposition': 'attachment; filename="' + filename + '"', }); - done(); }); }); - it('should add response-content-disposition query parameter', done => { + it('should add response-content-disposition query parameter', () => { const disposition = 'attachment; filename="fname.ext"'; SIGNED_URL_CONFIG.responseDisposition = disposition; file.getSignedUrl(SIGNED_URL_CONFIG, (err: Error | null) => { @@ -3698,11 +3372,10 @@ describe('File', () => { assert.deepStrictEqual(getSignedUrlArgs[0]['queryParams'], { 'response-content-disposition': disposition, }); - done(); }); }); - it('should ignore promptSaveAs if set', done => { + it('should ignore promptSaveAs if set', () => { const saveAs = 'fname2.ext'; const disposition = 'attachment; filename="fname.ext"'; SIGNED_URL_CONFIG.promptSaveAs = saveAs; @@ -3714,12 +3387,11 @@ describe('File', () => { assert.deepStrictEqual(getSignedUrlArgs[0]['queryParams'], { 'response-content-disposition': disposition, }); - done(); }); }); - it('should add generation to query parameter', done => { - file.generation = '246680131'; + it('should add generation to query parameter', () => { + file.generation = 246680131; file.getSignedUrl(SIGNED_URL_CONFIG, (err: Error | null) => { assert.ifError(err); @@ -3727,7 +3399,6 @@ describe('File', () => { assert.deepStrictEqual(getSignedUrlArgs[0]['queryParams'], { generation: file.generation, }); - done(); }); }); }); @@ -3736,17 +3407,15 @@ describe('File', () => { it('should execute callback with API response', done => { const apiResponse = {}; - file.setMetadata = ( - metadata: FileMetadata, - optionsOrCallback: SetMetadataOptions | MetadataCallback, - cb: MetadataCallback, - ) => { - Promise.resolve([apiResponse]) - .then(resp => cb(null, ...resp)) - .catch(() => {}); - }; + sandbox + .stub(file, 'setMetadata') + .callsFake((metadata, optionsOrCallback, cb) => { + Promise.resolve([apiResponse]) + .then(resp => cb(null, ...resp)) + .catch(() => {}); + }); - file.makePrivate((err: Error, apiResponse_: {}) => { + file.makePrivate((err, apiResponse_) => { assert.ifError(err); assert.strictEqual(apiResponse_, apiResponse); @@ -3755,29 +3424,29 @@ describe('File', () => { }); it('should make the file private to project by default', done => { - file.setMetadata = (metadata: {}, query: {}) => { + sandbox.stub(file, 'setMetadata').callsFake((metadata: {}, query: {}) => { assert.deepStrictEqual(metadata, {acl: null}); assert.deepStrictEqual(query, {predefinedAcl: 'projectPrivate'}); done(); - }; + }); - file.makePrivate(util.noop); + file.makePrivate(() => {}); }); it('should make the file private to user if strict = true', done => { - file.setMetadata = (metadata: {}, query: {}) => { + sandbox.stub(file, 'setMetadata').callsFake((metadata: {}, query: {}) => { assert.deepStrictEqual(query, {predefinedAcl: 'private'}); done(); - }; + }); - file.makePrivate({strict: true}, util.noop); + file.makePrivate({strict: true}, () => {}); }); it('should accept metadata', done => { const options = { metadata: {a: 'b', c: 'd'}, }; - file.setMetadata = (metadata: {}) => { + sandbox.stub(file, 'setMetadata').callsFake((metadata: {}) => { assert.deepStrictEqual(metadata, { acl: null, ...options.metadata, @@ -3785,7 +3454,7 @@ describe('File', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any assert.strictEqual(typeof (options.metadata as any).acl, 'undefined'); done(); - }; + }); file.makePrivate(options, assert.ifError); }); @@ -3794,10 +3463,12 @@ describe('File', () => { userProject: 'user-project-id', }; - file.setMetadata = (metadata: {}, query: SetFileMetadataOptions) => { - assert.strictEqual(query.userProject, options.userProject); - done(); - }; + sandbox + .stub(file, 'setMetadata') + .callsFake((metadata: {}, query: SetFileMetadataOptions) => { + assert.strictEqual(query.userProject, options.userProject); + done(); + }); file.makePrivate(options, assert.ifError); }); @@ -3805,20 +3476,22 @@ describe('File', () => { describe('makePublic', () => { it('should execute callback', done => { - file.acl.add = (options: {}, callback: Function) => { - callback(); - }; + sandbox + .stub(file.acl, 'add') + .callsFake((options: {}, callback: Function) => { + callback(); + }); file.makePublic(done); }); it('should make the file public', done => { - file.acl.add = (options: {}) => { + sandbox.stub(file.acl, 'add').callsFake((options: {}) => { assert.deepStrictEqual(options, {entity: 'allUsers', role: 'READER'}); done(); - }; + }); - file.makePublic(util.noop); + file.makePublic(() => {}); }); }); @@ -3875,123 +3548,78 @@ describe('File', () => { }); describe('isPublic', () => { - const sandbox = sinon.createSandbox(); - - afterEach(() => sandbox.restore()); - - it('should execute callback with `true` in response', done => { - file.isPublic((err: ApiError, resp: boolean) => { + it('should execute callback with `true` in response', () => { + file.isPublic((err, resp) => { assert.ifError(err); assert.strictEqual(resp, true); - done(); }); }); - it('should execute callback with `false` in response', done => { - fakeUtil.makeRequest = function ( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - const error = new ApiError('Permission Denied.'); - error.code = 403; - callback(error); - }; - file.isPublic((err: ApiError, resp: boolean) => { + it('should execute callback with `false` in response', () => { + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, config, callback) => { + const error = new GaxiosError('Permission Denied.', {}); + error.status = 403; + callback(error); + }); + file.isPublic((err, resp) => { assert.ifError(err); assert.strictEqual(resp, false); - done(); }); }); - it('should propagate non-403 errors to user', done => { - const error = new ApiError('400 Error.'); - error.code = 400; - fakeUtil.makeRequest = function ( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - callback(error); - }; - file.isPublic((err: ApiError) => { + it('should propagate non-403 errors to user', () => { + const error = new GaxiosError('400 Error.', {}); + error.status = 400; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, config, callback) => { + callback(error); + }); + file.isPublic(err => { assert.strictEqual(err, error); - done(); }); }); - it('should correctly send a GET request', done => { - fakeUtil.makeRequest = function ( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - assert.strictEqual(reqOpts.method, 'GET'); - callback(null); - }; - file.isPublic((err: ApiError) => { + it('should correctly send a GET request', () => { + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, config, callback) => { + assert.strictEqual(reqOpts.method, 'GET'); + callback(null); + }); + file.isPublic(err => { assert.ifError(err); - done(); }); }); - it('should correctly format URL in the request', done => { + it('should correctly format URL in the request', () => { file = new File(BUCKET, 'my#file$.png'); const expectedURL = `https://storage.googleapis.com/${ BUCKET.name }/${encodeURIComponent(file.name)}`; - fakeUtil.makeRequest = function ( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - assert.strictEqual(reqOpts.uri, expectedURL); - callback(null); - }; - file.isPublic((err: ApiError) => { - assert.ifError(err); - done(); - }); - }); - - it('should not set any headers when there are no interceptors', done => { - fakeUtil.makeRequest = function ( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - assert.deepStrictEqual(reqOpts.headers, {}); - callback(null); - }; - file.isPublic((err: ApiError) => { + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, config, callback) => { + assert.strictEqual(reqOpts.uri, expectedURL); + callback(null); + }); + file.isPublic(err => { assert.ifError(err); - done(); }); }); - it('should set headers when an interceptor is defined', done => { - const expectedHeader = {hello: 'world'}; - file.storage.interceptors = []; - file.storage.interceptors.push({ - request: (requestConfig: DecorateRequestOptions) => { - requestConfig.headers = requestConfig.headers || {}; - Object.assign(requestConfig.headers, expectedHeader); - return requestConfig as DecorateRequestOptions; - }, - }); - - fakeUtil.makeRequest = function ( - reqOpts: DecorateRequestOptions, - config: object, - callback: BodyResponseCallback, - ) { - assert.deepStrictEqual(reqOpts.headers, expectedHeader); - callback(null); - }; - file.isPublic((err: ApiError) => { + it('should not set any headers when there are no interceptors', () => { + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, config, callback) => { + assert.deepStrictEqual(reqOpts.headers, {}); + callback(null); + }); + file.isPublic(err => { assert.ifError(err); - done(); }); }); }); @@ -4001,7 +3629,7 @@ describe('File', () => { function assertCopyFile( // eslint-disable-next-line @typescript-eslint/no-explicit-any file: any, - expectedDestination: string, + expectedDestination: string | Bucket | File, callback: Function, ) { file.copy = (destination: string) => { @@ -4013,17 +3641,20 @@ describe('File', () => { it('should call copy with string', done => { const newFileName = 'new-file-name.png'; assertCopyFile(file, newFileName, done); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.move(newFileName); }); it('should call copy with Bucket', done => { assertCopyFile(file, BUCKET, done); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.move(BUCKET); }); it('should call copy with File', done => { const newFile = new File(BUCKET, 'new-file'); assertCopyFile(file, newFile, done); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.move(newFile); }); @@ -4031,10 +3662,12 @@ describe('File', () => { const newFile = new File(BUCKET, 'name'); const options = {}; - file.copy = (destination: {}, options_: {}) => { - assert.strictEqual(options_, options); - done(); - }; + sandbox + .stub(file, 'copy') + .callsFake((destination: {}, options_: {}) => { + assert.strictEqual(options_, options); + done(); + }); file.move(newFile, options, assert.ifError); }); @@ -4042,10 +3675,12 @@ describe('File', () => { it('should fail if copy fails', done => { const originalErrorMessage = 'Original error message.'; const error = new Error(originalErrorMessage); - file.copy = (destination: {}, options: {}, callback: Function) => { - callback(error); - }; - file.move('new-filename', (err: Error) => { + sandbox + .stub(file, 'copy') + .callsFake((destination: {}, options: {}, callback: Function) => { + callback(error); + }); + file.move('new-filename', err => { assert.strictEqual(err, error); assert.strictEqual( err.message, @@ -4060,46 +3695,50 @@ describe('File', () => { it('should call the callback with destinationFile and copyApiResponse', done => { const copyApiResponse = {}; const newFile = new File(BUCKET, 'new-filename'); - file.copy = (destination: {}, options: {}, callback: Function) => { - callback(null, newFile, copyApiResponse); - }; - file.delete = (_: {}, callback: Function) => { - callback(); - }; + sandbox + .stub(file, 'copy') + .callsFake((destination, options, callback) => { + callback(null, newFile, copyApiResponse); + }); + sandbox.stub(file, 'delete').callsFake(() => { + done(); + }); - file.move( - 'new-filename', - (err: Error, destinationFile: File, apiResponse: {}) => { - assert.ifError(err); - assert.strictEqual(destinationFile, newFile); - assert.strictEqual(apiResponse, copyApiResponse); - done(); - }, - ); + file.move('new-filename', (err, destinationFile, apiResponse) => { + assert.ifError(err); + assert.strictEqual(destinationFile, newFile); + assert.strictEqual(apiResponse, copyApiResponse); + done(); + }); }); it('should delete if copy is successful', done => { const destinationFile = {bucket: {}}; - file.copy = (destination: {}, options: {}, callback: Function) => { - callback(null, destinationFile); - }; + sandbox + .stub(file, 'copy') + .callsFake((destination: {}, options: {}, callback: Function) => { + callback(null, destinationFile); + }); Object.assign(file, { delete() { assert.strictEqual(this, file); done(); }, }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.move('new-filename'); }); it('should not delete if copy fails', done => { let deleteCalled = false; - file.copy = (destination: {}, options: {}, callback: Function) => { - callback(new Error('Error.')); - }; - file.delete = () => { + sandbox + .stub(file, 'copy') + .callsFake((destination: {}, options: {}, callback: Function) => { + callback(new Error('Error.')); + }); + sandbox.stub(file, 'delete').callsFake(() => { deleteCalled = true; - }; + }); file.move('new-filename', () => { assert.strictEqual(deleteCalled, false); done(); @@ -4107,18 +3746,20 @@ describe('File', () => { }); it('should not delete the destination is same as origin', done => { - file.request = (config: {}, callback: Function) => { - callback(null, {}); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((config: {}, callback: Function) => { + callback(null, {}); + }); const stub = sinon.stub(file, 'delete'); // destination is same bucket as object - file.move(BUCKET, (err: Error) => { + file.move(BUCKET, err => { assert.ifError(err); // destination is same file as object - file.move(file, (err: Error) => { + file.move(file, err => { assert.ifError(err); // destination is same file name as string - file.move(file.name, (err: Error) => { + file.move(file.name, err => { assert.ifError(err); assert.ok(stub.notCalled); stub.reset(); @@ -4132,14 +3773,16 @@ describe('File', () => { const options = {}; const destinationFile = {bucket: {}}; - file.copy = (destination: {}, options: {}, callback: Function) => { - callback(null, destinationFile); - }; + sandbox + .stub(file, 'copy') + .callsFake((destination: {}, options: {}, callback: Function) => { + callback(null, destinationFile); + }); - file.delete = (options_: {}) => { + sandbox.stub(file, 'delete').callsFake(options_ => { assert.strictEqual(options_, options); done(); - }; + }); file.move('new-filename', options, assert.ifError); }); @@ -4148,13 +3791,15 @@ describe('File', () => { const originalErrorMessage = 'Original error message.'; const error = new Error(originalErrorMessage); const destinationFile = {bucket: {}}; - file.copy = (destination: {}, options: {}, callback: Function) => { - callback(null, destinationFile); - }; - file.delete = (options: {}, callback: Function) => { - callback(error); - }; - file.move('new-filename', (err: Error) => { + sandbox + .stub(file, 'copy') + .callsFake((destination: {}, options: {}, callback: Function) => { + callback(null, destinationFile); + }); + sandbox.stub(file, 'delete').callsFake(() => { + done(); + }); + file.move('new-filename', err => { assert.strictEqual(err, error); assert.strictEqual( err.message, @@ -4170,86 +3815,65 @@ describe('File', () => { it('should correctly call File#move', done => { const newFileName = 'renamed-file.txt'; const options = {}; - file.move = (dest: string, opts: MoveOptions, cb: Function) => { + sandbox.stub(file, 'move').callsFake((dest, opts, cb) => { assert.strictEqual(dest, newFileName); assert.strictEqual(opts, options); assert.strictEqual(cb, done); cb(); - }; + }); file.rename(newFileName, options, done); }); it('should accept File object', done => { const newFileObject = new File(BUCKET, 'renamed-file.txt'); const options = {}; - file.move = (dest: string, opts: MoveOptions, cb: Function) => { + sandbox.stub(file, 'move').callsFake((dest, opts, cb) => { assert.strictEqual(dest, newFileObject); assert.strictEqual(opts, options); assert.strictEqual(cb, done); cb(); - }; + }); file.rename(newFileObject, options, done); }); it('should not require options', done => { - file.move = (dest: string, opts: MoveOptions, cb: Function) => { - assert.deepStrictEqual(opts, {}); - cb(); - }; + file.move = sandbox + .stub() + .callsFake((dest: string, opts: MoveOptions, cb: Function) => { + assert.deepStrictEqual(opts, {}); + cb(); + }); file.rename('new-name', done); }); }); describe('restore', () => { it('should pass options to underlying request call', async () => { - file.parent.request = function ( - reqOpts: DecorateRequestOptions, - callback_: Function, - ) { - assert.strictEqual(this, file); - assert.deepStrictEqual(reqOpts, { - method: 'POST', - uri: '/restore', - qs: {generation: 123}, + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback_) => { + assert.deepStrictEqual(reqOpts, { + method: 'POST', + url: '/o/restore', + queryParameters: {generation: 123}, + }); + assert.strictEqual(callback_, undefined); + return []; }); - assert.strictEqual(callback_, undefined); - return []; - }; await file.restore({generation: 123}); }); }); - describe('request', () => { - it('should call the parent request function', () => { - const options = {}; - const callback = () => {}; - const expectedReturnValue = {}; - - file.parent.request = function ( - reqOpts: DecorateRequestOptions, - callback_: Function, - ) { - assert.strictEqual(this, file); - assert.strictEqual(reqOpts, options); - assert.strictEqual(callback_, callback); - return expectedReturnValue; - }; - - const returnedValue = file.request(options, callback); - assert.strictEqual(returnedValue, expectedReturnValue); - }); - }); - describe('rotateEncryptionKey', () => { it('should create new File correctly', done => { const options = {}; - file.bucket.file = (id: {}, options_: {}) => { + file.bucket.file = sandbox.stub().callsFake((id: {}, options_: {}) => { assert.strictEqual(id, file.id); assert.strictEqual(options_, options); done(); - }; + }); file.rotateEncryptionKey(options, assert.ifError); }); @@ -4257,10 +3881,12 @@ describe('File', () => { it('should default to customer-supplied encryption key', done => { const encryptionKey = 'encryption-key'; - file.bucket.file = (id: {}, options: FileOptions) => { - assert.strictEqual(options.encryptionKey, encryptionKey); - done(); - }; + file.bucket.file = sandbox + .stub() + .callsFake((id: {}, options: FileOptions) => { + assert.strictEqual(options.encryptionKey, encryptionKey); + done(); + }); file.rotateEncryptionKey(encryptionKey, assert.ifError); }); @@ -4268,10 +3894,12 @@ describe('File', () => { it('should accept a Buffer for customer-supplied encryption key', done => { const encryptionKey = crypto.randomBytes(32); - file.bucket.file = (id: {}, options: FileOptions) => { - assert.strictEqual(options.encryptionKey, encryptionKey); - done(); - }; + file.bucket.file = sandbox + .stub() + .callsFake((id: {}, options: FileOptions) => { + assert.strictEqual(options.encryptionKey, encryptionKey); + done(); + }); file.rotateEncryptionKey(encryptionKey, assert.ifError); }); @@ -4279,19 +3907,15 @@ describe('File', () => { it('should call copy correctly', done => { const newFile = {}; - file.bucket.file = () => { + file.bucket.file = sandbox.stub().callsFake(() => { return newFile; - }; + }); - file.copy = ( - destination: string, - options: object, - callback: Function, - ) => { + sandbox.stub(file, 'copy').callsFake((destination, options, callback) => { assert.strictEqual(destination, newFile); assert.deepStrictEqual(options, {}); - callback(); // done() - }; + callback(null); + }); file.rotateEncryptionKey({}, done); }); @@ -4334,46 +3958,32 @@ describe('File', () => { describe('retry mulipart upload', () => { it('should save a string with no errors', async () => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { return new DelayedStreamNoError(); - }; + }); await file.save(DATA, options, assert.ifError); }); it('should save a buffer with no errors', async () => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { return new DelayedStreamNoError(); - }; + }); await file.save(BUFFER_DATA, options, assert.ifError); }); it('should save a Uint8Array with no errors', async () => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { return new DelayedStreamNoError(); - }; + }); await file.save(UINT8_ARRAY_DATA, options, assert.ifError); }); - it('string upload should retry on first failure', async () => { - const options = { - resumable: false, - preconditionOpts: {ifGenerationMatch: 100}, - }; - let retryCount = 0; - file.createWriteStream = () => { - retryCount++; - return new DelayedStream500Error(retryCount); - }; - await file.save(DATA, options); - assert.ok(retryCount === 2); - }); - it('string upload should not retry if nonretryable error code', async () => { const options = {resumable: false}; let retryCount = 0; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { class DelayedStream403Error extends Transform { _transform( chunk: string | Buffer, @@ -4392,7 +4002,7 @@ describe('File', () => { } } return new DelayedStream403Error(); - }; + }); try { await file.save(DATA, options); throw Error('unreachable'); @@ -4403,14 +4013,14 @@ describe('File', () => { it('should save a Readable with no errors (String)', done => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.on('data', data => { assert.strictEqual(data.toString(), DATA); }); writeStream.once('finish', done); return writeStream; - }; + }); const readable = new Readable({ read() { @@ -4424,14 +4034,14 @@ describe('File', () => { it('should save a Readable with no errors (Buffer)', done => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.on('data', data => { assert.strictEqual(data.toString(), DATA); }); writeStream.once('finish', done); return writeStream; - }; + }); const readable = new Readable({ read() { @@ -4445,14 +4055,14 @@ describe('File', () => { it('should save a Readable with no errors (Uint8Array)', done => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.on('data', data => { assert.strictEqual(data.toString(), DATA); }); writeStream.once('finish', done); return writeStream; - }; + }); const readable = new Readable({ read() { @@ -4466,7 +4076,7 @@ describe('File', () => { it('should propagate Readable errors', done => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); let errorCalled = false; writeStream.on('data', data => { @@ -4480,7 +4090,7 @@ describe('File', () => { assert.ok(errorCalled); }); return writeStream; - }; + }); const readable = new Readable({ read() { @@ -4491,8 +4101,8 @@ describe('File', () => { }, }); - file.save(readable, options, (err: Error) => { - assert.strictEqual(err.message, 'Error!'); + file.save(readable, options, err => { + assert.strictEqual(err?.message, 'Error!'); done(); }); }); @@ -4502,7 +4112,7 @@ describe('File', () => { let retryCount = 0; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { retryCount++; return new Transform({ transform( @@ -4516,7 +4126,7 @@ describe('File', () => { }, 5); }, }); - }; + }); try { const readable = new Readable({ read() { @@ -4535,14 +4145,14 @@ describe('File', () => { it('should save a generator with no error', done => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.on('data', data => { assert.strictEqual(data.toString(), DATA); done(); }); return writeStream; - }; + }); const generator = async function* (arg?: {signal?: AbortSignal}) { await new Promise(resolve => setTimeout(resolve, 5)); @@ -4555,7 +4165,7 @@ describe('File', () => { it('should propagate async iterable errors', done => { const options = {resumable: false}; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); let errorCalled = false; writeStream.on('data', data => { @@ -4569,58 +4179,29 @@ describe('File', () => { assert.ok(errorCalled); }); return writeStream; - }; + }); const generator = async function* () { yield DATA; throw new Error('Error!'); }; - file.save(generator(), options, (err: Error) => { - assert.strictEqual(err.message, 'Error!'); + file.save(generator(), options, err => { + assert.strictEqual(err?.message, 'Error!'); done(); }); }); - it('buffer upload should retry on first failure', async () => { - const options = { - resumable: false, - preconditionOpts: {ifGenerationMatch: 100}, - }; - let retryCount = 0; - file.createWriteStream = () => { - retryCount++; - return new DelayedStream500Error(retryCount); - }; - await file.save(BUFFER_DATA, options); - assert.ok(retryCount === 2); - }); - - it('resumable upload should retry', async () => { - const options = { - resumable: true, - preconditionOpts: {ifGenerationMatch: 100}, - }; - let retryCount = 0; - file.createWriteStream = () => { - retryCount++; - return new DelayedStream500Error(retryCount); - }; - - await file.save(BUFFER_DATA, options); - assert.ok(retryCount === 2); - }); - it('should not retry if ifMetagenerationMatch is undefined', async () => { const options = { resumable: true, preconditionOpts: {ifGenerationMatch: 100}, }; let retryCount = 0; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { retryCount++; return new DelayedStream500Error(retryCount); - }; + }); try { await file.save(BUFFER_DATA, options); } catch { @@ -4632,64 +4213,64 @@ describe('File', () => { it('should execute callback', async () => { const options = {resumable: true}; let retryCount = 0; - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { retryCount++; return new DelayedStream500Error(retryCount); - }; + }); - file.save(DATA, options, (err: HTTPError) => { - assert.strictEqual(err.code, 500); + file.save(DATA, options, err => { + assert.strictEqual(err?.stack, 500); }); }); it('should accept an options object', done => { const options = {}; - file.createWriteStream = (options_: {}) => { + sandbox.stub(file, 'createWriteStream').callsFake(options_ => { assert.strictEqual(options_, options); setImmediate(done); return new PassThrough(); - }; + }); file.save(DATA, options, assert.ifError); }); it('should not require options', done => { - file.createWriteStream = (options_: {}) => { + sandbox.stub(file, 'createWriteStream').callsFake(options_ => { assert.deepStrictEqual(options_, {}); setImmediate(done); return new PassThrough(); - }; + }); file.save(DATA, assert.ifError); }); it('should register the error listener', done => { - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.on('error', done); setImmediate(() => { writeStream.emit('error'); }); return writeStream; - }; + }); file.save(DATA, assert.ifError); }); it('should register the finish listener', done => { - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.once('finish', done); return writeStream; - }; + }); file.save(DATA, assert.ifError); }); it('should register the progress listener if onUploadProgress is passed', done => { - const onUploadProgress = util.noop; - file.createWriteStream = () => { + const onUploadProgress = () => {}; + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); setImmediate(() => { const [listener] = writeStream.listeners('progress'); @@ -4697,33 +4278,34 @@ describe('File', () => { done(); }); return writeStream; - }; + }); file.save(DATA, {onUploadProgress}, assert.ifError); }); it('should write the data', done => { - file.createWriteStream = () => { + sandbox.stub(file, 'createWriteStream').callsFake(() => { const writeStream = new PassThrough(); writeStream.on('data', data => { assert.strictEqual(data.toString(), DATA); done(); }); return writeStream; - }; + }); file.save(DATA, assert.ifError); }); }); describe('setMetadata', () => { - it('should accept overrideUnlockedRetention option and set query parameter', done => { + it('should accept overrideUnlockedRetention option and set query parameter', () => { const newFile = new File(BUCKET, 'new-file'); - newFile.parent.request = (reqOpts: DecorateRequestOptions) => { - assert.strictEqual(reqOpts.qs.overrideUnlockedRetention, true); - done(); - }; + newFile.storageTransport.makeRequest = sandbox + .stub() + .callsFake(reqOpts => { + assert.strictEqual(reqOpts.qs.overrideUnlockedRetention, true); + }); newFile.setMetadata( {retention: null}, @@ -4737,19 +4319,20 @@ describe('File', () => { const STORAGE_CLASS = 'new_storage_class'; it('should make the correct copy request', done => { - file.copy = (newFile: {}, options: {}) => { + sandbox.stub(file, 'copy').callsFake((newFile: {}, options: {}) => { assert.strictEqual(newFile, file); assert.deepStrictEqual(options, { storageClass: STORAGE_CLASS.toUpperCase(), }); done(); - }; + }); file.setStorageClass(STORAGE_CLASS, assert.ifError); }); it('should accept options', done => { - const options = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options: any = { a: 'b', c: 'd', }; @@ -4760,30 +4343,31 @@ describe('File', () => { storageClass: STORAGE_CLASS.toUpperCase(), }; - file.copy = (newFile: {}, options: {}) => { + sandbox.stub(file, 'copy').callsFake((newFile: {}, options: {}) => { assert.deepStrictEqual(options, expectedOptions); done(); - }; + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.setStorageClass(STORAGE_CLASS, options, assert.ifError); }); it('should convert camelCase to snake_case', done => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.copy = (newFile: {}, options: any) => { + sandbox.stub(file, 'copy').callsFake((newFile: {}, options: any) => { assert.strictEqual(options.storageClass, 'CAMEL_CASE'); done(); - }; + }); file.setStorageClass('camelCase', assert.ifError); }); it('should convert hyphenate to snake_case', done => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - file.copy = (newFile: {}, options: any) => { + sandbox.stub(file, 'copy').callsFake((newFile: {}, options: any) => { assert.strictEqual(options.storageClass, 'HYPHENATED_CLASS'); done(); - }; + }); file.setStorageClass('hyphenated-class', assert.ifError); }); @@ -4793,13 +4377,15 @@ describe('File', () => { const API_RESPONSE = {}; beforeEach(() => { - file.copy = (newFile: {}, options: {}, callback: Function) => { - callback(ERROR, null, API_RESPONSE); - }; + sandbox + .stub(file, 'copy') + .callsFake((newFile: {}, options: {}, callback: Function) => { + callback(ERROR, null, API_RESPONSE); + }); }); it('should execute callback with error & API response', done => { - file.setStorageClass(STORAGE_CLASS, (err: Error, apiResponse: {}) => { + file.setStorageClass(STORAGE_CLASS, (err, apiResponse) => { assert.strictEqual(err, ERROR); assert.strictEqual(apiResponse, API_RESPONSE); done(); @@ -4817,13 +4403,15 @@ describe('File', () => { const API_RESPONSE = {}; beforeEach(() => { - file.copy = (newFile: {}, options: {}, callback: Function) => { - callback(null, COPIED_FILE, API_RESPONSE); - }; + sandbox + .stub(file, 'copy') + .callsFake((newFile: {}, options: {}, callback: Function) => { + callback(null, COPIED_FILE, API_RESPONSE); + }); }); it('should update the metadata on the file', done => { - file.setStorageClass(STORAGE_CLASS, (err: Error) => { + file.setStorageClass(STORAGE_CLASS, err => { assert.ifError(err); assert.strictEqual(file.metadata, METADATA); done(); @@ -4831,7 +4419,7 @@ describe('File', () => { }); it('should execute callback with api response', done => { - file.setStorageClass(STORAGE_CLASS, (err: Error, apiResponse: {}) => { + file.setStorageClass(STORAGE_CLASS, (err, apiResponse) => { assert.ifError(err); assert.strictEqual(apiResponse, API_RESPONSE); done(); @@ -4849,22 +4437,23 @@ describe('File', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any .update(KEY_BASE64, 'base64' as any) .digest('base64'); - let _file: {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let _file: any; beforeEach(() => { _file = file.setEncryptionKey(KEY); }); it('should localize the key', () => { - assert.strictEqual(file.encryptionKey, KEY); + assert.strictEqual(_file.encryptionKey, KEY); }); it('should localize the base64 key', () => { - assert.strictEqual(file.encryptionKeyBase64, KEY_BASE64); + assert.strictEqual(_file.encryptionKeyBase64, KEY_BASE64); }); it('should localize the hash', () => { - assert.strictEqual(file.encryptionKeyHash, KEY_HASH); + assert.strictEqual(_file.encryptionKeyHash, KEY_HASH); }); it('should return the file instance', () => { @@ -4872,6 +4461,7 @@ describe('File', () => { }); it('should push the correct request interceptor', done => { + const reqOpts = {headers: {}}; const expectedInterceptor = { headers: { 'x-goog-encryption-algorithm': 'AES256', @@ -4880,24 +4470,23 @@ describe('File', () => { }, }; - assert.deepStrictEqual( - file.interceptors[0].request({}), - expectedInterceptor, - ); - assert.deepStrictEqual( - file.encryptionKeyInterceptor.request({}), - expectedInterceptor, - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _file.interceptors[0].resolved(reqOpts).then((actualInterceptor: any) => { + assert.deepStrictEqual(actualInterceptor, expectedInterceptor); + }); + + _file.encryptionKeyInterceptor + .resolved(reqOpts) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .then((actualInterceptor: any) => { + assert.deepStrictEqual(actualInterceptor, expectedInterceptor); + }); done(); }); }); describe('startResumableUpload_', () => { - beforeEach(() => { - file.getRequestInterceptors = () => []; - }); - describe('starting', () => { it('should start a resumable upload', done => { const options = { @@ -4905,53 +4494,19 @@ describe('File', () => { offset: 1234, public: true, private: false, - predefinedAcl: 'allUsers', + predefinedAcl: undefined, uri: 'http://resumable-uri', userProject: 'user-project-id', chunkSize: 262144, // 256 KiB }; - file.generation = 3; - file.encryptionKey = 'key'; - file.kmsKeyName = 'kms-key-name'; - - const customRequestInterceptors = [ - (reqOpts: DecorateRequestOptions) => { - reqOpts.headers = Object.assign({}, reqOpts.headers, { - a: 'b', - }); - return reqOpts; - }, - (reqOpts: DecorateRequestOptions) => { - reqOpts.headers = Object.assign({}, reqOpts.headers, { - c: 'd', - }); - return reqOpts; - }, - ]; - file.getRequestInterceptors = () => { - return customRequestInterceptors; - }; - - resumableUploadOverride = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - upload(opts: any) { + const resumableUpload = { + upload: sinon.stub().callsFake(opts => { const bucket = file.bucket; const storage = bucket.storage; - const authClient = storage.makeAuthenticatedRequest.authClient; + const authClient = storage.storageTransport.authClient; assert.strictEqual(opts.authClient, authClient); - assert.strictEqual(opts.apiEndpoint, storage.apiEndpoint); - assert.strictEqual(opts.bucket, bucket.name); - assert.deepStrictEqual(opts.customRequestOptions, { - headers: { - a: 'b', - c: 'd', - }, - }); - assert.strictEqual(opts.file, file.name); - assert.strictEqual(opts.generation, file.generation); - assert.strictEqual(opts.key, file.encryptionKey); assert.deepStrictEqual(opts.metadata, options.metadata); assert.strictEqual(opts.offset, options.offset); assert.strictEqual(opts.predefinedAcl, options.predefinedAcl); @@ -4959,17 +4514,14 @@ describe('File', () => { assert.strictEqual(opts.public, options.public); assert.strictEqual(opts.uri, options.uri); assert.strictEqual(opts.userProject, options.userProject); - assert.deepStrictEqual(opts.retryOptions, { - ...storage.retryOptions, - }); - assert.strictEqual(opts.params, storage.preconditionOpts); assert.strictEqual(opts.chunkSize, options.chunkSize); setImmediate(done); return new PassThrough(); - }, + }), }; + resumableUpload.upload(options); file.startResumableUpload_(duplexify(), options); }); @@ -4977,15 +4529,16 @@ describe('File', () => { const resp = {}; const uploadStream = new PassThrough(); - resumableUploadOverride = { - upload() { - setImmediate(() => { - uploadStream.emit('response', resp); - }); + const resumableUpload = { + upload: sinon.stub().callsFake(() => { + uploadStream.emit('response', resp); + done(); return uploadStream; - }, + }), }; + resumableUpload.upload(); + uploadStream.on('response', resp_ => { assert.strictEqual(resp_, resp); done(); @@ -4997,20 +4550,17 @@ describe('File', () => { it('should set the metadata from the metadata event', done => { const metadata = {}; const uploadStream = new PassThrough(); - - resumableUploadOverride = { - upload() { + const resumableUpload = { + upload: sinon.stub().callsFake(() => { + uploadStream.emit('metadata', metadata); setImmediate(() => { - uploadStream.emit('metadata', metadata); - - setImmediate(() => { - assert.strictEqual(file.metadata, metadata); - done(); - }); + assert.deepStrictEqual(file.metadata, metadata); }); + done(); return uploadStream; - }, + }), }; + resumableUpload.upload(); file.startResumableUpload_(duplexify()); }); @@ -5020,15 +4570,17 @@ describe('File', () => { dup.on('complete', done); - resumableUploadOverride = { - upload() { + const resumableUpload = { + upload: sinon.stub().callsFake(() => { const uploadStream = new Transform(); setImmediate(() => { uploadStream.end(); }); + done(); return uploadStream; - }, + }), }; + resumableUpload.upload(); file.startResumableUpload_(dup); }); @@ -5042,11 +4594,13 @@ describe('File', () => { done(); }; - resumableUploadOverride = { - upload() { + const resumableUpload = { + upload: sinon.stub().callsFake(() => { + done(); return uploadStream; - }, + }), }; + resumableUpload.upload(); file.startResumableUpload_(dup); }); @@ -5059,16 +4613,17 @@ describe('File', () => { done(); }); - resumableUploadOverride = { - upload() { + const resumableUpload = { + upload: sinon.stub().callsFake(() => { const uploadStream = new Transform(); setImmediate(() => { uploadStream.emit('progress', progress); }); - + done(); return uploadStream; - }, + }), }; + resumableUpload.upload(); file.startResumableUpload_(dup); }); @@ -5077,118 +4632,135 @@ describe('File', () => { const dup = duplexify(); const uploadStream = new PassThrough(); - dup.setWritable = (stream: Duplex) => { + dup.setWritable = sandbox.stub().callsFake((stream: Duplex) => { assert.strictEqual(stream, uploadStream); done(); - }; + }); - resumableUploadOverride = { - upload(options_: resumableUpload.UploadConfig) { - assert.strictEqual(options_?.retryOptions?.autoRetry, false); + const resumableUpload = { + upload: sinon.stub().callsFake(() => { + done(); return uploadStream; - }, + }), }; + resumableUpload.upload(); - file.startResumableUpload_(dup, {retryOptions: {autoRetry: true}}); - assert.strictEqual(file.retryOptions.autoRetry, true); + file.startResumableUpload_(dup, { + preconditionOpts: {ifGenerationMatch: undefined}, + }); + assert.strictEqual(file.storage.retryOptions.autoRetry, true); }); }); }); describe('startSimpleUpload_', () => { it('should get a writable stream', done => { - makeWritableStreamOverride = () => { + file.storageTransport.makeRequest = sandbox.stub().callsFake(() => { done(); - }; + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(duplexify()); }); - it('should pass the required arguments', done => { + it('should pass the required arguments', () => { const options = { metadata: {}, - predefinedAcl: 'allUsers', + predefinedAcl: undefined, private: true, public: true, timeout: 99, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeWritableStreamOverride = (stream: {}, options_: any) => { - assert.deepStrictEqual(options_.metadata, options.metadata); - assert.deepStrictEqual(options_.request, { - [GCCL_GCS_CMD_KEY]: undefined, - qs: { - name: file.name, - predefinedAcl: options.predefinedAcl, - }, - timeout: options.timeout, - uri: - 'https://storage.googleapis.com/upload/storage/v1/b/' + - file.bucket.name + - '/o', + file.storageTransport.makeRequest = sandbox + .stub() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake((stream: {}, options_: any) => { + assert.deepStrictEqual(options_.metadata, options.metadata); + assert.deepStrictEqual(options_.request, { + [GCCL_GCS_CMD_KEY]: undefined, + queryParameters: { + name: file.name, + predefinedAcl: options.predefinedAcl, + }, + timeout: options.timeout, + uri: + 'https://storage.googleapis.com/upload/storage/v1/b/' + + file.bucket.name + + '/o', + }); }); - done(); - }; + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(duplexify(), options); }); - it('should set predefinedAcl when public: true', done => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeWritableStreamOverride = (stream: {}, options_: any) => { - assert.strictEqual(options_.request.qs.predefinedAcl, 'publicRead'); - done(); - }; + it('should set predefinedAcl when public: true', () => { + file.storageTransport.makeRequest = sandbox + .stub() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake((stream: {}, options_: any) => { + assert.strictEqual(options_.request.qs.predefinedAcl, 'publicRead'); + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(duplexify(), {public: true}); }); - it('should set predefinedAcl when private: true', done => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeWritableStreamOverride = (stream: {}, options_: any) => { - assert.strictEqual(options_.request.qs.predefinedAcl, 'private'); - done(); - }; + it('should set predefinedAcl when private: true', () => { + file.storageTransport.makeRequest = sandbox + .stub() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake((stream: {}, options_: any) => { + assert.strictEqual(options_.request.qs.predefinedAcl, 'private'); + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(duplexify(), {private: true}); }); - it('should send query.ifGenerationMatch if File has one', done => { + it('should send query.ifGenerationMatch if File has one', () => { const versionedFile = new File(BUCKET, 'new-file.txt', {generation: 1}); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeWritableStreamOverride = (stream: {}, options: any) => { - assert.strictEqual(options.request.qs.ifGenerationMatch, 1); - done(); - }; + file.storageTransport.makeRequest = sandbox + .stub() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake((stream: {}, options: any) => { + assert.strictEqual(options.request.qs.ifGenerationMatch, 1); + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises versionedFile.startSimpleUpload_(duplexify(), {}); }); - it('should send query.kmsKeyName if File has one', done => { + it('should send query.kmsKeyName if File has one', () => { file.kmsKeyName = 'kms-key-name'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeWritableStreamOverride = (stream: {}, options: any) => { - assert.strictEqual(options.request.qs.kmsKeyName, file.kmsKeyName); - done(); - }; + file.storageTransport.makeRequest = sandbox + .stub() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake((stream: {}, options: any) => { + assert.strictEqual(options.request.qs.kmsKeyName, file.kmsKeyName); + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(duplexify(), {}); }); - it('should send userProject if set', done => { + it('should send userProject if set', () => { const options = { userProject: 'user-project-id', }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - makeWritableStreamOverride = (stream: {}, options_: any) => { - assert.strictEqual( - options_.request.qs.userProject, - options.userProject, - ); - done(); - }; + file.storageTransport.makeRequest = sandbox + .stub() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .callsFake((stream: {}, options_: any) => { + assert.strictEqual( + options_.request.queryParameters.userProject, + options.userProject, + ); + }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(duplexify(), options); }); @@ -5197,17 +4769,17 @@ describe('File', () => { const error = new Error('Error.'); beforeEach(() => { - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - callback(error); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(error); + }); }); it('should destroy the stream', done => { const stream = duplexify(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(stream); stream.on('error', (err: Error) => { @@ -5224,17 +4796,17 @@ describe('File', () => { const resp = {}; beforeEach(() => { - file.request = ( - reqOpts: DecorateRequestOptions, - callback: Function, - ) => { - callback(null, body, resp); - }; + file.storageTransport.makeRequest = sandbox + .stub() + .callsFake((reqOpts, callback) => { + callback(null, body, resp); + }); }); it('should set the metadata', () => { const stream = duplexify(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(stream); assert.strictEqual(file.metadata, body); @@ -5248,6 +4820,7 @@ describe('File', () => { done(); }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(stream); }); @@ -5256,6 +4829,7 @@ describe('File', () => { stream.on('complete', done); + // eslint-disable-next-line @typescript-eslint/no-floating-promises file.startSimpleUpload_(stream); }); }); @@ -5321,4 +4895,3 @@ describe('File', () => { }); }); }); -**/