You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

465 lines
12 KiB
JavaScript

"use strict";
const expect = require("chai").expect;
const dm = require("../src");
/* FIXME: Disallow null/nothing/undefined in model definitions, as they make no semantic sense? But allow them for eg. function guards. */
/* FIXME: Test passing an already-guarded collection into a guarded collection factory - either stand-alone or in the context of a model? */
let Event = dm.createType("Event", {
description: dm.string()
});
let Identity = dm.createType("Identity", {
label: dm.string(),
nickname: dm.string({
matches: /^[a-z0-9_-]+$/
}),
emailAddress: dm.string({
matches: /@/
}),
xmppAddress: dm.string({
matches: /@/
}).optional(),
notes: dm.string().default("(none)")
});
let User = dm.createType("User", {
name: dm.string(),
isActive: dm.boolean(),
registrationDate: dm.instanceOf(Date),
misc: dm.either(
dm.string(),
dm.number()
),
// undefinedValue: dm.undefined(),
// nullValue: dm.null(),
// nothingValue: dm.nothing(),
age: dm.number({
minimum: 0
}),
luckyNumbers: dm.setOf(dm.number({
minimum: 1,
maximum: 42
})),
events: dm.arrayOf(Event),
identities: dm.setOf(Identity),
mainIdentity: Identity,
alternateUser: dm.self().optional(),
codeWords: dm.mapOf(dm.string(), dm.string())
});
function generateUserData(props = {}) {
return Object.assign({}, {
name: "Sven Slootweg",
isActive: true,
registrationDate: new Date(),
age: 26,
luckyNumbers: new Set([13, 42]),
misc: 42,
// nullValue: null,
// undefinedValue: undefined,
// nothingValue: undefined,
events: [
Event({
description: "The user was created"
})
],
identities: new Set([
Identity({
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}),
Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net"
})
]),
mainIdentity: Identity({
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}),
codeWords: new Map([
["foo", "Foo"],
["bar", "Bar"]
])
}, props);
}
function compareObject(reference, obj) {
if (typeof reference === "object") {
for (let [key, value] of Object.entries(reference)) {
if (value instanceof Set) {
let objValues = Array.from(obj[key]);
let referenceValues = Array.from(value);
referenceValues.forEach((item, i) => {
compareObject(item, objValues[i]);
});
} else if (value instanceof Map) {
for (let [mapKey, item] of value.entries()) {
compareObject(item, obj[key].get(mapKey));
}
} else if (Array.isArray(value)) {
value.forEach((item, i) => {
compareObject(item, obj[key][i]);
});
} else if (typeof reference === "object") {
compareObject(value, obj[key]);
} else {
expect(obj[key]).to.equal(value);
}
}
} else {
expect(obj).to.equal(reference);
}
}
describe("models", () => {
describe("valid cases", () => {
let userInstance;
let date = new Date();
it("should succeed at creating valid objects", () => {
userInstance = User(generateUserData({
registrationDate: date
}));
});
it("should have the correct properties", () => {
compareObject({
name: "Sven Slootweg",
isActive: true,
registrationDate: date,
age: 26,
luckyNumbers: new Set([13, 42]),
misc: 42,
events: [{
description: "The user was created"
}],
identities: new Set([{
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}, {
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net",
notes: "(none)"
}]),
mainIdentity: {
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
},
codeWords: new Map([
["foo", "Foo"],
["bar", "Bar"]
])
}, userInstance);
});
it("should allow extraneous properties", () => {
let instance = User(generateUserData({
extraneousProperty: 42
}));
expect(instance.extraneousProperty).to.equal(42);
});
it("should allow a null-typed value for nothingValue as well", () => {
User(generateUserData({
nothingValue: null
}));
});
it("should allow a passing validation constraint", () => {
let Model = dm.createType("Model", {
value: dm.string().validate((value) => {
return true;
})
});
Model({
value: "foo"
});
});
// TODO: Tests for allowing NaN, Infinity, -Infinity, etc.
});
describe("failure cases", () => {
describe("required properties", () => {
it("should fail on a missing required property", () => {
expect(() => {
let data = generateUserData();
delete data.name;
User(data);
}).to.throw("Value is required for property 'name'");
});
it("should fail on a required property being null", () => {
expect(() => {
User(generateUserData({
name: null
}));
}).to.throw("Value is required for property 'name'");
});
it("should fail on a required property being undefined", () => {
expect(() => {
User(generateUserData({
name: undefined
}));
}).to.throw("Value is required for property 'name'");
});
});
describe("top-level properties", () => {
it("should require a string-typed name", () => {
expect(() => {
User(generateUserData({
name: 42
}));
}).to.throw("Expected a string, got a number instead");
});
it("should require a boolean-typed isActive", () => {
expect(() => {
User(generateUserData({
isActive: 42
}));
}).to.throw("Expected a boolean, got a number instead");
});
it("should require a number-typed age", () => {
expect(() => {
User(generateUserData({
age: "foo"
}));
}).to.throw("Expected a number, got a string instead");
});
it("should require either a number-typed or a string-typed misc value", () => {
expect(() => {
User(generateUserData({
misc: false
}));
}).to.throw("Expected one of (string, number), got a boolean instead");
});
it("should require a Date-typed registrationDate", () => {
expect(() => {
User(generateUserData({
registrationDate: 42
}));
}).to.throw("Expected an instance of Date, got a number instead");
});
it("should require a Set-typed luckyNumbers", () => {
expect(() => {
User(generateUserData({
luckyNumbers: 42
}));
}).to.throw("Expected a Set or a Set<number>, got a number instead");
});
it("should require a Map-typed codeWords", () => {
expect(() => {
User(generateUserData({
codeWords: 42
}));
}).to.throw("Expected a Map or a Map<string → string>, got a number instead");
});
});
describe("collections and nested instances", () => {
/* TODO: Do we need separate tests for guarded Maps/Sets? */
it("should require Event-typed values for its events array", () => {
expect(() => {
User(generateUserData({
events: [
42, 41, 40
]
}));
}).to.throw("Expected an instance of Event, got a number instead");
});
it("should require Identity-typed values for its identities Set", () => {
expect(() => {
User(generateUserData({
identities: new Set([
42, 41, 40
])
}));
}).to.throw("Expected an instance of Identity, got a number instead");
});
it("should require string-typed keys for its codeWords Map", () => {
expect(() => {
User(generateUserData({
codeWords: new Map([
[42, "foo"],
["bar", "baz"]
])
}));
}).to.throw("Expected a string, got a number instead");
});
it("should require string-typed values for its codeWords Map", () => {
expect(() => {
User(generateUserData({
codeWords: new Map([
["foo", 42],
["bar", "baz"]
])
}));
}).to.throw("Expected a string, got a number instead");
});
it("should reject an instance of the wrong custom type", () => {
expect(() => {
User(generateUserData({
mainIdentity: User(generateUserData())
}));
}).to.throw("Expected an instance of Identity, got an instance of User instead");
});
it("should reject an instance of the wrong type for a 'self' rule", () => {
expect(() => {
User(generateUserData({
alternateUser: Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net"
})
}));
}).to.throw("Expected an instance of User, got an instance of Identity instead");
});
it("should also validate values for a nested instance", () => {
expect(() => {
User(generateUserData({
mainIdentity: Identity({
label: "bot",
nickname: {},
emailAddress: "botpie91@cryto.net"
})
}));
}).to.throw("Expected a string, got an object instead");
});
it("should also validate values for nested instances in a Set", () => {
expect(() => {
User(generateUserData({
identities: new Set([
Identity({
label: "bot",
nickname: {},
emailAddress: "botpie91@cryto.net"
})
])
}));
}).to.throw("Expected a string, got an object instead");
});
});
describe("type options", () => {
describe("numbers", () => {
it("should detect violation of a minimum number", () => {
expect(() => {
User(generateUserData({
age: -1
}));
}).to.throw("Value for property 'age' must be at least 0");
});
it("should detect violation of a numeric range, at the lower bound", () => {
expect(() => {
User(generateUserData({
luckyNumbers: new Set([13, -1])
}));
}).to.throw("Value for property '<unknown>' must be at least 1");
});
it("should detect violation of a numeric range, at the upper bound", () => {
expect(() => {
User(generateUserData({
luckyNumbers: new Set([13, 2000])
}));
}).to.throw("Value for property '<unknown>' cannot be higher than 42");
});
it("should reject NaN", () => {
expect(() => {
User(generateUserData({
age: NaN
}));
}).to.throw("Specified value for property 'age' is NaN, but this is not allowed");
});
it("should reject positive Infinity", () => {
expect(() => {
User(generateUserData({
age: Infinity
}));
}).to.throw("Specified value for property 'age' is an infinite value, but this is not allowed");
});
it("should reject negative Infinity", () => {
expect(() => {
User(generateUserData({
age: -Infinity
}));
}).to.throw("Specified value for property 'age' is an infinite value, but this is not allowed");
});
});
describe("strings", () => {
it("should reject a string that fails the specified 'matches' regex", () => {
expect(() => {
User(generateUserData({
mainIdentity: Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "not-an-email-address"
})
}));
}).to.throw("Value for property 'emailAddress' failed `matches` condition");
});
});
/* TODO: Function guards */
});
describe("additional constraints", () => {
it("should reject a failing validation constraint", () => {
let Model = dm.createType("Model", {
value: dm.string().validate((value) => {
return new dm.ValidationError("Value failed nonsense validation rule");
})
});
expect(() => {
Model({
value: "foo"
});
}).to.throw("Value failed nonsense validation rule");
});
});
});
});