From 4cc4bf1e761a0e2c9d5fa863dc0a89d10dfb0883 Mon Sep 17 00:00:00 2001 From: Paul Beaudoin Date: Wed, 8 May 2024 11:57:51 -0400 Subject: [PATCH] Make app robust to scsb api issues Ensure that, if connecting to the SCSB API raises an error or responds unexpectedly when querying for item statuses, we set offsite item statuses to 'Not available' and proceed with other work. Includes light refactoring to improve readability https://jira.nypl.org/browse/SCC-4050 --- lib/availability_resolver.js | 67 ++-- lib/delivery-locations-resolver.js | 34 ++- test/availability_resolver.test.js | 372 ++++++++++++++--------- test/delivery-locations-resolver.test.js | 37 ++- 4 files changed, 335 insertions(+), 175 deletions(-) diff --git a/lib/availability_resolver.js b/lib/availability_resolver.js index 14fab431..8f2b7eb7 100644 --- a/lib/availability_resolver.js +++ b/lib/availability_resolver.js @@ -22,6 +22,22 @@ class AvailabilityResolver { this.barcodes = this._parseBarCodesFromESResponse() } + /** + * Given a map relating status strings to arrays of barcodes, + * returns a map relating each barcode to a status string. + */ + static invertBarcodeByStatusMapping (barcodesByStatus) { + if (!barcodesByStatus || typeof barcodesByStatus !== 'object') { + return {} + } + return Object.keys(barcodesByStatus) + .reduce((h, status) => { + const barcodeToStatusPairs = barcodesByStatus[status] + .map((barcode) => ({ [barcode]: status })) + return Object.assign(h, ...barcodeToStatusPairs) + }, {}) + } + // returns an updated elasticSearchResponse with the newest availability info from SCSB responseWithUpdatedAvailability (options = {}) { // If this serialization is a result of a hold request initializing, we want @@ -30,12 +46,21 @@ class AvailabilityResolver { ? () => this._checkScsbForRecapCustomerCode() : () => Promise.resolve() + // When options.recapBarcodesByStatus is set, we can use it in place of + // re-querying status by barcode: + const barcodeToStatusMap = async () => { + if (options.recapBarcodesByStatus) { + // Invert mapping to map barcodes to statuses: + return AvailabilityResolver.invertBarcodeByStatusMapping(options.recapBarcodesByStatus) + } else { + return this._createSCSBBarcodeAvailbilityMapping(this.barcodes) + } + } + // Get 1) barcode-availability mapping and 2) customer code query in // parallel because they don't depend on each other: return Promise.all([ - // TODO: When options.recapBarcodesByStatus is set, we should be able to - // use it in place of re-querying status by barcode: - this._createSCSBBarcodeAvailbilityMapping(this.barcodes), + barcodeToStatusMap(), updateRecapCustomerCodes() ]) .then((barcodeMappingAndCustomerCodeResult) => { @@ -181,22 +206,30 @@ class AvailabilityResolver { /** * Given an array of barcodes, returns a hash mapping barcode to SCSB availability */ - _createSCSBBarcodeAvailbilityMapping (barcodes) { + async _createSCSBBarcodeAvailbilityMapping (barcodes) { if (barcodes.length === 0) { - return Promise.resolve({}) + return {} } - return scsbClient.getItemsAvailabilityForBarcodes(this.barcodes) - .then((itemsStatus) => { - if (!Array.isArray(itemsStatus)) { - logger.warn(`Got bad itemAvailabilityStatus response from SCSB for barcodes (${barcodes}): ${JSON.stringify(itemsStatus)}`) - return {} - } - const barcodesAndAvailability = {} - itemsStatus.forEach((statusEntry) => { - barcodesAndAvailability[statusEntry.itemBarcode] = statusEntry.itemAvailabilityStatus - }) - return barcodesAndAvailability - }) + let itemsStatus + try { + itemsStatus = await scsbClient.getItemsAvailabilityForBarcodes(this.barcodes) + } catch (e) { + logger.warn(`Error retrieving SCSB statuses for barcodes: ${e}`) + return {} + } + + if (!Array.isArray(itemsStatus)) { + logger.warn(`Got bad itemAvailabilityStatus response from SCSB for barcodes (${barcodes}): ${JSON.stringify(itemsStatus)}`) + return {} + } + + // Convert SCSB API response into barcode => status map: + return itemsStatus + // Verify the entries have the properties we expect: + .filter((entry) => entry.itemBarcode && entry.itemAvailabilityStatus) + .reduce((h, entry) => { + return Object.assign(h, { [entry.itemBarcode]: entry.itemAvailabilityStatus }) + }, {}) } _parseBarCodesFromESResponse () { diff --git a/lib/delivery-locations-resolver.js b/lib/delivery-locations-resolver.js index 7c6f7ec7..1521ff07 100644 --- a/lib/delivery-locations-resolver.js +++ b/lib/delivery-locations-resolver.js @@ -7,15 +7,20 @@ const onsiteEddCriteria = require('../data/onsite-edd-criteria.json') const { isItemNyplOwned } = require('./ownership_determination') class DeliveryLocationsResolver { + static nyplCoreLocation (locationCode) { + return sierraLocations[locationCode] + } + static requestableBasedOnHoldingLocation (item) { - // Is this not requestable because of its holding location? - try { - const holdingLocationSierraCode = item.holdingLocation[0].id.split(':').pop() - return sierraLocations[holdingLocationSierraCode].requestable - } catch (e) { - logger.warn('There is an item in the index with missing or malformed holdingLocation', item) + const locationCode = this.extractLocationCode(item) + + if (!DeliveryLocationsResolver.nyplCoreLocation(locationCode)) { + logger.warn(`DeliveryLocationsResolver: Unrecognized holdingLocation for ${item.uri}: ${locationCode}`) return false } + + // Is this not requestable because of its holding location? + return DeliveryLocationsResolver.nyplCoreLocation(locationCode).requestable } // Currently, there is no physical delivery requests for onsite items through Discovery API @@ -24,11 +29,11 @@ class DeliveryLocationsResolver { // If holdingLocation given, strip code from @id for lookup: const locationCode = holdingLocation && holdingLocation.id ? holdingLocation.id.replace(/^loc:/, '') : null // Is Sierra location code mapped? - if (sierraLocations[locationCode] && sierraLocations[locationCode].sierraDeliveryLocations) { + if (DeliveryLocationsResolver.nyplCoreLocation(locationCode)?.sierraDeliveryLocations) { // It's mapped, but the sierraDeliveryLocation entities only have `code` and `label` // Do a second lookup to populate `deliveryLocationTypes` - return sierraLocations[locationCode].sierraDeliveryLocations.map((deliveryLocation) => { - deliveryLocation.deliveryLocationTypes = sierraLocations[deliveryLocation.code].deliveryLocationTypes + return DeliveryLocationsResolver.nyplCoreLocation(locationCode).sierraDeliveryLocations.map((deliveryLocation) => { + deliveryLocation.deliveryLocationTypes = DeliveryLocationsResolver.nyplCoreLocation(deliveryLocation.code).deliveryLocationTypes return deliveryLocation }) // Either holdingLocation is null or code not matched; Fall back on mocked data: @@ -141,11 +146,12 @@ class DeliveryLocationsResolver { } static extractLocationCode (item) { - try { - return item.holdingLocation[0].id.split(':').pop() - } catch (e) { - logger.warn('There is an item in the index with missing or malformed holdingLocation', item) + if (!Array.isArray(item.holdingLocation)) { + logger.warn(`DeliveryLocationsResolver#extractLocationCode: Item missing holdingLocation: ${item.uri}`) + return false } + + return item.holdingLocation[0]?.id?.split(':').pop() } static sortPosition (location) { @@ -224,7 +230,7 @@ class DeliveryLocationsResolver { deliveryLocation: [] } const holdingLocationCode = this.extractLocationCode(item) - const sierraData = sierraLocations[holdingLocationCode] + const sierraData = DeliveryLocationsResolver.nyplCoreLocation(holdingLocationCode) if (!sierraData) { // This case is mainly to satisfy a test which wants eddRequestable = false // for a made up location code. diff --git a/test/availability_resolver.test.js b/test/availability_resolver.test.js index 28d317a2..2ca4a3f8 100644 --- a/test/availability_resolver.test.js +++ b/test/availability_resolver.test.js @@ -52,184 +52,186 @@ const itemAvailabilityResponse = [ ] describe('Response with updated availability', function () { - beforeEach(() => { - sinon.stub(scsbClient, 'getItemsAvailabilityForBarcodes') - .callsFake(() => Promise.resolve(itemAvailabilityResponse)) - - sinon.stub(scsbClient, 'recapCustomerCodeByBarcode') - .callsFake(() => Promise.resolve('NC')) - }) - - afterEach(() => { - scsbClient.getItemsAvailabilityForBarcodes.restore() - scsbClient.recapCustomerCodeByBarcode.restore() - }) - - it('will change an items status to "Available" if ElasticSearch says it\'s unavailable but SCSB says it is Available', function () { - const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + describe('responseWithUpdatedAvailability', () => { + beforeEach(() => { + sinon.stub(scsbClient, 'getItemsAvailabilityForBarcodes') + .callsFake(() => Promise.resolve(itemAvailabilityResponse)) - const indexedAsUnavailableURI = 'i10283664' + sinon.stub(scsbClient, 'recapCustomerCodeByBarcode') + .callsFake(() => Promise.resolve('NC')) + }) - const indexedAsUnavailable = elasticSearchResponse.fakeElasticSearchResponseNyplItem().hits.hits[0]._source.items.find((item) => { - return item.uri === indexedAsUnavailableURI + afterEach(() => { + scsbClient.getItemsAvailabilityForBarcodes.restore() + scsbClient.recapCustomerCodeByBarcode.restore() }) - // Test that it's unavailable at first - expect(indexedAsUnavailable.status[0].id).to.equal('status:na') - expect(indexedAsUnavailable.status[0].label).to.equal('Not available') + it('will change an items status to "Available" if ElasticSearch says it\'s unavailable but SCSB says it is Available', function () { + const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) - return availabilityResolver.responseWithUpdatedAvailability() - .then((modifiedResponse) => { - const theItem = modifiedResponse.hits.hits[0]._source.items.find((item) => { - return item.uri === indexedAsUnavailableURI - }) + const indexedAsUnavailableURI = 'i10283664' - // Test AvailabilityResolver munges it into availability - expect(theItem.status[0].id).to.equal('status:a') - expect(theItem.status[0].label).to.equal('Available') + const indexedAsUnavailable = elasticSearchResponse.fakeElasticSearchResponseNyplItem().hits.hits[0]._source.items.find((item) => { + return item.uri === indexedAsUnavailableURI }) - }) - - it('will change an items status to "Unavailable" if ElasticSearch says it\'s Available but SCSB says it is Unvailable', function () { - const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) - const indexedAsAvailableURI = 'i102836649' - const indexedAsAvailable = elasticSearchResponse.fakeElasticSearchResponseNyplItem().hits.hits[0]._source.items.find((item) => { - return item.uri === indexedAsAvailableURI - }) + // Test that it's unavailable at first + expect(indexedAsUnavailable.status[0].id).to.equal('status:na') + expect(indexedAsUnavailable.status[0].label).to.equal('Not available') - // Test that it's available at first - expect(indexedAsAvailable.status[0].id).to.equal('status:a') - expect(indexedAsAvailable.status[0].label).to.equal('Available') + return availabilityResolver.responseWithUpdatedAvailability() + .then((modifiedResponse) => { + const theItem = modifiedResponse.hits.hits[0]._source.items.find((item) => { + return item.uri === indexedAsUnavailableURI + }) - return availabilityResolver.responseWithUpdatedAvailability() - .then((modifiedResponse) => { - const theItem = modifiedResponse.hits.hits[0]._source.items.find((item) => { - return item.uri === indexedAsAvailableURI + // Test AvailabilityResolver munges it into availability + expect(theItem.status[0].id).to.equal('status:a') + expect(theItem.status[0].label).to.equal('Available') }) - - // Test AvailabilityResolver munges it into temporarily unavailable - expect(theItem.status[0].id).to.equal('status:na') - expect(theItem.status[0].label).to.equal('Not available') - }) - }) - - it('will return the original ElasticSearchResponse\'s status for the item if the SCSB can\'t find an item with the barcode', function () { - const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) - - const indexedButNotAvailableInSCSBURI = 'i22566485' - const indexedButNotAvailableInSCSB = elasticSearchResponse.fakeElasticSearchResponseNyplItem().hits.hits[0]._source.items.find((item) => { - return item.uri === indexedButNotAvailableInSCSBURI }) - expect(indexedButNotAvailableInSCSB.status[0].id).to.equal('status:a') - expect(indexedButNotAvailableInSCSB.status[0].label).to.equal('Available') - - return availabilityResolver.responseWithUpdatedAvailability() - .then((modifiedResponse) => { - const theItem = modifiedResponse.hits.hits[0]._source.items.find((item) => { - return item.uri === indexedButNotAvailableInSCSBURI - }) + it('will change an items status to "Unavailable" if ElasticSearch says it\'s Available but SCSB says it is Unvailable', function () { + const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) - // As this item is not available in SCSB, the elasticSearchResponse's availability for the item was returned - expect(theItem.status[0].id).to.equal('status:a') - expect(theItem.status[0].label).to.equal('Available') + const indexedAsAvailableURI = 'i102836649' + const indexedAsAvailable = elasticSearchResponse.fakeElasticSearchResponseNyplItem().hits.hits[0]._source.items.find((item) => { + return item.uri === indexedAsAvailableURI }) - }) - it('includes the latest availability status of items', function () { - const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + // Test that it's available at first + expect(indexedAsAvailable.status[0].id).to.equal('status:a') + expect(indexedAsAvailable.status[0].label).to.equal('Available') - return availabilityResolver.responseWithUpdatedAvailability() - .then((response) => { - const items = response.hits.hits[0]._source.items + return availabilityResolver.responseWithUpdatedAvailability() + .then((modifiedResponse) => { + const theItem = modifiedResponse.hits.hits[0]._source.items.find((item) => { + return item.uri === indexedAsAvailableURI + }) - // A ReCAP item with Discovery status 'Available', but SCSB - // status 'Not Available' should be made 'Not Available' - const unavailableItem = items.find((item) => { - return item.uri === 'i102836649' + // Test AvailabilityResolver munges it into temporarily unavailable + expect(theItem.status[0].id).to.equal('status:na') + expect(theItem.status[0].label).to.equal('Not available') }) - expect(unavailableItem.status[0].id).to.equal('status:na') - expect(unavailableItem.status[0].label).to.equal('Not available') + }) - // A ReCAP item with Discovery status 'Not Avaiable', but SCSB - // status 'Available' should be made available: - const availableItem = items.find((item) => { - return item.uri === 'i10283664' - }) - expect(availableItem.status[0].id).to.equal('status:a') - expect(availableItem.status[0].label).to.equal('Available') + it('will return the original ElasticSearchResponse\'s status for the item if the SCSB can\'t find an item with the barcode', function () { + const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + + const indexedButNotAvailableInSCSBURI = 'i22566485' + const indexedButNotAvailableInSCSB = elasticSearchResponse.fakeElasticSearchResponseNyplItem().hits.hits[0]._source.items.find((item) => { + return item.uri === indexedButNotAvailableInSCSBURI }) - }) - describe('CUL item', function () { - let availabilityResolver = null + expect(indexedButNotAvailableInSCSB.status[0].id).to.equal('status:a') + expect(indexedButNotAvailableInSCSB.status[0].label).to.equal('Available') - before(function () { - availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseCulItem()) - }) - - it('marks CUL item Available when SCSB API indicates it is so', function () { return availabilityResolver.responseWithUpdatedAvailability() - .then((response) => { - const items = response.hits.hits[0]._source.items + .then((modifiedResponse) => { + const theItem = modifiedResponse.hits.hits[0]._source.items.find((item) => { + return item.uri === indexedButNotAvailableInSCSBURI + }) - const availableItem = items.find((item) => item.uri === 'ci1455504') - expect(availableItem.requestable[0]).to.equal(true) - expect(availableItem.status[0].label).to.equal('Available') + // As this item is not available in SCSB, the elasticSearchResponse's availability for the item was returned + expect(theItem.status[0].id).to.equal('status:a') + expect(theItem.status[0].label).to.equal('Available') }) }) - it('marks CUL item not avilable when SCSB API indicates it is so', function () { + it('includes the latest availability status of items', function () { + const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + return availabilityResolver.responseWithUpdatedAvailability() .then((response) => { const items = response.hits.hits[0]._source.items - const notAvailableItem = items.find((item) => item.uri === 'ci14555049999') - expect(notAvailableItem.status[0].label).to.equal('Not available') - }) - }) - }) + // A ReCAP item with Discovery status 'Available', but SCSB + // status 'Not Available' should be made 'Not Available' + const unavailableItem = items.find((item) => { + return item.uri === 'i102836649' + }) + expect(unavailableItem.status[0].id).to.equal('status:na') + expect(unavailableItem.status[0].label).to.equal('Not available') - describe('checks recapCustomerCodes when options specifies', () => { - let availabilityResolver = null - it('logs an error when item\'s code does not match SCSB', () => { - availabilityResolver = new AvailabilityResolver(recapScsbQueryMismatch()) - const loggerSpy = sinon.spy(logger, 'error') - return availabilityResolver.responseWithUpdatedAvailability({ queryRecapCustomerCode: true }) - .then(() => { - expect(loggerSpy.calledOnce).to.equal(true) - logger.error.restore() + // A ReCAP item with Discovery status 'Not Avaiable', but SCSB + // status 'Available' should be made available: + const availableItem = items.find((item) => { + return item.uri === 'i10283664' + }) + expect(availableItem.status[0].id).to.equal('status:a') + expect(availableItem.status[0].label).to.equal('Available') }) }) - it('updates recapCustomerCode when item\'s code does not match SCSB', () => { - return availabilityResolver.responseWithUpdatedAvailability() - .then((response) => { - const items = response.hits.hits[0]._source.items - // A ReCAP item with customer code XX - const queryItem = items.find((item) => { - return item.uri === 'i10283667' + describe('CUL item', function () { + let availabilityResolver = null + + before(function () { + availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseCulItem()) + }) + + it('marks CUL item Available when SCSB API indicates it is so', function () { + return availabilityResolver.responseWithUpdatedAvailability() + .then((response) => { + const items = response.hits.hits[0]._source.items + + const availableItem = items.find((item) => item.uri === 'ci1455504') + expect(availableItem.requestable[0]).to.equal(true) + expect(availableItem.status[0].label).to.equal('Available') }) - expect(queryItem.recapCustomerCode[0]).to.equal('NC') - }) - }) + }) - it('does nothing when current recapCustomerCode and SCSB code are a match', () => { - availabilityResolver = new AvailabilityResolver(recapScsbQueryMatch()) - const loggerSpy = sinon.spy(logger, 'error') - return availabilityResolver.responseWithUpdatedAvailability() - .then(() => { - expect(loggerSpy.notCalled).to.equal(true) - logger.error.restore() - }) + it('marks CUL item not avilable when SCSB API indicates it is so', function () { + return availabilityResolver.responseWithUpdatedAvailability() + .then((response) => { + const items = response.hits.hits[0]._source.items + + const notAvailableItem = items.find((item) => item.uri === 'ci14555049999') + expect(notAvailableItem.status[0].label).to.equal('Not available') + }) + }) }) - it('does not query SCSB unless specified in options', () => { - return availabilityResolver.responseWithUpdatedAvailability() - .then(() => { - expect(scsbClient.recapCustomerCodeByBarcode.notCalled).to.equal(true) - }) + describe('checks recapCustomerCodes when options specifies', () => { + let availabilityResolver = null + it('logs an error when item\'s code does not match SCSB', () => { + availabilityResolver = new AvailabilityResolver(recapScsbQueryMismatch()) + const loggerSpy = sinon.spy(logger, 'error') + return availabilityResolver.responseWithUpdatedAvailability({ queryRecapCustomerCode: true }) + .then(() => { + expect(loggerSpy.calledOnce).to.equal(true) + logger.error.restore() + }) + }) + + it('updates recapCustomerCode when item\'s code does not match SCSB', () => { + return availabilityResolver.responseWithUpdatedAvailability() + .then((response) => { + const items = response.hits.hits[0]._source.items + // A ReCAP item with customer code XX + const queryItem = items.find((item) => { + return item.uri === 'i10283667' + }) + expect(queryItem.recapCustomerCode[0]).to.equal('NC') + }) + }) + + it('does nothing when current recapCustomerCode and SCSB code are a match', () => { + availabilityResolver = new AvailabilityResolver(recapScsbQueryMatch()) + const loggerSpy = sinon.spy(logger, 'error') + return availabilityResolver.responseWithUpdatedAvailability() + .then(() => { + expect(loggerSpy.notCalled).to.equal(true) + logger.error.restore() + }) + }) + + it('does not query SCSB unless specified in options', () => { + return availabilityResolver.responseWithUpdatedAvailability() + .then(() => { + expect(scsbClient.recapCustomerCodeByBarcode.notCalled).to.equal(true) + }) + }) }) }) @@ -374,4 +376,98 @@ describe('Response with updated availability', function () { }) }) }) + + describe('SCSB outage', () => { + before(() => { + sinon.stub(scsbClient, 'getItemsAvailabilityForBarcodes') + .callsFake(() => { + throw new Error('oh no!') + }) + }) + + after(() => { + scsbClient.getItemsAvailabilityForBarcodes.restore() + }) + + it('makes recap items na when scsb is out', async () => { + const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + + const modifiedResponse = await availabilityResolver.responseWithUpdatedAvailability() + + // Just examine items in rc locations: + const recapItems = modifiedResponse.hits.hits[0]._source.items + .filter((item) => item.holdingLocation && item.holdingLocation[0] && /^loc:rc/.test(item.holdingLocation[0].id)) + + expect(recapItems).to.have.lengthOf(3) + + // Assert that all recap items are Not available: + recapItems.forEach((item) => { + // Test AvailabilityResolver munges it into availability + expect(item.status[0].id).to.equal('status:na') + expect(item.status[0].label).to.equal('Not available') + }) + }) + }) + + describe('SCSB bad responses', () => { + // Imagine some unexpected responses: + ; [ + // Unexpected status string: + [{ itemBarcode: '10005468369', itemAvailabilityStatus: 'fladeedle' }], + // An entry without a barcode: + [{ itemAvailabilityStatus: 'Available' }], + // Truthy but useless: + [{ }], + // Some kind of error response: + { error: 'some other error' }, + // HTML! + 'oh no html { + it(`makes recap items "na" when scsb returns unexpected responses (#${index})`, async () => { + // Stub the scsb client to return the bad response: + sinon.stub(scsbClient, 'getItemsAvailabilityForBarcodes') + .callsFake(() => Promise.resolve(badResponse)) + + const availabilityResolver = new AvailabilityResolver(elasticSearchResponse.fakeElasticSearchResponseNyplItem()) + const modifiedResponse = await availabilityResolver.responseWithUpdatedAvailability() + + // Get the single item for which we've mocked the bad scsb response above: + const item = modifiedResponse.hits.hits[0]._source.items + .find((item) => item.identifier && item.identifier[0] === 'urn:barcode:10005468369') + + // Expect the item's Available status to have been flipped to 'na' even + // through scsb api returned a weird response: + expect(item.status[0].id).to.equal('status:na') + expect(item.status[0].label).to.equal('Not available') + + // Restore the client method: + scsbClient.getItemsAvailabilityForBarcodes.restore() + }) + }) + }) + + describe('invertBarcodeByStatusMapping', () => { + it('returns empty map if invalid input given', () => { + ;[null, false, true, 'fladeedle'].forEach((badValue) => { + expect(AvailabilityResolver.invertBarcodeByStatusMapping(badValue)).to.deep.eq({}) + }) + }) + + it('inverts a status to barcode map', () => { + const map = AvailabilityResolver.invertBarcodeByStatusMapping({ + Available: ['b1', 'b2'], + 'Not available': ['b3', 'b4'] + }) + expect(map).to.deep.eq({ + b1: 'Available', + b2: 'Available', + b3: 'Not available', + b4: 'Not available' + }) + }) + }) }) diff --git a/test/delivery-locations-resolver.test.js b/test/delivery-locations-resolver.test.js index 57da2765..21672f5d 100644 --- a/test/delivery-locations-resolver.test.js +++ b/test/delivery-locations-resolver.test.js @@ -1,3 +1,5 @@ +const sinon = require('sinon') + const DeliveryLocationsResolver = require('../lib/delivery-locations-resolver') const sampleItems = { @@ -149,13 +151,36 @@ function takeThisPartyPartiallyOffline () { describe('Delivery-locations-resolver', function () { before(takeThisPartyPartiallyOffline) - it('will hide "Scholar" deliveryLocation for LPA or SC only deliverable items, patron is scholar type', function () { - return DeliveryLocationsResolver.attachDeliveryLocationsAndEddRequestability([sampleItems.onsiteOnlySchomburg], 'mala').then((items) => { - expect(items[0].deliveryLocation).to.not.have.lengthOf(0) + describe('SC delivery locations', () => { + before(() => { + // Override NYPL-Core lookup for scff3 to make it requestable: + sinon.stub(DeliveryLocationsResolver, 'nyplCoreLocation').callsFake(() => { + return { + sierraDeliveryLocations: [ + { + code: 'sc', + label: 'Schomburg Center - Research and Reference Division', + locationsApiSlug: 'schomburg', + deliveryLocationTypes: ['Research'] + } + ], + requestable: true + } + }) + }) - // Confirm the known scholar rooms are not included: - scholarRooms.forEach((scholarRoom) => { - expect(items[0].deliveryLocation).to.not.include(scholarRoom) + after(() => { + DeliveryLocationsResolver.nyplCoreLocation.restore() + }) + + it('will hide "Scholar" deliveryLocation for LPA or SC only deliverable items, patron is scholar type', function () { + return DeliveryLocationsResolver.attachDeliveryLocationsAndEddRequestability([sampleItems.onsiteOnlySchomburg], 'mala').then((items) => { + expect(items[0].deliveryLocation).to.not.have.lengthOf(0) + + // Confirm the known scholar rooms are not included: + scholarRooms.forEach((scholarRoom) => { + expect(items[0].deliveryLocation).to.not.include(scholarRoom) + }) }) }) })