octonom
Version:
Object Document Mapper for any database
255 lines (214 loc) • 10.8 kB
text/typescript
import { stub } from 'sinon';
import { ArrayCollection } from '../array-collection';
import { SanitizationError, ValidationError } from '../errors';
import { Model } from '../model';
import { ArrayInstance, ArraySchema } from './array';
import { ModelSchema } from './model';
import { ReferenceSchema } from './reference';
import { StringSchema } from './string';
describe('ArraySchema', () => {
class TestModel extends Model {
public foo: string;
}
TestModel.setSchema('foo', new StringSchema());
describe('constructor()', () => {
it('should be instantiatable with options', () => {
const schema = new ArraySchema({elementSchema: new StringSchema()});
expect(schema).to.have.property('options').that.eql({elementSchema: {options: {}}});
});
});
describe('create()', () => {
it('should throw a SanitizationError if value is not an array', () => {
const schema = new ArraySchema({elementSchema: new StringSchema()});
expect(() => schema.create(42)).to.throw(SanitizationError, 'Value is not an array.');
});
it('should throw a SanitizationError if elements cannot be sanitized', () => {
const schema = new ArraySchema({elementSchema: new StringSchema()});
expect(() => schema.create([42])).to.throw(SanitizationError, 'Value is not a string.');
});
it('should return empty array if required and undefined', () => {
const schema = new ArraySchema({elementSchema: new StringSchema(), required: true});
expect(schema.create(undefined, {defaults: true})).to.have.property('value').that.eql([]);
});
it('should return an array of strings', () => {
const schema = new ArraySchema({elementSchema: new StringSchema()});
expect(schema.create(['bar'])).to.have.property('value').that.eql(['bar']);
});
it('should return an array of models with object input', () => {
const schema = new ArraySchema({elementSchema: new ModelSchema({model: TestModel})});
const instance = schema.create([{foo: 'bar'}]);
expect(instance.value).to.be.an('array').and.to.eql([{foo: 'bar'}]);
expect(instance.value[0]).to.be.an.instanceOf(TestModel);
});
it('should return an array of models with model input', () => {
const schema = new ArraySchema({elementSchema: new ModelSchema({model: TestModel})});
const instance = schema.create([new TestModel({foo: 'bar'})]);
expect(instance.value).to.be.an('array').and.to.eql([{foo: 'bar'}]);
expect(instance.value[0]).to.be.an.instanceOf(TestModel);
});
describe('proxifyArray()', () => {
const schema = new ArraySchema<string>({elementSchema: new StringSchema()});
let beforeChangeStub;
let beforeChangeArray: string[];
let afterChangeStub;
let afterChangeArray: string[];
let arrayInstance: ArrayInstance<string>;
beforeEach(() => {
arrayInstance = schema.create(['foo', 'bar', 'bar', 'bla']);
beforeChangeStub = stub(arrayInstance, 'beforeChange').callsFake(
(path, value, oldInstance) => beforeChangeArray = schema.toObject(oldInstance),
);
afterChangeStub = stub(arrayInstance, 'afterChange').callsFake(
(path, value, newInstance) => afterChangeArray = schema.toObject(newInstance),
);
});
function testArrayProxy(arrayBefore, arrayAfter) {
expect(beforeChangeStub).to.be.calledOnce.and.calledWith([], arrayAfter, arrayInstance);
expect(beforeChangeArray).to.eql(arrayBefore);
expect(arrayInstance.value).to.eql(arrayAfter);
expect(arrayInstance.instanceArray.map(el => el.value)).to.eql(arrayAfter);
arrayInstance.instanceArray.forEach((instance, index) => {
expect(instance.parent).to.eql({instance: arrayInstance, path: index});
});
expect(afterChangeStub).to.be.calledOnce.and.calledWith([], arrayAfter, arrayInstance);
expect(afterChangeArray).to.eql(arrayAfter);
}
it('should be an array', () => {
expect(arrayInstance.value).to.be.an('array').and.an.instanceOf(Array);
});
it('should allow setting, getting, and deleting a non-number key', () => {
(arrayInstance.value as any).test = true;
expect(arrayInstance.value).to.have.property('test', true);
delete (arrayInstance.value as any).test;
expect(arrayInstance.value).to.not.have.property('test');
});
it.skip('should intercept copyWithin()');
it.skip('should intercept fill()');
it('should intercept pop()', () => {
const instance = arrayInstance.instanceArray[arrayInstance.instanceArray.length - 1];
expect(arrayInstance.value.pop()).to.equal('bla');
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['foo', 'bar', 'bar']);
expect(instance).to.not.have.property('parent');
});
it('should intercept push()', () => {
expect(arrayInstance.value.push('baz', 'boo')).to.equal(6);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['foo', 'bar', 'bar', 'bla', 'baz', 'boo']);
});
it('should intercept reverse()', () => {
const array = arrayInstance.value;
expect(arrayInstance.value.reverse()).to.equal(array);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['bla', 'bar', 'bar', 'foo']);
});
it('should intercept sort()', () => {
const array = arrayInstance.value;
expect(arrayInstance.value.sort()).to.equal(array);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['bar', 'bar', 'bla', 'foo']);
});
it('should intercept sort() with custom compare function', () => {
const array = arrayInstance.value;
expect(arrayInstance.value.sort((a, b) => a < b ? 1 : -1)).to.equal(array);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['foo', 'bla', 'bar', 'bar']);
});
it('should intercept splice(start)', () => {
expect(arrayInstance.value.splice(3)).to.eql(['bla']);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['foo', 'bar', 'bar']);
});
it('should intercept splice(start, deleteCount)', () => {
expect(arrayInstance.value.splice(2, 1)).to.eql(['bar']);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['foo', 'bar', 'bla']);
});
it('should intercept splice(start, deleteCount, ...elements)', () => {
expect(arrayInstance.value.splice(1, 1, 'boo', 'baz')).to.eql(['bar']);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['foo', 'boo', 'baz', 'bar', 'bla']);
});
it('should intercept unshift()', () => {
expect(arrayInstance.value.unshift('boo', 'baz')).to.equal(6);
testArrayProxy(['foo', 'bar', 'bar', 'bla'], ['boo', 'baz', 'foo', 'bar', 'bar', 'bla']);
});
});
});
describe('populate()', () => {
class ReferencedModel extends Model {
public id: string;
}
ReferencedModel.setSchema('id', new StringSchema());
const collection = new ArrayCollection<ReferencedModel>(ReferencedModel, {modelIdField: 'id'});
collection.insert(new ReferencedModel({id: '0xACAB'}));
collection.insert(new ReferencedModel({id: '4711'}));
it('should populate an array of strings', async () => {
const schema = new ArraySchema({
elementSchema: new ReferenceSchema({collection: () => collection}),
});
const instance = schema.create(['0xACAB', '4711']);
const result = await schema.populate(instance, true);
expect(result).to.equal(instance.value).and.to.eql([{id: '0xACAB'}, {id: '4711'}]);
});
it('should populate an array of mixed strings and populated models', async () => {
const schema = new ArraySchema({
elementSchema: new ReferenceSchema({collection: () => collection}),
});
const instance = schema.create(['0xACAB', new ReferencedModel({id: '4711'})]);
const result = await schema.populate(instance, true);
expect(result).to.equal(instance.value).and.to.eql([{id: '0xACAB'}, {id: '4711'}]);
});
it('should populate an array of models', async () => {
class ElementModel extends Model {
public reference: string | ReferencedModel;
}
ElementModel.setSchema('reference', new ReferenceSchema({collection: () => collection}));
const schema = new ArraySchema({
elementSchema: new ModelSchema({model: ElementModel}),
});
const instance = schema.create([
new ElementModel({reference: '0xACAB'}),
new ElementModel({reference: '4711'}),
]);
const result = await schema.populate(instance, {reference: true});
expect(result).to.equal(instance.value).and.to.be.an('array')
.and.to.eql([{reference: {id: '0xACAB'}}, {reference: {id: '4711'}}]);
});
});
describe('toObject()', () => {
it('should return a new array', () => {
const schema = new ArraySchema({elementSchema: new StringSchema()});
const array = ['foo'];
const result = schema.toObject(schema.create(array));
expect(result).to.not.equal(array);
expect(result).to.eql(array);
});
});
describe('validate()', () => {
it('should throw if value has less than min elements', async () => {
const schema = new ArraySchema({elementSchema: new StringSchema(), minLength: 2});
await expect(schema.validate(schema.create(['foo'])))
.to.be.rejectedWith(ValidationError, 'Array must have at least 2 elements.');
});
it('should throw if value has more than max elements', async () => {
const schema = new ArraySchema({elementSchema: new StringSchema(), maxLength: 2});
await expect(schema.validate(schema.create(['foo', 'bar', 'baz'])))
.to.be.rejectedWith(ValidationError, 'Array must have at most 2 elements.');
});
it('should run custom validator', async () => {
const schema = new ArraySchema<string>({
elementSchema: new StringSchema(),
validate: async (instance: ArrayInstance<string>) => {
if (instance.value.indexOf('baz') !== -1) {
throw new ValidationError('baz is not allowed.');
}
},
});
await schema.validate(schema.create(['foo']));
await expect(schema.validate(schema.create(['foo', 'bar', 'baz'])))
.to.be.rejectedWith(ValidationError, 'baz is not allowed.');
});
it('should validate an array of strings', async () => {
const schema = new ArraySchema({elementSchema: new StringSchema()});
await schema.validate(schema.create(['foo']));
});
it('should validate an array of models', async () => {
const schema = new ArraySchema({elementSchema: new ModelSchema({model: TestModel})});
const array = [new TestModel({foo: 'bar'})];
await schema.validate(schema.create(array));
});
});
});