@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
544 lines (522 loc) • 16.6 kB
text/typescript
import { PgSchema } from "../../pg/PgSchema";
import type { Row } from "../../types";
import { ID } from "../../types";
import { EntNotReadableError } from "../errors/EntNotReadableError";
import { Or } from "../predicates/Or";
import { True } from "../predicates/True";
import { AllowIf } from "../rules/AllowIf";
import { DenyIf } from "../rules/DenyIf";
import { Require } from "../rules/Require";
import { Validation } from "../Validation";
import { createVC, ValidationTester } from "./test-utils";
const companySchema = new PgSchema(
'ent.validation"privacy"company',
{
id: { type: ID, autoInsert: "gen_id()" },
tenant_id: { type: ID },
name: { type: String, autoInsert: "''" },
},
[],
);
const vc = createVC();
test("0000: load succeeds when first rule allows", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(_vc, _row) {
return tester.respond("First", true);
}),
new AllowIf(async function Second(_vc, _row) {
return tester.respond("Second", false);
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
vc: vc.toLowerInternal("42"),
}),
).toBe(true);
});
test("0010: load succeeds when any rule allows", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(_vc, _row) {
return tester.respond("First", false);
}),
new AllowIf(async function Second(_vc, _row) {
return tester.respond("Second", true);
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(true);
});
test("0020: load fails when first rule throws", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(_vc, _row) {
return tester.respond("First", Error("wild"));
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(false);
});
test("0030: insert succeeds when all require allow", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(async function First(_vc, row) {
return tester.respond("First", true, row);
}),
new Require(async function Second(_vc, row) {
return tester.respond("Second", true, row);
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
vc: vc.toLowerInternal("42"),
}),
).toBe(true);
});
test("0040: insert fails when any require denies", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(async function First(_vc, _row) {
return tester.respond("First", true);
}),
new Require(async function Second(_vc, _row) {
return tester.respond("Second", false);
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
});
test("0041: update fails when any require denies", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [],
update: [
new Require(async function First(_vc, _row) {
return tester.respond("First", true);
}),
new Require(async function Second(_vc, _row) {
return tester.respond("Second", false);
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateUpdate",
updateInput: {},
vc: vc.toLowerInternal("42"),
}),
).toBe(false);
});
test("0042: delete fails when any require denies", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [],
delete: [
new Require(async function First(_vc, _row) {
return tester.respond("First", true);
}),
new Require(async function Second(_vc, _row) {
return tester.respond("Second", false);
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateDelete",
vc: vc.toLowerInternal("42"),
}),
).toBe(false);
});
test("0050: insert fails when any require throws", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(async function First(_vc, _row) {
return tester.respond("First", true);
}),
new Require(async function Second(_vc, _row) {
return tester.respond("Second", Error("wild"));
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
});
test("0060: load succeeds when any rule allows even if another rule throws EntNotReadableError", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(vc, _row) {
return tester.respond(
"First",
// Not a "wild" exception (since derived from EntAccessError).
new EntNotReadableError(
"other_table",
vc.toString(),
{ id: "987" },
"ent access error",
),
);
}),
new AllowIf(async function Second(_vc, _row) {
return tester.respond("Second", true);
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(true);
});
test("0070: load fails when any rule allows but another rule throws any wild exception", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(_vc, _row) {
// "Wild" means "not derived from EntAccessError"
return tester.respond("First", Error("wild"));
}),
new AllowIf(async function Second(_vc, _row) {
return tester.respond("Second", true);
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(false);
});
test("0080: validations fail when no rules defined", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
});
test("0090: load fails with nice error message if only one rule", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(_vc, _row) {
return tester.respond("First", Error("wild"));
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new AllowIf(async function First(_vc, _row) {
return tester.respond("First", false);
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(false);
});
test("0100: insert fails with nice error message if only one rule", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(async function First(_vc, _row) {
return tester.respond("First", Error("wild"));
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(async function First(_vc, _row) {
return tester.respond("First", false);
}),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
});
test("0110: insert succeeds when DenyIf rule evaluates", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new DenyIf(async function First(_vc, _row) {
return tester.respond("First", false);
}),
new Require(new True()),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(true);
});
test("0120: load fails when DenyIf rule throws", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [
new DenyIf(async function First(vc, _row) {
return tester.respond(
"First",
new EntNotReadableError(
"other_table",
vc.toString(),
{ id: "987" },
"ent access error",
),
);
}),
],
insert: [],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
}),
).toBe(false);
});
test("0130: fail when tenant user id mismatches", async () => {
const tester = new ValidationTester();
const validation = new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
tenantPrincipalField: "tenant_id",
load: [],
insert: [],
});
expect(
await tester.matchSnapshot({
validation,
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateLoad",
vc: vc.toLowerInternal("999"),
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation,
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
vc: vc.toLowerInternal("999"),
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation,
row: { id: "123" } as Row<typeof companySchema.table>,
method: "validateInsert",
vc: vc.toLowerInternal("999"),
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation,
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateUpdate",
updateInput: {},
vc: vc.toLowerInternal("999"),
}),
).toBe(false);
expect(
await tester.matchSnapshot({
validation,
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateDelete",
vc: vc.toLowerInternal("999"),
}),
).toBe(false);
});
test("0140: load succeeds when some of Or predicates succeed", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(
new Or(
async function First(_vc, _row) {
return tester.respond(
"First",
new EntNotReadableError(
"other_table",
vc.toString(),
{ id: "987" },
"ent access error",
),
);
},
async function Second(_vc, _row) {
return tester.respond("Second", true);
},
),
),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(true);
});
test("0150: load fails with nice error when all of Or predicates fail", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(
new Or(
async function First(_vc, _row) {
return tester.respond(
"First",
new EntNotReadableError(
"other_table",
vc.toString(),
{ id: "987" },
{ myKey: "ent access error" },
),
);
},
async function Second(_vc, _row) {
return tester.respond("Second", false);
},
),
),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
});
test("0160: load crashes when some predicates fail with a wild error", async () => {
const tester = new ValidationTester();
expect(
await tester.matchSnapshot({
validation: new Validation<typeof companySchema.table>("table", {
inferPrincipal: async (vc) => vc.toGuest(),
load: [],
insert: [
new Require(
new Or(
async function First(_vc, _row) {
return tester.respond("First", Error("wild"));
},
async function Second(_vc, _row) {
return tester.respond("Second", true);
},
),
),
],
}),
row: { id: "123", tenant_id: "42", name: "hi" },
method: "validateInsert",
}),
).toBe(false);
});