From cb41a4fe3c6b87e0d1a1a3454beeb48fd7dc34f9 Mon Sep 17 00:00:00 2001 From: Rebecca Meritz Date: Wed, 10 Jun 2020 15:11:34 -0700 Subject: [PATCH] Add test coverage for `lib/table.js` (#178) * Add tests initial tests for table.js * Add Table#select tests * Add table tests for handling of error cases - Is there a better way to rest the 'handler override' than switching to beforeEach/afterEach? * Add tests to Table#list * Add tests for Table#forEach * Remove unused fetchNextPage from select tests * Improve error tests by adding status and msg check * Improve forEach tests - Add tests with more than one record - Add test with no records - Add iteration counter to ensure all expectations are run * Ensure testing correct errors in select test * Use then with an error statement instead of catch To test that errors are thrown in tests. This way if tests unexpectly fufill the promise they immediately error instead of just timeing out. --- lib/table.js | 1 - test/create.test.js | 35 +++++- test/delete.test.js | 29 ++++- test/find.test.js | 44 +++++++ test/list.test.js | 283 ++++++++++++++++++++++++++++++++++++++++++++ test/select.test.js | 129 ++++++++++++++++++++ test/table.test.js | 25 ++++ test/update.test.js | 38 +++++- 8 files changed, 577 insertions(+), 7 deletions(-) create mode 100644 test/find.test.js create mode 100644 test/list.test.js create mode 100644 test/select.test.js create mode 100644 test/table.test.js diff --git a/lib/table.js b/lib/table.js index 5a114686..be3505e9 100644 --- a/lib/table.js +++ b/lib/table.js @@ -1,4 +1,3 @@ -// istanbul ignore file 'use strict'; var isArray = require('lodash/isArray'); diff --git a/test/create.test.js b/test/create.test.js index 28f92dcc..931f333f 100644 --- a/test/create.test.js +++ b/test/create.test.js @@ -4,16 +4,18 @@ var testHelpers = require('./test_helpers'); describe('record creation', function() { var airtable; + var testExpressApp; var teardownAsync; - beforeAll(function() { + beforeEach(function() { return testHelpers.getMockEnvironmentAsync().then(function(env) { airtable = env.airtable; + testExpressApp = env.testExpressApp; teardownAsync = env.teardownAsync; }); }); - afterAll(function() { + afterEach(function() { return teardownAsync(); }); @@ -51,6 +53,35 @@ describe('record creation', function() { ); }); + it('can throw an error if create fails', function(done) { + testExpressApp.set('handler override', function(req, res) { + res.status(402).json({ + error: {message: 'foo bar'}, + }); + }); + + return airtable + .base('app123') + .table('Table') + .create( + { + foo: 'boo', + bar: 'yar', + }, + {typecast: true} + ) + .then( + function() { + throw new Error('Promise unexpectly fufilled.'); + }, + function(err) { + expect(err.statusCode).toBe(402); + expect(err.message).toBe('foo bar'); + done(); + } + ); + }); + it('can add the "typecast" parameter when creating one record', function() { return airtable .base('app123') diff --git a/test/delete.test.js b/test/delete.test.js index 517679a0..8ec98016 100644 --- a/test/delete.test.js +++ b/test/delete.test.js @@ -4,16 +4,18 @@ var testHelpers = require('./test_helpers'); describe('record deletion', function() { var airtable; + var testExpressApp; var teardownAsync; - beforeAll(function() { + beforeEach(function() { return testHelpers.getMockEnvironmentAsync().then(function(env) { airtable = env.airtable; + testExpressApp = env.testExpressApp; teardownAsync = env.teardownAsync; }); }); - afterAll(function() { + afterEach(function() { return teardownAsync(); }); @@ -50,6 +52,29 @@ describe('record deletion', function() { }); }); + it('can throw an error if delete fails', function(done) { + testExpressApp.set('handler override', function(req, res) { + res.status(402).json({ + error: {message: 'foo bar'}, + }); + }); + + return airtable + .base('app123') + .table('Table') + .destroy(['rec123', 'rec456']) + .then( + function() { + throw new Error('Promise unexpectly fufilled.'); + }, + function(err) { + expect(err.statusCode).toBe(402); + expect(err.message).toBe('foo bar'); + done(); + } + ); + }); + it('can delete multiple records and call a callback', function(done) { airtable .base('app123') diff --git a/test/find.test.js b/test/find.test.js new file mode 100644 index 00000000..92fe0db9 --- /dev/null +++ b/test/find.test.js @@ -0,0 +1,44 @@ +'use strict'; + +var testHelpers = require('./test_helpers'); + +describe('record retrival', function() { + var airtable; + var testExpressApp; + var teardownAsync; + + beforeAll(function() { + return testHelpers.getMockEnvironmentAsync().then(function(env) { + airtable = env.airtable; + testExpressApp = env.testExpressApp; + teardownAsync = env.teardownAsync; + }); + }); + + afterAll(function() { + return teardownAsync(); + }); + + it('can find one record', function() { + var recordId = 'record1'; + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/record1?'); + res.json({ + id: req.params.recordId, + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }); + }); + + return airtable + .base('app123') + .table('Table') + .find(recordId) + .then(function(foundRecord) { + expect(foundRecord.id).toBe(recordId); + expect(foundRecord.get('Name')).toBe('Rebecca'); + }); + }); +}); diff --git a/test/list.test.js b/test/list.test.js new file mode 100644 index 00000000..2c9e5746 --- /dev/null +++ b/test/list.test.js @@ -0,0 +1,283 @@ +'use strict'; + +var testHelpers = require('./test_helpers'); + +describe('list records', function() { + var airtable; + var testExpressApp; + var teardownAsync; + + beforeAll(function() { + return testHelpers.getMockEnvironmentAsync().then(function(env) { + airtable = env.airtable; + testExpressApp = env.testExpressApp; + teardownAsync = env.teardownAsync; + }); + }); + + afterAll(function() { + return teardownAsync(); + }); + + it('lists records with a limit and offset without opts', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/?limit=50&offset=offset000'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .list(50, 'offset000', function(err, records, offset) { + expect(err).toBeNull(); + expect(records.length).toBe(1); + expect(records[0].getId()).toBe('recordA'); + expect(records[0].get('Name')).toBe('Rebecca'); + expect(offset).toBe('offsetABC'); + done(); + }); + }); + + it('lists records with a limit, offset, and opts', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe( + '/v0/app123/Table/?limit=50&offset=offset000&otherOptions=getpassedalong' + ); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .list(50, 'offset000', {otherOptions: 'getpassedalong'}, function( + err, + records, + offset + ) { + expect(err).toBeNull(); + expect(records.length).toBe(1); + expect(records[0].getId()).toBe('recordA'); + expect(records[0].get('Name')).toBe('Rebecca'); + expect(offset).toBe('offsetABC'); + done(); + }); + }); + + it('can throw an error if list fails', function(done) { + testExpressApp.set('handler override', function(req, res) { + res.status(402).json({ + error: {message: 'foo bar'}, + }); + }); + + return airtable + .base('app123') + .table('Table') + .list(50, 'offset000', function(err, records, offset) { + expect(err.statusCode).toBe(402); + expect(err.message).toBe('foo bar'); + expect(records).toBeUndefined(); + expect(offset).toBeUndefined(); + done(); + }); + }); + + it('iterates through each record with opts', function(done) { + var iterationCounter = 0; + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/?limit=100&opts=arepassedalong'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + { + id: 'recordB', + fields: {Name: 'Mike'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + }); + }); + + return airtable + .base('app123') + .table('Table') + .forEach( + {opts: 'arepassedalong'}, + function(record) { + if (iterationCounter === 0) { + expect(record.getId()).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + } else if (iterationCounter === 1) { + expect(record.getId()).toBe('recordB'); + expect(record.get('Name')).toBe('Mike'); + } + iterationCounter++; + }, + function() { + expect(iterationCounter).toBe(2); + done(); + } + ); + }); + + it('iterates through each record without opts', function(done) { + var iterationCounter = 0; + var json = { + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + }; + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/?limit=100'); + res.json(json); + }); + + return airtable + .base('app123') + .table('Table') + .forEach( + function(record) { + expect(record.getId()).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + iterationCounter++; + }, + function() { + expect(iterationCounter).toBe(1); + done(); + } + ); + }); + + it('iterates through records without any records', function(done) { + var iterationCounter = 0; + var json = { + records: [], + }; + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/?limit=100'); + res.json(json); + }); + + return airtable + .base('app123') + .table('Table') + .forEach( + function() { + iterationCounter++; + }, + function() { + expect(iterationCounter).toBe(0); + done(); + } + ); + }); + + it('can throw an error if forEach fails', function(done) { + testExpressApp.set('handler override', function(req, res) { + res.status(402).json({ + error: {message: 'foo bar'}, + }); + }); + + return airtable + .base('app123') + .table('Table') + .forEach( + function() {}, + function(err) { + expect(err.statusCode).toBe(402); + expect(err.message).toBe('foo bar'); + done(); + } + ); + }); + + it('iterates through each record and handles pagination', function(done) { + var iterationCounter = 0; + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/?limit=100'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offset123', + }); + }); + + return airtable + .base('app123') + .table('Table') + .forEach( + function(record) { + if (iterationCounter === 0) { + expect(record.getId()).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table/?limit=100&offset=offset123'); + // Don't include an offset in second page of results + // to indicate that it is the final page of results + res.json({ + records: [ + { + id: 'recordB', + fields: {Name: 'Mike'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + }); + }); + } else if (iterationCounter === 1) { + expect(record.getId()).toBe('recordB'); + expect(record.get('Name')).toBe('Mike'); + } + iterationCounter++; + }, + function() { + expect(iterationCounter).toBe(2); + done(); + } + ); + }); +}); diff --git a/test/select.test.js b/test/select.test.js new file mode 100644 index 00000000..6657e3ce --- /dev/null +++ b/test/select.test.js @@ -0,0 +1,129 @@ +'use strict'; + +var testHelpers = require('./test_helpers'); + +describe('record selection', function() { + var airtable; + var testExpressApp; + var teardownAsync; + + beforeAll(function() { + return testHelpers.getMockEnvironmentAsync().then(function(env) { + airtable = env.airtable; + testExpressApp = env.testExpressApp; + teardownAsync = env.teardownAsync; + }); + }); + + afterAll(function() { + return teardownAsync(); + }); + + it('selects records without params', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table?'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .select() + .eachPage(function page(records) { + expect(records.length).toBe(1); + records.forEach(function(record) { + expect(record.id).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + }); + done(); + }); + }); + + it('selects records with valid params', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe( + '/v0/app123/Table?maxRecords=50&sort%5B0%5D%5Bfield%5D=Name&sort%5B0%5D%5Bdirection%5D=desc' + ); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .select({maxRecords: 50, sort: [{field: 'Name', direction: 'desc'}]}) + .eachPage(function page(records) { + records.forEach(function(record) { + expect(record.id).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + }); + done(); + }); + }); + + it('selects records filters out invalid parameters and extra arguments without erroring', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe('/v0/app123/Table?'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .select({ignoredParam: 'ignore me'}, {anotherIgnored: 'param'}) + .eachPage(function page(records) { + records.forEach(function(record) { + expect(record.id).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + }); + done(); + }); + }); + + it('selects records errors on invalid parameters', function() { + return expect(() => { + airtable + .base('app123') + .table('Table') + .select({maxRecords: 'should not be a string'}); + }).toThrow(/`maxRecords` should be a number/); + }); + + it('selects records errors when the params are not a plain object', function() { + return expect(() => { + airtable + .base('app123') + .table('Table') + .select('?invalid=params'); + }).toThrow(/should be a plain object/); + }); +}); diff --git a/test/table.test.js b/test/table.test.js new file mode 100644 index 00000000..bc2a90fc --- /dev/null +++ b/test/table.test.js @@ -0,0 +1,25 @@ +'use strict'; + +var testHelpers = require('./test_helpers'); + +describe('Table', function() { + var airtable; + var teardownAsync; + + beforeAll(function() { + return testHelpers.getMockEnvironmentAsync().then(function(env) { + airtable = env.airtable; + teardownAsync = env.teardownAsync; + }); + }); + + afterAll(function() { + return teardownAsync(); + }); + + it('throws an error without a table reference', function() { + return expect(() => { + airtable.base('app123').table(); + }).toThrow(); + }); +}); diff --git a/test/update.test.js b/test/update.test.js index 3f089481..2a9cbf6b 100644 --- a/test/update.test.js +++ b/test/update.test.js @@ -4,16 +4,18 @@ var testHelpers = require('./test_helpers'); describe('record updates', function() { var airtable; + var testExpressApp; var teardownAsync; - beforeAll(function() { + beforeEach(function() { return testHelpers.getMockEnvironmentAsync().then(function(env) { airtable = env.airtable; + testExpressApp = env.testExpressApp; teardownAsync = env.teardownAsync; }); }); - afterAll(function() { + afterEach(function() { return teardownAsync(); }); @@ -163,6 +165,38 @@ describe('record updates', function() { expect(updatedRecords[1].get('typecasted')).toBe(true); }); }); + + it('can throw an error if update fails', function(done) { + testExpressApp.set('handler override', function(req, res) { + res.status(402).json({ + error: {message: 'foo bar'}, + }); + }); + + return airtable + .base('app123') + .table('Table') + .update([ + { + id: 'rec123', + fields: {foo: 'boo'}, + }, + { + id: 'rec456', + fields: {bar: 'yar'}, + }, + ]) + .then( + function() { + throw new Error('Promise unexpectly fufilled.'); + }, + function(err) { + expect(err.statusCode).toBe(402); + expect(err.message).toBe('foo bar'); + done(); + } + ); + }); }); describe('destructive updates', function() {