diff --git a/src/controllers/validation/locations.js b/src/controllers/validation/locations.js index 255df3c..b310313 100644 --- a/src/controllers/validation/locations.js +++ b/src/controllers/validation/locations.js @@ -27,6 +27,7 @@ export default { .max(1000), searchString: Joi.string().allow(''), organizationName: Joi.string().min(3), + noServices: Joi.boolean(), zipcodes: Joi.array().max(200).items(Joi.string().length(5).regex(/\d+/)), taxonomyId: Joi.string(), openAt: Joi.date().iso(), diff --git a/src/models/location.js b/src/models/location.js index dc9e1a9..247a2c3 100644 --- a/src/models/location.js +++ b/src/models/location.js @@ -565,6 +565,9 @@ module.exports = (sequelize, DataTypes, Op) => { totalNumLocations = (await Location.findUniqueLocationIds( filterParameters, [distanceCondition], + {}, + null, + noServices, )).length; locationIds = await Location.findUniqueLocationIds( @@ -585,7 +588,13 @@ module.exports = (sequelize, DataTypes, Op) => { // aren't natively supported by sequelize and would require a raw query. // For now, the simplicity and security of sequelize seems worth the slight performance hit. if (minResults && locationIds.length < minResults) { - totalNumLocations = (await Location.findUniqueLocationIds(filterParameters, [])).length; + totalNumLocations = (await Location.findUniqueLocationIds( + filterParameters, + [], + {}, + null, + noServices, + )).length; locationIds = await Location.findUniqueLocationIds(filterParameters, [], { order, limit: minResults, @@ -593,12 +602,18 @@ module.exports = (sequelize, DataTypes, Op) => { }, selectedAttributeForOrderBy, noServices); } } else { - totalNumLocations = (await Location.findUniqueLocationIds(filterParameters, [])).length; + totalNumLocations = (await Location.findUniqueLocationIds( + filterParameters, + [], + {}, + null, + noServices, + )).length; locationIds = await Location.findUniqueLocationIds(filterParameters, [], { limit, offset, order, - }, selectedAttributeForOrderBy); + }, selectedAttributeForOrderBy, noServices); } const additionalLocationData = locationFieldsOnly ? [ diff --git a/test/integration/find-locations.test.js b/test/integration/find-locations.test.js index 66c5976..184c08b 100644 --- a/test/integration/find-locations.test.js +++ b/test/integration/find-locations.test.js @@ -337,6 +337,29 @@ describe('find locations', () => { ])); })); + it('should include locations without linked services when requested', async () => { + const noServiceLocation = await organization.createLocation({ + name: 'Service-less center', + position: pointNearOrigin, + }); + + return request(app).get('/locations').query({ + organizationName: 'test org', + noServices: true, + }) + .expect(200) + .then((res) => { + const returnedLocations = res.body; + expect(returnedLocations).toHaveLength(4); + expect(returnedLocations).toEqual(expect.arrayContaining([ + expect.objectContaining({ name: primaryLocation.name }), + expect.objectContaining({ name: otherServiceLocation.name }), + expect.objectContaining({ name: farLocation.name }), + expect.objectContaining({ name: noServiceLocation.name }), + ])); + }); + }); + it('should not match locations whose organization doesn\'t have the string in its name', () => request(app).get('/locations').query({ organizationName: 'center' }) .expect(200) diff --git a/test/setup.js b/test/setup.js index 05f38c1..2eb9f81 100644 --- a/test/setup.js +++ b/test/setup.js @@ -6,7 +6,25 @@ jest.setTimeout(10000); process.env.DATABASE_NAME = 'test'; process.env.DATABASE_LOGGING = 'false'; -const models = require('../src/models'); +jest.mock('openai', () => function OpenAI() { + return { + chat: { + completions: { + create: jest.fn(), + }, + }, + }; +}); + +jest.mock('@aws-sdk/client-cognito-identity-provider', () => ({ + CognitoIdentityProviderClient: jest.fn(() => ({ + send: jest.fn(), + })), + ListUsersCommand: jest.fn(), +})); + +const unitTestRun = process.argv.some(arg => arg.includes('test/unit')); +const models = unitTestRun ? null : require('../src/models'); async function execScript(script) { // run the migrations @@ -25,6 +43,10 @@ async function execScript(script) { } beforeAll(async () => { + if (unitTestRun) { + return; + } + // reset the database state await models.sequelize.query(` DO $$ @@ -53,6 +75,10 @@ beforeAll(async () => { await execScript('npx sequelize-cli db:migrate --name 20240607172205-age-filter'); }); afterAll(async () => { + if (unitTestRun) { + return; + } + // eslint-disable-next-line no-implied-eval await execScript('npx sequelize-cli db:migrate:undo --name 20240607172205-age-filter'); await models.sequelize.close(); diff --git a/test/unit/location-search.test.js b/test/unit/location-search.test.js new file mode 100644 index 0000000..6179736 --- /dev/null +++ b/test/unit/location-search.test.js @@ -0,0 +1,38 @@ +import createLocationModel from '../../src/models/location'; + +describe('Location.search', () => { + function createModel() { + const Location = {}; + const sequelize = { + define: jest.fn(() => Location), + literal: jest.fn(value => value), + models: { + EventRelatedInfo: {}, + HolidaySchedule: {}, + Organization: {}, + Service: {}, + }, + }; + + createLocationModel(sequelize, {}, {}); + return Location; + } + + it('threads noServices through non-radius organization searches', async () => { + const Location = createModel(); + const filterParameters = { organizationName: 'Housing Works' }; + + Location.findUniqueLocationIds = jest.fn() + .mockResolvedValueOnce(['location-1']) + .mockResolvedValueOnce([]); + + await Location.search({ + filterParameters, + noServices: true, + }); + + expect(Location.findUniqueLocationIds).toHaveBeenCalledTimes(2); + expect(Location.findUniqueLocationIds.mock.calls.map(call => call[4])) + .toEqual([true, true]); + }); +});