@defra-fish/dynamics-lib
Version:
Framework to support integration with dynamics
604 lines (551 loc) • 22.9 kB
JavaScript
import {
Contact,
Permission,
persist,
retrieveMultiple,
findById,
findByAlternateKey,
findByExample,
executePagedQuery,
executeQuery,
retrieveMultipleAsMap,
retrieveGlobalOptionSets
} from '../../index.js'
import TestEntity from '../../__mocks__/TestEntity.js'
import { v4 as uuidv4 } from 'uuid'
import MockDynamicsWebApi from 'dynamics-web-api'
import { PredefinedQuery } from '../../queries/predefined-query.js'
import { BaseEntity, EntityDefinition } from '../../entities/base.entity.js'
describe('entity manager', () => {
beforeEach(async () => {
MockDynamicsWebApi.__reset()
})
describe('persist', () => {
it('persists a new entity using the create operation', async () => {
const resultUuid = uuidv4()
MockDynamicsWebApi.__setResponse('executeBatch', [resultUuid])
const t = new TestEntity()
t.strVal = 'Fester'
t.intVal = 1
t.boolVal = true
const result = await persist([t])
expect(MockDynamicsWebApi.prototype.createRequest).toHaveBeenCalled()
expect(result).toHaveLength(1)
expect(result[0]).toEqual(resultUuid)
})
it('persists an existing entity using the update operation', async () => {
const resultUuid = uuidv4()
MockDynamicsWebApi.__setResponse('executeBatch', [resultUuid])
const t = TestEntity.fromResponse(
{
'@odata.etag': 'W/"202465000"',
idval: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
strval: 'Fester',
intval: 1,
boolval: true
},
{}
)
const result = await persist([t])
expect(MockDynamicsWebApi.prototype.updateRequest).toHaveBeenCalled()
expect(result).toHaveLength(1)
expect(result[0]).toEqual(resultUuid)
})
it('persists a new entity by impersonating the user', async () => {
const resultUuid = uuidv4()
MockDynamicsWebApi.__setResponse('executeBatch', [resultUuid])
const t = new TestEntity()
t.strVal = 'Fester'
t.intVal = 1
t.boolVal = true
const result = await persist([t], 'foo')
expect(MockDynamicsWebApi.prototype.createRequest).toHaveBeenCalled()
expect(MockDynamicsWebApi.prototype.executeBatch).toHaveBeenCalledWith(
expect.objectContaining({
impersonateAAD: 'foo'
})
)
expect(result).toHaveLength(1)
expect(result[0]).toEqual(resultUuid)
})
it('throws an error object on failure', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
MockDynamicsWebApi.__throwWithErrorsOnBatchExecute()
const newEntity = Object.assign(new TestEntity(), { strVal: 'test', intVal: 0 })
const existingEntity = TestEntity.fromResponse(
{
'@odata.etag': 'W/"202465000"',
idval: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
strval: 'Fester',
intval: 1,
boolval: true
},
{}
)
await expect(persist([newEntity, existingEntity])).rejects.toThrow('Test error')
// Expect the console error to contain details of the batch data (one createRequest, one updateRequest plus the exception object)
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringMatching('Error persisting batch. Data: %j, Exception: %o'),
expect.arrayContaining([{ createRequest: expect.any(Object) }, { updateRequest: expect.any(Object) }]),
expect.any(Error)
)
})
it('throws an error on implementation failure', async () => {
await expect(persist([null])).rejects.toThrow("Cannot read properties of null (reading 'isNew')")
})
})
describe('retrieveMultiple', () => {
it('retrieves a single entity type', async () => {
MockDynamicsWebApi.__setResponse('executeBatch', [
{
value: [
{
'@odata.etag': 'EXAMPLE_CONTACT',
contactid: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
firstname: 'Fester',
lastname: 'Tester',
birthdate: '1946-01-01',
emailaddress1: 'fester@tester.com'
}
]
}
])
const result = await retrieveMultiple(Contact).execute()
expect(result).toHaveLength(1)
const contact = result[0]
expect(contact).toBeInstanceOf(Contact)
expect(contact).toMatchObject(
expect.objectContaining({
etag: 'EXAMPLE_CONTACT',
id: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
firstName: 'Fester',
lastName: 'Tester',
birthDate: '1946-01-01',
email: 'fester@tester.com'
})
)
})
it('retrieves a multiple entity types', async () => {
MockDynamicsWebApi.__setResponse('executeBatch', [
{
value: [
{
'@odata.etag': 'EXAMPLE_CONTACT',
contactid: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
firstname: 'Fester',
lastname: 'Tester',
birthdate: '1946-01-01',
emailaddress1: 'fester@tester.com'
}
]
},
{
value: [
{
'@odata.etag': 'EXAMPLE_PERMISSION',
defra_permissionid: '347a9083-361e-ea11-a810-000d3a25c5d6',
defra_name: '00000000-2WC3FDR-CD379B',
defra_issuedate: '2019-12-13T09:00:00Z',
defra_startdate: '2019-12-14T00:00:00Z',
defra_enddate: '2020-12-13T23:59:59Z',
defra_stagingid: '71ad9a25-2a03-406b-a0e3-f4ff37799374'
}
]
}
])
const result = await retrieveMultiple(Contact, Permission).execute()
expect(result).toHaveLength(2)
const contact = result[0][0]
expect(contact).toBeInstanceOf(Contact)
expect(contact).toMatchObject(
expect.objectContaining({
etag: 'EXAMPLE_CONTACT',
id: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
firstName: 'Fester',
lastName: 'Tester',
birthDate: '1946-01-01',
email: 'fester@tester.com'
})
)
const permission = result[1][0]
expect(permission).toBeInstanceOf(Permission)
expect(permission).toMatchObject(
expect.objectContaining({
etag: 'EXAMPLE_PERMISSION',
id: '347a9083-361e-ea11-a810-000d3a25c5d6',
referenceNumber: '00000000-2WC3FDR-CD379B',
issueDate: '2019-12-13T09:00:00Z',
startDate: '2019-12-14T00:00:00Z',
endDate: '2020-12-13T23:59:59Z',
stagingId: '71ad9a25-2a03-406b-a0e3-f4ff37799374'
})
)
})
it('throws an error object on failure', async () => {
MockDynamicsWebApi.__throwWithErrorsOnBatchExecute()
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(retrieveMultiple(TestEntity).execute()).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
it('throws an error on implementation failure', async () => {
MockDynamicsWebApi.__throwWithErrorOn('executeBatch')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(retrieveMultiple(TestEntity).execute()).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
describe('retrieveMultipleAsMap', () => {
it('retrieves a multiple entity types as a map', async () => {
MockDynamicsWebApi.__setResponse('executeBatch', [
{
value: [
{
'@odata.etag': 'EXAMPLE_CONTACT',
contactid: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
firstname: 'Fester',
lastname: 'Tester',
birthdate: '1946-01-01',
emailaddress1: 'fester@tester.com'
}
]
},
{
value: [
{
'@odata.etag': 'EXAMPLE_PERMISSION',
defra_permissionid: '347a9083-361e-ea11-a810-000d3a25c5d6',
defra_name: '00000000-2WC3FDR-CD379B',
defra_issuedate: '2019-12-13T09:00:00Z',
defra_startdate: '2019-12-14T00:00:00Z',
defra_enddate: '2020-12-13T23:59:59Z',
defra_stagingid: '71ad9a25-2a03-406b-a0e3-f4ff37799374'
}
]
}
])
const result = await retrieveMultipleAsMap(Contact, Permission).execute()
expect(result).toMatchObject({
contacts: expect.arrayContaining([
expect.objectContaining({
etag: 'EXAMPLE_CONTACT',
id: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6',
firstName: 'Fester',
lastName: 'Tester',
birthDate: '1946-01-01',
email: 'fester@tester.com'
})
]),
permissions: expect.arrayContaining([
expect.objectContaining({
etag: 'EXAMPLE_PERMISSION',
id: '347a9083-361e-ea11-a810-000d3a25c5d6',
referenceNumber: '00000000-2WC3FDR-CD379B',
issueDate: '2019-12-13T09:00:00Z',
startDate: '2019-12-14T00:00:00Z',
endDate: '2020-12-13T23:59:59Z',
stagingId: '71ad9a25-2a03-406b-a0e3-f4ff37799374'
})
])
})
})
it('throws an error object on failure', async () => {
MockDynamicsWebApi.__throwWithErrorsOnBatchExecute()
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(retrieveMultipleAsMap(TestEntity).execute()).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
it('throws an error on implementation failure', async () => {
MockDynamicsWebApi.__throwWithErrorOn('executeBatch')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(retrieveMultipleAsMap(TestEntity).execute()).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
const optionSetInstance = {
name: expect.stringMatching(/^[a-z]+_?[a-z]+$/),
options: expect.objectContaining({
910400000: expect.objectContaining({
id: 910400000,
label: expect.anything(),
description: expect.anything()
})
})
}
describe('retrieveGlobalOptionSets', () => {
it('retrieves a full listing', async () => {
const result = await retrieveGlobalOptionSets().execute()
expect(result).toMatchObject({
defra_concessionproof: expect.objectContaining(optionSetInstance),
defra_country: expect.objectContaining(optionSetInstance),
defra_datasource: expect.objectContaining(optionSetInstance),
defra_datatype: expect.objectContaining(optionSetInstance),
defra_daymonthyear: expect.objectContaining(optionSetInstance),
defra_duration: expect.objectContaining(optionSetInstance),
defra_environmentagencyarea: expect.objectContaining(optionSetInstance),
defra_financialtransactionsource: expect.objectContaining(optionSetInstance),
defra_financialtransactiontype: expect.objectContaining(optionSetInstance),
defra_fulfilmentrequestfilestatus: expect.objectContaining(optionSetInstance),
defra_fulfilmentrequeststatus: expect.objectContaining(optionSetInstance),
defra_notificationstatus: expect.objectContaining(optionSetInstance),
defra_paymenttype: expect.objectContaining(optionSetInstance),
defra_permitsubtype: expect.objectContaining(optionSetInstance),
defra_permittype: expect.objectContaining(optionSetInstance),
defra_poclfiledataerrorstatus: expect.objectContaining(optionSetInstance),
defra_poclfiledataerrortype: expect.objectContaining(optionSetInstance),
defra_poclfilestatus: expect.objectContaining(optionSetInstance),
defra_preferredcontactmethod: expect.objectContaining(optionSetInstance)
})
})
it('throws an error object on failure', async () => {
MockDynamicsWebApi.__throwWithErrorOn('retrieveGlobalOptionSets')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(retrieveGlobalOptionSets().execute()).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
describe('findById', () => {
it('finds by a primary key guid', async () => {
MockDynamicsWebApi.__setResponse('retrieveRequest', {
'@odata.etag': 'W/"202465000"',
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
strval: 'example'
})
const result = await findById(TestEntity, '9f1b34a0-0c66-e611-80dc-c4346bad0190')
expect(MockDynamicsWebApi.prototype.retrieveRequest).toBeCalledWith({
key: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
collection: TestEntity.definition.dynamicsCollection,
select: TestEntity.definition.select
})
expect(result).toBeInstanceOf(TestEntity)
})
it('returns null if not found', async () => {
const notFoundError = new Error('Not found')
notFoundError.status = 404
MockDynamicsWebApi.__throwWithErrorOn('retrieveRequest', notFoundError)
const result = await findById(TestEntity, '9f1b34a0-0c66-e611-80dc-c4346bad0190')
expect(result).toEqual(null)
})
it('throws an exception on general errors', async () => {
MockDynamicsWebApi.__throwWithErrorOn('retrieveRequest')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(findById(TestEntity, '9f1b34a0-0c66-e611-80dc-c4346bad0190')).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
describe('findByAlternateKey', () => {
it('finds by an alternate key', async () => {
MockDynamicsWebApi.__setResponse('retrieveRequest', {
'@odata.etag': 'W/"202465000"',
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
strval: 'example'
})
const result = await findByAlternateKey(TestEntity, 'example')
expect(MockDynamicsWebApi.prototype.retrieveRequest).toBeCalledWith({
key: "strval='example'",
collection: TestEntity.definition.dynamicsCollection,
select: TestEntity.definition.select
})
expect(result).toBeInstanceOf(TestEntity)
})
it('escapes special characters in the key', async () => {
MockDynamicsWebApi.__setResponse('retrieveRequest', {
'@odata.etag': 'W/"202465000"',
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
strval: 'example'
})
const result = await findByAlternateKey(TestEntity, "test & example'")
expect(MockDynamicsWebApi.prototype.retrieveRequest).toBeCalledWith({
key: "strval='test %26 example'''",
collection: TestEntity.definition.dynamicsCollection,
select: TestEntity.definition.select
})
expect(result).toBeInstanceOf(TestEntity)
})
})
describe('findByExample', () => {
it('builds a select statement appropriate to the type definition for each field', async () => {
MockDynamicsWebApi.__setResponse('retrieveMultipleRequest', { value: [{}] })
const lookup = new TestEntity()
lookup.strVal = 'StringData'
lookup.intVal = 123
lookup.decVal = 123.45
lookup.boolVal = true
lookup.dateVal = '1946-01-01'
lookup.dateTimeVal = '1946-01-01T01:02:03Z'
lookup.optionSetVal = { id: 910400000, label: 'test', description: 'test' }
const expectedLookupSelect =
"strval eq 'StringData' and intval eq 123 and decval eq 123.45 and boolval eq true and dateval eq 1946-01-01 and datetimeval eq 1946-01-01T01:02:03Z and optionsetval eq 910400000"
const result = await findByExample(lookup)
expect(MockDynamicsWebApi.prototype.retrieveMultipleRequest).toBeCalledWith({
collection: TestEntity.definition.dynamicsCollection,
select: TestEntity.definition.select,
filter: expect.stringMatching(`${TestEntity.definition.defaultFilter} and ${expectedLookupSelect}`)
})
expect(result).toHaveLength(1)
expect(result[0]).toBeInstanceOf(TestEntity)
})
it('does not require the entity to define a default filter', async () => {
MockDynamicsWebApi.__setResponse('retrieveMultipleRequest', { value: [{}] })
class SameEntity extends BaseEntity {
static _definition = new EntityDefinition(() => ({
localName: 'sampleEntity',
dynamicsCollection: 'sample',
mappings: {
id: { field: 'idval', type: 'string' },
testVal: { field: 'testval', type: 'string' }
}
}))
/**
* @returns {EntityDefinition} the definition providing mappings between Dynamics entity and the local entity
*/
static get definition () {
return SameEntity._definition
}
/**
* The testVal field
* @type {string}
*/
get testVal () {
return super._getState('testVal')
}
set testVal (testVal) {
super._setState('testVal', testVal)
}
}
const lookup = new SameEntity()
lookup.testVal = 'StringData'
const expectedLookupSelect = "testval eq 'StringData'"
const result = await findByExample(lookup)
expect(MockDynamicsWebApi.prototype.retrieveMultipleRequest).toBeCalledWith({
collection: SameEntity.definition.dynamicsCollection,
select: SameEntity.definition.select,
filter: expect.stringMatching(`${expectedLookupSelect}`)
})
expect(result).toHaveLength(1)
expect(result[0]).toBeInstanceOf(SameEntity)
})
it('only serializes fields into the select statement if they are set', async () => {
MockDynamicsWebApi.__setResponse('retrieveMultipleRequest', { value: [{}] })
const lookup = new TestEntity()
lookup.strVal = 'StringData'
const expectedLookupSelect = "strval eq 'StringData'"
const result = await findByExample(lookup)
expect(MockDynamicsWebApi.prototype.retrieveMultipleRequest).toBeCalledWith({
collection: TestEntity.definition.dynamicsCollection,
select: TestEntity.definition.select,
filter: expect.stringMatching(`${TestEntity.definition.defaultFilter} and ${expectedLookupSelect}`)
})
expect(result).toHaveLength(1)
expect(result[0]).toBeInstanceOf(TestEntity)
})
it('throws an error object on failure', async () => {
MockDynamicsWebApi.__throwWithErrorOn('retrieveMultipleRequest')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(findByExample(new TestEntity())).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
describe('executeQuery', () => {
it('calls retrieveMultipleRequest on the Dynamics API with the request data', async () => {
MockDynamicsWebApi.__setResponse('retrieveMultipleRequest', {
value: [
{
'@odata.etag': 'W/"202465000"',
idval: '9f1b34a0-0c66-e611-80dc-c4346bad0190',
strval: 'example'
}
]
})
const result = await executeQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }))
expect(MockDynamicsWebApi.prototype.retrieveMultipleRequest).toBeCalledWith({
collection: TestEntity.definition.dynamicsCollection,
filter: "strval eq 'example'",
select: TestEntity.definition.select
})
expect(result).toContainEqual({
entity: expect.any(TestEntity),
expanded: {}
})
})
it('throws an error object on failure', async () => {
MockDynamicsWebApi.__throwWithErrorOn('retrieveMultipleRequest')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(executeQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }))).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
describe('executePagedQuery', () => {
beforeEach(async () => {
MockDynamicsWebApi.__reset()
MockDynamicsWebApi.__setNextResponses(
'retrieveMultipleRequest',
{
value: [
{
'@odata.etag': 'RECORD 1',
idval: 'RECORD 1'
},
{
'@odata.etag': 'RECORD 2',
idval: 'RECORD 2'
}
],
oDataNextLink: 'http://example.com/nextlink1'
},
{
value: [
{
'@odata.etag': 'RECORD 3',
idval: 'RECORD 3'
}
]
}
)
})
it('makes successive requests to retrieve all records while oDataNextLink is returned in the response', async () => {
const results = []
/**
* @param {Array<PredefinedQueryResult<TestEntity>>} page
* @returns {Promise<void>}
*/
const onPageReceived = async page => {
results.push(...page)
}
await executePagedQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }), onPageReceived)
expect(results).toHaveLength(3)
expect(results).toStrictEqual(
expect.arrayContaining([
{ entity: expect.objectContaining({ id: 'RECORD 1' }), expanded: {} },
{ entity: expect.objectContaining({ id: 'RECORD 2' }), expanded: {} },
{ entity: expect.objectContaining({ id: 'RECORD 3' }), expanded: {} }
])
)
})
it('allows the maximum number of pages to be capped', async () => {
const results = []
/**
* @param {Array<PredefinedQueryResult<TestEntity>>} page
* @returns {Promise<void>}
*/
const onPageReceived = async page => {
results.push(...page)
}
await executePagedQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }), onPageReceived, 1)
expect(results).toHaveLength(2)
expect(results).toStrictEqual(
expect.arrayContaining([
{ entity: expect.objectContaining({ id: 'RECORD 1' }), expanded: {} },
{ entity: expect.objectContaining({ id: 'RECORD 2' }), expanded: {} }
])
)
})
it('throws an error object on failure', async () => {
MockDynamicsWebApi.__throwWithErrorOn('retrieveMultipleRequest')
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
await expect(
executePagedQuery(new PredefinedQuery({ root: TestEntity, filter: "strval eq 'example'" }), () => {}, 1)
).rejects.toThrow('Test error')
expect(consoleErrorSpy).toHaveBeenCalled()
})
})
})