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.
437 lines
11 KiB
JavaScript
437 lines
11 KiB
JavaScript
6 years ago
|
"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 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
|
||
|
})),
|
||
|
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,
|
||
|
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 (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,
|
||
|
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 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");
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|