mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
138 lines
4.5 KiB
JavaScript
138 lines
4.5 KiB
JavaScript
'use strict';
|
|
|
|
const fse = require('fs-extra');
|
|
const sinon = require('sinon');
|
|
const chai = require('chai');
|
|
const provisionTempDir = require('@serverless/test/provision-tmp-dir');
|
|
const { join } = require('path');
|
|
const { expect } = require('chai');
|
|
const fsp = require('fs').promises;
|
|
const fs = require('fs');
|
|
const sinonChai = require('sinon-chai');
|
|
const safeMoveFile = require('../../../../../lib/utils/fs/safeMoveFile');
|
|
|
|
chai.use(sinonChai);
|
|
|
|
/**
|
|
* Returns and Error object that resembles the EXDEV errors that are thrown
|
|
* when you try to rename across device boundaries.
|
|
*
|
|
* @returns an Error object that resembles the EXDEV errors are thrown
|
|
*/
|
|
const createFakeExDevError = () => {
|
|
// Properties captured by using fs.renameSync in the Node v12.20.1 REPL
|
|
const fakeCrossDeviceError = new Error(
|
|
"Error: EXDEV: cross-device link not permitted, rename '/foo/bar' -> '/bar/baz'"
|
|
);
|
|
// This is the important property that safeMoveFile looks for
|
|
// to fallback to copy then rename behaviour
|
|
fakeCrossDeviceError.code = 'EXDEV';
|
|
|
|
// These other properties aren't used but it doesn't hurt to have an accurate error object
|
|
fakeCrossDeviceError.errno = -18;
|
|
fakeCrossDeviceError.syscall = 'rename';
|
|
fakeCrossDeviceError.path = '/foo/bar';
|
|
fakeCrossDeviceError.dest = '/bar/baz';
|
|
return fakeCrossDeviceError;
|
|
};
|
|
|
|
/**
|
|
* The name of the file that will be moved
|
|
*/
|
|
const artifactName = 'foo-bar.zip';
|
|
|
|
describe('test/unit/lib/utils/fs/safeMoveFile.test.js', () => {
|
|
let sourceDir;
|
|
let destinationDir;
|
|
let sourceFile;
|
|
let destinationFile;
|
|
|
|
let renameStub;
|
|
|
|
beforeEach(async () => {
|
|
sourceDir = await provisionTempDir();
|
|
destinationDir = await provisionTempDir();
|
|
sourceFile = join(sourceDir, artifactName);
|
|
destinationFile = join(destinationDir, artifactName);
|
|
|
|
renameStub = sinon.stub(fsp, 'rename');
|
|
// Allow the fs calls to act as normal until we want to force the rename to fail
|
|
renameStub.callThrough();
|
|
|
|
// Write a test file to rename
|
|
await fse.writeFile(sourceFile, 'source data');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up test directories after each test so they don't interfere with each other
|
|
await fse.remove(destinationDir);
|
|
await fse.remove(sourceDir);
|
|
// Reset stubbed methods
|
|
fsp.rename.restore();
|
|
});
|
|
|
|
/**
|
|
* Run the test suite with the provided post assertion function.
|
|
* The post assertion function will be called after each test scenario is executed
|
|
* so that the test scenarios can be reused.
|
|
*
|
|
* @param {*} postAssertion the function that is called after every test scenario
|
|
*/
|
|
const runTestScenariosWithPostAssertion = (postAssertion) => {
|
|
describe('when file at target path does not exist', () => {
|
|
it('should move file to target destination', async () => {
|
|
await safeMoveFile(sourceFile, destinationFile);
|
|
|
|
const sourceExists = fs.existsSync(sourceFile);
|
|
const destinationExists = fs.existsSync(destinationFile);
|
|
|
|
expect(sourceExists).to.be.false;
|
|
expect(destinationExists).to.be.true;
|
|
|
|
postAssertion();
|
|
});
|
|
});
|
|
|
|
describe('when file at target path already exists', () => {
|
|
beforeEach(async () => {
|
|
// Create an existing file that is at the destination
|
|
fse.ensureFileSync(destinationFile);
|
|
fse.writeFileSync(destinationFile, 'existing destination data');
|
|
});
|
|
|
|
it('should overwrite the file at the target destination', async () => {
|
|
await safeMoveFile(sourceFile, destinationFile);
|
|
|
|
// Check that the file was actually overwritten
|
|
const cachedData = fse.readFileSync(destinationFile).toString();
|
|
expect(cachedData).not.to.eq('existing destination data');
|
|
|
|
const sourceExists = await fs.existsSync(sourceFile);
|
|
expect(sourceExists).to.be.false;
|
|
|
|
postAssertion();
|
|
});
|
|
});
|
|
};
|
|
|
|
describe('when file is moved in context of same device', () => {
|
|
runTestScenariosWithPostAssertion(() => {
|
|
// Happy path: Only rename is called
|
|
expect(renameStub).to.have.be.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('when file is moved to different device', () => {
|
|
beforeEach(async () => {
|
|
const error = createFakeExDevError();
|
|
renameStub.onFirstCall().rejects(error);
|
|
});
|
|
|
|
runTestScenariosWithPostAssertion(() => {
|
|
// Rename is called twice because the first call will fail due to the cross device rename
|
|
// The second call is across the same device
|
|
expect(renameStub).to.have.been.calledTwice;
|
|
});
|
|
});
|
|
});
|