Skip to content

Commit

Permalink
Merge pull request #56 from aserto-dev/custom_errors
Browse files Browse the repository at this point in the history
add custom errors
  • Loading branch information
gimmyxd authored Nov 29, 2023
2 parents 894faf4 + 92d3089 commit b51c587
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 31 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"codecov",
"connectrpc",
"displaystatemap",
"instanceof",
"keyof",
"morty",
"njwt",
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,21 @@ Get an object instance with the type `type-name` and the id `object-id`. For exa

```typescript
const user = await directoryClient.object({ objectType: 'user', objectId: '[email protected]' });

// Handle a specific Directory Error
import { NotFoundError } from "@aserto/aserto-node"

try {
directoryClient.object({
objectType: "user",
objectId: "[email protected]",
});
} catch (error) {
if (error instanceof NotFoundError) {
// handle the case where the object was not found
}
throw error;
}
```

#### 'relation' function
Expand Down
108 changes: 104 additions & 4 deletions __tests__/directory/v3/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@ import {
SetRelationResponse,
} from "@aserto/node-directory/src/gen/cjs/aserto/directory/writer/v3/writer_pb";
import { Struct } from "@bufbuild/protobuf";
import { ConnectError } from "@connectrpc/connect";
import { Code, ConnectError } from "@connectrpc/connect";
import { createAsyncIterable } from "@connectrpc/connect/protocol";
import * as connectNode from "@connectrpc/connect-node";

import { DirectoryServiceV3, DirectoryV3 } from "../../../lib/index";
import {
DirectoryServiceV3,
DirectoryV3,
EtagMismatchError,
InvalidArgumentError,
NotFoundError,
UnauthenticatedError,
} from "../../../lib/index";
jest.mock("fs");

describe("DirectoryV3", () => {
Expand Down Expand Up @@ -234,7 +241,7 @@ describe("DirectoryV3", () => {
it("handles ConnectError", async () => {
const mockCheckPermission = jest
.spyOn(directory.ReaderClient, "checkPermission")
.mockRejectedValue(new ConnectError("connect error", 5));
.mockRejectedValue(new ConnectError("connect error", Code.Canceled));

const params = {
subjectId: "[email protected]",
Expand All @@ -243,8 +250,42 @@ describe("DirectoryV3", () => {
objectType: "group",
objectId: "admin",
};

// error class
await expect(directory.checkPermission(params)).rejects.toThrow(
ConnectError
);

// error message
await expect(directory.checkPermission(params)).rejects.toThrow(
'"checkPermission" failed with code: 5, message: [not_found] connect error'
'"checkPermission" failed with code: 1, message: [canceled] connect error'
);

mockCheckPermission.mockReset();
});

it("handles Unauthenticated Error", async () => {
const mockCheckPermission = jest
.spyOn(directory.ReaderClient, "checkPermission")
.mockRejectedValue(
new ConnectError("Invalid credentials", Code.Unauthenticated)
);

const params = {
subjectId: "[email protected]",
subjectType: "user",
permission: "read",
objectType: "group",
objectId: "admin",
};

// error class
await expect(directory.checkPermission(params)).rejects.toThrow(
UnauthenticatedError
);
// error message
await expect(directory.checkPermission(params)).rejects.toThrow(
"Authentication failed: [unauthenticated] Invalid credentials"
);

mockCheckPermission.mockReset();
Expand Down Expand Up @@ -372,6 +413,23 @@ describe("DirectoryV3", () => {

mockGetObject.mockReset();
});

it("handles NotFound Error", async () => {
const mockGetObject = jest
.spyOn(directory.ReaderClient, "getObject")
.mockRejectedValue(new ConnectError("Not found", Code.NotFound));

const params = { objectId: "123", objectType: "user" };

// error class
await expect(directory.object(params)).rejects.toThrow(NotFoundError);
// error message
await expect(directory.object(params)).rejects.toThrow(
"object not found"
);

mockGetObject.mockReset();
});
});

describe("objects", () => {
Expand Down Expand Up @@ -454,6 +512,48 @@ describe("DirectoryV3", () => {

mockSetObject.mockReset();
});

it("handles InvalidArgument Error", async () => {
const mockSetObject = jest
.spyOn(directory.WriterClient, "setObject")
.mockRejectedValue(
new ConnectError("Invalid argument", Code.InvalidArgument)
);

const params = {};

// error class
await expect(directory.setObject(params)).rejects.toThrow(
InvalidArgumentError
);
// error message
await expect(directory.setObject(params)).rejects.toThrow(
"setObject: [invalid_argument] Invalid argument"
);

mockSetObject.mockReset();
});

it("handles EtagMissmatch Error", async () => {
const mockSetObject = jest
.spyOn(directory.WriterClient, "setObject")
.mockRejectedValue(
new ConnectError("Invalid argument", Code.FailedPrecondition)
);

const params = {};

// error class
await expect(directory.setObject(params)).rejects.toThrow(
EtagMismatchError
);
// error message
await expect(directory.setObject(params)).rejects.toThrow(
"invalid etag in setObject request"
);

mockSetObject.mockReset();
});
});

describe("objectMany", () => {
Expand Down
51 changes: 27 additions & 24 deletions __tests__/integration/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
createAsyncIterable,
DirectoryServiceV3,
DirectoryV3,
EtagMismatchError,
getSSLCredentials,
NotFoundError,
policyInstance,
readAsyncIterable,
} from "../../lib";
Expand Down Expand Up @@ -94,6 +96,19 @@ types:
).resolves.not.toThrow();
});

xit("throws EtagMismatchError when setting the same object without Etag", async () => {
await expect(
directoryClient.setObject({
object: {
type: "user",
id: "test-user",
displayName: "updated",
etag: "updated",
},
})
).rejects.toThrow(EtagMismatchError);
});

it("sets a another object", async () => {
await expect(
directoryClient.setObject({
Expand Down Expand Up @@ -255,7 +270,7 @@ types:
).resolves.not.toThrow();
});

it("throws error when getting a delete relation", async () => {
it("throws NotFoundError when getting a delete relation", async () => {
await expect(
directoryClient.relation({
subjectId: "test-user",
Expand All @@ -264,9 +279,7 @@ types:
objectId: "test-group",
objectType: "group",
})
).rejects.toThrow(
'"relation" failed with code: 5, message: [not_found] E20051 key not found'
);
).rejects.toThrow(NotFoundError);
});

it("list user objects", async () => {
Expand Down Expand Up @@ -313,20 +326,16 @@ types:
).resolves.not.toThrow();
});

it("throws error when getting a deleted user object", async () => {
it("throws NotFoundError when getting a deleted user object", async () => {
await expect(
directoryClient.object({ objectType: "user", objectId: "test-user" })
).rejects.toThrow(
'"object" failed with code: 5, message: [not_found] E20051 key not found'
);
).rejects.toThrow(NotFoundError);
});

it("throws error when getting a deleted group object", async () => {
it("throws NotFoundError when getting a deleted group object", async () => {
await expect(
directoryClient.object({ objectType: "group", objectId: "test-group" })
).rejects.toThrow(
'"object" failed with code: 5, message: [not_found] E20051 key not found'
);
).rejects.toThrow(NotFoundError);
});

it("returns [] when there are no objects", async () => {
Expand Down Expand Up @@ -432,26 +441,22 @@ types:
).resolves.not.toThrow();
});

it("throws error when getting a deleted user object", async () => {
it("throws NotFoundError when getting a deleted user object", async () => {
await expect(
directoryClient.object({ objectType: "user", objectId: "import-user" })
).rejects.toThrow(
'"object" failed with code: 5, message: [not_found] E20051 key not found'
);
).rejects.toThrow(NotFoundError);
});

it("throws error when getting a deleted group object", async () => {
it("throws NotFoundError when getting a deleted group object", async () => {
await expect(
directoryClient.object({
objectType: "group",
objectId: "import-group",
})
).rejects.toThrow(
'"object" failed with code: 5, message: [not_found] E20051 key not found'
);
).rejects.toThrow(NotFoundError);
});

it("throws error when getting a delete relation", async () => {
it("throws NotFoundError when getting a delete relation", async () => {
await expect(
directoryClient.relation({
subjectId: "import-user",
Expand All @@ -460,9 +465,7 @@ types:
objectId: "import-group",
objectType: "group",
})
).rejects.toThrow(
'"relation" failed with code: 5, message: [not_found] E20051 key not found'
);
).rejects.toThrow(NotFoundError);
});
});

Expand Down
40 changes: 40 additions & 0 deletions lib/directory/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Custom error class for directory service operations.
* Extends the built-in Error class.
*
* @class DirectoryService
* @extends Error
*/
class DirectoryServiceError extends Error {}
/**
* Object or Relation is not found.
* Extends the DirectoryServiceError class.
*
* @class NotFoundError
* @extends DirectoryServiceError
*/
export class NotFoundError extends DirectoryServiceError {}
/**
* "Invalid Argument" error.
* Extends the DirectoryServiceError class.
*
* @class InvalidArgumentError
* @extends DirectoryServiceError
*/
export class InvalidArgumentError extends DirectoryServiceError {}
/**
* "Etag Mismatch" error.
* Extends the DirectoryServiceError class.
*
* @class EtagMismatchError
* @extends DirectoryServiceError
*/
export class EtagMismatchError extends DirectoryServiceError {}
/**
* "Unauthenticated" error.
* Extends the DirectoryServiceError class.
*
* @class UnauthenticatedError
* @extends DirectoryServiceError
*/
export class UnauthenticatedError extends DirectoryServiceError {}
32 changes: 29 additions & 3 deletions lib/directory/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Struct,
} from "@bufbuild/protobuf";
import {
Code,
ConnectError,
createPromiseClient,
Interceptor,
Expand All @@ -33,6 +34,12 @@ import {
} from "@connectrpc/connect";
import { createGrpcTransport } from "@connectrpc/connect-node";

import {
EtagMismatchError,
InvalidArgumentError,
NotFoundError,
UnauthenticatedError,
} from "../errors";
import {
CheckPermissionRequest,
CheckRelationRequest,
Expand Down Expand Up @@ -465,9 +472,28 @@ export async function* createAsyncIterable<T>(items: T[]): AsyncIterable<T> {

function handleError(error: unknown, method: string) {
if (error instanceof ConnectError) {
throw new Error(
`"${method}" failed with code: ${error.code}, message: ${error.message}`
);
switch (error.code) {
case Code.Unauthenticated: {
throw new UnauthenticatedError(
`Authentication failed: ${error.message}`
);
}
case Code.NotFound: {
throw new NotFoundError(`${method} not found: ${error.message}`);
}
case Code.InvalidArgument: {
throw new InvalidArgumentError(`${method}: ${error.message}`);
}
case Code.FailedPrecondition: {
throw new EtagMismatchError(
`invalid etag in ${method} request: ${error.message}`
);
}
default: {
error.message = `"${method}" failed with code: ${error.code}, message: ${error.message}`;
throw error;
}
}
} else {
throw error;
}
Expand Down
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ export {
SubIdentityMapper,
DirectoryConfig as ServiceConfig,
};

export * from "./directory/errors";

0 comments on commit b51c587

Please sign in to comment.