From f164a9f290805319a86f6af34388eb286c4252c0 Mon Sep 17 00:00:00 2001 From: Andreas Lundqvist <31646645+momentiris@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:16:45 +0100 Subject: [PATCH] test: run database tests in CI (#174) * use transactions for db tests * add mssql actions * convert offer-adapter tests to use parameter db * dotenv with path in config * set env ci db pwd * remove redundant jest config config * use db transaction context for delete-listing and get-listings-with-applicants * convert all tests to use transaction context * allow read commited snapshot to allow parallelised jest tests that uses transactions * use transaction context * run without parallelisation for now * move test config stuff out of application code and remove unused exports * clean up --- .env.ci | 5 + .github/workflows/lint-and-test.yml | 11 + .gitignore | 2 +- .jest/migrate.ts | 27 + .jest/teardown.ts | 26 + jest.config.js | 21 +- knexfile.js | 1 + package.json | 10 +- src/common/config.ts | 9 +- src/services/lease-service/adapters/db.ts | 52 +- .../lease-service/adapters/listing-adapter.ts | 61 +- .../lease-service/adapters/offer-adapter.ts | 16 +- src/services/lease-service/get-tenant.ts | 1 - src/services/lease-service/offer-service.ts | 27 +- src/services/lease-service/routes/listings.ts | 5 +- src/services/lease-service/routes/offers.ts | 4 +- ...ernal-parking-space-listings-from-xpand.ts | 12 +- .../application-profile-adapter.test.ts | 257 +++-- ...-profile-housing-reference-adapter.test.ts | 237 +++-- .../listing-adapter/delete-listing.test.ts | 128 +-- .../get-listings-with-applicants.test.ts | 641 ++++++------ .../adapters/listing-adapter/index.test.ts | 652 ++++++------ .../tests/adapters/offer-adapter.test.ts | 927 +++++++++--------- .../lease-service/tests/offer-service.test.ts | 846 ++++++++-------- .../tests/routes/listings.test.ts | 14 +- ...-parking-space-listings-from-xpand.test.ts | 217 ++-- src/services/lease-service/tests/testUtils.ts | 26 +- ...date-or-create-application-profile.test.ts | 500 +++++----- 28 files changed, 2481 insertions(+), 2254 deletions(-) create mode 100644 .env.ci create mode 100644 .jest/migrate.ts create mode 100644 .jest/teardown.ts diff --git a/.env.ci b/.env.ci new file mode 100644 index 00000000..33c1fd5c --- /dev/null +++ b/.env.ci @@ -0,0 +1,5 @@ +LEASING_DATABASE__PASSWORD=Passw0rd! +LEASING_DATABASE__HOST=localhost +LEASING_DATABASE__USER=sa +LEASING_DATABASE__PORT=1433 +LEASING_DATABASE__DATABASE=tenants-leases-test diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 62ca9fe5..909b270e 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -7,6 +7,14 @@ on: workflow_call: jobs: test: + services: + sql: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: Passw0rd! + ACCEPT_EULA: Y + ports: + - "1433:1433" runs-on: ubuntu-latest steps: - name: Checkout repository @@ -15,6 +23,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' + - name: Create db + run: | + /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Passw0rd! -Q "CREATE DATABASE [tenants-leases-test];" - name: Install dependencies run: npm ci - name: Run tests diff --git a/.gitignore b/.gitignore index d31a70dc..175eacde 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ config.json # Jetbrains IDE .idea/ -.vscode/ \ No newline at end of file +.vscode/ diff --git a/.jest/migrate.ts b/.jest/migrate.ts new file mode 100644 index 00000000..3fdbdfc6 --- /dev/null +++ b/.jest/migrate.ts @@ -0,0 +1,27 @@ +import path from 'path' +import knex from 'knex' + +import Config from '../src/common/config' + +export default async function migrate() { + const db = knex({ + client: 'mssql', + connection: Config.leasingDatabase, + useNullAsDefault: true, + migrations: { + tableName: 'migrations', + directory: path.join(__dirname, '../migrations'), + }, + }) + + await db.migrate + .latest() + .then(() => { + console.log('Migrations applied') + }) + .catch((error) => { + console.error('Error applying migrations', error) + }) + + await db.destroy() +} diff --git a/.jest/teardown.ts b/.jest/teardown.ts new file mode 100644 index 00000000..f237f06d --- /dev/null +++ b/.jest/teardown.ts @@ -0,0 +1,26 @@ +import path from 'path' +import knex from 'knex' + +import Config from '../src/common/config' + +export default async function teardown() { + const db = knex({ + client: 'mssql', + connection: Config.leasingDatabase, + useNullAsDefault: true, + migrations: { + tableName: 'migrations', + directory: path.join(__dirname, '../migrations'), + }, + }) + + try { + await db.migrate.rollback().then(() => { + console.log('Migrations rolled back') + }) + } catch (error) { + console.error('Error rolling back migrations:', error) + } + + await db.destroy() +} diff --git a/jest.config.js b/jest.config.js index f621b11d..7737a63d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,24 +2,11 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - modulePathIgnorePatterns: [ - '/build/', - //todo: excludes the db tests so that GH-action does not remove data by accident - //todo: remove below line when test db connection exists for GH-action - ].concat( - process.env.NODE_ENV === 'test-ci' - ? [ - '/src/services/lease-service/tests/adapters/', - '/src/services/lease-service/tests/sync-internal-parking-space-listings-from-xpand.test.ts', - '/src/services/lease-service/tests/offer-service.test.ts', - '/src/services/lease-service/tests/update-or-create-application-profile.test.ts', - ] - : [] - ), - //todo: maxWorkers: 1 runs all tests in sequence so that we don't get deadlocks for db tests - //todo: implement a more elegant solution (run db tests in sequence, all other tests in parallel) - maxWorkers: 1, + modulePathIgnorePatterns: ['/build/'], transformIgnorePatterns: ['node_modules/(?!(onecore-types)/)'], extensionsToTreatAsEsm: ['.d.ts, .ts'], setupFiles: ['/.jest/common.ts'], + maxWorkers: 1, + globalSetup: '/.jest/migrate.ts', + globalTeardown: '/.jest/teardown.ts', } diff --git a/knexfile.js b/knexfile.js index 038577c5..3be3d9a8 100644 --- a/knexfile.js +++ b/knexfile.js @@ -1,4 +1,5 @@ // Update with your config settings. + require('dotenv').config() /** diff --git a/package.json b/package.json index 152dd205..0843bfac 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,11 @@ "node": "20.*" }, "scripts": { - "test": "jest", - "test:watch": "jest --watch", + "test": "DOTENV_CONFIG_PATH=.env.test node -r dotenv/config node_modules/jest/bin/jest --config jest.config.js", + "test:watch": "DOTENV_CONFIG_PATH=.env.test jest --watch", "build": "tsc", - "start": "npm run migrate:up && node index", - "dev": "npm run migrate:up && nodemon -e ts,js --exec ts-node src/index ", + "start": "npm run migrate:up && node -r dotenv/config index", + "dev": "DOTENV_CONFIG_PATH=.env npm run migrate:up && nodemon -e ts,js --exec ts-node -r dotenv/config src/index ", "lint": "eslint src/", "migrate:make": "knex migrate:make", "migrate:up": "knex migrate:latest --env dev", @@ -19,7 +19,7 @@ "seed": "knex seed:run --env dev", "script:expire-listings": "node scripts/expire-listings.js", "ts:watch": "tsc --watch --noEmit", - "test:ci": "NODE_ENV=test-ci jest --silent", + "test:ci": "DOTENV_CONFIG_PATH=.env.ci node -r dotenv/config node_modules/jest/bin/jest --config jest.config.js", "ts:ci": "tsc --noEmit" }, "author": "", diff --git a/src/common/config.ts b/src/common/config.ts index 14f0cac9..3fc29641 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,12 +1,5 @@ import configPackage from '@iteam/config' -import dotenv from 'dotenv' -import path from 'path' - -if (process.env.NODE_ENV == 'test') { - dotenv.config({ path: path.join(__dirname, '../../.env.test') }) -} else { - dotenv.config() -} +import 'dotenv/config' export interface Config { port: number diff --git a/src/services/lease-service/adapters/db.ts b/src/services/lease-service/adapters/db.ts index fc6e889a..cdb6d937 100644 --- a/src/services/lease-service/adapters/db.ts +++ b/src/services/lease-service/adapters/db.ts @@ -1,55 +1,11 @@ import knex from 'knex' -import Config from '../../../common/config' -import * as path from 'path' -const getStandardConfig = () => ({ - client: 'mssql', - connection: Config.leasingDatabase, -}) +import Config from '../../../common/config' -const getTestConfig = () => { - return { +export const createDbClient = () => + knex({ client: 'mssql', connection: Config.leasingDatabase, - useNullAsDefault: true, - migrations: { - tableName: 'migrations', - directory: path.join(__dirname, '../../../../migrations'), - }, - } -} - -const getConfigBasedOnEnvironment = () => { - const environment = process.env.NODE_ENV || 'dev' - return environment === 'test' ? getTestConfig() : getStandardConfig() -} - -export const db = knex(getConfigBasedOnEnvironment()) - -const migrate = async () => { - await db.migrate - .latest() - .then(() => { - console.log('Migrations applied') - }) - .catch((error) => { - console.error('Error applying migrations', error) - }) -} - -const teardown = async () => { - console.log('Rolling back migrations') - try { - await db.migrate.rollback().then(() => { - console.log('Migrations rolled back') - }) - } catch (error) { - console.error('Error rolling back migrations:', error) - } - - await db.destroy().then(() => { - console.log('Database destroyed') }) -} -export { migrate, teardown } +export const db = createDbClient() diff --git a/src/services/lease-service/adapters/listing-adapter.ts b/src/services/lease-service/adapters/listing-adapter.ts index e264b3d8..857bb908 100644 --- a/src/services/lease-service/adapters/listing-adapter.ts +++ b/src/services/lease-service/adapters/listing-adapter.ts @@ -51,10 +51,11 @@ function transformDbApplicant(row: DbApplicant): Applicant { } const createListing = async ( - listingData: Omit + listingData: Omit, + dbConnection = db ): Promise> => { try { - const insertedRow = await db('Listing') + const insertedRow = await dbConnection('Listing') .insert({ RentalObjectCode: listingData.rentalObjectCode, Address: listingData.address, @@ -109,9 +110,10 @@ const createListing = async ( * @returns {Promise} - Promise that resolves to the existing listing if it exists. */ const getActiveListingByRentalObjectCode = async ( - rentalObjectCode: string + rentalObjectCode: string, + dbConnection = db ): Promise => { - const listing = await db('Listing') + const listing = await dbConnection('Listing') .where({ RentalObjectCode: rentalObjectCode, Status: ListingStatus.Active, @@ -125,10 +127,11 @@ const getActiveListingByRentalObjectCode = async ( } const getListingById = async ( - listingId: number + listingId: number, + dbConnection = db ): Promise => { logger.info({ listingId }, `Getting listing ${listingId} from leasing DB`) - const result = await db + const result = await dbConnection .from('listing AS l') .select( 'l.*', @@ -184,10 +187,11 @@ const getListingById = async ( * @returns {Promise} - Returns the applicant. */ const getApplicantById = async ( - applicantId: number + applicantId: number, + dbConnection = db ): Promise => { logger.info({ applicantId }, 'Getting applicant from leasing DB') - const applicant = await db('Applicant') + const applicant = await dbConnection('Applicant') .where({ Id: applicantId, }) @@ -206,13 +210,16 @@ const getApplicantById = async ( return transformDbApplicant(applicant) } -const createApplication = async (applicationData: Omit) => { +const createApplication = async ( + applicationData: Omit, + dbConnection = db +) => { logger.info( { contactCode: applicationData.contactCode }, 'Creating application in listing DB' ) - const insertedRow = await db('applicant') + const insertedRow = await dbConnection('applicant') .insert({ Name: applicationData.name, NationalRegistrationNumber: applicationData.nationalRegistrationNumber, @@ -253,6 +260,7 @@ const updateApplicantStatus = async ( } const getListingsWithApplicants = async ( + db: Knex, opts?: GetListingsWithApplicantsFilterParams ): Promise, 'unknown'>> => { try { @@ -274,7 +282,7 @@ const getListingsWithApplicants = async ( ) .with({ type: 'ready-for-offer' }, () => db.raw( - `WHERE l.Status = ? + `WHERE l.Status = ? AND EXISTS ( SELECT 1 FROM applicant a @@ -291,7 +299,7 @@ const getListingsWithApplicants = async ( ) .with({ type: 'offered' }, () => db.raw( - `WHERE l.Status = ? + `WHERE l.Status = ? AND EXISTS ( SELECT 1 FROM offer o @@ -362,8 +370,11 @@ const getListingsWithApplicants = async ( * @returns {Promise} - Returns the applicants. */ -const getApplicantsByContactCode = async (contactCode: string) => { - const result = await db('Applicant') +const getApplicantsByContactCode = async ( + contactCode: string, + dbConnection = db +) => { + const result = await dbConnection('Applicant') .where({ ContactCode: contactCode }) .select>('*') @@ -379,9 +390,10 @@ const getApplicantsByContactCode = async (contactCode: string) => { */ const getApplicantByContactCodeAndListingId = async ( contactCode: string, - listingId: number + listingId: number, + dbConnection = db ) => { - const result = await db('Applicant') + const result = await dbConnection('Applicant') .where({ ContactCode: contactCode, ListingId: listingId, @@ -400,8 +412,12 @@ const getApplicantByContactCodeAndListingId = async ( * @param {number} listingId - The ID of the listing the applicant belongs to. * @returns {Promise} - Returns true if applicant belongs to listing, false if not. */ -const applicationExists = async (contactCode: string, listingId: number) => { - const result = await db('applicant') +const applicationExists = async ( + contactCode: string, + listingId: number, + dbConnection = db +) => { + const result = await dbConnection('applicant') .where({ ContactCode: contactCode, ListingId: listingId, @@ -415,9 +431,9 @@ const applicationExists = async (contactCode: string, listingId: number) => { return true } -const getExpiredListings = async () => { +const getExpiredListings = async (dbConnection = db) => { const currentDate = new Date() - const listings = await db('listing') + const listings = await dbConnection('listing') .where('PublishedTo', '<', currentDate) .andWhere('Status', '=', ListingStatus.Active) return listings @@ -464,10 +480,11 @@ const updateListingStatuses = async ( } const deleteListing = async ( - listingId: number + listingId: number, + dbConnection = db ): Promise> => { try { - await db('listing').delete().where('Id', listingId) + await dbConnection('listing').delete().where('Id', listingId) return { ok: true, data: null } } catch (err) { logger.error(err, 'listingAdapter.deleteListing') diff --git a/src/services/lease-service/adapters/offer-adapter.ts b/src/services/lease-service/adapters/offer-adapter.ts index bd78850b..34806b94 100644 --- a/src/services/lease-service/adapters/offer-adapter.ts +++ b/src/services/lease-service/adapters/offer-adapter.ts @@ -127,6 +127,7 @@ export async function create( }, } } catch (err) { + console.log('err: ', err) logger.error(err, 'Error creating offer') return { ok: false, err: 'unknown' } } @@ -147,9 +148,10 @@ type GetOffersForContactQueryResult = Array< > export async function getOffersForContact( - contactCode: string + contactCode: string, + dbConnection = db ): Promise> { - const rows = await db + const rows = await dbConnection .select( 'offer.*', 'listing.RentalObjectCode', @@ -206,9 +208,10 @@ export async function getOffersForContact( export async function getOfferByContactCodeAndOfferId( contactCode: string, - offerId: number + offerId: number, + dbConnection: Knex = db ): Promise { - const row = await db + const row = await dbConnection .select( 'offer.Id', 'offer.SentAt', @@ -274,10 +277,11 @@ export async function getOfferByContactCodeAndOfferId( } export async function getOfferByOfferId( - offerId: number + offerId: number, + dbConnection = db ): Promise> { try { - const row = await db + const row = await dbConnection .select( 'offer.Id', 'offer.SentAt', diff --git a/src/services/lease-service/get-tenant.ts b/src/services/lease-service/get-tenant.ts index d83b679c..ec2cfd5d 100644 --- a/src/services/lease-service/get-tenant.ts +++ b/src/services/lease-service/get-tenant.ts @@ -3,7 +3,6 @@ import { Lease, Tenant } from 'onecore-types' import { AdapterResult } from './adapters/types' import * as estateCodeAdapter from './adapters/xpand/estate-code-adapter' import * as tenantLeaseAdapter from './adapters/xpand/tenant-lease-adapter' -import * as xpandSoapAdapter from './adapters/xpand/xpand-soap-adapter' import * as priorityListService from './priority-list-service' import { logger } from 'onecore-utilities' diff --git a/src/services/lease-service/offer-service.ts b/src/services/lease-service/offer-service.ts index 33569f5b..60106dce 100644 --- a/src/services/lease-service/offer-service.ts +++ b/src/services/lease-service/offer-service.ts @@ -1,16 +1,18 @@ import { Knex } from 'knex' import { ApplicantStatus, ListingStatus, OfferStatus } from 'onecore-types' -import { db } from './adapters/db' import * as listingAdapter from './adapters/listing-adapter' import * as offerAdapter from './adapters/offer-adapter' import { AdapterResult } from './adapters/types' -export const acceptOffer = async (params: { - applicantId: number - listingId: number - offerId: number -}): Promise< +export const acceptOffer = async ( + db: Knex, + params: { + applicantId: number + listingId: number + offerId: number + } +): Promise< AdapterResult< null, | 'update-listing' @@ -65,11 +67,14 @@ export const acceptOffer = async (params: { } } -export const denyOffer = async (params: { - applicantId: number - offerId: number - listingId: number -}): Promise< +export const denyOffer = async ( + db: Knex, + params: { + applicantId: number + offerId: number + listingId: number + } +): Promise< AdapterResult< null, 'update-applicant' | 'update-offer' | 'update-offer-applicant' | 'unknown' diff --git a/src/services/lease-service/routes/listings.ts b/src/services/lease-service/routes/listings.ts index b3d2d669..8415fadd 100644 --- a/src/services/lease-service/routes/listings.ts +++ b/src/services/lease-service/routes/listings.ts @@ -16,6 +16,7 @@ import * as priorityListService from '../priority-list-service' import * as syncParkingSpacesFromXpandService from '../sync-internal-parking-space-listings-from-xpand' import * as listingAdapter from '../adapters/listing-adapter' import { getTenant } from '../get-tenant' +import { db } from '../adapters/db' /** * @swagger @@ -431,7 +432,7 @@ export const routes = (router: KoaRouter) => { .otherwise(() => undefined) const listingsWithApplicants = - await listingAdapter.getListingsWithApplicants(opts) + await listingAdapter.getListingsWithApplicants(db, opts) if (!listingsWithApplicants.ok) { logger.error( @@ -473,7 +474,7 @@ export const routes = (router: KoaRouter) => { router.post('/listings/sync-internal-from-xpand', async (ctx) => { const metadata = generateRouteMetadata(ctx) const result = - await syncParkingSpacesFromXpandService.syncInternalParkingSpaces() + await syncParkingSpacesFromXpandService.syncInternalParkingSpaces(db) if (!result.ok) { logger.error( diff --git a/src/services/lease-service/routes/offers.ts b/src/services/lease-service/routes/offers.ts index f256ce51..3d65fafa 100644 --- a/src/services/lease-service/routes/offers.ts +++ b/src/services/lease-service/routes/offers.ts @@ -298,7 +298,7 @@ export const routes = (router: KoaRouter) => { } } - const result = await offerService.acceptOffer({ + const result = await offerService.acceptOffer(db, { listingId: offer.data.listingId, applicantId: offer.data.offeredApplicant.id, offerId: offer.data.id, @@ -359,7 +359,7 @@ export const routes = (router: KoaRouter) => { } } - const result = await offerService.denyOffer({ + const result = await offerService.denyOffer(db, { applicantId: offer.data.offeredApplicant.id, offerId: offer.data.id, listingId: offer.data.listingId, diff --git a/src/services/lease-service/sync-internal-parking-space-listings-from-xpand.ts b/src/services/lease-service/sync-internal-parking-space-listings-from-xpand.ts index fc8d615b..4f3a12e6 100644 --- a/src/services/lease-service/sync-internal-parking-space-listings-from-xpand.ts +++ b/src/services/lease-service/sync-internal-parking-space-listings-from-xpand.ts @@ -1,4 +1,5 @@ import { Listing, ListingStatus } from 'onecore-types' +import { Knex } from 'knex' import { z } from 'zod' import * as xpandSoapAdapter from './adapters/xpand/xpand-soap-adapter' @@ -65,9 +66,9 @@ const ValidInternalParkingSpace = z.object({ * The ones who succeeded goes into an insertions.inserted array and the ones * who failed goes into an insertions.failed array. */ -export async function syncInternalParkingSpaces(): Promise< - AdapterResult -> { +export async function syncInternalParkingSpaces( + db: Knex +): Promise> { const result = await xpandSoapAdapter.getPublishedInternalParkingSpaces() if (!result.ok) { @@ -92,6 +93,7 @@ export async function syncInternalParkingSpaces(): Promise< } const insertions = await insertListings( + db, patchParkingSpacesWithResidentialAreaResult.data ) @@ -106,11 +108,11 @@ export async function syncInternalParkingSpaces(): Promise< } } -function insertListings(items: Array) { +function insertListings(db: Knex, items: Array) { return Promise.all( items.map(async (listing) => ({ listing, - insertionResult: await listingAdapter.createListing(listing), + insertionResult: await listingAdapter.createListing(listing, db), })) ) } diff --git a/src/services/lease-service/tests/adapters/application-profile-adapter.test.ts b/src/services/lease-service/tests/adapters/application-profile-adapter.test.ts index e41e81dc..44eae35a 100644 --- a/src/services/lease-service/tests/adapters/application-profile-adapter.test.ts +++ b/src/services/lease-service/tests/adapters/application-profile-adapter.test.ts @@ -1,150 +1,147 @@ import assert from 'node:assert' -import { db, migrate, teardown } from '../../adapters/db' import * as applicationProfileAdapter from '../../adapters/application-profile-adapter' -import { clearDb } from '../testUtils' +import { withContext } from '../testUtils' -beforeAll(async () => { - await migrate() -}) +describe('application-profile-adapter', () => { + describe(applicationProfileAdapter.create, () => { + it('creates application profile', () => + withContext(async (ctx) => { + const profile = await applicationProfileAdapter.create(ctx.db, { + contactCode: '1234', + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + housingType: 'foo', + housingTypeDescription: 'bar', + landlord: 'baz', + }) + assert(profile.ok) + + const inserted = await applicationProfileAdapter.getByContactCode( + ctx.db, + profile.data.contactCode + ) + + assert(inserted.ok) + expect(inserted).toMatchObject({ ok: true, data: profile.data }) + })) + + it('fails if existing profile for contact code already exists', () => + withContext(async (ctx) => { + const profile = await applicationProfileAdapter.create(ctx.db, { + contactCode: '1234', + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + }) -afterEach(async () => { - await clearDb(db) -}) + assert(profile.ok) -afterAll(async () => { - await teardown() -}) + const duplicate = await applicationProfileAdapter.create(ctx.db, { + contactCode: '1234', + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + }) -describe('application-profile-adapter', () => { - describe(applicationProfileAdapter.create, () => { - it('creates application profile', async () => { - const profile = await applicationProfileAdapter.create(db, { - contactCode: '1234', - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - housingType: 'foo', - housingTypeDescription: 'bar', - landlord: 'baz', - }) - assert(profile.ok) - - const inserted = await applicationProfileAdapter.getByContactCode( - db, - profile.data.contactCode - ) - - assert(inserted.ok) - expect(inserted).toMatchObject({ ok: true, data: profile.data }) - }) - - it('fails if existing profile for contact code already exists', async () => { - const profile = await applicationProfileAdapter.create(db, { - contactCode: '1234', - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - }) - - assert(profile.ok) - - const duplicate = await applicationProfileAdapter.create(db, { - contactCode: '1234', - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - }) - - expect(duplicate).toMatchObject({ - ok: false, - err: 'conflict-contact-code', - }) - }) + expect(duplicate).toMatchObject({ + ok: false, + err: 'conflict-contact-code', + }) + })) }) describe(applicationProfileAdapter.getByContactCode, () => { - it('returns err if not found', async () => { - const result = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - expect(result).toMatchObject({ ok: false, err: 'not-found' }) - }) - - it('gets application profile', async () => { - await applicationProfileAdapter.create(db, { - contactCode: '1234', - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - }) - - const result = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - - expect(result).toMatchObject({ - ok: true, - data: { - id: expect.any(Number), + it('returns err if not found', () => + withContext(async (ctx) => { + const result = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + expect(result).toMatchObject({ ok: false, err: 'not-found' }) + })) + + it('gets application profile', () => + withContext(async (ctx) => { + await applicationProfileAdapter.create(ctx.db, { contactCode: '1234', + expiresAt: new Date(), numAdults: 1, numChildren: 1, - expiresAt: expect.any(Date), - createdAt: expect.any(Date), - }, - }) - }) + }) + + const result = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + + expect(result).toMatchObject({ + ok: true, + data: { + id: expect.any(Number), + contactCode: '1234', + numAdults: 1, + numChildren: 1, + expiresAt: expect.any(Date), + createdAt: expect.any(Date), + }, + }) + })) }) describe(applicationProfileAdapter.update, () => { - it('returns err if no update', async () => { - const result = await applicationProfileAdapter.update( - db, - 'contact-code', - { - expiresAt: new Date(), + it('returns err if no update', () => + withContext(async (ctx) => { + const result = await applicationProfileAdapter.update( + ctx.db, + 'contact-code', + { + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + } + ) + + expect(result).toMatchObject({ ok: false, err: 'no-update' }) + })) + + it('updates application profile', () => + withContext(async (ctx) => { + const profile = await applicationProfileAdapter.create(ctx.db, { + contactCode: '1234', + expiresAt: null, numAdults: 1, numChildren: 1, - } - ) - - expect(result).toMatchObject({ ok: false, err: 'no-update' }) - }) - - it('updates application profile', async () => { - const profile = await applicationProfileAdapter.create(db, { - contactCode: '1234', - expiresAt: null, - numAdults: 1, - numChildren: 1, - }) - - assert(profile.ok) - await applicationProfileAdapter.update(db, profile.data.contactCode, { - expiresAt: new Date(), - numAdults: 2, - numChildren: 2, - }) - - const updated = await applicationProfileAdapter.getByContactCode( - db, - profile.data.contactCode - ) - - expect(updated).toMatchObject({ - ok: true, - data: { - id: expect.any(Number), - contactCode: '1234', - numAdults: 2, - numChildren: 2, - expiresAt: expect.any(Date), - createdAt: expect.any(Date), - }, - }) - }) + }) + + assert(profile.ok) + await applicationProfileAdapter.update( + ctx.db, + profile.data.contactCode, + { + expiresAt: new Date(), + numAdults: 2, + numChildren: 2, + } + ) + + const updated = await applicationProfileAdapter.getByContactCode( + ctx.db, + profile.data.contactCode + ) + + expect(updated).toMatchObject({ + ok: true, + data: { + id: expect.any(Number), + contactCode: '1234', + numAdults: 2, + numChildren: 2, + expiresAt: expect.any(Date), + createdAt: expect.any(Date), + }, + }) + })) }) }) diff --git a/src/services/lease-service/tests/adapters/application-profile-housing-reference-adapter.test.ts b/src/services/lease-service/tests/adapters/application-profile-housing-reference-adapter.test.ts index 4ebac3e6..1010f4b3 100644 --- a/src/services/lease-service/tests/adapters/application-profile-housing-reference-adapter.test.ts +++ b/src/services/lease-service/tests/adapters/application-profile-housing-reference-adapter.test.ts @@ -1,23 +1,11 @@ import assert from 'node:assert' +import { Knex } from 'knex' -import { db, migrate, teardown } from '../../adapters/db' import * as adapter from '../../adapters/application-profile-housing-reference-adapter' import * as applicationProfileAdapter from '../../adapters/application-profile-adapter' -import { clearDb } from '../testUtils' +import { withContext } from '../testUtils' -beforeAll(async () => { - await migrate() -}) - -afterEach(async () => { - await clearDb(db) -}) - -afterAll(async () => { - await teardown() -}) - -async function createApplicationProfile() { +async function createApplicationProfile(db: Knex) { const profile = await applicationProfileAdapter.create(db, { contactCode: '1234', expiresAt: new Date(), @@ -31,114 +19,121 @@ async function createApplicationProfile() { describe('application-profile-housing-reference-adapter', () => { describe(adapter.create, () => { - it('inserts application profile housing reference', async () => { - const applicationProfile = await createApplicationProfile() - const reference = await adapter.create(db, { - applicationProfileId: applicationProfile.id, - email: null, - phone: '01234', - reviewStatus: 'foo', - reviewStatusReason: null, - reviewedAt: null, - expiresAt: new Date(), - }) - - assert(reference.ok) - - const inserted = await adapter.findByApplicationProfileId( - db, - applicationProfile.id - ) - - assert(inserted.ok) - - expect(inserted.data).toEqual({ - id: expect.any(Number), - applicationProfileId: applicationProfile.id, - email: null, - phone: '01234', - reviewStatus: 'foo', - reviewStatusReason: null, - reviewedAt: null, - expiresAt: expect.any(Date), - createdAt: expect.any(Date), - }) - }) - - it('rejects duplicate application profile id', async () => { - const applicationProfile = await createApplicationProfile() - const reference = await adapter.create(db, { - applicationProfileId: applicationProfile.id, - email: null, - phone: '01234', - reviewStatus: 'foo', - reviewStatusReason: null, - reviewedAt: null, - expiresAt: new Date(), - }) - - const duplicateReference = await adapter.create(db, { - applicationProfileId: applicationProfile.id, - email: null, - phone: '01234', - reviewStatus: 'foo', - reviewStatusReason: null, - reviewedAt: null, - expiresAt: new Date(), - }) - - assert(reference.ok) - expect(duplicateReference).toEqual({ - ok: false, - err: 'conflict-application-profile-id', - }) - }) + it('inserts application profile housing reference', () => + withContext(async (ctx) => { + const applicationProfile = await createApplicationProfile(ctx.db) + const reference = await adapter.create(ctx.db, { + applicationProfileId: applicationProfile.id, + email: null, + phone: '01234', + reviewStatus: 'foo', + reviewStatusReason: null, + reviewedAt: null, + expiresAt: new Date(), + }) + + assert(reference.ok) + + const inserted = await adapter.findByApplicationProfileId( + ctx.db, + applicationProfile.id + ) + + assert(inserted.ok) + + expect(inserted.data).toEqual({ + id: expect.any(Number), + applicationProfileId: applicationProfile.id, + email: null, + phone: '01234', + reviewStatus: 'foo', + reviewStatusReason: null, + reviewedAt: null, + expiresAt: expect.any(Date), + createdAt: expect.any(Date), + }) + })) + + it('rejects duplicate application profile id', () => + withContext(async (ctx) => { + const applicationProfile = await createApplicationProfile(ctx.db) + const reference = await adapter.create(ctx.db, { + applicationProfileId: applicationProfile.id, + email: null, + phone: '01234', + reviewStatus: 'foo', + reviewStatusReason: null, + reviewedAt: null, + expiresAt: new Date(), + }) + + const duplicateReference = await adapter.create(ctx.db, { + applicationProfileId: applicationProfile.id, + email: null, + phone: '01234', + reviewStatus: 'foo', + reviewStatusReason: null, + reviewedAt: null, + expiresAt: new Date(), + }) + + assert(reference.ok) + expect(duplicateReference).toEqual({ + ok: false, + err: 'conflict-application-profile-id', + }) + })) }) describe(adapter.update, () => { - it('returns err if no update', async () => { - const result = await adapter.update(db, 1, { - email: null, - phone: '01234', - reviewStatus: 'foo', - reviewStatusReason: null, - reviewedAt: null, - expiresAt: new Date(), - }) - - expect(result).toMatchObject({ ok: false, err: 'no-update' }) - }) - - it('updates application profile', async () => { - const profile = await createApplicationProfile() - - await adapter.create(db, { - applicationProfileId: profile.id, - email: null, - phone: '01234', - reviewStatus: 'foo', - reviewStatusReason: null, - reviewedAt: null, - expiresAt: new Date(), - }) - - await adapter.update(db, profile.id, { - email: null, - phone: '01234', - reviewStatus: 'bar', - reviewStatusReason: null, - reviewedAt: new Date(), - expiresAt: new Date(), - }) - - const updated = await adapter.findByApplicationProfileId(db, profile.id) - - assert(updated.ok) - expect(updated.data).toMatchObject({ - applicationProfileId: profile.id, - reviewStatus: 'bar', - reviewedAt: expect.any(Date), - }) - }) + it('returns err if no update', () => + withContext(async (ctx) => { + const result = await adapter.update(ctx.db, 1, { + email: null, + phone: '01234', + reviewStatus: 'foo', + reviewStatusReason: null, + reviewedAt: null, + expiresAt: new Date(), + }) + + expect(result).toMatchObject({ ok: false, err: 'no-update' }) + })) + + it('updates application profile', () => + withContext(async (ctx) => { + const profile = await createApplicationProfile(ctx.db) + + await adapter.create(ctx.db, { + applicationProfileId: profile.id, + email: null, + phone: '01234', + reviewStatus: 'foo', + reviewStatusReason: null, + reviewedAt: null, + expiresAt: new Date(), + }) + + await adapter.update(ctx.db, profile.id, { + email: null, + phone: '01234', + reviewStatus: 'bar', + reviewStatusReason: null, + reviewedAt: new Date(), + expiresAt: new Date(), + }) + + const updated = await adapter.findByApplicationProfileId( + ctx.db, + profile.id + ) + + assert(updated.ok) + expect(updated.data).toMatchObject({ + applicationProfileId: profile.id, + reviewStatus: 'bar', + reviewedAt: expect.any(Date), + }) + })) }) }) diff --git a/src/services/lease-service/tests/adapters/listing-adapter/delete-listing.test.ts b/src/services/lease-service/tests/adapters/listing-adapter/delete-listing.test.ts index fdebb861..3c20b008 100644 --- a/src/services/lease-service/tests/adapters/listing-adapter/delete-listing.test.ts +++ b/src/services/lease-service/tests/adapters/listing-adapter/delete-listing.test.ts @@ -1,62 +1,80 @@ import assert from 'node:assert' -import { migrate, db, teardown } from '../../../adapters/db' import * as listingAdapter from '../../../adapters/listing-adapter' import * as factory from '../../factories' -import { clearDb } from '../../testUtils' - -beforeAll(async () => { - await migrate() -}) -afterEach(async () => { - await clearDb(db) -}) -afterAll(async () => { - await teardown() -}) +import { withContext } from '../../testUtils' describe(listingAdapter.deleteListing, () => { - it('does not delete if listing has applicants', async () => { - const listing = await listingAdapter.createListing(factory.listing.build()) - assert(listing.ok) - - await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const deletion = await listingAdapter.deleteListing(listing.data.id) - const deletedListing = await listingAdapter.getListingById(listing.data.id) - - expect(deletion).toEqual({ ok: false, err: 'conflict' }) - expect(deletedListing).not.toBe(undefined) - }) - - it('deletes if there are no other entities relying on it', async () => { - const listing = await listingAdapter.createListing(factory.listing.build()) - assert(listing.ok) - - const deletion = await listingAdapter.deleteListing(listing.data.id) - const deletedListing = await listingAdapter.getListingById(listing.data.id) - - expect(deletion).toMatchObject({ ok: true }) - expect(deletedListing).toBe(undefined) - }) - - it('only deletes one', async () => { - const listing_1 = await listingAdapter.createListing( - factory.listing.build() - ) - const listing_2 = await listingAdapter.createListing( - factory.listing.build() - ) - assert(listing_1.ok) - assert(listing_2.ok) - - const deletion = await listingAdapter.deleteListing(listing_1.data.id) - const remainingListings = await listingAdapter.getListingsWithApplicants() - assert(remainingListings.ok) - - expect(deletion).toMatchObject({ ok: true }) - expect(remainingListings.data).toHaveLength(1) - }) + it('does not delete if listing has applicants', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build(), + ctx.db + ) + assert(listing.ok) + + await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const deletion = await listingAdapter.deleteListing( + listing.data.id, + ctx.db + ) + const deletedListing = await listingAdapter.getListingById( + listing.data.id, + ctx.db + ) + + expect(deletion).toEqual({ ok: false, err: 'conflict' }) + expect(deletedListing).not.toBe(undefined) + })) + + it('deletes if there are no other entities relying on it', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build(), + ctx.db + ) + assert(listing.ok) + + const deletion = await listingAdapter.deleteListing( + listing.data.id, + ctx.db + ) + const deletedListing = await listingAdapter.getListingById( + listing.data.id, + ctx.db + ) + + expect(deletion).toMatchObject({ ok: true }) + expect(deletedListing).toBe(undefined) + })) + + it('only deletes one', () => + withContext(async (ctx) => { + const listing_1 = await listingAdapter.createListing( + factory.listing.build(), + ctx.db + ) + const listing_2 = await listingAdapter.createListing( + factory.listing.build(), + ctx.db + ) + assert(listing_1.ok) + assert(listing_2.ok) + + const deletion = await listingAdapter.deleteListing( + listing_1.data.id, + ctx.db + ) + const remainingListings = await listingAdapter.getListingsWithApplicants( + ctx.db + ) + assert(remainingListings.ok) + + expect(deletion).toMatchObject({ ok: true }) + expect(remainingListings.data).toHaveLength(1) + })) }) diff --git a/src/services/lease-service/tests/adapters/listing-adapter/get-listings-with-applicants.test.ts b/src/services/lease-service/tests/adapters/listing-adapter/get-listings-with-applicants.test.ts index 6745df3c..c4ee6886 100644 --- a/src/services/lease-service/tests/adapters/listing-adapter/get-listings-with-applicants.test.ts +++ b/src/services/lease-service/tests/adapters/listing-adapter/get-listings-with-applicants.test.ts @@ -1,334 +1,377 @@ import assert from 'node:assert' import { ListingStatus, OfferStatus } from 'onecore-types' -import { db, migrate, teardown } from '../../../adapters/db' import * as listingAdapter from '../../../adapters/listing-adapter' import * as offerAdapter from '../../../adapters/offer-adapter' import * as factory from './../../factories' -import { clearDb } from '../../testUtils' - -beforeAll(async () => { - await migrate() -}) - -afterEach(async () => { - await clearDb(db) -}) - -afterAll(async () => { - await teardown() -}) +import { withContext } from '../../testUtils' describe(listingAdapter.getListingsWithApplicants, () => { - it('returns a formatted list of listings and corresponding applicants', async () => { - const listing1 = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - const listing2 = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '2' }) - ) - assert(listing1.ok) - assert(listing2.ok) - await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing1.data.id }) - ) - await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing2.data.id }) - ) - const listings = await listingAdapter.getListingsWithApplicants() - assert(listings.ok) - const [fst, snd] = listings.data - - expect(fst.applicants).toHaveLength(1) - expect(fst.applicants?.[0]?.listingId).toBe(fst.id) - - expect(snd.applicants).toHaveLength(1) - expect(snd.applicants?.[0]?.listingId).toBe(snd.id) - }) - - describe('filtering', () => { - it('only gets published listings', async () => { - const publishedListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, - }) - ) - - const _expiredListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Expired, - }) - ) - - assert(publishedListing.ok) - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'published' }, - }) - assert(listings.ok) - - expect(listings.data).toEqual([ - expect.objectContaining({ id: publishedListing.data.id }), - ]) - }) - - it('only gets ready-for-offer listings', async () => { - const listingWithoutOffer = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Expired, - }) - ) - - assert(listingWithoutOffer.ok) - const listingWithOffer = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Expired, - }) + it('returns a formatted list of listings and corresponding applicants', () => + withContext(async (ctx) => { + const listing1 = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db ) - - assert(listingWithOffer.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listingWithoutOffer.data.id }) + const listing2 = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '2' }), + ctx.db ) - - const expiredListingOffer = await offerAdapter.create(db, { - applicantId: applicant.id, - expiresAt: new Date(), - listingId: listingWithOffer.data.id, - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - listingId: listingWithOffer.data.id, - }), - ], - }) - - assert(expiredListingOffer.ok) - - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'ready-for-offer' }, - }) - assert(listings.ok) - - expect(listings.data).toEqual([ - expect.objectContaining({ id: listingWithoutOffer.data.id }), - ]) - }) - - it('ready-for-offer listings has applicants', async () => { - const listingWithApplicants = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Expired, - }) - ) - - assert(listingWithApplicants.ok) - const listingWithoutApplicants = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Expired, - }) + assert(listing1.ok) + assert(listing2.ok) + await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing1.data.id }), + ctx.db ) - - assert(listingWithoutApplicants.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listingWithApplicants.data.id }) + await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing2.data.id }), + ctx.db ) - - assert(applicant) - - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'ready-for-offer' }, - }) - + const listings = await listingAdapter.getListingsWithApplicants(ctx.db) assert(listings.ok) + const [fst, snd] = listings.data - expect(listings.data).toEqual([ - expect.objectContaining({ id: listingWithApplicants.data.id }), - ]) - }) + expect(fst.applicants).toHaveLength(1) + expect(fst.applicants?.[0]?.listingId).toBe(fst.id) - it('only gets offered listings', async () => { - const listingWithoutOffer = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Expired, - }) - ) - - assert(listingWithoutOffer.ok) - const listingWithOffer = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Expired, - }) - ) - - assert(listingWithOffer.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listingWithOffer.data.id }) - ) + expect(snd.applicants).toHaveLength(1) + expect(snd.applicants?.[0]?.listingId).toBe(snd.id) + })) - const expiredListingOffer = await offerAdapter.create(db, { - applicantId: applicant.id, - expiresAt: new Date(), - listingId: listingWithOffer.data.id, - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - listingId: listingWithOffer.data.id, + describe('filtering', () => { + it('only gets published listings', () => + withContext(async (ctx) => { + const publishedListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, }), - ], - }) - - assert(expiredListingOffer.ok) - - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'offered' }, - }) - assert(listings.ok) - - expect(listings.data).toEqual([ - expect.objectContaining({ id: listingWithOffer.data.id }), - ]) - }) - - it('offered listings has active offer', async () => { - const listingWithExpiredOffer = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Expired, - }) - ) - assert(listingWithExpiredOffer.ok) - - const listingWithActiveOffer = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Expired, - }) - ) - assert(listingWithActiveOffer.ok) + ctx.db + ) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listingWithActiveOffer.data.id }) - ) - - const expiredOffer = await offerAdapter.create(db, { - applicantId: applicant.id, - expiresAt: new Date('1970-01-01'), - listingId: listingWithExpiredOffer.data.id, - status: OfferStatus.Expired, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - listingId: listingWithExpiredOffer.data.id, + await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Expired, }), - ], - }) - - const activeOffer = await offerAdapter.create(db, { - applicantId: applicant.id, - expiresAt: new Date(), - listingId: listingWithActiveOffer.data.id, - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - listingId: listingWithActiveOffer.data.id, + ctx.db + ) + + assert(publishedListing.ok) + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'published' }, + } + ) + assert(listings.ok) + + expect(listings.data).toEqual([ + expect.objectContaining({ id: publishedListing.data.id }), + ]) + })) + + it('only gets ready-for-offer listings', () => + withContext(async (ctx) => { + const listingWithoutOffer = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Expired, }), - ], - }) - - assert(expiredOffer.ok) - assert(activeOffer.ok) - - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'offered' }, - }) - assert(listings.ok) - - expect(listings.data).toEqual([ - expect.objectContaining({ id: listingWithActiveOffer.data.id }), - ]) - }) - - it('only gets historical listings', async () => { - const activeListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, + ctx.db + ) + + assert(listingWithoutOffer.ok) + const listingWithOffer = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Expired, + }), + ctx.db + ) + + assert(listingWithOffer.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listingWithoutOffer.data.id }), + ctx.db + ) + + const expiredListingOffer = await offerAdapter.create(ctx.db, { + applicantId: applicant.id, + expiresAt: new Date(), + listingId: listingWithOffer.data.id, + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + listingId: listingWithOffer.data.id, + }), + ], }) - ) - assert(activeListing.ok) - const assignedListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Assigned, + assert(expiredListingOffer.ok) + + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'ready-for-offer' }, + } + ) + assert(listings.ok) + + expect(listings.data).toEqual([ + expect.objectContaining({ id: listingWithoutOffer.data.id }), + ]) + })) + + it('ready-for-offer listings has applicants', () => + withContext(async (ctx) => { + const listingWithApplicants = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Expired, + }), + ctx.db + ) + + assert(listingWithApplicants.ok) + const listingWithoutApplicants = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Expired, + }), + ctx.db + ) + + assert(listingWithoutApplicants.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listingWithApplicants.data.id }), + ctx.db + ) + + assert(applicant) + + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'ready-for-offer' }, + } + ) + + assert(listings.ok) + + expect(listings.data).toEqual([ + expect.objectContaining({ id: listingWithApplicants.data.id }), + ]) + })) + + it('only gets offered listings', () => + withContext(async (ctx) => { + const listingWithoutOffer = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Expired, + }), + ctx.db + ) + + assert(listingWithoutOffer.ok) + const listingWithOffer = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Expired, + }), + ctx.db + ) + + assert(listingWithOffer.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listingWithOffer.data.id }), + ctx.db + ) + + const expiredListingOffer = await offerAdapter.create(ctx.db, { + applicantId: applicant.id, + expiresAt: new Date(), + listingId: listingWithOffer.data.id, + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + listingId: listingWithOffer.data.id, + }), + ], }) - ) - assert(assignedListing.ok) - await listingAdapter.createApplication( - factory.applicant.build({ listingId: assignedListing.data.id }) - ) + assert(expiredListingOffer.ok) + + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'offered' }, + } + ) + assert(listings.ok) + + expect(listings.data).toEqual([ + expect.objectContaining({ id: listingWithOffer.data.id }), + ]) + })) + + it('offered listings has active offer', () => + withContext(async (ctx) => { + const listingWithExpiredOffer = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Expired, + }), + ctx.db + ) + assert(listingWithExpiredOffer.ok) + + const listingWithActiveOffer = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Expired, + }), + ctx.db + ) + assert(listingWithActiveOffer.ok) - const closedListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.Closed, + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ + listingId: listingWithActiveOffer.data.id, + }), + ctx.db + ) + + const expiredOffer = await offerAdapter.create(ctx.db, { + applicantId: applicant.id, + expiresAt: new Date('1970-01-01'), + listingId: listingWithExpiredOffer.data.id, + status: OfferStatus.Expired, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + listingId: listingWithExpiredOffer.data.id, + }), + ], }) - ) - assert(closedListing.ok) - await listingAdapter.createApplication( - factory.applicant.build({ listingId: closedListing.data.id }) - ) - - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'historical' }, - }) - assert(listings.ok) - expect(listings.data).toEqual([ - expect.objectContaining({ id: assignedListing.data.id }), - expect.objectContaining({ id: closedListing.data.id }), - ]) - }) - - it('only gets needs-republish listings', async () => { - const activeListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, + const activeOffer = await offerAdapter.create(ctx.db, { + applicantId: applicant.id, + expiresAt: new Date(), + listingId: listingWithActiveOffer.data.id, + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + listingId: listingWithActiveOffer.data.id, + }), + ], }) - ) - assert(activeListing.ok) - const needsRepublishListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - status: ListingStatus.NoApplicants, - }) - ) + assert(expiredOffer.ok) + assert(activeOffer.ok) + + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'offered' }, + } + ) + assert(listings.ok) + + expect(listings.data).toEqual([ + expect.objectContaining({ id: listingWithActiveOffer.data.id }), + ]) + })) + + it('only gets historical listings', () => + withContext(async (ctx) => { + const activeListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) + + assert(activeListing.ok) + const assignedListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Assigned, + }), + ctx.db + ) + + assert(assignedListing.ok) + await listingAdapter.createApplication( + factory.applicant.build({ listingId: assignedListing.data.id }), + ctx.db + ) + + const closedListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.Closed, + }), + ctx.db + ) + + assert(closedListing.ok) + await listingAdapter.createApplication( + factory.applicant.build({ listingId: closedListing.data.id }), + ctx.db + ) + + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'historical' }, + } + ) + assert(listings.ok) + expect(listings.data).toEqual([ + expect.objectContaining({ id: assignedListing.data.id }), + expect.objectContaining({ id: closedListing.data.id }), + ]) + })) + + it('only gets needs-republish listings', () => + withContext(async (ctx) => { + const activeListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) + + assert(activeListing.ok) + const needsRepublishListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + status: ListingStatus.NoApplicants, + }), + ctx.db + ) - assert(needsRepublishListing.ok) + assert(needsRepublishListing.ok) - const listings = await listingAdapter.getListingsWithApplicants({ - by: { type: 'needs-republish' }, - }) + const listings = await listingAdapter.getListingsWithApplicants( + ctx.db, + { + by: { type: 'needs-republish' }, + } + ) - assert(listings.ok) + assert(listings.ok) - expect(listings.data).toEqual([ - expect.objectContaining({ id: needsRepublishListing.data.id }), - ]) - }) + expect(listings.data).toEqual([ + expect.objectContaining({ id: needsRepublishListing.data.id }), + ]) + })) }) }) diff --git a/src/services/lease-service/tests/adapters/listing-adapter/index.test.ts b/src/services/lease-service/tests/adapters/listing-adapter/index.test.ts index 8e145e92..ff9df9fb 100644 --- a/src/services/lease-service/tests/adapters/listing-adapter/index.test.ts +++ b/src/services/lease-service/tests/adapters/listing-adapter/index.test.ts @@ -1,356 +1,400 @@ import { ApplicantStatus, ListingStatus } from 'onecore-types' import assert from 'node:assert' -import { db, migrate, teardown } from '../../../adapters/db' import * as listingAdapter from '../../../adapters/listing-adapter' import * as factory from './../../factories' -import { clearDb } from '../../testUtils' - -beforeAll(async () => { - await migrate() -}) - -afterEach(async () => { - await clearDb(db) -}) - -afterAll(async () => { - await teardown() -}) +import { withContext } from '../../testUtils' describe('listing-adapter', () => { describe(listingAdapter.createListing, () => { - it('inserts a new listing in the database', async () => { - const insertedListing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(insertedListing.ok) - const listingFromDatabase = await db('listing').first() - expect(listingFromDatabase).toBeDefined() - expect(listingFromDatabase.Id).toEqual(insertedListing.data.id) - }) - - it('fails on duplicate combination of ListingStatus.Active and RentalObjectCode', async () => { - await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, - }) - ) + it('inserts a new listing in the database', () => + withContext(async (ctx) => { + const insertedListing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(insertedListing.ok) + const listingFromDatabase = await ctx.db('listing').first() + expect(listingFromDatabase).toBeDefined() + expect(listingFromDatabase.Id).toEqual(insertedListing.data.id) + })) + + it('fails on duplicate combination of ListingStatus.Active and RentalObjectCode', () => + withContext(async (ctx) => { + await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) - const insertionResult = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, - }) - ) - - expect(insertionResult).toEqual({ - ok: false, - err: 'conflict-active-listing', - }) - }) - - it('allows duplicate combination of Listing status Assigned and RentalObjectCode', async () => { - await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Assigned, - }) - ) + const insertionResult = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) - const insertionResult = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, + expect(insertionResult).toEqual({ + ok: false, + err: 'conflict-active-listing', }) - ) - - expect(insertionResult).toMatchObject({ - ok: true, - }) - }) - - it('allows duplicate combination of Listing status Closed and RentalObjectCode', async () => { - await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Closed, - }) - ) + })) + + it('allows duplicate combination of Listing status Assigned and RentalObjectCode', () => + withContext(async (ctx) => { + await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Assigned, + }), + ctx.db + ) - const insertionResult = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, + const insertionResult = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) + + expect(insertionResult).toMatchObject({ + ok: true, }) - ) + })) + + it('allows duplicate combination of Listing status Closed and RentalObjectCode', () => + withContext(async (ctx) => { + await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Closed, + }), + ctx.db + ) - expect(insertionResult).toMatchObject({ - ok: true, - }) - }) - }) + const insertionResult = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) - describe(listingAdapter.getActiveListingByRentalObjectCode, () => { - it('returns an active listing by rental object code', async () => { - const insertedListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Active, - }) - ) - assert(insertedListing.ok) - const insertedListing2 = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - status: ListingStatus.Closed, + expect(insertionResult).toMatchObject({ + ok: true, }) - ) - assert(insertedListing2.ok) + })) + }) - const listingFromDatabase = - await listingAdapter.getActiveListingByRentalObjectCode( + describe(listingAdapter.getActiveListingByRentalObjectCode, () => { + it('returns an active listing by rental object code', () => + withContext(async (ctx) => { + const insertedListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Active, + }), + ctx.db + ) + assert(insertedListing.ok) + const insertedListing2 = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + status: ListingStatus.Closed, + }), + ctx.db + ) + assert(insertedListing2.ok) + + const listingFromDatabase = + await listingAdapter.getActiveListingByRentalObjectCode( + insertedListing.data.rentalObjectCode, + ctx.db + ) + expect(listingFromDatabase?.rentalObjectCode).toBeDefined() + expect(listingFromDatabase?.rentalObjectCode).toEqual( insertedListing.data.rentalObjectCode ) - expect(listingFromDatabase?.rentalObjectCode).toBeDefined() - expect(listingFromDatabase?.rentalObjectCode).toEqual( - insertedListing.data.rentalObjectCode - ) - expect(listingFromDatabase?.status).toEqual(ListingStatus.Active) - }) + expect(listingFromDatabase?.status).toEqual(ListingStatus.Active) + })) }) describe(listingAdapter.getListingById, () => { - it('returns a listing by id', async () => { - const insertedListing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - expect(insertedListing).toBeDefined() - assert(insertedListing.ok) - const listingFromDatabase = await listingAdapter.getListingById( - insertedListing.data.id - ) - expect(listingFromDatabase?.id).toBeDefined() - expect(listingFromDatabase?.id).toEqual(insertedListing.data.id) - }) + it('returns a listing by id', () => + withContext(async (ctx) => { + const insertedListing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + expect(insertedListing).toBeDefined() + assert(insertedListing.ok) + const listingFromDatabase = await listingAdapter.getListingById( + insertedListing.data.id, + ctx.db + ) + expect(listingFromDatabase?.id).toBeDefined() + expect(listingFromDatabase?.id).toEqual(insertedListing.data.id) + })) }) describe(listingAdapter.getApplicantById, () => { - it('returns an applicant by id', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - - assert(listing.ok) - const insertedApplicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - const applicantFromDatabase = await listingAdapter.getApplicantById( - insertedApplicant.id - ) - expect(applicantFromDatabase).toBeDefined() - expect(applicantFromDatabase?.id).toEqual(insertedApplicant.id) - }) + it('returns an applicant by id', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + + assert(listing.ok) + const insertedApplicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + const applicantFromDatabase = await listingAdapter.getApplicantById( + insertedApplicant.id, + ctx.db + ) + expect(applicantFromDatabase).toBeDefined() + expect(applicantFromDatabase?.id).toEqual(insertedApplicant.id) + })) }) describe(listingAdapter.createApplication, () => { - it('inserts a new application in the database', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const insertedApplicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - expect(insertedApplicant).toBeDefined() - const applicantFromDatabase = await db('applicant').first() - expect(applicantFromDatabase).toBeDefined() - expect(applicantFromDatabase.Id).toEqual(insertedApplicant.id) - }) + it('inserts a new application in the database', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const insertedApplicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + expect(insertedApplicant).toBeDefined() + const applicantFromDatabase = await ctx.db('applicant').first() + expect(applicantFromDatabase).toBeDefined() + expect(applicantFromDatabase.Id).toEqual(insertedApplicant.id) + })) }) describe(listingAdapter.updateApplicantStatus, () => { - it('updates an applicants status', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const insertedApplicant = await listingAdapter.createApplication( - factory.applicant.build({ - listingId: listing.data.id, - status: ApplicantStatus.Active, - }) - ) - - const result = await listingAdapter.updateApplicantStatus( - insertedApplicant.id, - ApplicantStatus.OfferAccepted - ) - expect(result.ok).toBe(true) - const updatedApplicant = await listingAdapter.getApplicantById( - insertedApplicant.id - ) - expect(updatedApplicant?.status).toEqual(ApplicantStatus.OfferAccepted) - }) + it('updates an applicants status', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const insertedApplicant = await listingAdapter.createApplication( + factory.applicant.build({ + listingId: listing.data.id, + status: ApplicantStatus.Active, + }), + ctx.db + ) + + const result = await listingAdapter.updateApplicantStatus( + insertedApplicant.id, + ApplicantStatus.OfferAccepted, + ctx.db + ) + expect(result.ok).toBe(true) + const updatedApplicant = await listingAdapter.getApplicantById( + insertedApplicant.id, + ctx.db + ) + expect(updatedApplicant?.status).toEqual(ApplicantStatus.OfferAccepted) + })) }) describe(listingAdapter.getApplicantsByContactCode, () => { - it('returns an applicant by contact code', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - - assert(listing.ok) - const insertedApplicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - const applicantFromDatabase = - await listingAdapter.getApplicantsByContactCode( - insertedApplicant.contactCode - ) - - expect(applicantFromDatabase).toBeDefined() - expect(applicantFromDatabase).toHaveLength(1) - if (applicantFromDatabase != undefined) { - expect(applicantFromDatabase[0]).toBeDefined() - expect(applicantFromDatabase[0].id).toEqual(insertedApplicant.id) - } - }) - - it('returns empty list for non existing applicant', async () => { - const applicantFromDatabase = - await listingAdapter.getApplicantsByContactCode( - 'NON_EXISTING_CONTACT_CODE' - ) - - expect(applicantFromDatabase).toEqual([]) - }) + it('returns an applicant by contact code', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + + assert(listing.ok) + const insertedApplicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + const applicantFromDatabase = + await listingAdapter.getApplicantsByContactCode( + insertedApplicant.contactCode, + ctx.db + ) + + expect(applicantFromDatabase).toBeDefined() + expect(applicantFromDatabase).toHaveLength(1) + if (applicantFromDatabase != undefined) { + expect(applicantFromDatabase[0]).toBeDefined() + expect(applicantFromDatabase[0].id).toEqual(insertedApplicant.id) + } + })) + + it('returns empty list for non existing applicant', () => + withContext(async (ctx) => { + const applicantFromDatabase = + await listingAdapter.getApplicantsByContactCode( + 'NON_EXISTING_CONTACT_CODE', + ctx.db + ) + + expect(applicantFromDatabase).toEqual([]) + })) }) describe(listingAdapter.getApplicantByContactCodeAndListingId, () => { - it('returns an applicant by contact code and listing id', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - - assert(listing.ok) - const insertedApplicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const applicantFromDatabase = - await listingAdapter.getApplicantByContactCodeAndListingId( - insertedApplicant.contactCode, - listing.data.id + it('returns an applicant by contact code and listing id', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db ) - expect(applicantFromDatabase).toBeDefined() - expect(applicantFromDatabase?.id).toEqual(insertedApplicant.id) - expect(applicantFromDatabase?.listingId).toEqual( - insertedApplicant.listingId - ) - }) - it('returns undefined for non existing applicant', async () => { - const applicantFromDatabase = - await listingAdapter.getApplicantByContactCodeAndListingId( - 'NON_EXISTING_CONTACT_CODE', - -1 + assert(listing.ok) + const insertedApplicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db ) - expect(applicantFromDatabase).toBeUndefined() - }) + const applicantFromDatabase = + await listingAdapter.getApplicantByContactCodeAndListingId( + insertedApplicant.contactCode, + listing.data.id, + ctx.db + ) + expect(applicantFromDatabase).toBeDefined() + expect(applicantFromDatabase?.id).toEqual(insertedApplicant.id) + expect(applicantFromDatabase?.listingId).toEqual( + insertedApplicant.listingId + ) + })) + + it('returns undefined for non existing applicant', () => + withContext(async (ctx) => { + const applicantFromDatabase = + await listingAdapter.getApplicantByContactCodeAndListingId( + 'NON_EXISTING_CONTACT_CODE', + -1, + ctx.db + ) + + expect(applicantFromDatabase).toBeUndefined() + })) }) describe(listingAdapter.applicationExists, () => { - it('returns true if application exists', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const insertedApplicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const applicantExists = await listingAdapter.applicationExists( - insertedApplicant.contactCode, - listing.data.id - ) - - expect(applicantExists).toBe(true) - }) - - it('returns false if application does not exist', async () => { - const applicantExists = await listingAdapter.applicationExists( - 'nonExistingContactCode', - 123456 - ) - - expect(applicantExists).toBe(false) - }) + it('returns true if application exists', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const insertedApplicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const applicantExists = await listingAdapter.applicationExists( + insertedApplicant.contactCode, + listing.data.id, + ctx.db + ) + + expect(applicantExists).toBe(true) + })) + + it('returns false if application does not exist', () => + withContext(async (ctx) => { + const applicantExists = await listingAdapter.applicationExists( + 'nonExistingContactCode', + 123456, + ctx.db + ) + + expect(applicantExists).toBe(false) + })) }) describe(listingAdapter.getExpiredListings, () => { - it('returns expired listings', async () => { - const today = new Date() - const oneWeekInTheFuture = new Date( - today.getTime() + 7 * 24 * 60 * 60 * 1000 - ) - //active listing - await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '1', - publishedTo: oneWeekInTheFuture, - }) - ) - - const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000) - const expiredListing = await listingAdapter.createListing( - factory.listing.build({ - rentalObjectCode: '2', - publishedTo: oneWeekAgo, - status: ListingStatus.Active, - }) - ) - - assert(expiredListing.ok) - const expiredListings = await listingAdapter.getExpiredListings() - expect(expiredListings).toBeDefined() - expect(expiredListings).toHaveLength(1) - expect(expiredListings[0].Id).toEqual(expiredListing.data.id) - expect(expiredListings[0].RentalObjectCode).toEqual( - expiredListing.data.rentalObjectCode - ) - }) + it('returns expired listings', () => + withContext(async (ctx) => { + const today = new Date() + const oneWeekInTheFuture = new Date( + today.getTime() + 7 * 24 * 60 * 60 * 1000 + ) + //active listing + await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '1', + publishedTo: oneWeekInTheFuture, + }), + ctx.db + ) + + const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000) + const expiredListing = await listingAdapter.createListing( + factory.listing.build({ + rentalObjectCode: '2', + publishedTo: oneWeekAgo, + status: ListingStatus.Active, + }), + ctx.db + ) + + assert(expiredListing.ok) + const expiredListings = await listingAdapter.getExpiredListings(ctx.db) + expect(expiredListings).toBeDefined() + expect(expiredListings).toHaveLength(1) + expect(expiredListings[0].Id).toEqual(expiredListing.data.id) + expect(expiredListings[0].RentalObjectCode).toEqual( + expiredListing.data.rentalObjectCode + ) + })) }) describe(listingAdapter.updateListingStatuses, () => { - it('updates the status of listings from an array of listing ids', async () => { - const listing1 = await listingAdapter.createListing( - factory.listing.build() - ) - - const listing2 = await listingAdapter.createListing( - factory.listing.build() - ) - - assert(listing1.ok) - assert(listing2.ok) - await listingAdapter.createListing(factory.listing.build({})) - - const result = await listingAdapter.updateListingStatuses( - [listing1.data.id, listing2.data.id], - ListingStatus.Expired - ) - expect(result.ok).toEqual(true) - const listing1FromDatabase = await listingAdapter.getListingById( - listing1.data.id - ) - const listing2FromDatabase = await listingAdapter.getListingById( - listing2.data.id - ) - expect(listing1FromDatabase?.status).toEqual(ListingStatus.Expired) - expect(listing2FromDatabase?.status).toEqual(ListingStatus.Expired) - }) + it('updates the status of listings from an array of listing ids', () => + withContext(async (ctx) => { + const listing1 = await listingAdapter.createListing( + factory.listing.build(), + ctx.db + ) + + const listing2 = await listingAdapter.createListing( + factory.listing.build(), + ctx.db + ) + + assert(listing1.ok) + assert(listing2.ok) + await listingAdapter.createListing(factory.listing.build(), ctx.db) + + const result = await listingAdapter.updateListingStatuses( + [listing1.data.id, listing2.data.id], + ListingStatus.Expired, + ctx.db + ) + expect(result.ok).toEqual(true) + const listing1FromDatabase = await listingAdapter.getListingById( + listing1.data.id, + ctx.db + ) + const listing2FromDatabase = await listingAdapter.getListingById( + listing2.data.id, + ctx.db + ) + expect(listing1FromDatabase?.status).toEqual(ListingStatus.Expired) + expect(listing2FromDatabase?.status).toEqual(ListingStatus.Expired) + })) }) }) diff --git a/src/services/lease-service/tests/adapters/offer-adapter.test.ts b/src/services/lease-service/tests/adapters/offer-adapter.test.ts index 37e4958c..1ffd1442 100644 --- a/src/services/lease-service/tests/adapters/offer-adapter.test.ts +++ b/src/services/lease-service/tests/adapters/offer-adapter.test.ts @@ -1,505 +1,526 @@ import { OfferStatus } from 'onecore-types' import assert from 'node:assert' -import { Knex } from 'knex' -import { db, migrate, teardown } from '../../adapters/db' import * as offerAdapter from '../../adapters/offer-adapter' import * as factory from './../factories' import * as listingAdapter from '../../adapters/listing-adapter' -import { clearDb } from '../testUtils' +import { withContext } from '../testUtils' -beforeAll(async () => { - await migrate() -}) +describe('offer-adapter', () => { + describe(offerAdapter.create, () => { + it('fails gracefully if applicant not found', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) -afterEach(async () => { - await clearDb(db) -}) + assert(listing.ok) + const offer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + id: -1, + }), + ], + listingId: listing.data.id, + applicantId: -1, + }) + expect(offer.ok).toBe(false) + })) + + it('inserts a new offer in the database', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant_one = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) -afterAll(async () => { - await teardown() -}) + const applicant_two = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) -describe('offer-adapter', () => { - describe(offerAdapter.create, () => { - it('fails gracefully if applicant not found', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - - assert(listing.ok) - const offer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - id: -1, - }), - ], - listingId: listing.data.id, - applicantId: -1, - }) - expect(offer.ok).toBe(false) - }) - - it('inserts a new offer in the database', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant_one = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const applicant_two = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const insertedOffer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: applicant_one.id, - priority: 2, - }), - factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: applicant_two.id, - priority: 2, - sortOrder: 2, - }), - ], - listingId: listing.data.id, - applicantId: applicant_one.id, - }) - - assert(insertedOffer.ok) - const [offerFromDb] = await db.raw( - `SELECT * from offer WHERE id = ${insertedOffer.data.id}` - ) - - expect(offerFromDb.ListingId).toEqual(listing.data.id) - expect(offerFromDb.ApplicantId).toEqual(applicant_one.id) - }) - - it('inserts offer applicants', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant_one = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - const applicant_two = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offerApplicant = factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: applicant_one.id, - priority: 2, - sortOrder: 1, - }) - - const offerApplicantWithPriorityNull = factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: applicant_two.id, - priority: null, - sortOrder: 1, - }) - - const insertedOffer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [offerApplicant, offerApplicantWithPriorityNull], - listingId: listing.data.id, - applicantId: applicant_one.id, - }) - - assert(insertedOffer.ok) - const selectedApplicantsFromDb = await db.raw( - 'SELECT * FROM offer_applicant ORDER BY sortOrder ASC' - ) - - expect(selectedApplicantsFromDb).toHaveLength(2) - expect(selectedApplicantsFromDb).toEqual([ - { - id: expect.any(Number), + const insertedOffer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + listingId: listing.data.id, + applicantId: applicant_one.id, + priority: 2, + }), + factory.offerApplicant.build({ + listingId: listing.data.id, + applicantId: applicant_two.id, + priority: 2, + sortOrder: 2, + }), + ], listingId: listing.data.id, - offerId: insertedOffer.data.id, applicantId: applicant_one.id, - applicantStatus: offerApplicant.status, - applicantApplicationType: offerApplicant.applicationType, - applicantQueuePoints: offerApplicant.queuePoints, - applicantAddress: offerApplicant.address, - applicantHasParkingSpace: true, - applicantHousingLeaseStatus: offerApplicant.housingLeaseStatus, - applicantPriority: offerApplicant.priority, - createdAt: expect.any(Date), + }) + + assert(insertedOffer.ok) + const [offerFromDb] = await ctx.db.raw( + `SELECT * from offer WHERE id = ${insertedOffer.data.id}` + ) + + expect(offerFromDb.ListingId).toEqual(listing.data.id) + expect(offerFromDb.ApplicantId).toEqual(applicant_one.id) + })) + + it('inserts offer applicants', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant_one = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + const applicant_two = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offerApplicant = factory.offerApplicant.build({ + listingId: listing.data.id, + applicantId: applicant_one.id, + priority: 2, sortOrder: 1, - }, - { - id: expect.any(Number), + }) + + const offerApplicantWithPriorityNull = factory.offerApplicant.build({ listingId: listing.data.id, - offerId: insertedOffer.data.id, applicantId: applicant_two.id, - applicantStatus: offerApplicantWithPriorityNull.status, - applicantApplicationType: - offerApplicantWithPriorityNull.applicationType, - applicantQueuePoints: offerApplicantWithPriorityNull.queuePoints, - applicantAddress: offerApplicantWithPriorityNull.address, - applicantHasParkingSpace: true, - applicantHousingLeaseStatus: - offerApplicantWithPriorityNull.housingLeaseStatus, - applicantPriority: offerApplicantWithPriorityNull.priority, - createdAt: expect.any(Date), - sortOrder: 2, - }, - ]) - }) + priority: null, + sortOrder: 1, + }) + + const insertedOffer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [offerApplicant, offerApplicantWithPriorityNull], + listingId: listing.data.id, + applicantId: applicant_one.id, + }) + + assert(insertedOffer.ok) + const selectedApplicantsFromDb = await ctx.db.raw( + 'SELECT * FROM offer_applicant ORDER BY sortOrder ASC' + ) + + expect(selectedApplicantsFromDb).toHaveLength(2) + expect(selectedApplicantsFromDb).toEqual([ + { + id: expect.any(Number), + listingId: listing.data.id, + offerId: insertedOffer.data.id, + applicantId: applicant_one.id, + applicantStatus: offerApplicant.status, + applicantApplicationType: offerApplicant.applicationType, + applicantQueuePoints: offerApplicant.queuePoints, + applicantAddress: offerApplicant.address, + applicantHasParkingSpace: true, + applicantHousingLeaseStatus: offerApplicant.housingLeaseStatus, + applicantPriority: offerApplicant.priority, + createdAt: expect.any(Date), + sortOrder: 1, + }, + { + id: expect.any(Number), + listingId: listing.data.id, + offerId: insertedOffer.data.id, + applicantId: applicant_two.id, + applicantStatus: offerApplicantWithPriorityNull.status, + applicantApplicationType: + offerApplicantWithPriorityNull.applicationType, + applicantQueuePoints: offerApplicantWithPriorityNull.queuePoints, + applicantAddress: offerApplicantWithPriorityNull.address, + applicantHasParkingSpace: true, + applicantHousingLeaseStatus: + offerApplicantWithPriorityNull.housingLeaseStatus, + applicantPriority: offerApplicantWithPriorityNull.priority, + createdAt: expect.any(Date), + sortOrder: 2, + }, + ]) + })) }) describe(offerAdapter.getOffersForContact, () => { - it('gets the offers for a contact', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const insertOffer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - }), - ], - listingId: listing.data.id, - applicantId: applicant.id, - }) - - assert(insertOffer.ok) - - const offersFromDb = await offerAdapter.getOffersForContact( - applicant.contactCode - ) - expect(offersFromDb).toHaveLength(1) - expect(offersFromDb[0].offeredApplicant.contactCode).toEqual( - applicant.contactCode - ) - }) - - it('returns empty list if applicant has no offers', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const insertOffer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - }), - ], - listingId: listing.data.id, - applicantId: applicant.id, - }) + it('gets the offers for a contact', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) - assert(insertOffer.ok) + const insertOffer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + }), + ], + listingId: listing.data.id, + applicantId: applicant.id, + }) + + assert(insertOffer.ok) + + const offersFromDb = await offerAdapter.getOffersForContact( + applicant.contactCode, + ctx.db + ) + expect(offersFromDb).toHaveLength(1) + expect(offersFromDb[0].offeredApplicant.contactCode).toEqual( + applicant.contactCode + ) + })) + + it('returns empty list if applicant has no offers', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) - const offersFromDb = await offerAdapter.getOffersForContact( - 'NON_EXISTING_CONTACT_CODE' - ) + const insertOffer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + }), + ], + listingId: listing.data.id, + applicantId: applicant.id, + }) + + assert(insertOffer.ok) - expect(offersFromDb).toEqual([]) - }) + const offersFromDb = await offerAdapter.getOffersForContact( + 'NON_EXISTING_CONTACT_CODE', + ctx.db + ) + + expect(offersFromDb).toEqual([]) + })) }) describe(offerAdapter.getOfferByContactCodeAndOfferId, () => { - it('gets the offer a a contact', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - }), - ], - listingId: listing.data.id, - applicantId: applicant.id, - }) - - assert(offer.ok) - const offersFromDb = await offerAdapter.getOfferByContactCodeAndOfferId( - applicant.contactCode, - offer.data.id - ) - expect(offersFromDb).toBeDefined() - expect(offersFromDb?.id).toEqual(offer.data.id) - expect(offersFromDb?.offeredApplicant.contactCode).toEqual( - applicant.contactCode - ) - }) - - it('returns empty object if offer does not exist', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offersFromDb = await offerAdapter.getOfferByContactCodeAndOfferId( - applicant.contactCode, - 123456 - ) - expect(offersFromDb).toBeUndefined() - }) - - it('returns empty object if applicant does not exist', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - }), - ], - listingId: listing.data.id, - applicantId: applicant.id, - }) - - assert(offer.ok) - const offersFromDb = await offerAdapter.getOfferByContactCodeAndOfferId( - 'NON_EXISTING_CONTACT_CODE', - offer.data.id - ) - expect(offersFromDb).toBeUndefined() - }) + it('gets the offer a a contact', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + }), + ], + listingId: listing.data.id, + applicantId: applicant.id, + }) + + assert(offer.ok) + const offersFromDb = await offerAdapter.getOfferByContactCodeAndOfferId( + applicant.contactCode, + offer.data.id, + ctx.db + ) + expect(offersFromDb).toBeDefined() + expect(offersFromDb?.id).toEqual(offer.data.id) + expect(offersFromDb?.offeredApplicant.contactCode).toEqual( + applicant.contactCode + ) + })) + + it('returns empty object if offer does not exist', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offersFromDb = await offerAdapter.getOfferByContactCodeAndOfferId( + applicant.contactCode, + 123456, + ctx.db + ) + expect(offersFromDb).toBeUndefined() + })) + + it('returns empty object if applicant does not exist', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + }), + ], + listingId: listing.data.id, + applicantId: applicant.id, + }) + + assert(offer.ok) + const offersFromDb = await offerAdapter.getOfferByContactCodeAndOfferId( + 'NON_EXISTING_CONTACT_CODE', + offer.data.id, + ctx.db + ) + expect(offersFromDb).toBeUndefined() + })) }) describe(offerAdapter.getOfferByOfferId, () => { - it('gets an offer by id', async () => { - const listingRes = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - - assert(listingRes.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listingRes.data.id }) - ) - - const offer = await offerAdapter.create(db, { - expiresAt: new Date(), - status: OfferStatus.Active, - selectedApplicants: [ - factory.offerApplicant.build({ - applicantId: applicant.id, - contactCode: applicant.contactCode, - }), - ], - listingId: listingRes.data.id, - applicantId: applicant.id, - }) - - assert(offer.ok) - const res = await offerAdapter.getOfferByOfferId(offer.data.id) - - expect(res.ok).toBeTruthy() - if (res.ok) { - expect(res.data.id).toEqual(offer.data.id) - expect(res.data.offeredApplicant.contactCode).toEqual( - applicant.contactCode + it('gets an offer by id', () => + withContext(async (ctx) => { + const listingRes = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + + assert(listingRes.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listingRes.data.id }), + ctx.db ) - } - }) - it('returns empty object if offer does not exist', async () => { - const res = await offerAdapter.getOfferByOfferId(123456) - expect(res.ok).toBeFalsy() - if (!res.ok) expect(res.err).toBe('not-found') - }) + const offer = await offerAdapter.create(ctx.db, { + expiresAt: new Date(), + status: OfferStatus.Active, + selectedApplicants: [ + factory.offerApplicant.build({ + applicantId: applicant.id, + contactCode: applicant.contactCode, + }), + ], + listingId: listingRes.data.id, + applicantId: applicant.id, + }) + + assert(offer.ok) + const res = await offerAdapter.getOfferByOfferId(offer.data.id, ctx.db) + + expect(res.ok).toBeTruthy() + if (res.ok) { + expect(res.data.id).toEqual(offer.data.id) + expect(res.data.offeredApplicant.contactCode).toEqual( + applicant.contactCode + ) + } + })) + + it('returns empty object if offer does not exist', () => + withContext(async (ctx) => { + const res = await offerAdapter.getOfferByOfferId(123456, ctx.db) + expect(res.ok).toBeFalsy() + if (!res.ok) expect(res.err).toBe('not-found') + })) }) describe(offerAdapter.getOffersWithOfferApplicantsByListingId, () => { - it('fails correctly', async () => { - const res = await offerAdapter.getOffersWithOfferApplicantsByListingId( - { - raw: jest.fn().mockRejectedValueOnce('boom'), - } as unknown as Knex, - 1 - ) - expect(res).toEqual({ ok: false, err: 'unknown' }) - }) - - it('gets offers by listing id', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - - const applicants = [ - factory.applicant.build({ listingId: listing.data.id, name: 'A' }), - factory.applicant.build({ listingId: listing.data.id, name: 'B' }), - ] - - const insertedApplicants = await Promise.all( - applicants.map(listingAdapter.createApplication) - ) - - const insertedOffer_1 = await offerAdapter.create(db, { - status: OfferStatus.Active, - expiresAt: new Date(), - listingId: listing.data.id, - applicantId: insertedApplicants[0].id, - selectedApplicants: [ - factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: insertedApplicants[0].id, - }), - factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: insertedApplicants[1].id, - }), - ], - }) - - const insertedOffer_2 = await offerAdapter.create(db, { - status: OfferStatus.Expired, - expiresAt: new Date(), - listingId: listing.data.id, - applicantId: insertedApplicants[1].id, - selectedApplicants: [ - factory.offerApplicant.build({ - listingId: listing.data.id, - applicantId: insertedApplicants[1].id, - }), - ], - }) - - assert(insertedOffer_1.ok) - assert(insertedOffer_2.ok) - - const res = await offerAdapter.getOffersWithOfferApplicantsByListingId( - db, - listing.data.id - ) - assert(res.ok) - expect(res.data).toEqual([ - expect.objectContaining({ - id: insertedOffer_1.data.id, - sentAt: null, - expiresAt: expect.any(Date), - answeredAt: null, + it('gets offers by listing id', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + + const applicants = [ + factory.applicant.build({ listingId: listing.data.id, name: 'A' }), + factory.applicant.build({ listingId: listing.data.id, name: 'B' }), + ] + + const insertedApplicants = await Promise.all( + applicants.map((applicant) => + listingAdapter.createApplication(applicant, ctx.db) + ) + ) + + const insertedOffer_1 = await offerAdapter.create(ctx.db, { status: OfferStatus.Active, + expiresAt: new Date(), listingId: listing.data.id, - createdAt: expect.any(Date), - offeredApplicant: expect.objectContaining({ - id: insertedApplicants[0].id, - name: insertedApplicants[0].name, - contactCode: insertedApplicants[0].contactCode, - applicationDate: expect.any(Date), - applicationType: insertedApplicants[0].applicationType, - status: insertedApplicants[0].status, - listingId: listing.data.id, - nationalRegistrationNumber: - insertedApplicants[0].nationalRegistrationNumber, - }), + applicantId: insertedApplicants[0].id, selectedApplicants: [ - expect.objectContaining({ + factory.offerApplicant.build({ + listingId: listing.data.id, applicantId: insertedApplicants[0].id, - applicationDate: expect.any(Date), - name: insertedApplicants[0].name, }), - expect.objectContaining({ + factory.offerApplicant.build({ + listingId: listing.data.id, applicantId: insertedApplicants[1].id, - applicationDate: expect.any(Date), - name: insertedApplicants[1].name, }), ], - }), - expect.objectContaining({ - id: insertedOffer_2.data.id, + }) + + const insertedOffer_2 = await offerAdapter.create(ctx.db, { status: OfferStatus.Expired, + expiresAt: new Date(), listingId: listing.data.id, - offeredApplicant: expect.objectContaining({ - id: insertedApplicants[1].id, - }), + applicantId: insertedApplicants[1].id, selectedApplicants: [ - expect.objectContaining({ + factory.offerApplicant.build({ + listingId: listing.data.id, applicantId: insertedApplicants[1].id, - applicationDate: expect.any(Date), - name: insertedApplicants[1].name, }), ], - }), - ]) - }) + }) + + assert(insertedOffer_1.ok) + assert(insertedOffer_2.ok) + + const res = await offerAdapter.getOffersWithOfferApplicantsByListingId( + ctx.db, + listing.data.id + ) + assert(res.ok) + expect(res.data).toEqual([ + expect.objectContaining({ + id: insertedOffer_1.data.id, + sentAt: null, + expiresAt: expect.any(Date), + answeredAt: null, + status: OfferStatus.Active, + listingId: listing.data.id, + createdAt: expect.any(Date), + offeredApplicant: expect.objectContaining({ + id: insertedApplicants[0].id, + name: insertedApplicants[0].name, + contactCode: insertedApplicants[0].contactCode, + applicationDate: expect.any(Date), + applicationType: insertedApplicants[0].applicationType, + status: insertedApplicants[0].status, + listingId: listing.data.id, + nationalRegistrationNumber: + insertedApplicants[0].nationalRegistrationNumber, + }), + selectedApplicants: [ + expect.objectContaining({ + applicantId: insertedApplicants[0].id, + applicationDate: expect.any(Date), + name: insertedApplicants[0].name, + }), + expect.objectContaining({ + applicantId: insertedApplicants[1].id, + applicationDate: expect.any(Date), + name: insertedApplicants[1].name, + }), + ], + }), + expect.objectContaining({ + id: insertedOffer_2.data.id, + status: OfferStatus.Expired, + listingId: listing.data.id, + offeredApplicant: expect.objectContaining({ + id: insertedApplicants[1].id, + }), + selectedApplicants: [ + expect.objectContaining({ + applicantId: insertedApplicants[1].id, + applicationDate: expect.any(Date), + name: insertedApplicants[1].name, + }), + ], + }), + ]) + })) }) describe(offerAdapter.updateOfferSentAt, () => { - it('returns err if there was no update', async () => { - const res = await offerAdapter.updateOfferSentAt(db, 1, new Date()) - expect(res).toEqual({ ok: false, err: 'no-update' }) - }) - - it('updates sent at', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ rentalObjectCode: '1' }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - selectedApplicants: factory.offerApplicant.buildList(1, { + it('returns err if there was no update', () => + withContext(async (ctx) => { + const res = await offerAdapter.updateOfferSentAt(ctx.db, 1, new Date()) + expect(res).toEqual({ ok: false, err: 'no-update' }) + })) + + it('updates sent at', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ rentalObjectCode: '1' }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + selectedApplicants: factory.offerApplicant.buildList(1, { + applicantId: applicant.id, + }), applicantId: applicant.id, - }), - applicantId: applicant.id, - expiresAt: new Date(), - listingId: listing.data.id, - status: OfferStatus.Active, - }) - - assert(offer.ok) - - const res = await offerAdapter.updateOfferSentAt( - db, - offer.data.id, - new Date() - ) - expect(res).toEqual({ ok: true, data: null }) - const updatedOffer = await offerAdapter.getOfferByOfferId(offer.data.id) - assert(updatedOffer.ok) - expect(updatedOffer.data.sentAt).not.toBeNull() - }) + expiresAt: new Date(), + listingId: listing.data.id, + status: OfferStatus.Active, + }) + + assert(offer.ok) + + const res = await offerAdapter.updateOfferSentAt( + ctx.db, + offer.data.id, + new Date() + ) + expect(res).toEqual({ ok: true, data: null }) + const updatedOffer = await offerAdapter.getOfferByOfferId( + offer.data.id, + ctx.db + ) + assert(updatedOffer.ok) + expect(updatedOffer.data.sentAt).not.toBeNull() + })) }) }) diff --git a/src/services/lease-service/tests/offer-service.test.ts b/src/services/lease-service/tests/offer-service.test.ts index 4a38f7b8..3638be65 100644 --- a/src/services/lease-service/tests/offer-service.test.ts +++ b/src/services/lease-service/tests/offer-service.test.ts @@ -4,409 +4,461 @@ import assert from 'node:assert' import * as factory from './factories' import * as listingAdapter from '../adapters/listing-adapter' import * as offerAdapter from '../adapters/offer-adapter' -import { db, migrate, teardown } from '../adapters/db' import * as service from '../offer-service' -import { clearDb } from './testUtils' - -beforeAll(async () => { - await migrate() -}) - -beforeEach(async () => { - await clearDb(db) -}) - -afterAll(async () => { - await teardown() -}) +import { withContext } from './testUtils' afterEach(jest.restoreAllMocks) describe('acceptOffer', () => { - it('returns gracefully if listing update fails', async () => { - const updateListingStatusSpy = jest.spyOn( - listingAdapter, - 'updateListingStatuses' - ) - - updateListingStatusSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - - assert(offer.ok) - - const res = await service.acceptOffer({ - applicantId: applicant.id, - listingId: listing.data.id, - offerId: offer.data.id, - }) - - expect(updateListingStatusSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: false, err: 'update-listing' }) - }) - - it('rollbacks listing status change if update applicant fails', async () => { - const updateApplicantStatusSpy = jest.spyOn( - listingAdapter, - 'updateApplicantStatus' - ) - - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - - updateApplicantStatusSpy.mockResolvedValueOnce({ - ok: false, - err: 'unknown', - }) - - assert(offer.ok) - - const res = await service.acceptOffer({ - applicantId: applicant.id, - listingId: listing.data.id, - offerId: offer.data.id, - }) - - expect(updateApplicantStatusSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: false, err: 'update-applicant' }) - - const listingFromDB = await listingAdapter.getListingById(listing.data.id) - expect(listingFromDB?.status).toBe(listing.data.status) - }) - - it('rollbacks listing status change and applicant status change if update offer fails', async () => { - const updateOfferSpy = jest.spyOn(offerAdapter, 'updateOfferAnsweredStatus') - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - - updateOfferSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) - - assert(offer.ok) - - const res = await service.acceptOffer({ - applicantId: applicant.id, - listingId: listing.data.id, - offerId: offer.data.id, - }) - - expect(updateOfferSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: false, err: 'update-offer' }) - - const listingFromDB = await listingAdapter.getListingById(listing.data.id) - const applicantFromDB = await listingAdapter.getApplicantById(applicant.id) - expect(listingFromDB?.status).toBe(listing.data.status) - expect(applicantFromDB?.status).toBe(applicant?.status) - }) - - it('updates listing, applicant and offer', async () => { - const updateListingStatusSpy = jest.spyOn( - listingAdapter, - 'updateListingStatuses' - ) - - const updateApplicantStatusSpy = jest.spyOn( - listingAdapter, - 'updateApplicantStatus' - ) - - const updateOfferSpy = jest.spyOn(offerAdapter, 'updateOfferAnsweredStatus') - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - - assert(offer.ok) - - const res = await service.acceptOffer({ - applicantId: applicant.id, - listingId: listing.data.id, - offerId: offer.data.id, - }) - - expect(updateListingStatusSpy).toHaveBeenCalled() - expect(updateApplicantStatusSpy).toHaveBeenCalled() - expect(updateOfferSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: true, data: null }) - - assert(offer.ok) - - const updatedListing = await listingAdapter.getListingById(listing.data.id) - const updatedApplicant = await listingAdapter.getApplicantById(applicant.id) - const updatedOffer = await offerAdapter.getOfferByOfferId(offer.data.id) - assert(updatedOffer.ok) - - expect(updatedListing?.status).toBe(ListingStatus.Assigned) - expect(updatedApplicant?.status).toBe(ApplicantStatus.OfferAccepted) - // TODO: Offer status is incorrectly a string because of db column type!! - expect(Number(updatedOffer.data.status)).toBe(OfferStatus.Accepted) - }) - - it('updates offer applicant', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const initialApplicantStatus = ApplicantStatus.Active - const offeredApplicant = factory.offerApplicant.build({ - status: initialApplicantStatus, - applicantId: applicant.id, - }) - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [offeredApplicant], - expiresAt: new Date(), - }) - - assert(offer.ok) - - const [offerApplicantInDbBeforeAccept] = await db('offer_applicant') - - expect(offerApplicantInDbBeforeAccept.applicantStatus).toEqual( - initialApplicantStatus - ) - - const res = await service.acceptOffer({ - applicantId: applicant.id, - listingId: listing.data.id, - offerId: offer.data.id, - }) - - expect(res.ok).toBe(true) - const [offerApplicantInDbAfterAccept] = await db('offer_applicant') - - expect(offerApplicantInDbAfterAccept.applicantStatus).toEqual( - ApplicantStatus.OfferAccepted - ) - }) + it('returns gracefully if listing update fails', () => + withContext(async (ctx) => { + const updateListingStatusSpy = jest.spyOn( + listingAdapter, + 'updateListingStatuses' + ) + + updateListingStatusSpy.mockResolvedValueOnce({ + ok: false, + err: 'unknown', + }) + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + + assert(offer.ok) + + const res = await service.acceptOffer(ctx.db, { + applicantId: applicant.id, + listingId: listing.data.id, + offerId: offer.data.id, + }) + + expect(updateListingStatusSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: false, err: 'update-listing' }) + })) + + it('rollbacks listing status change if update applicant fails', () => + withContext(async (ctx) => { + const updateApplicantStatusSpy = jest.spyOn( + listingAdapter, + 'updateApplicantStatus' + ) + + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + + updateApplicantStatusSpy.mockResolvedValueOnce({ + ok: false, + err: 'unknown', + }) + + assert(offer.ok) + + const res = await service.acceptOffer(ctx.db, { + applicantId: applicant.id, + listingId: listing.data.id, + offerId: offer.data.id, + }) + + expect(updateApplicantStatusSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: false, err: 'update-applicant' }) + + const listingFromDB = await listingAdapter.getListingById( + listing.data.id, + ctx.db + ) + expect(listingFromDB?.status).toBe(listing.data.status) + })) + + it('rollbacks listing status change and applicant status change if update offer fails', () => + withContext(async (ctx) => { + const updateOfferSpy = jest.spyOn( + offerAdapter, + 'updateOfferAnsweredStatus' + ) + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + + updateOfferSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) + + assert(offer.ok) + + const res = await service.acceptOffer(ctx.db, { + applicantId: applicant.id, + listingId: listing.data.id, + offerId: offer.data.id, + }) + + expect(updateOfferSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: false, err: 'update-offer' }) + + const listingFromDB = await listingAdapter.getListingById( + listing.data.id, + ctx.db + ) + const applicantFromDB = await listingAdapter.getApplicantById( + applicant.id, + ctx.db + ) + expect(listingFromDB?.status).toBe(listing.data.status) + expect(applicantFromDB?.status).toBe(applicant?.status) + })) + + it('updates listing, applicant and offer', () => + withContext(async (ctx) => { + const updateListingStatusSpy = jest.spyOn( + listingAdapter, + 'updateListingStatuses' + ) + + const updateApplicantStatusSpy = jest.spyOn( + listingAdapter, + 'updateApplicantStatus' + ) + + const updateOfferSpy = jest.spyOn( + offerAdapter, + 'updateOfferAnsweredStatus' + ) + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + + assert(offer.ok) + + const res = await service.acceptOffer(ctx.db, { + applicantId: applicant.id, + listingId: listing.data.id, + offerId: offer.data.id, + }) + + expect(updateListingStatusSpy).toHaveBeenCalled() + expect(updateApplicantStatusSpy).toHaveBeenCalled() + expect(updateOfferSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: true, data: null }) + + assert(offer.ok) + + const updatedListing = await listingAdapter.getListingById( + listing.data.id, + ctx.db + ) + const updatedApplicant = await listingAdapter.getApplicantById( + applicant.id, + ctx.db + ) + const updatedOffer = await offerAdapter.getOfferByOfferId( + offer.data.id, + ctx.db + ) + assert(updatedOffer.ok) + + expect(updatedListing?.status).toBe(ListingStatus.Assigned) + expect(updatedApplicant?.status).toBe(ApplicantStatus.OfferAccepted) + expect(Number(updatedOffer.data.status)).toBe(OfferStatus.Accepted) + })) + + it('updates offer applicant', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const initialApplicantStatus = ApplicantStatus.Active + const offeredApplicant = factory.offerApplicant.build({ + status: initialApplicantStatus, + applicantId: applicant.id, + }) + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [offeredApplicant], + expiresAt: new Date(), + }) + + assert(offer.ok) + + const [offerApplicantInDbBeforeAccept] = await ctx.db('offer_applicant') + + expect(offerApplicantInDbBeforeAccept.applicantStatus).toEqual( + initialApplicantStatus + ) + + const res = await service.acceptOffer(ctx.db, { + applicantId: applicant.id, + listingId: listing.data.id, + offerId: offer.data.id, + }) + + expect(res.ok).toBe(true) + const [offerApplicantInDbAfterAccept] = await ctx.db('offer_applicant') + + expect(offerApplicantInDbAfterAccept.applicantStatus).toEqual( + ApplicantStatus.OfferAccepted + ) + })) }) describe('denyOffer', () => { - it('returns gracefully if offer update fails', async () => { - const updateOfferStatusSpy = jest.spyOn( - offerAdapter, - 'updateOfferAnsweredStatus' - ) - - updateOfferStatusSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) - - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - assert(offer.ok) - - const res = await service.denyOffer({ - applicantId: applicant.id, - offerId: offer.data.id, - listingId: listing.data.id, - }) - - expect(updateOfferStatusSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: false, err: 'update-offer' }) - }) - - it('rollbacks applicant status change if update offer fails', async () => { - const updateOfferStatusSpy = jest.spyOn( - offerAdapter, - 'updateOfferAnsweredStatus' - ) - - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - - assert(offer.ok) - updateOfferStatusSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) - const res = await service.denyOffer({ - applicantId: applicant.id, - offerId: offer.data.id, - listingId: listing.data.id, - }) - - expect(updateOfferStatusSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: false, err: 'update-offer' }) - - const applicantFromDb = await listingAdapter.getApplicantById(applicant.id) - expect(applicantFromDb?.status).toBe(applicant.status) - }) - - it('updates applicant and offer', async () => { - const updateApplicantStatusSpy = jest.spyOn( - listingAdapter, - 'updateApplicantStatus' - ) - - const updateOfferSpy = jest.spyOn(offerAdapter, 'updateOfferAnsweredStatus') - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [ - factory.offerApplicant.build({ applicantId: applicant.id }), - ], - expiresAt: new Date(), - }) - - assert(offer.ok) - - const res = await service.denyOffer({ - applicantId: applicant.id, - offerId: offer.data.id, - listingId: listing.data.id, - }) - - expect(updateApplicantStatusSpy).toHaveBeenCalled() - expect(updateOfferSpy).toHaveBeenCalled() - expect(res).toEqual({ ok: true, data: null }) - - const updatedApplicant = await listingAdapter.getApplicantById(applicant.id) - const updatedOffer = await offerAdapter.getOfferByOfferId(offer.data.id) - assert(updatedOffer.ok) - - expect(updatedApplicant?.status).toBe(ApplicantStatus.OfferDeclined) - expect(updatedOffer.data.status).toBe(OfferStatus.Declined) - }) - - it('updates offer applicant', async () => { - const listing = await listingAdapter.createListing( - factory.listing.build({ status: ListingStatus.Expired }) - ) - assert(listing.ok) - const applicant = await listingAdapter.createApplication( - factory.applicant.build({ listingId: listing.data.id }) - ) - - const initialApplicantStatus = ApplicantStatus.Active - const offeredApplicant = factory.offerApplicant.build({ - status: initialApplicantStatus, - applicantId: applicant.id, - }) - const offer = await offerAdapter.create(db, { - status: OfferStatus.Active, - listingId: listing.data.id, - applicantId: applicant.id, - selectedApplicants: [offeredApplicant], - expiresAt: new Date(), - }) - - assert(offer.ok) - - const [offerApplicantInDbBeforeAccept] = await db('offer_applicant') - - expect(offerApplicantInDbBeforeAccept.applicantStatus).toEqual( - initialApplicantStatus - ) - - const res = await service.denyOffer({ - applicantId: applicant.id, - listingId: listing.data.id, - offerId: offer.data.id, - }) - - expect(res.ok).toBe(true) - const [offerApplicantInDbAfterAccept] = await db('offer_applicant') - - expect(offerApplicantInDbAfterAccept.applicantStatus).toEqual( - ApplicantStatus.OfferDeclined - ) - }) + it('returns gracefully if offer update fails', () => + withContext(async (ctx) => { + const updateOfferStatusSpy = jest.spyOn( + offerAdapter, + 'updateOfferAnsweredStatus' + ) + + updateOfferStatusSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) + + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + assert(offer.ok) + + const res = await service.denyOffer(ctx.db, { + applicantId: applicant.id, + offerId: offer.data.id, + listingId: listing.data.id, + }) + + expect(updateOfferStatusSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: false, err: 'update-offer' }) + })) + + it('rollbacks applicant status change if update offer fails', () => + withContext(async (ctx) => { + const updateOfferStatusSpy = jest.spyOn( + offerAdapter, + 'updateOfferAnsweredStatus' + ) + + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + + assert(offer.ok) + updateOfferStatusSpy.mockResolvedValueOnce({ ok: false, err: 'unknown' }) + const res = await service.denyOffer(ctx.db, { + applicantId: applicant.id, + offerId: offer.data.id, + listingId: listing.data.id, + }) + + expect(updateOfferStatusSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: false, err: 'update-offer' }) + + const applicantFromDb = await listingAdapter.getApplicantById( + applicant.id, + ctx.db + ) + expect(applicantFromDb?.status).toBe(applicant.status) + })) + + it('updates applicant and offer', () => + withContext(async (ctx) => { + const updateApplicantStatusSpy = jest.spyOn( + listingAdapter, + 'updateApplicantStatus' + ) + + const updateOfferSpy = jest.spyOn( + offerAdapter, + 'updateOfferAnsweredStatus' + ) + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [ + factory.offerApplicant.build({ applicantId: applicant.id }), + ], + expiresAt: new Date(), + }) + + assert(offer.ok) + + const res = await service.denyOffer(ctx.db, { + applicantId: applicant.id, + offerId: offer.data.id, + listingId: listing.data.id, + }) + + expect(updateApplicantStatusSpy).toHaveBeenCalled() + expect(updateOfferSpy).toHaveBeenCalled() + expect(res).toEqual({ ok: true, data: null }) + + const updatedApplicant = await listingAdapter.getApplicantById( + applicant.id, + ctx.db + ) + const updatedOffer = await offerAdapter.getOfferByOfferId( + offer.data.id, + ctx.db + ) + assert(updatedOffer.ok) + + expect(updatedApplicant?.status).toBe(ApplicantStatus.OfferDeclined) + expect(updatedOffer.data.status).toBe(OfferStatus.Declined) + })) + + it('updates offer applicant', () => + withContext(async (ctx) => { + const listing = await listingAdapter.createListing( + factory.listing.build({ status: ListingStatus.Expired }), + ctx.db + ) + assert(listing.ok) + const applicant = await listingAdapter.createApplication( + factory.applicant.build({ listingId: listing.data.id }), + ctx.db + ) + + const initialApplicantStatus = ApplicantStatus.Active + const offeredApplicant = factory.offerApplicant.build({ + status: initialApplicantStatus, + applicantId: applicant.id, + }) + const offer = await offerAdapter.create(ctx.db, { + status: OfferStatus.Active, + listingId: listing.data.id, + applicantId: applicant.id, + selectedApplicants: [offeredApplicant], + expiresAt: new Date(), + }) + + assert(offer.ok) + + const [offerApplicantInDbBeforeAccept] = await ctx.db('offer_applicant') + + expect(offerApplicantInDbBeforeAccept.applicantStatus).toEqual( + initialApplicantStatus + ) + + const res = await service.denyOffer(ctx.db, { + applicantId: applicant.id, + listingId: listing.data.id, + offerId: offer.data.id, + }) + + expect(res.ok).toBe(true) + const [offerApplicantInDbAfterAccept] = await ctx.db('offer_applicant') + + expect(offerApplicantInDbAfterAccept.applicantStatus).toEqual( + ApplicantStatus.OfferDeclined + ) + })) }) diff --git a/src/services/lease-service/tests/routes/listings.test.ts b/src/services/lease-service/tests/routes/listings.test.ts index 44e7d30d..0bc42568 100644 --- a/src/services/lease-service/tests/routes/listings.test.ts +++ b/src/services/lease-service/tests/routes/listings.test.ts @@ -97,9 +97,12 @@ describe('GET /listings-with-applicants', () => { const res = await request(app.callback()).get( '/listings-with-applicants?type=published' ) - expect(getListingsWithApplicantsSpy).toHaveBeenCalledWith({ - by: { type: 'published' }, - }) + expect(getListingsWithApplicantsSpy).toHaveBeenCalledWith( + expect.anything(), + { + by: { type: 'published' }, + } + ) expect(res.status).toBe(200) expect(res.body).toEqual({ content: [expect.objectContaining({ id: expect.any(Number) })], @@ -116,7 +119,10 @@ describe('GET /listings-with-applicants', () => { '/listings-with-applicants?type=invalid-value' ) - expect(getListingsWithApplicantsSpy).toHaveBeenCalledWith(undefined) + expect(getListingsWithApplicantsSpy).toHaveBeenCalledWith( + expect.anything(), + undefined + ) expect(res.status).toBe(200) expect(res.body).toEqual({ content: [expect.objectContaining({ id: expect.any(Number) })], diff --git a/src/services/lease-service/tests/sync-internal-parking-space-listings-from-xpand.test.ts b/src/services/lease-service/tests/sync-internal-parking-space-listings-from-xpand.test.ts index 6c4e4670..b4f89c2f 100644 --- a/src/services/lease-service/tests/sync-internal-parking-space-listings-from-xpand.test.ts +++ b/src/services/lease-service/tests/sync-internal-parking-space-listings-from-xpand.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert' -import { db, migrate, teardown } from '../adapters/db' import * as service from '../sync-internal-parking-space-listings-from-xpand' import * as xpandSoapAdapter from '../adapters/xpand/xpand-soap-adapter' import * as leaseAdapter from '../adapters/xpand/tenant-lease-adapter' import * as factories from './factories' +import { withContext } from './testUtils' const getResidentialAreaSpy = jest.spyOn( leaseAdapter, @@ -42,135 +42,136 @@ describe(service.parseInternalParkingSpacesToInsertableListings, () => { }) describe(service.syncInternalParkingSpaces, () => { - beforeAll(async () => { - await migrate() - }) - - afterEach(async () => { - await db('listing').del() + afterEach(() => { jest.resetAllMocks() }) - afterAll(async () => { - await teardown() - }) - - it('inserts parking spaces as listings', async () => { - getResidentialAreaSpy.mockResolvedValue({ - ok: true, - data: { code: 'foo', caption: 'bar' }, - }) - - const internalParkingSpaceMocks = - factories.soapInternalParkingSpace.buildList(10) - const soapSpy = jest - .spyOn(xpandSoapAdapter, 'getPublishedInternalParkingSpaces') - .mockResolvedValueOnce({ + it('inserts parking spaces as listings', () => + withContext(async (ctx) => { + getResidentialAreaSpy.mockResolvedValue({ ok: true, - data: internalParkingSpaceMocks, + data: { code: 'foo', caption: 'bar' }, }) - const result = await service.syncInternalParkingSpaces() + const internalParkingSpaceMocks = + factories.soapInternalParkingSpace.buildList(10) + const soapSpy = jest + .spyOn(xpandSoapAdapter, 'getPublishedInternalParkingSpaces') + .mockResolvedValueOnce({ + ok: true, + data: internalParkingSpaceMocks, + }) + + const result = await service.syncInternalParkingSpaces(ctx.db) + + expect(soapSpy).toHaveBeenCalledTimes(1) + assert(result.ok) + expect(result.data.insertions.failed).toHaveLength(0) + expect(result.data.insertions.inserted).toHaveLength( + internalParkingSpaceMocks.length + ) + + const insertedListings = ( + await ctx.db('listing').select('rentalObjectCode') + ).map((v) => v.rentalObjectCode) + + expect(insertedListings.length).toEqual(internalParkingSpaceMocks.length) + })) + + it('fails with error if fail to patch with residential data', () => + withContext(async (ctx) => { + getResidentialAreaSpy.mockRejectedValue({ + ok: false, + err: null, + }) - expect(soapSpy).toHaveBeenCalledTimes(1) - assert(result.ok) - expect(result.data.insertions.failed).toHaveLength(0) - expect(result.data.insertions.inserted).toHaveLength( - internalParkingSpaceMocks.length - ) + const internalParkingSpaceMocks = + factories.soapInternalParkingSpace.buildList(10) - const insertedListings = ( - await db('listing').select('rentalObjectCode') - ).map((v) => v.rentalObjectCode) + const soapSpy = jest + .spyOn(xpandSoapAdapter, 'getPublishedInternalParkingSpaces') + .mockResolvedValueOnce({ + ok: true, + data: internalParkingSpaceMocks, + }) - expect(insertedListings.length).toEqual(internalParkingSpaceMocks.length) - }) + const result = await service.syncInternalParkingSpaces(ctx.db) - it('fails with error if fail to patch with residential data', async () => { - getResidentialAreaSpy.mockRejectedValue({ - ok: false, - err: null, - }) + expect(soapSpy).toHaveBeenCalledTimes(1) + expect(result).toEqual({ ok: false, err: 'get-residential-area' }) + })) - const internalParkingSpaceMocks = - factories.soapInternalParkingSpace.buildList(10) - - const soapSpy = jest - .spyOn(xpandSoapAdapter, 'getPublishedInternalParkingSpaces') - .mockResolvedValueOnce({ + it('fails gracefully on duplicates and invalid entries and inserts the rest', () => + withContext(async (ctx) => { + getResidentialAreaSpy.mockResolvedValue({ ok: true, - data: internalParkingSpaceMocks, + data: { code: 'foo', caption: 'bar' }, }) - const result = await service.syncInternalParkingSpaces() + const valid_1 = factories.soapInternalParkingSpace.build({ + RentalObjectCode: '1', + }) + const valid_2 = factories.soapInternalParkingSpace.build({ + RentalObjectCode: '2', + }) + const valid_2_duplicate = factories.soapInternalParkingSpace.build({ + RentalObjectCode: '2', + }) + const invalid = factories.soapInternalParkingSpace.build({ + RentalObjectCode: '3', + PublishedTo: 'not a date', + }) - expect(soapSpy).toHaveBeenCalledTimes(1) - expect(result).toEqual({ ok: false, err: 'get-residential-area' }) - }) + const internalParkingSpaces = [ + valid_1, + valid_2, + valid_2_duplicate, + invalid, + ] + const soapSpy = jest + .spyOn(xpandSoapAdapter, 'getPublishedInternalParkingSpaces') + .mockResolvedValueOnce({ + ok: true, + data: internalParkingSpaces, + }) - it('fails gracefully on duplicates and invalid entries and inserts the rest', async () => { - getResidentialAreaSpy.mockResolvedValue({ - ok: true, - data: { code: 'foo', caption: 'bar' }, - }) + const result = await service.syncInternalParkingSpaces(ctx.db) - const valid_1 = factories.soapInternalParkingSpace.build({ - RentalObjectCode: '1', - }) - const valid_2 = factories.soapInternalParkingSpace.build({ - RentalObjectCode: '2', - }) - const valid_2_duplicate = factories.soapInternalParkingSpace.build({ - RentalObjectCode: '2', - }) - const invalid = factories.soapInternalParkingSpace.build({ - RentalObjectCode: '3', - PublishedTo: 'not a date', - }) + expect(soapSpy).toHaveBeenCalledTimes(1) - const internalParkingSpaces = [valid_1, valid_2, valid_2_duplicate, invalid] - const soapSpy = jest - .spyOn(xpandSoapAdapter, 'getPublishedInternalParkingSpaces') - .mockResolvedValueOnce({ + expect(result).toEqual({ ok: true, - data: internalParkingSpaces, - }) - - const result = await service.syncInternalParkingSpaces() - - expect(soapSpy).toHaveBeenCalledTimes(1) - - expect(result).toEqual({ - ok: true, - data: { - invalid: [ - expect.objectContaining({ - rentalObjectCode: '3', - errors: [{ code: 'invalid_date', path: 'PublishedTo' }], - }), - ], - insertions: { - failed: [ + data: { + invalid: [ expect.objectContaining({ - err: 'conflict-active-listing', - listing: expect.objectContaining({ rentalObjectCode: '2' }), + rentalObjectCode: '3', + errors: [{ code: 'invalid_date', path: 'PublishedTo' }], }), ], - inserted: expect.arrayContaining([ - expect.objectContaining({ rentalObjectCode: '1' }), - expect.objectContaining({ rentalObjectCode: '2' }), - ]), + insertions: { + failed: [ + expect.objectContaining({ + err: 'conflict-active-listing', + listing: expect.objectContaining({ rentalObjectCode: '2' }), + }), + ], + inserted: expect.arrayContaining([ + expect.objectContaining({ rentalObjectCode: '1' }), + expect.objectContaining({ rentalObjectCode: '2' }), + ]), + }, }, - }, - }) - - const insertedListings = await db('listing').select('rentalObjectCode') + }) - expect(insertedListings).toEqual( - expect.arrayContaining([ - expect.objectContaining({ rentalObjectCode: '1' }), - expect.objectContaining({ rentalObjectCode: '2' }), - ]) - ) - }) + const insertedListings = await ctx + .db('listing') + .select('rentalObjectCode') + + expect(insertedListings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rentalObjectCode: '1' }), + expect.objectContaining({ rentalObjectCode: '2' }), + ]) + ) + })) }) diff --git a/src/services/lease-service/tests/testUtils.ts b/src/services/lease-service/tests/testUtils.ts index a6dd470d..01f07b46 100644 --- a/src/services/lease-service/tests/testUtils.ts +++ b/src/services/lease-service/tests/testUtils.ts @@ -1,10 +1,22 @@ import { Knex } from 'knex' +import { createDbClient } from '../adapters/db' -export async function clearDb(db: Knex) { - await db('offer_applicant').del() - await db('offer').del() - await db('applicant').del() - await db('listing').del() - await db('application_profile_housing_reference').del() - await db('application_profile').del() +export async function withContext( + callback: (ctx: { db: Knex.Transaction }) => Promise +) { + const db = createDbClient() + try { + await db.transaction(async (trx) => { + await callback({ + db: trx, + }) + + throw 'rollback' + }) + } catch (e: unknown) { + if (e === 'rollback') return e + throw e + } finally { + await db.destroy() + } } diff --git a/src/services/lease-service/tests/update-or-create-application-profile.test.ts b/src/services/lease-service/tests/update-or-create-application-profile.test.ts index 91c72aa2..64329a45 100644 --- a/src/services/lease-service/tests/update-or-create-application-profile.test.ts +++ b/src/services/lease-service/tests/update-or-create-application-profile.test.ts @@ -1,288 +1,292 @@ import assert from 'node:assert' import { ApplicationProfile } from 'onecore-types' -import { migrate, db, teardown } from '../adapters/db' -import { updateOrCreateApplicationProfile } from '../update-or-create-application-profile' import * as applicationProfileAdapter from '../adapters/application-profile-adapter' import * as housingReferenceAdapter from '../adapters/application-profile-housing-reference-adapter' -import { clearDb } from './testUtils' import * as factory from './factories' - -beforeAll(async () => { - await migrate() -}) - -beforeEach(async () => { - await clearDb(db) -}) - -afterAll(async () => { - await teardown() -}) +import { updateOrCreateApplicationProfile } from '../update-or-create-application-profile' +import { withContext } from './testUtils' describe(updateOrCreateApplicationProfile.name, () => { - describe('when no profile exists ', () => { - it('creates application profile', async () => { - const res = await updateOrCreateApplicationProfile(db, '1234', { - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - housingType: 'foo', - landlord: 'baz', - housingTypeDescription: 'qux', - }) + describe('when no profile exists', () => { + it('creates application profile', () => + withContext(async (ctx) => { + const res = await updateOrCreateApplicationProfile(ctx.db, '1234', { + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + housingType: 'foo', + landlord: 'baz', + housingTypeDescription: 'qux', + }) - expect(res).toMatchObject({ ok: true }) - const inserted = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - assert(inserted.ok) - expect(inserted).toMatchObject({ - ok: true, - data: expect.objectContaining({ contactCode: '1234' }), - }) - }) + expect(res).toMatchObject({ ok: true }) + const inserted = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + assert(inserted.ok) + expect(inserted).toMatchObject({ + ok: true, + data: expect.objectContaining({ contactCode: '1234' }), + }) + })) - it('creates application profile and housing reference', async () => { - const res = await updateOrCreateApplicationProfile(db, '1234', { - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - housingType: 'foo', - landlord: 'baz', - housingTypeDescription: 'qux', - housingReference: factory.applicationProfileHousingReference.build(), - }) + it('creates application profile and housing reference', () => + withContext(async (ctx) => { + const res = await updateOrCreateApplicationProfile(ctx.db, '1234', { + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + housingType: 'foo', + landlord: 'baz', + housingTypeDescription: 'qux', + housingReference: factory.applicationProfileHousingReference.build(), + }) - expect(res).toMatchObject({ ok: true }) - const insertedProfile = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - assert(insertedProfile.ok) - expect(insertedProfile).toMatchObject({ - ok: true, - data: expect.objectContaining>({ - contactCode: '1234', - housingReference: expect.objectContaining({ id: expect.any(Number) }), - }), - }) - }) + expect(res).toMatchObject({ ok: true }) + const insertedProfile = + await applicationProfileAdapter.getByContactCode(ctx.db, '1234') + assert(insertedProfile.ok) + expect(insertedProfile).toMatchObject({ + ok: true, + data: expect.objectContaining>({ + contactCode: '1234', + housingReference: expect.objectContaining({ + id: expect.any(Number), + }), + }), + }) + })) - it('if create reference fails, profile is not created', async () => { - jest - .spyOn(housingReferenceAdapter, 'create') - .mockResolvedValueOnce({ ok: false, err: 'unknown' }) + it('if create reference fails, profile is not created', () => + withContext(async (ctx) => { + jest + .spyOn(housingReferenceAdapter, 'create') + .mockResolvedValueOnce({ ok: false, err: 'unknown' }) - const res = await updateOrCreateApplicationProfile(db, '1234', { - expiresAt: new Date(), - numAdults: 1, - numChildren: 1, - housingType: 'foo', - landlord: 'baz', - housingTypeDescription: 'qux', - housingReference: factory.applicationProfileHousingReference.build(), - }) + const res = await updateOrCreateApplicationProfile(ctx.db, '1234', { + expiresAt: new Date(), + numAdults: 1, + numChildren: 1, + housingType: 'foo', + landlord: 'baz', + housingTypeDescription: 'qux', + housingReference: factory.applicationProfileHousingReference.build(), + }) - expect(res).toMatchObject({ - ok: false, - err: 'create-reference', - }) + expect(res).toMatchObject({ + ok: false, + err: 'create-reference', + }) - const insertedProfile = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) + const insertedProfile = + await applicationProfileAdapter.getByContactCode(ctx.db, '1234') - expect(insertedProfile).toMatchObject({ ok: false, err: 'not-found' }) - }) + expect(insertedProfile).toMatchObject({ ok: false, err: 'not-found' }) + })) }) - describe('when profile exists ', () => { - it('updates application profile', async () => { - const existingProfile = await applicationProfileAdapter.create( - db, - factory.applicationProfile.build({ contactCode: '1234', numAdults: 1 }) - ) - assert(existingProfile.ok) + describe('when profile exists', () => { + it('updates application profile', () => + withContext(async (ctx) => { + const existingProfile = await applicationProfileAdapter.create( + ctx.db, + factory.applicationProfile.build({ + contactCode: '1234', + numAdults: 1, + }) + ) + assert(existingProfile.ok) - const res = await updateOrCreateApplicationProfile( - db, - existingProfile.data.contactCode, - { - expiresAt: new Date(), - numAdults: 2, - numChildren: 2, - housingType: 'bar', - landlord: 'quux', - housingTypeDescription: 'corge', - } - ) + const res = await updateOrCreateApplicationProfile( + ctx.db, + existingProfile.data.contactCode, + { + expiresAt: new Date(), + numAdults: 2, + numChildren: 2, + housingType: 'bar', + landlord: 'quux', + housingTypeDescription: 'corge', + } + ) - expect(res).toMatchObject({ ok: true }) - const updated = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - assert(updated.ok) - expect(updated).toMatchObject({ - ok: true, - data: expect.objectContaining({ - contactCode: '1234', - numAdults: 2, - }), - }) - }) + expect(res).toMatchObject({ ok: true }) + const updated = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + assert(updated.ok) + expect(updated).toMatchObject({ + ok: true, + data: expect.objectContaining({ + contactCode: '1234', + numAdults: 2, + }), + }) + })) - it('updates application profile and housing reference', async () => { - const existingProfile = await applicationProfileAdapter.create( - db, - factory.applicationProfile.build({ contactCode: '1234', numAdults: 1 }) - ) - assert(existingProfile.ok) + it('updates application profile and housing reference', () => + withContext(async (ctx) => { + const existingProfile = await applicationProfileAdapter.create( + ctx.db, + factory.applicationProfile.build({ + contactCode: '1234', + numAdults: 1, + }) + ) + assert(existingProfile.ok) - const existingReference = await housingReferenceAdapter.create( - db, - factory.applicationProfileHousingReference.build({ - applicationProfileId: existingProfile.data.id, - email: 'foo', - }) - ) + const existingReference = await housingReferenceAdapter.create( + ctx.db, + factory.applicationProfileHousingReference.build({ + applicationProfileId: existingProfile.data.id, + email: 'foo', + }) + ) - assert(existingReference.ok) + assert(existingReference.ok) - const res = await updateOrCreateApplicationProfile( - db, - existingProfile.data.contactCode, - { - expiresAt: new Date(), - numAdults: 2, - numChildren: 2, - housingType: 'bar', - landlord: 'quux', - housingTypeDescription: 'corge', - housingReference: { ...existingReference.data, email: 'bar' }, - } - ) - assert(res.ok) + const res = await updateOrCreateApplicationProfile( + ctx.db, + existingProfile.data.contactCode, + { + expiresAt: new Date(), + numAdults: 2, + numChildren: 2, + housingType: 'bar', + landlord: 'quux', + housingTypeDescription: 'corge', + housingReference: { ...existingReference.data, email: 'bar' }, + } + ) + assert(res.ok) - expect(res).toMatchObject({ ok: true }) - const updated = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - assert(updated.ok) - expect(updated).toMatchObject({ - ok: true, - data: expect.objectContaining({ - contactCode: '1234', - numAdults: 2, - housingReference: expect.objectContaining({ - applicationProfileId: existingProfile.data.id, - email: 'bar', + expect(res).toMatchObject({ ok: true }) + const updated = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + assert(updated.ok) + expect(updated).toMatchObject({ + ok: true, + data: expect.objectContaining({ + contactCode: '1234', + numAdults: 2, + housingReference: expect.objectContaining({ + applicationProfileId: existingProfile.data.id, + email: 'bar', + }), }), - }), - }) - }) + }) + })) - it('updates application profile and creates housing reference', async () => { - const existingProfile = await applicationProfileAdapter.create( - db, - factory.applicationProfile.build({ contactCode: '1234', numAdults: 1 }) - ) - assert(existingProfile.ok) + it('updates application profile and creates housing reference', () => + withContext(async (ctx) => { + const existingProfile = await applicationProfileAdapter.create( + ctx.db, + factory.applicationProfile.build({ + contactCode: '1234', + numAdults: 1, + }) + ) + assert(existingProfile.ok) - const res = await updateOrCreateApplicationProfile( - db, - existingProfile.data.contactCode, - { - expiresAt: new Date(), - numAdults: 2, - numChildren: 2, - housingType: 'bar', - landlord: 'quux', - housingTypeDescription: 'corge', - housingReference: { - ...factory.applicationProfileHousingReference.build({ + const res = await updateOrCreateApplicationProfile( + ctx.db, + existingProfile.data.contactCode, + { + expiresAt: new Date(), + numAdults: 2, + numChildren: 2, + housingType: 'bar', + landlord: 'quux', + housingTypeDescription: 'corge', + housingReference: { + ...factory.applicationProfileHousingReference.build({ + email: 'foo', + }), + }, + } + ) + assert(res.ok) + + expect(res).toMatchObject({ ok: true }) + const updated = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + assert(updated.ok) + expect(updated).toMatchObject({ + ok: true, + data: expect.objectContaining({ + contactCode: '1234', + numAdults: 2, + housingReference: expect.objectContaining({ + applicationProfileId: existingProfile.data.id, email: 'foo', }), - }, - } - ) - assert(res.ok) - - expect(res).toMatchObject({ ok: true }) - const updated = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - assert(updated.ok) - expect(updated).toMatchObject({ - ok: true, - data: expect.objectContaining({ - contactCode: '1234', - numAdults: 2, - housingReference: expect.objectContaining({ - applicationProfileId: existingProfile.data.id, - email: 'foo', }), - }), - }) - }) + }) + })) - it('if update reference fails, profile is not updated', async () => { - const existingProfile = await applicationProfileAdapter.create( - db, - factory.applicationProfile.build({ contactCode: '1234', numAdults: 1 }) - ) - assert(existingProfile.ok) + it('if update reference fails, profile is not updated', () => + withContext(async (ctx) => { + const existingProfile = await applicationProfileAdapter.create( + ctx.db, + factory.applicationProfile.build({ + contactCode: '1234', + numAdults: 1, + }) + ) + assert(existingProfile.ok) - const existingReference = await housingReferenceAdapter.create( - db, - factory.applicationProfileHousingReference.build({ - applicationProfileId: existingProfile.data.id, - email: 'foo', - }) - ) + const existingReference = await housingReferenceAdapter.create( + ctx.db, + factory.applicationProfileHousingReference.build({ + applicationProfileId: existingProfile.data.id, + email: 'foo', + }) + ) - assert(existingReference.ok) + assert(existingReference.ok) - jest - .spyOn(housingReferenceAdapter, 'update') - .mockResolvedValueOnce({ ok: false, err: 'no-update' }) + jest + .spyOn(housingReferenceAdapter, 'update') + .mockResolvedValueOnce({ ok: false, err: 'no-update' }) - const res = await updateOrCreateApplicationProfile( - db, - existingProfile.data.contactCode, - { - expiresAt: new Date(), - numAdults: 2, - numChildren: 2, - housingType: 'bar', - landlord: 'quux', - housingTypeDescription: 'corge', - housingReference: { ...existingReference.data, email: 'bar' }, - } - ) + const res = await updateOrCreateApplicationProfile( + ctx.db, + existingProfile.data.contactCode, + { + expiresAt: new Date(), + numAdults: 2, + numChildren: 2, + housingType: 'bar', + landlord: 'quux', + housingTypeDescription: 'corge', + housingReference: { ...existingReference.data, email: 'bar' }, + } + ) - expect(res).toMatchObject({ ok: false, err: 'create-reference' }) - const updated = await applicationProfileAdapter.getByContactCode( - db, - '1234' - ) - assert(updated.ok) - expect(updated).toMatchObject({ - ok: true, - data: expect.objectContaining({ - contactCode: '1234', - numAdults: 1, - housingReference: expect.objectContaining({ - applicationProfileId: existingProfile.data.id, - email: 'foo', + expect(res).toMatchObject({ ok: false, err: 'create-reference' }) + const updated = await applicationProfileAdapter.getByContactCode( + ctx.db, + '1234' + ) + assert(updated.ok) + expect(updated).toMatchObject({ + ok: true, + data: expect.objectContaining({ + contactCode: '1234', + numAdults: 1, + housingReference: expect.objectContaining({ + applicationProfileId: existingProfile.data.id, + email: 'foo', + }), }), - }), - }) - }) + }) + })) }) })