src/model/__tests__/ModelDefinition.spec.js
import fixtures from '../../__fixtures__/fixtures'
import { DIRTY_PROPERTY_LIST } from '../ModelBase'
import Model from '../Model'
import ModelDefinitions from '../ModelDefinitions'
import ModelCollectionProperty from '../ModelCollectionProperty'
import ModelCollection from '../ModelCollection'
import ModelDefinition from '../ModelDefinition'
jest.mock('../ModelCollection')
jest.mock('../ModelCollectionProperty')
jest.mock('../Model')
describe('ModelDefinition', () => {
let modelDefinition
let mockModelCollectionCreate
let mockModelCollectionPropertyCreate
beforeEach(() => {
modelDefinition = new ModelDefinition({
displayName: 'Data Elements',
singular: 'dataElement',
plural: 'dataElements',
})
mockModelCollectionCreate = jest.fn(ModelCollection, 'create')
mockModelCollectionCreate.mockReturnValue(
new ModelCollection(modelDefinition, [], {})
)
mockModelCollectionPropertyCreate = jest.fn(
ModelCollectionProperty,
'create'
)
mockModelCollectionPropertyCreate.mockReturnValue(
new ModelCollectionProperty(
{},
modelDefinition,
'propName',
[],
undefined
)
)
})
it('should not be allowed to be called without new', () => {
expect(() => ModelDefinition()).toThrowErrorMatchingSnapshot()
})
it('should create a ModelDefinition object', () => {
expect(modelDefinition).toBeInstanceOf(ModelDefinition)
})
it('should not add epiEndpoint when it does not exist', () => {
expect(modelDefinition.apiEndpoint).toBeUndefined()
})
it('should throw an error when a name is not specified', () => {
function shouldThrow() {
return new ModelDefinition()
}
expect(shouldThrow).toThrowError('Value should be provided')
})
it('should throw an error when plural is not specified', () => {
function shouldThrow() {
return new ModelDefinition({
displayName: 'Data Elements',
singular: 'dataElement',
})
}
expect(shouldThrow).toThrowError('Plural should be provided')
})
describe('instance', () => {
it('should not be able to change the name', () => {
const isWritable = Object.getOwnPropertyDescriptor(
modelDefinition,
'name'
).writable
const isConfigurable = Object.getOwnPropertyDescriptor(
modelDefinition,
'name'
).configurable
expect(isWritable).toBe(false)
expect(isConfigurable).toBe(false)
})
it('should not change the name', () => {
function shouldThrow() {
modelDefinition.name = 'anotherName'
if (modelDefinition.name !== 'anotherName') {
throw new Error('')
}
}
expect(shouldThrow).toThrowError()
expect(modelDefinition.name).toBe('dataElement')
})
it('should have the correct displayName', () => {
expect(modelDefinition.displayName).toBe('Data Elements')
})
it('should not change the displayName', () => {
function shouldThrow() {
modelDefinition.displayName = 'Another Name'
}
expect(shouldThrow).toThrowError()
expect(modelDefinition.displayName).toBe('Data Elements')
})
it('should not be able to change the isMetaData', () => {
const isWritable = Object.getOwnPropertyDescriptor(
modelDefinition,
'isMetaData'
).writable
const isConfigurable = Object.getOwnPropertyDescriptor(
modelDefinition,
'isMetaData'
).configurable
expect(isWritable).toBe(false)
expect(isConfigurable).toBe(false)
})
it('should not change the isMetaData', () => {
function shouldThrow() {
modelDefinition.isMetaData = true
if (modelDefinition.isMetaData !== true) {
throw new Error('')
}
}
expect(modelDefinition.isMetaData).toBe(false)
expect(shouldThrow).toThrowError()
})
})
describe('createFromSchema', () => {
let dataElementModelDefinition
beforeEach(() => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement'),
fixtures.get('/api/attributes').attributes
)
})
it('should be a method on ModelDefinition', () => {
expect(ModelDefinition.createFromSchema).toBeDefined()
})
it('should throw if the schema is not provided', () => {
expect(ModelDefinition.createFromSchema).toThrowError(
'Schema should be provided'
)
})
describe('dataElementSchema', () => {
it('should return a ModelDefinition object', () => {
expect(dataElementModelDefinition).toBeInstanceOf(
ModelDefinition
)
})
it('should set the name on the definition', () => {
expect(dataElementModelDefinition.name).toBe('dataElement')
})
it('should set if it is a metadata model', () => {
expect(dataElementModelDefinition.isMetaData).toBe(true)
})
it('should set the epiEndpoint', () => {
expect(dataElementModelDefinition.apiEndpoint).toBe(
'https://play.dhis2.org/demo/api/dataElements'
)
})
it('should set metadata to false if it is not a metadata model', () => {
const nonMetaDataModel = fixtures.get(
'/api/schemas/dataElement'
)
nonMetaDataModel.metadata = false
dataElementModelDefinition =
ModelDefinition.createFromSchema(nonMetaDataModel)
expect(dataElementModelDefinition.isMetaData).toBe(false)
})
it('should a properties property for each of the schema properties', () => {
expect(
Object.keys(dataElementModelDefinition.modelProperties)
.length
).toBe(37)
})
it('should not be able to modify the modelProperties array', () => {
function shouldThrow() {
dataElementModelDefinition.modelProperties.anotherKey = {}
// TODO: There is an implementation bug in PhantomJS that does not properly freeze the array
if (
Object.keys(dataElementModelDefinition.modelProperties)
.length === 37
) {
throw new Error()
}
}
expect(shouldThrow).toThrowError()
expect(
Object.keys(dataElementModelDefinition.modelProperties)
.length
).toBe(37)
})
it('should store property constants', () => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
expect(
dataElementModelDefinition.modelProperties.aggregationType
.constants
).toEqual([
'SUM',
'AVERAGE',
'AVERAGE_SUM_ORG_UNIT',
'COUNT',
'STDDEV',
'VARIANCE',
'MIN',
'MAX',
'NONE',
'CUSTOM',
'DEFAULT',
'AVERAGE_SUM_INT',
'AVERAGE_SUM_INT_DISAGGREGATION',
'AVERAGE_INT',
'AVERAGE_INT_DISAGGREGATION',
'AVERAGE_BOOL',
])
})
})
describe('modelProperties', () => {
let modelProperties
beforeEach(() => {
modelProperties = dataElementModelDefinition.modelProperties
})
it('should be an object', () => {
expect(modelProperties.name).toBeInstanceOf(Object)
})
it('should throw an error when a type is not found', () => {
const schema = fixtures.get('/api/schemas/dataElement')
function shouldThrow() {
ModelDefinition.createFromSchema(schema)
}
schema.properties.push({
name: 'unknownProperty',
propertyType: 'uio.some.unknown.type',
})
expect(shouldThrow).toThrowError(
'Type from schema "uio.some.unknown.type" not found available type list.'
)
})
it('should not add properties that do not have a name', () => {
const schema = fixtures.get('/api/schemas/dataElement')
const expectedProperties = [
'aggregationLevels',
'zeroIsSignificant',
'displayDescription',
'optionSet',
'id',
'created',
'description',
'displayFormName',
'commentOptionSet',
'name',
'externalAccess',
'valueType',
'href',
'dataElementGroups',
'publicAccess',
'aggregationType',
'formName',
'lastUpdated',
'dataSetElements',
'code',
'access',
'url',
'domainType',
'legendSet',
'legendSets',
'categoryCombo',
'attributeValues',
'optionSetValue',
'userGroupAccesses',
'userAccesses',
'shortName',
'displayName',
'displayShortName',
'user',
'translations',
'dimensionItem',
'dimensionItemType',
]
schema.properties.push({ propertyType: 'TEXT' })
const definition = ModelDefinition.createFromSchema(schema)
expect(Object.keys(definition.modelProperties).sort()).toEqual(
expectedProperties.sort()
)
})
it('should use the collection name for collections', () => {
expect(modelProperties.dataElementGroups).toBeDefined()
expect(modelProperties.dataElementGroup).toBeUndefined()
})
it('should add a get method to the propertyDescriptor', () => {
expect(modelProperties.name.get).toBeInstanceOf(Function)
})
it('should add a set method to the propertyDescriptor for name', () => {
expect(modelProperties.name.set).toBeInstanceOf(Function)
})
it('should not have a set method for dimensionItem', () => {
expect(modelProperties.dimensionItem.set).not.toBeInstanceOf(
Function
)
})
it('should create getter function on the propertyDescriptor', () => {
const model = {
dataValues: {
name: 'Mark',
},
}
expect(modelProperties.name.get.call(model)).toBe('Mark')
})
it('should create setter function on the propertyDescriptor', () => {
const model = {
dataValues: {},
}
model[DIRTY_PROPERTY_LIST] = new Set([])
modelProperties.name.set.call(model, 'James')
expect(model.dataValues.name).toBe('James')
})
describe('setter', () => {
let model
beforeEach(() => {
model = {
dirty: false,
dataValues: {},
}
model[DIRTY_PROPERTY_LIST] = new Set([])
})
it('should set the dirty property to true when a value is set', () => {
modelProperties.name.set.call(model, 'James')
expect(model.dirty).toBe(true)
})
it('should not set the dirty property to true when the value is the same', () => {
model.dataValues.name = 'James'
modelProperties.name.set.call(model, 'James')
expect(model.dirty).toBe(false)
})
it('should set the dirty property when a different object is added', () => {
model.dataValues.name = { name: 'James' }
modelProperties.name.set.call(model, {
name: 'James',
last: 'Doe',
})
expect(model.dirty).toBe(true)
})
})
})
describe('modelValidations', () => {
let modelValidations
beforeEach(() => {
modelValidations = dataElementModelDefinition.modelValidations
})
describe('created', () => {
it('should set the data object as a type for date fields', () => {
expect(modelValidations.created.type).toBe('DATE')
})
it('should be owned by this schema', () => {
expect(modelValidations.created.owner).toBe(true)
})
})
describe('externalAccess', () => {
it('should set the boolean datatype for externalAccess', () => {
expect(modelValidations.externalAccess.type).toBe('BOOLEAN')
})
it('should not be owned by this schema', () => {
expect(modelValidations.externalAccess.owner).toBe(false)
})
})
describe('id', () => {
it('should have a maxLength', () => {
expect(modelValidations.id.max).toBe(11)
})
})
describe('name', () => {
it('should have have a type property', () => {
expect(modelValidations.name.type).toBe('TEXT')
})
it('should have a persisted property', () => {
expect(modelValidations.name.persisted).toBe(true)
})
it('should have a required property', () => {
expect(modelValidations.name.required).toBe(true)
})
it('should have an owner property', () => {
expect(modelValidations.name.owner).toBe(true)
})
})
it('should add the referenceType to the optionSet and commentOptionSet', () => {
expect(modelValidations.commentOptionSet.referenceType).toBe(
'optionSet'
)
expect(modelValidations.optionSet.referenceType).toBe(
'optionSet'
)
})
it('should add the referenceType to the categoryCombo property', () => {
expect(modelValidations.categoryCombo.referenceType).toBe(
'categoryCombo'
)
})
it('should add the referenceType to the user property', () => {
expect(modelValidations.user.referenceType).toBe('user')
})
it('should not add a referenceType for a property that are not a reference', () => {
expect(modelValidations.name.referenceType).toBeUndefined()
})
describe('ordered', () => {
it('should set ordered to false when the property is not available', () => {
expect(modelValidations.name.ordered).toBe(false)
})
it('should set ordered to false when the ordered property is available and is false', () => {
const dataElementSchemaFixture = fixtures.get(
'/api/schemas/dataElement'
)
dataElementSchemaFixture.properties[0].ordered = false
dataElementModelDefinition =
ModelDefinition.createFromSchema(
dataElementSchemaFixture,
fixtures.get('/api/attributes').attributes
)
modelValidations =
dataElementModelDefinition.modelValidations
expect(modelValidations.aggregationType.ordered).toBe(false)
})
it('should set ordered to true when the ordered property is available and is true', () => {
const dataElementSchemaFixture = fixtures.get(
'/api/schemas/dataElement'
)
dataElementSchemaFixture.properties[0].ordered = true
dataElementModelDefinition =
ModelDefinition.createFromSchema(
dataElementSchemaFixture,
fixtures.get('/api/attributes').attributes
)
modelValidations =
dataElementModelDefinition.modelValidations
expect(modelValidations.aggregationLevels.ordered).toBe(
true
)
})
})
describe('collection reference', () => {
let indicatorGroupModelDefinition
beforeEach(() => {
const indicatorGroupSchema = fixtures.get(
'/api/schemas/indicatorGroup'
)
indicatorGroupModelDefinition =
ModelDefinition.createFromSchema(indicatorGroupSchema)
modelValidations =
indicatorGroupModelDefinition.modelValidations
})
it('should add a reference type for a collection of references', () => {
expect(modelValidations.indicators.referenceType).toBe(
'indicator'
)
})
it('should not add a reference type for a collection of complex objects', () => {
expect(
modelValidations.userGroupAccesses.referenceType
).toBeUndefined()
})
})
describe('embedded object property', () => {
let indicatorGroupModelDefinition
beforeEach(() => {
const legendSetSchema = fixtures.get(
'/api/schemas/legendSet'
)
indicatorGroupModelDefinition =
ModelDefinition.createFromSchema(legendSetSchema)
modelValidations =
indicatorGroupModelDefinition.modelValidations
})
it('should have set the embedded property validation for userGroupAcceses to true', () => {
expect(
modelValidations.userGroupAccesses.embeddedObject
).toBe(true)
})
it('should have set the embedded property validation for attributeValues to false', () => {
expect(
modelValidations.attributeValues.embeddedObject
).toBe(false)
})
it('should set the embedded object to false for simple types', () => {
expect(modelValidations.name.embeddedObject).toBe(false)
})
})
})
describe('specialized definitions', () => {
let UserModelDefinition
let userModelDefinition
let DataSetModelDefinition
let dataSetModelDefinition
beforeEach(() => {
UserModelDefinition = ModelDefinition.specialClasses.user
userModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/user')
)
DataSetModelDefinition = ModelDefinition.specialClasses.dataSet
dataSetModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataSet')
)
})
it('should return a UserModelDefinition for the user schema', () => {
expect(userModelDefinition).toBeInstanceOf(UserModelDefinition)
})
it('should return a DataSetModelDefinition for the data set schema', () => {
expect(dataSetModelDefinition).toBeInstanceOf(
DataSetModelDefinition
)
})
})
describe('attribute properties', () => {
let attributeProperties
beforeEach(() => {
attributeProperties =
dataElementModelDefinition.attributeProperties
})
it('should have added the attribute properties onto the model', () => {
expect(attributeProperties).toBeDefined()
})
it('should be descriptor objects', () => {
expect(attributeProperties.name).toBeInstanceOf(Object)
})
})
})
describe('create()', () => {
let dataElementModelDefinition
beforeEach(() => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
// TODO: This is currently not a pure unit test as we haven't mocked out Model
it('should return an instance of Model', () => {
expect(dataElementModelDefinition.create()).toBeInstanceOf(Model)
})
describe('with default values', () => {
const orgUnitGroupSchema = fixtures.get(
'/api/schemas/organisationUnitGroupSet'
)
const organisationUnitGroupSetModelDefinition =
ModelDefinition.createFromSchema(orgUnitGroupSchema)
let model
beforeEach(() => {
model = organisationUnitGroupSetModelDefinition.create()
})
it('should set the default data dimension', () => {
expect(model.dataDimension).toBe(true)
})
})
describe('collection properties', () => {
let orgunitModelDefinition
let userModelDefinition
beforeEach(() => {
const orgUnitSchema = fixtures.get(
'/api/schemas/organisationUnit'
)
userModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/user')
)
orgunitModelDefinition =
ModelDefinition.createFromSchema(orgUnitSchema)
// TODO: Mock the ModelDefinitions singleton, so we can get rid of this logic
if (!ModelDefinitions.getModelDefinitions().user) {
ModelDefinitions.getModelDefinitions().add(
userModelDefinition
)
}
if (!ModelDefinitions.getModelDefinitions().organisationUnit) {
ModelDefinitions.getModelDefinitions().add(
orgunitModelDefinition
)
}
})
afterEach(() => {
ModelCollectionProperty.create.mockClear()
})
describe('with data', () => {
beforeEach(() => {
userModelDefinition.create({
organisationUnits: [
{ name: 'Kenya', id: 'FTRrcoaog83' },
{ name: 'Oslo', id: 'P3jJH5Tu5VC' },
],
})
})
it('should create a ModelCollectionProperty.create for a collection of objects', () => {
expect(
ModelCollectionProperty.create
).toHaveBeenCalledTimes(9)
})
it('should create a ModelCollectionProperty with the correct values', () => {
expect(
ModelCollectionProperty.create.mock.calls[0]
).toMatchSnapshot()
})
})
describe('without data', () => {
beforeEach(() => {
userModelDefinition.create()
})
it('should create a ModelCollectionProperty.create for a collection of objects', () => {
expect(
ModelCollectionProperty.create
).toHaveBeenCalledTimes(3)
})
it('should create a ModelCollectionProperty without data', () => {
const passedModelInstance =
ModelCollectionProperty.create.mock.calls[0][0]
const modelDefinitionForCollection =
ModelCollectionProperty.create.mock.calls[0][1]
const modelCollectionPropName =
ModelCollectionProperty.create.mock.calls[0][2]
const modelCollectionData =
ModelCollectionProperty.create.mock.calls[0][3]
// First argument to ModelCollectionPrototype.create
expect(passedModelInstance).toMatchSnapshot()
// Second argument to ModelCollectionProperty.create
expect(modelDefinitionForCollection.name).toBe(
orgunitModelDefinition.name
)
expect(modelDefinitionForCollection.plural).toBe(
orgunitModelDefinition.plural
)
// Third argument to ModelCollectionProperty.create
// teiSearchOrganisationUnits is the first collection property on the user model
expect(modelCollectionPropName).toEqual(
'teiSearchOrganisationUnits'
)
// Fourth argument to ModelCollectionProperty.create
expect(modelCollectionData).toEqual(undefined)
})
})
})
})
describe('get', () => {
let dataElementModelDefinition
beforeEach(() => {
ModelDefinition.prototype.api = {
get: jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve({
name: 'BS_COLL (N, DSD) TARGET: Blood Units Donated',
})
})
),
}
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
it('should throw an error when the given id is not a string', () => {
function shouldThrow() {
dataElementModelDefinition.get()
}
expect(shouldThrow).toThrowError('Identifier should be provided')
})
it('should return a promise', () => {
const modelPromise = dataElementModelDefinition.get('d4343fsss')
expect(modelPromise.then).toBeInstanceOf(Function)
})
it('should call the api for the requested id', () => {
dataElementModelDefinition.get('d4343fsss')
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements/d4343fsss',
{
fields: ':all,attributeValues[:all,attribute[id,name,displayName]]',
}
)
})
it('should set the data onto the model when it is available', () =>
dataElementModelDefinition
.get('d4343fsss')
.then((dataElementModel) => {
expect(dataElementModel.name).toBe(
'BS_COLL (N, DSD) TARGET: Blood Units Donated'
)
}))
it('should reject the promise with the message when the request fails', () => {
ModelDefinition.prototype.api.get = jest.fn().mockReturnValue(
Promise.reject({
httpStatus: 'Not Found',
httpStatusCode: 404,
status: 'ERROR',
message:
'DataElementCategory with id sdfsf could not be found.',
})
)
return dataElementModelDefinition
.get('d4343fsss')
.catch((dataElementError) => {
expect(dataElementError).toBe(
'DataElementCategory with id sdfsf could not be found.'
)
})
})
it('should reject with the promise payload when no message was returned', () => {
const responsePayload = '500 error string'
ModelDefinition.prototype.api.get = jest
.fn()
.mockReturnValue(Promise.reject(responsePayload))
return dataElementModelDefinition
.get('d4343fsss')
.catch((dataElementError) => {
expect(dataElementError).toBe(responsePayload)
})
})
describe('multiple', () => {
it('should return a ModelCollection object', () => {
const dataElementsResult = fixtures.get('/api/dataElements')
ModelDefinition.prototype.api.get = jest
.fn()
.mockReturnValue(Promise.resolve(dataElementsResult))
return dataElementModelDefinition
.get(['id1', 'id2'])
.then((dataElementCollection) => {
expect(dataElementCollection).toBeInstanceOf(
ModelCollection
)
})
})
it('should call the api with the in filter', () => {
const dataElementsResult = fixtures.get('/api/dataElements')
ModelDefinition.prototype.api.get = jest
.fn()
.mockReturnValue(Promise.resolve(dataElementsResult))
return dataElementModelDefinition
.get(['id1', 'id2'])
.then(() => {
expect(
ModelDefinition.prototype.api.get
).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{ filter: ['id:in:[id1,id2]'], fields: ':all' }
)
})
})
})
})
describe('list', () => {
const dataElementsResult = fixtures.get('/api/dataElements')
let dataElementModelDefinition
beforeEach(() => {
ModelDefinition.prototype.api = {
get: jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve(dataElementsResult)
})
),
}
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
it('should be a function', () => {
expect(dataElementModelDefinition.list).toBeInstanceOf(Function)
})
it('should call the get method on the api', () => {
dataElementModelDefinition.list()
expect(ModelDefinition.prototype.api.get).toBeCalled()
})
it('should return a promise', () => {
expect(dataElementModelDefinition.list()).toBeInstanceOf(Promise)
})
it('should call the get method on the api with the endpoint of the model', () => {
dataElementModelDefinition.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
}
)
})
it('should return a model collection object', () =>
dataElementModelDefinition.list().then((dataElementCollection) => {
expect(dataElementCollection).toBeInstanceOf(ModelCollection)
}))
it('should call the model collection constructor with the correct data', () =>
dataElementModelDefinition.list().then(() => {
const firstCallArguments = ModelCollection.create.mock.calls[0]
expect(firstCallArguments).toMatchSnapshot()
}))
it('should call the api get method with the correct parameters after filters are set', () => {
dataElementModelDefinition.filter().on('name').like('John').list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John'],
}
)
})
it('should return a separate modelDefinition when filter is called', () => {
expect(dataElementModelDefinition.filter).not.toBe(
dataElementModelDefinition
)
})
it('should not influence the list method of the default modelDefinition', () => {
dataElementModelDefinition.filter().on('name').like('John').list()
dataElementModelDefinition.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
}
)
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John'],
}
)
})
it('should support multiple filters', () => {
dataElementModelDefinition
.filter()
.on('name')
.like('John')
.filter()
.on('username')
.equals('admin')
.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John', 'username:eq:admin'],
}
)
})
it('should work with operator-filter', () => {
dataElementModelDefinition
.filter()
.on('name')
.operator('like', 'John')
.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John'],
}
)
})
it('should work with chained operator-filter', () => {
dataElementModelDefinition
.filter()
.on('name')
.operator('like', 'John')
.filter()
.on('username')
.operator('token', 'admin')
.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John', 'username:token:admin'],
}
)
})
it('should work with rootJunction', () => {
dataElementModelDefinition
.filter()
.logicMode('OR')
.on('name')
.like('John')
.filter()
.logicMode('OR')
.on('username')
.token('admin')
.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John', 'username:token:admin'],
rootJunction: 'OR',
}
)
})
it('should not try to filter by "undefined"', () => {
dataElementModelDefinition.list({ filter: undefined })
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
}
)
})
it('should work by constructing filters before calling list', () => {
const filters = dataElementModelDefinition.filter()
filters.logicMode('OR')
filters.on('name').like('John')
filters.on('username').token('admin')
filters.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
filter: ['name:like:John', 'username:token:admin'],
rootJunction: 'OR',
}
)
})
})
describe('clone', () => {
const dataElementsResult = fixtures.get('/api/dataElements')
let dataElementModelDefinition
beforeEach(() => {
ModelDefinition.prototype.api = {
get: jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve(dataElementsResult)
})
),
}
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
it('should be a method', () => {
expect(dataElementModelDefinition.clone).toBeInstanceOf(Function)
})
it('should return a cloned modelDefinition', () => {
expect(dataElementModelDefinition.clone()).not.toBe(
dataElementModelDefinition
)
})
it('should deep equal the creator', () => {
const clonedDefinition = dataElementModelDefinition.clone()
expect(clonedDefinition.name).toBe(dataElementModelDefinition.name)
expect(clonedDefinition.plural).toBe(
dataElementModelDefinition.plural
)
expect(clonedDefinition.isMetaData).toBe(
dataElementModelDefinition.isMetaData
)
expect(clonedDefinition.apiEndpoint).toBe(
dataElementModelDefinition.apiEndpoint
)
expect(clonedDefinition.modelProperties).toBe(
dataElementModelDefinition.modelProperties
)
})
it('should not have reset the filter', () => {
const clonedDefinition = dataElementModelDefinition.clone()
expect(clonedDefinition.filters).not.toBe(
dataElementModelDefinition.filters
)
})
it('should still work like normal modelDefinition', () => {
const clonedDefinition = dataElementModelDefinition.clone()
clonedDefinition.list()
expect(ModelDefinition.prototype.api.get).toBeCalledWith(
'https://play.dhis2.org/demo/api/dataElements',
{
fields: ':all',
}
)
})
})
describe('saving', () => {
let apiUpdateStub
let apiPostStub
let model
let userModelDefinition
beforeEach(() => {
const singleUserAllFields = fixtures.get('/singleUserAllFields')
apiUpdateStub = jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve({
name: 'BS_COLL (N, DSD) TARGET: Blood Units Donated',
})
})
)
apiPostStub = jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve({
name: 'BS_COLL (N, DSD) TARGET: Blood Units Donated',
})
})
)
ModelDefinition.prototype.api = {
update: apiUpdateStub,
post: apiPostStub,
}
userModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/user')
)
class Model {
constructor() {
this.dataValues = {}
this[DIRTY_PROPERTY_LIST] = new Set([])
this.getCollectionChildrenPropertyNames = jest
.fn()
.mockReturnValue([])
this.getEmbeddedObjectCollectionPropertyNames = jest
.fn()
.mockReturnValue([])
this.getReferenceProperties = jest.fn().mockReturnValue([])
}
}
model = new Model()
Object.keys(singleUserAllFields).forEach((key) => {
model.dataValues[key] = singleUserAllFields[key]
model[key] = singleUserAllFields[key]
})
Object.defineProperty(model, 'modelDefinition', {
value: userModelDefinition,
})
})
describe('save()', () => {
it('should be a method that returns a promise', () => {
expect(userModelDefinition.save(model)).toBeInstanceOf(Promise)
})
it('should call the update method on the api', () => {
userModelDefinition.save(model)
expect(apiUpdateStub).toBeCalled()
})
it('should pass only the properties that are owned to the api', () => {
const expectedPayload = fixtures.get('/singleUserOwnerFields')
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][1]).toEqual(expectedPayload)
})
it('should let a falsy value pass as an owned property', () => {
const expectedPayload = fixtures.get('/singleUserOwnerFields')
expectedPayload.surname = ''
model.dataValues.surname = ''
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][1].surname).toEqual(
expectedPayload.surname
)
})
it('should not let undefined pass as a value', () => {
const expectedPayload = fixtures.get('/singleUserOwnerFields')
delete expectedPayload.surname
model.dataValues.surname = undefined
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][1].surname).toEqual(
expectedPayload.surname
)
})
it('should not let null pass as a value', () => {
const expectedPayload = fixtures.get('/singleUserOwnerFields')
delete expectedPayload.surname
model.dataValues.surname = null
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][1].surname).toEqual(
expectedPayload.surname
)
})
it('should save to the url set on the model', () => {
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][0]).toBe(
fixtures.get('/singleUserAllFields').href
)
})
it('should be able to construct a valid save url without an href set on the model', () => {
delete model.dataValues.href
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][0]).toBe(
fixtures.get('/singleUserAllFields').href
)
})
it('should call the update method on the api with the replace strategy option set to true', () => {
userModelDefinition.save(model)
expect(apiUpdateStub.mock.calls[0][2]).toBe(true)
})
it('should save a new object using a post', () => {
// Objects without id are concidered "new"
delete model.id
userModelDefinition.save(model)
expect(apiPostStub).toBeCalled()
})
it('should translate a collection property to an array of ids', () => {
model.getCollectionChildrenPropertyNames.mockReturnValue([
'organisationUnits',
])
model.dataValues.organisationUnits = new Set([
{ name: 'Kenya', id: 'FTRrcoaog83' },
{ name: 'Oslo', id: 'P3jJH5Tu5VC' },
])
userModelDefinition.save(model)
expect(
apiUpdateStub.mock.calls[0][1].organisationUnits
).toEqual([{ id: 'FTRrcoaog83' }, { id: 'P3jJH5Tu5VC' }])
})
it('should not add invalid objects that do not have an id', () => {
model.getCollectionChildrenPropertyNames.mockReturnValue([
'organisationUnits',
])
model.dataValues.organisationUnits = new Set([
{ name: 'Kenya' },
{ name: 'Oslo', id: 'P3jJH5Tu5VC' },
])
userModelDefinition.save(model)
expect(
apiUpdateStub.mock.calls[0][1].organisationUnits
).toEqual([{ id: 'P3jJH5Tu5VC' }])
})
})
describe('saveNew()', () => {
it('should be a method that returns a promise', () => {
expect(userModelDefinition.saveNew(model)).toBeInstanceOf(
Promise
)
})
it('should call the update method on the api', () => {
userModelDefinition.saveNew(model)
expect(apiPostStub).toBeCalled()
})
it('should pass only the properties that are owned to the api', () => {
const expectedPayload = fixtures.get('/singleUserOwnerFields')
userModelDefinition.saveNew(model)
expect(apiPostStub.mock.calls[0][1]).toEqual(expectedPayload)
})
})
})
describe('delete', () => {
let apiDeleteStub
let model
let userModelDefinition
beforeEach(() => {
const singleUserAllFields = fixtures.get('/singleUserAllFields')
apiDeleteStub = jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve()
})
)
ModelDefinition.prototype.api = {
delete: apiDeleteStub,
}
userModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/user')
)
class Model {
constructor() {
this.dataValues = {}
this.modelDefinition = userModelDefinition
this[DIRTY_PROPERTY_LIST] = new Set([])
}
}
model = new Model()
Object.keys(singleUserAllFields).forEach((key) => {
model.dataValues[key] = singleUserAllFields[key]
model[key] = singleUserAllFields[key]
})
})
it('should call the delete method on the api', () => {
userModelDefinition.delete(model)
expect(apiDeleteStub).toBeCalled()
})
it('should call delete with the url', () => {
userModelDefinition.delete(model)
expect(apiDeleteStub).toBeCalledWith(model.href)
})
it('should return a promise', () => {
expect(userModelDefinition.delete(model)).toBeInstanceOf(Promise)
})
it('should create the url from the endpoint and model.id when the href is not available', () => {
model.dataValues.href = undefined
userModelDefinition.delete(model)
expect(apiDeleteStub).toBeCalledWith(
'http://localhost:8080/dhis/api/users/aUplAx3DOWy'
)
})
})
describe('getOwnedPropertyNames', () => {
let dataElementModelDefinition
beforeEach(() => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
it('should return only the owned properties', () => {
const expectedDataElementProperties = [
'lastUpdated',
'code',
'id',
'created',
'name',
'formName',
'legendSets',
'shortName',
'zeroIsSignificant',
'publicAccess',
'commentOptionSet',
'aggregationType',
'valueType',
'url',
'optionSet',
'domainType',
'description',
'categoryCombo',
'user',
'aggregationLevels',
'attributeValues',
'userAccesses',
'userGroupAccesses',
'translations',
].sort()
const ownProperties =
dataElementModelDefinition.getOwnedPropertyNames()
expect(ownProperties.sort()).toEqual(expectedDataElementProperties)
})
})
describe('isTranslatable', () => {
let dataElementModelDefinition
let userModelDefinition
beforeEach(() => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
userModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/user')
)
})
it('should be a function', () => {
expect(typeof dataElementModelDefinition.isTranslatable).toBe(
'function'
)
})
it('should return true if the schema supports translations', () => {
expect(dataElementModelDefinition.isTranslatable()).toBe(true)
})
it('should return false if the schema can not be translated', () => {
expect(userModelDefinition.isTranslatable()).toBe(false)
})
})
describe('getTranslatableProperties()', () => {
let dataElementModelDefinition
beforeEach(() => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
it('should be a function', () => {
expect(
typeof dataElementModelDefinition.getTranslatableProperties
).toBe('function')
})
it('should return the translatable properties', () => {
expect(
dataElementModelDefinition.getTranslatableProperties()
).toEqual(['description', 'formName', 'name', 'shortName'])
})
it('should return only the properties that have a translatableKey', () => {
const dataElementSchema = fixtures.get('/api/schemas/dataElement')
dataElementSchema.properties = dataElementSchema.properties.map(
({ translationKey, ...props }) => ({ ...props })
)
dataElementModelDefinition =
ModelDefinition.createFromSchema(dataElementSchema)
expect(
dataElementModelDefinition.getTranslatableProperties()
).toEqual([])
})
})
describe('getTranslatablePropertiesWithKeys()', () => {
let dataElementModelDefinition
beforeEach(() => {
dataElementModelDefinition = ModelDefinition.createFromSchema(
fixtures.get('/api/schemas/dataElement')
)
})
it('should be a function', () => {
expect(
typeof dataElementModelDefinition.getTranslatablePropertiesWithKeys
).toBe('function')
})
it('should return the translatable properties with their keys', () => {
expect(
dataElementModelDefinition.getTranslatablePropertiesWithKeys()
).toEqual([
{ name: 'description', translationKey: 'DESCRIPTION' },
{ name: 'formName', translationKey: 'FORM_NAME' },
{ name: 'name', translationKey: 'NAME' },
{ name: 'shortName', translationKey: 'SHORT_NAME' },
])
})
it('should return only the properties that have a translatableKey', () => {
const dataElementSchema = fixtures.get('/api/schemas/dataElement')
dataElementSchema.properties = dataElementSchema.properties.map(
({ translationKey, ...props }) => ({ ...props })
)
dataElementModelDefinition =
ModelDefinition.createFromSchema(dataElementSchema)
expect(
dataElementModelDefinition.getTranslatablePropertiesWithKeys()
).toEqual([])
})
})
})
describe('ModelDefinition subsclasses', () => {
let getOnApiStub
beforeEach(() => {
getOnApiStub = jest.fn().mockReturnValue(Promise.resolve())
ModelDefinition.prototype.api = {
get: getOnApiStub,
}
})
describe('UserModelDefinition', () => {
let UserModelDefinitionClass
let userModelDefinition
beforeEach(() => {
UserModelDefinitionClass = ModelDefinition.specialClasses.user
userModelDefinition = new UserModelDefinitionClass(
{
singular: 'user',
plural: 'users',
displayName: 'Users',
},
{},
{}
)
})
it('should be instance of Model', () => {
expect(userModelDefinition).toBeInstanceOf(ModelDefinition)
})
it('should call the get function with the extra parameters', () => {
userModelDefinition.get('myUserId')
expect(getOnApiStub).toBeCalledWith('/myUserId', {
fields: ':all,userCredentials[:owner]',
})
})
})
describe('DataSetModelDefinition', () => {
let DataSetModelDefinitionClass
let dataSetModelDefinition
beforeEach(() => {
DataSetModelDefinitionClass = ModelDefinition.specialClasses.dataSet
dataSetModelDefinition = new DataSetModelDefinitionClass(
fixtures.get('/api/schemas/dataSet'),
{},
{},
{},
{}
)
})
it('handles compulsory data element operands correctly', () => {
const dataSet = dataSetModelDefinition.create({
compulsoryDataElementOperands: ['one', 'two', 'three'],
})
expect(dataSet).toBeInstanceOf(Model)
expect(dataSet.dataValues.compulsoryDataElementOperands).toEqual([
'one',
'two',
'three',
])
})
})
describe('OrganisationUnitDefinition', () => {
let OrganisationUnitModelDefinitionClass
let organisationUnitModelDefinition
beforeEach(() => {
OrganisationUnitModelDefinitionClass =
ModelDefinition.specialClasses.organisationUnit
organisationUnitModelDefinition =
new OrganisationUnitModelDefinitionClass(
{
singular: 'organisationUnit',
plural: 'organisationUnits',
apiEndpoint: 'organisationUnits',
},
{},
{},
{},
{}
)
})
it('should use the special root orgunit id when fetching lists', () => {
expect.assertions(1)
return organisationUnitModelDefinition
.list({ root: 'myRootId' })
.catch(() => {
expect(getOnApiStub).toBeCalledWith(
'organisationUnits/myRootId',
{
fields: ':all',
}
)
})
})
it('should handle list queries without special `root` parameters', () => {
expect.assertions(1)
return organisationUnitModelDefinition.list().catch(() => {
expect(getOnApiStub).toBeCalledWith('organisationUnits', {
fields: ':all',
})
})
})
})
})