Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add custom errors #56

Merged
merged 4 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Loading