diff --git a/src/__utils__/helpers.ts b/src/__utils__/helpers.ts index 063076d..290ceee 100644 --- a/src/__utils__/helpers.ts +++ b/src/__utils__/helpers.ts @@ -1,9 +1,119 @@ +import { + ClientSecurity, + NewClient, + ZedClientInterface, + RelationshipUpdate_Operation, +} from '../v1.js' + /** - * Generates a random token with a prefix to support - * unique, idempotent local testing. - * @param prefix - * @returns + * Generates a random token to support unique, idempotent local testing. */ -export function generateTestToken(prefix: string): string { - return `${prefix}-${Date.now().toString()}` -} \ No newline at end of file +export function generateTestToken(): string { + return Math.random().toString() +} + +/* + * Asserts that a value or expression is non-falsey. + * It's mostly useful to use a statement to narrow a type + * (as opposed to using an if statement, which is annoying + * in tests) + */ +export function assert(val: unknown, msg = "Assertion failed"): asserts val { + if(!val) throw new Error(msg); +} + +export const testClient = () => NewClient( + generateTestToken(), + 'localhost:50051', + ClientSecurity.INSECURE_LOCALHOST_ALLOWED + ) + +const testSchema = ` +caveat likes_harry_potter(likes bool) { + likes == true +} + +definition post { + relation writer: user + relation reader: user + relation caveated_reader: user with likes_harry_potter + + permission write = writer + permission view = reader + writer + permission view_as_fan = caveated_reader + writer +} +definition user {} +` + +export const writeTestSchema = async (client: ZedClientInterface) => { + await client.promises.writeSchema({ + schema: testSchema + }) +} + +export const writeTestTuples = async (client: ZedClientInterface) => { + const emilia = { + object: { + objectType: "user", + objectId: "emilia" + }, + optionalRelation: "", + } + const beatrice = { + object: { + objectType: "user", + objectId: "beatrice" + }, + optionalRelation: "", + } + const postOne = { + objectType: "post", + objectId: "post-one" + } + const postTwo = { + objectType: "post", + objectId: "post-two" + } + await client.promises.writeRelationships({ + // NOTE: optionalPreconditions seems like it should be omittable, + // but the way that the typescript plugin handles repeated fields + // is to treat them as required. + optionalPreconditions: [], + updates: [ + { + operation: RelationshipUpdate_Operation.CREATE, + relationship: { + subject: emilia, + relation: "writer", + resource: postOne, + } + }, + { + operation: RelationshipUpdate_Operation.CREATE, + relationship: { + subject: emilia, + relation: "writer", + resource: postTwo, + } + }, + { + operation: RelationshipUpdate_Operation.CREATE, + relationship: { + subject: beatrice, + relation: "reader", + resource: postOne, + } + }, + { + operation: RelationshipUpdate_Operation.CREATE, + relationship: { + subject: beatrice, + relation: "caveated_reader", + resource: postOne, + optionalCaveat: { caveatName: "likes_harry_potter" } + } + }, + ] + }) + return { emilia, beatrice, postOne, postTwo }; +} diff --git a/src/v1-promise.test.ts b/src/v1-promise.test.ts index 8f50b0b..e24f5cf 100644 --- a/src/v1-promise.test.ts +++ b/src/v1-promise.test.ts @@ -1,5 +1,12 @@ import * as grpc from '@grpc/grpc-js'; -import { generateTestToken } from './__utils__/helpers.js'; +import { getOneofValue } from '@protobuf-ts/runtime'; +import { + generateTestToken, + testClient, + writeTestSchema, + writeTestTuples, + assert, +} from './__utils__/helpers.js'; import { Struct } from './authzedapi/google/protobuf/struct.js'; import { BulkExportRelationshipsRequest, @@ -22,6 +29,8 @@ import { } from './v1.js'; import { describe, it, expect, beforeEach } from 'vitest' +const fullyConsistent: Consistency = { requirement: { oneofKind: "fullyConsistent", fullyConsistent: true }}; + describe('a check with an unknown namespace', () => { it('should raise a failed precondition', async () => { const resource = ObjectReference.create({ @@ -43,7 +52,7 @@ describe('a check with an unknown namespace', () => { }); const { promises: client } = NewClient( - generateTestToken('v1-promise-test-unknown'), + generateTestToken(), 'localhost:50051', ClientSecurity.INSECURE_LOCALHOST_ALLOWED ); @@ -111,7 +120,7 @@ describe('a check with an known namespace', () => { it('should succeed', async () => { const { promises: client } = NewClient( - generateTestToken('v1-promise-namespace'), + generateTestToken(), 'localhost:50051', ClientSecurity.INSECURE_LOCALHOST_ALLOWED ); @@ -132,7 +141,7 @@ describe('a check with an known namespace', () => { it('should succeed with full signatures', async () => { const { promises: client } = NewClient( - generateTestToken('v1-promise-namespace'), + generateTestToken(), 'localhost:50051', ClientSecurity.INSECURE_LOCALHOST_ALLOWED ); @@ -167,7 +176,7 @@ describe('a check with an known namespace', () => { it('should succeed', async () => { // Write some schema. const { promises: client } = NewClient( - generateTestToken('v1-promise-caveats'), + generateTestToken(), 'localhost:50051', ClientSecurity.INSECURE_LOCALHOST_ALLOWED ); @@ -310,6 +319,61 @@ describe('a check with an known namespace', () => { }); }); +describe("checkBulkPermissions", () => { + it("can check bulk permissions", async () => { + const client = testClient(); + await writeTestSchema(client); + const { emilia, postOne } = await writeTestTuples(client); + const response = await client.promises.checkBulkPermissions({ + consistency: fullyConsistent, + items: [ + { + resource: postOne, + permission: "view", + subject: emilia, + } + ] + }) + expect(response.pairs.length).to.equal(2) + // These assertions are annoying but necessary to keep typescript happy. + assert(!(response.pairs[0].response.oneofKind === "error")) + assert(response.pairs[0].response.oneofKind) + expect(response.pairs[0].response.item.permissionship === CheckPermissionResponse_Permissionship.HAS_PERMISSION) + + assert(!(response.pairs[1].response.oneofKind === "error")) + assert(response.pairs[1].response.oneofKind) + expect(response.pairs[1].response.item.permissionship === CheckPermissionResponse_Permissionship.HAS_PERMISSION) + }) + + it("can check bulk permissions with a deadline", async () => { + const client = testClient(); + await writeTestSchema(client); + const { emilia, postOne } = await writeTestTuples(client); + const response = await client.promises.checkBulkPermissions( + { + consistency: fullyConsistent, + items: [ + { + resource: postOne, + permission: "view", + subject: emilia, + } + ] + }, + {deadline: Date.now() + 5000}, + ) + expect(response.pairs.length).to.equal(2) + // These assertions are annoying but necessary to keep typescript happy. + assert(!(response.pairs[0].response.oneofKind === "error")) + assert(response.pairs[0].response.oneofKind) + expect(response.pairs[0].response.item.permissionship === CheckPermissionResponse_Permissionship.HAS_PERMISSION) + + assert(!(response.pairs[1].response.oneofKind === "error")) + assert(response.pairs[1].response.oneofKind) + expect(response.pairs[1].response.item.permissionship === CheckPermissionResponse_Permissionship.HAS_PERMISSION) + }) +}) + describe('Lookup APIs', () => { let token: string; @@ -346,7 +410,7 @@ describe('Lookup APIs', () => { }); beforeEach(async () => { - token = generateTestToken('v1-promise-lookup'); + token = generateTestToken(); const { promises: client } = NewClient( token, 'localhost:50051', @@ -459,7 +523,7 @@ describe('Experimental Service', () => { let token: string; beforeEach(async () => { - token = generateTestToken('v1-experimental-service'); + token = generateTestToken(); const { promises: client } = NewClient( token, 'localhost:50051', diff --git a/src/v1.test.ts b/src/v1.test.ts index f1e7c47..f90db73 100644 --- a/src/v1.test.ts +++ b/src/v1.test.ts @@ -49,7 +49,7 @@ describe("a check with an unknown namespace", () => { }); const client = NewClient( - generateTestToken("v1-test-unknown"), + generateTestToken(), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED ); @@ -66,7 +66,7 @@ describe("a check with an known namespace", () => { it("should succeed", () => new Promise((done) => { // Write some schema. const client = NewClient( - generateTestToken("v1-namespace"), + generateTestToken(), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED, PreconnectServices.PERMISSIONS_SERVICE | PreconnectServices.SCHEMA_SERVICE @@ -172,7 +172,7 @@ describe("a check with an known namespace", () => { it("should succeed", () => new Promise((done) => { // Write some schema. const client = NewClient( - generateTestToken("v1-namespace-caveats"), + generateTestToken(), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED ); @@ -288,7 +288,7 @@ describe("Lookup APIs", () => { let token: string; beforeEach(() => new Promise((done) => { - token = generateTestToken("v1-lookup"); + token = generateTestToken(); const client = NewClient( token, "localhost:50051", @@ -461,7 +461,7 @@ describe("a check with a negative timeout", () => { }); const client = NewClient( - generateTestToken("v1-test-unknown"), + generateTestToken(), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED, PreconnectServices.NONE, @@ -482,7 +482,7 @@ describe("Experimental Service", () => { let token: string; beforeEach(() => new Promise((done) => { - token = generateTestToken("v1-experimental-service"); + token = generateTestToken(); const client = NewClient( token, "localhost:50051",