Skip to content
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# dependencies (bun install)
node_modules
package-lock.json

# output
out
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![npm downloads](https://img.shields.io/npm/dm/oauth-callback.svg)](https://npmjs.com/package/oauth-callback)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kriasoft/oauth-callback/blob/main/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
[![Run on Replit](https://img.shields.io/badge/Run%20on-Replit-orange.svg)](https://replit.com/new/github/kriasoft/oauth-callback)

A lightweight OAuth 2.0 callback handler for Node.js, Deno, and Bun with built-in browser flow and MCP SDK integration. Perfect for CLI tools, desktop applications, and development environments that need to capture OAuth authorization codes.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@
},
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.17.3",
"@types/bun": "^1.2.20",
"@types/deno": "^2.3.0",
"@types/node": "^24.3.0",
"@types/open": "^6.2.1",
"@types/bun": "^1.2.20",
"bun-types": "^1.2.20",
"mermaid": "^11.9.0",
"prettier": "^3.6.2",
"publint": "^0.3.12",
Expand Down
204 changes: 204 additions & 0 deletions src/error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { expect, test, describe } from "bun:test";
import { OAuthError, TimeoutError } from "./errors";

describe("OAuthError", () => {
describe("constructor", () => {
test("sets all properties correctly", () => {
const e = new OAuthError(
"access_denied",
"User denied access",
"https://ex.com/e",
);
expect(e).toBeInstanceOf(OAuthError);
expect(e).toBeInstanceOf(Error);
expect(e.name).toBe("OAuthError");
expect(e.error).toBe("access_denied");
expect(e.error_description).toBe("User denied access");
expect(e.error_uri).toBe("https://ex.com/e");
expect(typeof e.stack).toBe("string");
expect(e.stack!.startsWith("OAuthError")).toBeTrue();
});

test("uses error code as message when description is not provided", () => {
const error = new OAuthError("server_error");
expect(error.message).toBe("server_error");
expect(error.error_description).toBeUndefined();
});

test("sets optional parameters correctly", () => {
const error1 = new OAuthError("invalid_request", "Bad request");
expect(error1.error_description).toBe("Bad request");
expect(error1.error_uri).toBeUndefined();

const error2 = new OAuthError("invalid_request");
expect(error2.error_description).toBeUndefined();
expect(error2.error_uri).toBeUndefined();
});
});

describe("inheritance", () => {
test("extends native Error class", () => {
const error = new OAuthError("invalid_request");
expect(error instanceof Error).toBe(true);
expect(error instanceof OAuthError).toBe(true);
});

test("has correct name property", () => {
const error = new OAuthError("any_error");
expect(error.name).toBe("OAuthError");
});

test("maintains proper stack trace", () => {
const error = new OAuthError("unauthorized_client");
expect(typeof error.stack).toBe("string");
expect(error.stack!.includes("OAuthError")).toBe(true);
expect(error.stack!.includes(error.message)).toBe(true);
});
});

describe("edge cases", () => {
test("handles empty string parameters", () => {
const error = new OAuthError("", "", "");
expect(error.error).toBe("");
expect(error.message).toBe("");
expect(error.error_description).toBe("");
expect(error.error_uri).toBe("");
});

test("handles very long strings", () => {
const longString = "x".repeat(10000);
const error = new OAuthError(longString, longString, longString);
expect(error.error).toBe(longString);
expect(error.error_description).toBe(longString);
expect(error.error_uri).toBe(longString);
expect(error.message).toBe(longString);
});

test("handles special characters and unicode", () => {
const special = "错误 🚨 <script>alert('xss')</script>";
const error = new OAuthError(special, special, special);
expect(error.error).toBe(special);
expect(error.error_description).toBe(special);
expect(error.error_uri).toBe(special);
});
});

describe("serialization", () => {
test("JSON.stringify includes custom properties and name", () => {
const error = new OAuthError(
"access_denied",
"User denied access",
"https://example.com/error",
);
const json = JSON.stringify(error);
const parsed = JSON.parse(json);

expect(parsed.error).toBe("access_denied");
expect(parsed.error_description).toBe("User denied access");
expect(parsed.error_uri).toBe("https://example.com/error");
expect(parsed.name).toBe("OAuthError");
expect(parsed.message).toBeUndefined();
expect(parsed.stack).toBeUndefined();
});

test("JSON.stringify with minimal properties", () => {
const error = new OAuthError("invalid_grant");
const json = JSON.stringify(error);
const parsed = JSON.parse(json);

expect(parsed.error).toBe("invalid_grant");
expect(parsed.error_description).toBeUndefined();
expect(parsed.error_uri).toBeUndefined();
expect(parsed.name).toBe("OAuthError");
});
});

describe("comparison", () => {
test("two errors with same properties are not equal by reference", () => {
const e1 = new OAuthError("same", "desc", "uri");
const e2 = new OAuthError("same", "desc", "uri");
expect(e1 === e2).toBe(false);
expect(e1).not.toBe(e2);
});

test("errors have different stack traces even with same properties", () => {
const e1 = new OAuthError("same");
const e2 = new OAuthError("same");
expect(e1.stack).not.toBe(e2.stack);
});
});
});

describe("TimeoutError", () => {
describe("constructor", () => {
test("has correct name property", () => {
const error = new TimeoutError();
expect(error.name).toBe("TimeoutError");
});

test("uses default message when not provided", () => {
const error = new TimeoutError();
expect(error.message).toBe("OAuth callback timed out");
});

test("uses custom message when provided", () => {
const error = new TimeoutError("It took too long, please try again.");
expect(error.message).toBe("It took too long, please try again.");
});
});

describe("inheritance", () => {
test("extends native Error class", () => {
const error = new TimeoutError();
expect(error instanceof Error).toBe(true);
expect(error instanceof TimeoutError).toBe(true);
});

test("maintains proper stack trace", () => {
const error = new TimeoutError("Custom timeout message");
expect(typeof error.stack).toBe("string");
expect(error.stack!.includes("TimeoutError")).toBe(true);
expect(error.stack!.includes(error.message)).toBe(true);
});
});

describe("edge cases", () => {
test("handles empty string message", () => {
const error = new TimeoutError("");
expect(error.message).toBe("");
});

test("handles very long message", () => {
const longMessage = "Timeout: " + "x".repeat(10000);
const error = new TimeoutError(longMessage);
expect(error.message).toBe(longMessage);
});
});

describe("serialization", () => {
test("JSON.stringify only includes name property", () => {
const error = new TimeoutError("Custom message");
const json = JSON.stringify(error);
const parsed = JSON.parse(json);

expect(parsed).toEqual({ name: "TimeoutError" });
expect(parsed.message).toBeUndefined();
expect(parsed.stack).toBeUndefined();
});
});

describe("comparison", () => {
test("two errors with same message are not equal by reference", () => {
const e1 = new TimeoutError("Same message");
const e2 = new TimeoutError("Same message");
expect(e1 === e2).toBe(false);
expect(e1).not.toBe(e2);
});

test("errors have different stack traces even with same message", () => {
const e1 = new TimeoutError();
const e2 = new TimeoutError();
expect(e1.stack).not.toBe(e2.stack);
});
});
});
Loading