src/model/__tests__/ModelBase.spec.js
import ModelValidation from '../ModelValidation'
import ModelBase, { DIRTY_PROPERTY_LIST } from '../ModelBase'
jest.mock('../ModelValidation')
describe('ModelBase', () => {
// TODO: For some reason we have to setup the mock before the beforeEach and reset the spy, should figure out a way to perhaps do this differently.
let validateAgainstSchemaSpy
beforeEach(() => {
validateAgainstSchemaSpy =
ModelValidation.getModelValidation().validateAgainstSchema
validateAgainstSchemaSpy.mockReset()
})
it('should have a save method', () => {
const modelBase = new ModelBase()
expect(typeof modelBase.save).toBe('function')
})
it('should have a validate method', () => {
const modelBase = new ModelBase()
expect(typeof modelBase.validate).toBe('function')
})
it('should have a clone method', () => {
const modelBase = new ModelBase()
expect(typeof modelBase.clone).toBe('function')
})
describe('saving', () => {
let modelDefinition
let model
let validateFunction
beforeEach(() => {
validateFunction = jest.fn()
modelDefinition = {
apiEndpoint: '/dataElements',
save: jest.fn().mockReturnValue(Promise.resolve()),
saveNew: jest.fn().mockReturnValue(Promise.resolve()),
}
class Model extends ModelBase {
constructor(modelDef) {
super()
this.modelDefinition = modelDef
this.validate = validateFunction
this.dirty = true
this[DIRTY_PROPERTY_LIST] = new Set(['name'])
this.dataValues = {}
}
}
model = new Model(modelDefinition)
Object.defineProperty(model, 'id', {
get() {
return this.dataValues.id
},
})
})
describe('create()', () => {
it('should call validate before calling save', () => {
validateFunction.mockReturnValue(
Promise.resolve({ status: true })
)
model.create()
// TODO: Fix assertion when the .toBeCalledBefore(model.save) is available https://github.com/facebook/jest/issues/4402
expect(model.validate).toBeCalled()
})
it('should call saveNew on the model', () => {
validateFunction.mockReturnValue(
Promise.resolve({ status: true })
)
return model.create().then(() => {
expect(modelDefinition.saveNew).toBeCalledWith(model)
})
})
it('should not call saveNew when validate fails', () => {
validateFunction.mockReturnValue(
Promise.resolve({ status: false })
)
return model
.create()
.catch((e) => e)
.then(() => {
expect(modelDefinition.save).not.toBeCalled()
})
})
})
describe('save()', () => {
beforeEach(() => {
model.validate.mockReturnValue(
Promise.resolve({ status: true })
)
})
it('should call the save on the model modelDefinition with itself as a parameter', () =>
model.save().then(() => {
expect(modelDefinition.save).toBeCalledWith(model)
}))
it('should call validate before calling save', () => {
model.save()
expect(model.validate).toBeCalled()
})
it('should not call save when validate fails', () => {
model.validate.mockReturnValue(
Promise.resolve({ status: false })
)
return model
.save()
.catch((e) => e)
.then(() => {
expect(modelDefinition.save).not.toBeCalled()
})
})
it('should not call save when the model is not dirty', () => {
model.dirty = false
model.save()
expect(modelDefinition.save).not.toBeCalled()
})
it('should reset dirty to false after save', () => {
expect.assertions(1)
return model.save().then(() => {
expect(model.dirty).toBe(false)
})
})
it('should reset the DIRTY_PROPERTY_LIST to an empty set after save', () => {
expect.assertions(1)
return model.save().then(() => {
expect(model[DIRTY_PROPERTY_LIST].size).toBe(0)
})
})
it('should return a promise that resolves to an empty object when the model is not dirty', () => {
model.dirty = false
expect.assertions(1)
return model.save().then((result) => {
expect(result).toEqual({})
})
})
it('should return rejected promise when the model is not valid', () => {
model.validate.mockReturnValue(
Promise.resolve({ status: false })
)
expect.assertions(1)
return model.save().catch((message) => {
expect(message).toEqual({ status: false })
})
})
it('should set the newly created id onto the model', () => {
modelDefinition.save.mockReturnValue(
Promise.resolve({
httpStatus: 'Created',
response: {
uid: 'DXyJmlo9rge',
},
})
)
return model.save().then(() => {
expect(model.id).toBe('DXyJmlo9rge')
})
})
it('should set the correct href property onto the object', () => {
modelDefinition.save.mockReturnValue(
Promise.resolve({
httpStatus: 'Created',
response: {
uid: 'DXyJmlo9rge',
},
})
)
return model.save().then(() => {
expect(model.dataValues.href).toBe(
'/dataElements/DXyJmlo9rge'
)
})
})
it("should set the dirty children's dirty flag back to false", () => {
model.modelDefinition.modelValidations = {
organisationUnits: {
owner: true,
},
}
model.organisationUnits = {
size: 1,
dirty: true,
}
return model.save().then(() => {
expect(model.organisationUnits.dirty).toBe(false)
})
})
})
})
describe('validate', () => {
let modelValidations
let model
beforeEach(() => {
modelValidations = {
age: {
persisted: true,
type: 'NUMBER',
required: true,
min: 0,
max: 2342,
owner: true,
unique: false,
},
}
class Model extends ModelBase {
constructor(validations) {
super()
this.modelDefinition = {}
this.modelDefinition.modelValidations = validations
this.dataValues = {
age: 4,
}
}
}
model = new Model(modelValidations)
validateAgainstSchemaSpy.mockReturnValue(Promise.resolve([]))
})
it('should fail when the async validate fails', () => {
const message = 'Validation against schema endpoint failed.'
validateAgainstSchemaSpy.mockReturnValue(Promise.reject(message))
expect.assertions(1)
return model.validate().catch((errMessage) => {
expect(errMessage).toBe(message)
})
})
it('should call the validateAgainstSchema method on the modelValidator', () => {
expect.assertions(1)
return model.validate().then(() => {
expect(validateAgainstSchemaSpy).toBeCalled()
})
})
it('should call validateAgainstSchema with the model', () => {
expect.assertions(1)
return model.validate().then(() => {
expect(validateAgainstSchemaSpy).toBeCalledWith(model)
})
})
it('should return false when there are the asyncValidation against the schema failed', () => {
validateAgainstSchemaSpy.mockReturnValue(
Promise.resolve([
{ message: 'Required property missing.', property: 'name' },
])
)
expect.assertions(1)
return model.validate().then((validationState) => {
expect(validationState.status).toBe(false)
})
})
it('should return false when there are the asyncValidation against the schema failed', () => {
validateAgainstSchemaSpy.mockReturnValue(Promise.resolve([]))
expect.assertions(1)
return model.validate().then((validationState) => {
expect(validationState.status).toBe(true)
})
})
})
describe('clone', () => {
let modelDefinition
let model
class Model extends ModelBase {
constructor(modelDef) {
super()
this.modelDefinition = modelDef
this.validate = jest
.fn()
.mockReturnValue(Promise.resolve({ status: true }))
this.dirty = false
this[DIRTY_PROPERTY_LIST] = new Set([])
this.dataValues = {
id: 'DXyJmlo9rge',
name: 'My metadata object',
}
}
static create(modelDef) {
model = new Model(modelDef)
Object.defineProperty(model, 'id', {
get() {
return this.dataValues.id
},
enumerable: true,
})
Object.defineProperty(model, 'name', {
get() {
return this.dataValues.name
},
set(newValue) {
this.dataValues.name = newValue
},
enumerable: true,
})
Object.defineProperty(model, 'userGroups', {
get() {
return this.dataValues.userGroups
},
enumerable: true,
})
return model
}
}
beforeEach(() => {
modelDefinition = {
apiEndpoint: '/dataElements',
save: jest.fn().mockReturnValue(Promise.resolve()),
saveNew: jest.fn().mockReturnValue(Promise.resolve()),
create: jest
.fn()
.mockReturnValue(Model.create(modelDefinition)),
modelValidations: {
id: {},
name: {},
userGroups: {
type: 'COLLECTION',
},
},
}
model = Model.create(modelDefinition)
})
it('should call create on the modelDefinition', () => {
model.clone()
expect(modelDefinition.create).toBeCalled()
})
it('should pass all the dataValues to the create function', () => {
model.clone()
expect(modelDefinition.create).toBeCalledWith({
id: 'DXyJmlo9rge',
name: 'My metadata object',
})
})
it('should pass collections arrays of ids', () => {
// Would generally be a ModelCollection, but it extends Map so for simplicity we use Map directly.
model.dataValues.userGroups = new Map([
['P3jJH5Tu5VC', { id: 'P3jJH5Tu5VC' }],
['FQ2o8UBlcrS', { id: 'FQ2o8UBlcrS' }],
])
model.clone()
expect(modelDefinition.create).toBeCalledWith({
id: 'DXyJmlo9rge',
name: 'My metadata object',
userGroups: [{ id: 'P3jJH5Tu5VC' }, { id: 'FQ2o8UBlcrS' }],
})
})
it('should retain all of the values in the child collections', () => {
model.dataValues.userGroups = new Map([
[
'P3jJH5Tu5VC',
{
id: 'P3jJH5Tu5VC',
name: 'Administrators',
clone() {
return { id: 'P3jJH5Tu5VC', name: 'Administrators' }
},
},
],
[
'FQ2o8UBlcrS',
{
id: 'FQ2o8UBlcrS',
name: 'Super users',
clone() {
return { id: 'FQ2o8UBlcrS', name: 'Super users' }
},
},
],
])
model.clone()
expect(modelDefinition.create).toBeCalledWith({
id: 'DXyJmlo9rge',
name: 'My metadata object',
userGroups: [
{ id: 'P3jJH5Tu5VC', name: 'Administrators' },
{ id: 'FQ2o8UBlcrS', name: 'Super users' },
],
})
})
it('should return an independent clone', () => {
const modelClone = model.clone()
expect(model).not.toBe(modelClone)
modelClone.name = 'NewName'
expect(modelClone.dataValues.name).toBe('NewName')
expect(model.dataValues.name).toBe('My metadata object')
})
it('should preserve the dirty state of the original model', () => {
model.dirty = true
model[DIRTY_PROPERTY_LIST] = new Set(['name'])
const modelClone = model.clone()
expect(modelClone.dirty).toBe(true)
})
})
describe('delete', () => {
let modelDefinition
let model
beforeEach(() => {
modelDefinition = {
delete: jest.fn().mockReturnValue(
new Promise((resolve) => {
resolve()
})
),
}
class Model extends ModelBase {
constructor(modelDef) {
super()
this.modelDefinition = modelDef
this.validate = jest
.fn()
.mockReturnValue(Promise.resolve({ status: true }))
this.dirty = true
this[DIRTY_PROPERTY_LIST] = new Set(['name'])
}
}
model = new Model(modelDefinition)
})
it('should have a delete method', () => {
expect(model.delete).toBeInstanceOf(Function)
})
it('should call delete on the modeldefinition when called', () => {
model.delete()
expect(model.modelDefinition.delete).toBeCalled()
})
it('should call the modelDefinition.delete method with the model', () => {
model.delete()
expect(model.modelDefinition.delete).toBeCalledWith(model)
})
it('should return a promise', () => {
expect(model.delete()).toBeInstanceOf(Promise)
})
})
describe('getCollectionChildren', () => {
let model
beforeEach(() => {
model = new ModelBase()
model.modelDefinition = {
modelValidations: {
dataElements: {
owner: true,
},
userGroups: {},
},
}
model.dataElements = {
name: 'dataElements',
dirty: true,
size: 2,
}
model.userGroups = {
name: 'userGroups',
}
})
it('should return the collection children', () => {
expect(model.getCollectionChildren()).toContain(model.dataElements)
})
it('should not return the children that are not collections', () => {
expect(model.getCollectionChildren()).not.toContain(
model.userGroups
)
})
})
describe('getCollectionChildrenPropertyNames', () => {
let model
beforeEach(() => {
model = new ModelBase()
model.modelDefinition = {
modelValidations: {
dataElements: {
type: 'COLLECTION',
},
dataEntryForm: {
type: 'COMPLEX',
},
},
}
model.dataElements = []
})
it('should return the correct property collections', () => {
expect(model.getCollectionChildrenPropertyNames()).toContain(
'dataElements'
)
})
it('should not return the collection for the property if there is no modelValidation for the property', () => {
model.indicators = []
expect(model.getCollectionChildrenPropertyNames()).not.toContain(
'indicators'
)
})
it('should not return the collection for the property if there is no modelValidation for the property', () => {
model.modelDefinition.modelValidations.indicators = {
type: 'COLLECTION',
}
expect(model.getCollectionChildrenPropertyNames()).not.toContain(
'indicators'
)
})
})
describe('getReferenceProperties', () => {
let model
beforeEach(() => {
model = new ModelBase()
model.modelDefinition = {
modelValidations: {
dataElements: {
type: 'COLLECTION',
embeddedObject: false,
},
dataEntryForm: {
type: 'COMPLEX',
},
user: {
type: 'REFERENCE',
embeddedObject: false,
},
accesses: {
type: 'REFERENCE',
embeddedObject: true,
},
},
}
model.dataElements = []
model.user = {
id: 'xE7jOejl9FI',
firstName: 'John',
}
model.accesses = {
read: true,
write: true,
}
})
it('should return the correct reference properties', () => {
expect(model.getReferenceProperties()).toContain('user')
})
it('should not return the reference property if there is no modelValidation for the property', () => {
model.randomObject = {}
expect(model.getReferenceProperties()).not.toContain('randomObject')
})
it('should not return the reference property if there is no value for the property', () => {
model.modelDefinition.modelValidations.randomObject = {
type: 'REFERENCE',
}
expect(model.getReferenceProperties()).not.toContain('randomObject')
})
it('should not return the property if the reference property is embedded', () => {
expect(model.getReferenceProperties()).not.toContain('accesses')
})
})
describe('getEmbeddedObjectCollectionPropertyNames', () => {
let model
beforeEach(() => {
model = new ModelBase()
model.modelDefinition = {
modelValidations: {
dataElements: {
type: 'COLLECTION',
embeddedObject: false,
},
dataEntryForm: {
type: 'COMPLEX',
},
legends: {
type: 'COLLECTION',
embeddedObject: true,
},
},
}
model.dataElements = []
model.legends = []
})
it('should include the embedded collection', () => {
expect(model.getEmbeddedObjectCollectionPropertyNames()).toContain(
'legends'
)
})
it('should not include non embedded object collections', () => {
expect(
model.getEmbeddedObjectCollectionPropertyNames()
).not.toContain('dataElements')
})
})
describe('getDirtyChildren', () => {
let model
beforeEach(() => {
model = new ModelBase()
model.modelDefinition = {
modelValidations: {
dataElements: {
owner: true,
},
},
}
model.dataElements = {
name: 'dataElements',
dirty: true,
size: 2,
}
})
it('should return the dirty children properties', () => {
expect(model.getDirtyChildren()).toEqual([model.dataElements])
})
})
describe('toJSON', () => {
let model
beforeEach(() => {
model = new ModelBase()
})
it('should be a function', () => {
expect(typeof model.toJSON).toBe('function')
})
it('should not throw an exception on `toJSON` for base models', () => {
expect(model.toJSON()).toEqual({})
})
it('should return a json representation of the model', () => {
model.modelDefinition = {
modelValidations: {
name: {
owner: true,
},
dataElements: {
owner: true,
type: 'COLLECTION',
},
},
}
model.dataValues = {
name: 'ANC',
dataElements: new Map([
['P3jJH5Tu5VC', { id: 'P3jJH5Tu5VC', name: 'anc1' }],
['FQ2o8UBlcrS', { id: 'FQ2o8UBlcrS', name: 'anc2' }],
]),
}
model.name = model.dataValues.name
model.dataElements = model.dataValues.dataElements
const expected = {
name: 'ANC',
dataElements: [{ id: 'P3jJH5Tu5VC' }, { id: 'FQ2o8UBlcrS' }],
}
expect(model.toJSON()).toEqual(expected)
})
})
})