Jarede
Jarede

Reputation: 3498

Trying to stub a function results in Descriptor for property is non-configurable and non-writable

I'm trying to write out a unit test that stubs the getSignedUrl function from the @aws-sdk/s3-request-presigner package, however when I try stub out the function with sinon, I receive the error:

  • TypeError: Descriptor for property getSignedUrl is non-configurable and non-writable
const s3RequestSigner = require("@aws-sdk/s3-request-presigner");
const expect = require('chai').expect;
const sinon = require('sinon')
....
it('should throw an error when getSignedUrl rejects', async function() {
  const sandbox = sinon.createSandbox();
  sandbox.stub(s3RequestSigner, "getSignedUrl").rejects("fakeUrl");
  sandbox.restore();
})

I'm using node.js 16 and writing javascript rather than typescript. Is there a way to mock out my function, i'm struggling to write my tests otherwise?

Upvotes: 9

Views: 11221

Answers (4)

Vinh Le
Vinh Le

Reputation: 1

Easy to stub

import { createSandbox } from 'sinon';
import * as s3PresignedPost from '@aws-sdk/s3-presigned-post/dist-cjs/createPresignedPost';

const sandbox = createSandbox();

const provider = new MyProvider();


describe('Generate get signed url', () => {
    it('Error', async () => {
      const errorStub = sandbox.stub(s3RequestPresigner, 'getSignedUrl').rejects(errorObject);

      const error: Error = await provider.functionCallgetSignedUrlInside(bucketName, urlPath).catch((error) => error);
      sandbox.assert.calledOnce(errorStub);
      expect(error.message).to.eq(errorObject.message);
    });

    it('Success', async () => {
      const url = `${baseUrl}/${bucketName}-${Date.now()}`;
      const successStub = sandbox.stub(s3RequestPresigner, 'getSignedUrl').resolves(url);

      const result = await storage.functionCallgetSignedUrlInside(bucketName, urlPath);
      sandbox.assert.calledOnce(successStub);
      expect(result).to.eq(url);
    });
  });

Upvotes: -1

Mike Kravtsov
Mike Kravtsov

Reputation: 81

Another possible solution is to use mockery. E.g. to mock uuid

import { expect } from 'chai';
import mockery from 'mockery';
import sinon from 'sinon';

describe('domain/books', () => {
  let createBook;
  let uuidStub;

  before(async () => {
    mockery.enable({
      warnOnReplace: false,
      warnOnUnregistered: false,
    });
    uuidStub = sinon.stub();
    mockery.registerMock('uuid', { v4: uuidStub });

    ({ createBook } = await import('../domain/books.js'));
  });

  afterEach(() => {
    sinon.resetHistory();
  });

  after(() => {
    sinon.restore();
    mockery.disable();
    mockery.deregisterAll();
  });

  describe('createBook', () => {
    it('should save a book and return the id', () => {
      const id = 'abc123';
      uuidStub.returns(id);

      const { id: bookId } = createBook({
        title: 'My Book',
        author: 'Jane Doe',
      });
      expect(bookId).to.equal(id);
    });
  });
});

The mockery setup is a bit tedious, but the library saved me a number of times.

Upvotes: 0

Jacob Brogan
Jacob Brogan

Reputation: 41

I came up with the following workaround for ES6 modules. You can wrap getSignedUrl in your own module and mock that module instead. This approach should work for any modules where sinon is unable to mock a "non-configurable and non-writable" method.

For example:

my-s3-client-internals.js - Your custom wrapper module

// You'll need to import the original method, assign it to
// a new const, then export that const
import { getSignedUrl as getSignedUrl_orig } from '@aws-sdk/s3-request-presigner';

export const getSignedUrl = getSignedUrl_orig;

my-s3-client.js - Consumer of getSignedUrl

// Import the method instead from your custom file
import { getSignedUrl } from './my-s3-client-internals';

// Call it however you normally would, for example:
export const getUrl(bucket, key) {
  const command = new GetObjectCommand({ Bucket: bucket, Key: key });
  return getSignedUrl(client, command, { expiresIn: 300 });
}

my-s3-client.spec.js - Unit tests for the consumer module

import { getUrl } from './my-s3-client';
import * as clientInternals from './my-s3-client-internals';
import sinon from 'sinon';

it('does something', () => {
  // Mock the method exported from your wrapper module
  sinon.stub(clientInternals, 'getSignedUrl')
    .callsFake(async (client, command, options) => {
      return 'fake-url';
    });

  // Then call your consumer method to test
  const url = await getUrl('test-bucket', 'test-key');
  expect(url).to.equal('fake-url');
});

Upvotes: 4

Jarede
Jarede

Reputation: 3498

So I won't make this the official answer, unless there are no better solutions, but this is what my research has brought about a solution.

The issue is related to this: https://github.com/sinonjs/sinon/issues/2377

Where sinon will throw an error when the Object.descriptor is non-configurable.

There is no obvious way around that currently, that I can find. The way to solve it is to use proxyquire:

const sinon = require('sinon')
const proxyquire =  require('proxyquire')
...
it('should throw an error when getSignedUrl rejects', async function() {
    const fakeurl = 'hello world'
    const fakeURL = sinon.stub().resolves(fakeurl)
    const handler = proxyquire(
      '../../handlers/presigned_url',
      {
        '@aws-sdk/s3-request-presigner': {
          'getSignedUrl': async () => {
            return fakeURL()
          }
        }
      }
    )

This will then resolve with whatever you want fakeurl to be.

Upvotes: 3

Related Questions