fix: resolve type errors across test suite

This commit is contained in:
2026-02-26 12:49:04 -06:00
parent 827b018f25
commit 51eb36f4a6
26 changed files with 2847 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
import { describe, test, expect } from "bun:test";
import { Hono } from "hono";
import { apiResponse } from "../../src/modules/api-utils/apiResponse";
import { ZodError } from "zod";
import GenericError from "../../src/Errors/GenericError";
/**
* Tests the error-handling middleware registered in server.ts.
* We replicate the onError logic on a fresh Hono instance to test
* in isolation without importing all routes.
*/
function createAppWithErrorHandling() {
const app = new Hono();
app.onError((err, ctx) => {
const errClassName = err.constructor.name;
if (
errClassName.toLowerCase().includes("prisma") ||
err.message.toLowerCase().includes("prisma") ||
err.name.toLowerCase().includes("prisma")
) {
return ctx.json(apiResponse.internalError(), 500);
}
if (err instanceof ZodError || err.name === "ZodError") {
const zodResp = apiResponse.zodError(err as ZodError);
return ctx.json(zodResp, zodResp.status as any);
}
const response = apiResponse.error(err);
return ctx.json(response, response.status);
});
return app;
}
describe("Server error handling", () => {
test("Prisma errors return 500 InternalServerError", async () => {
const app = createAppWithErrorHandling();
app.get("/test", () => {
const err = new Error("prisma query failed");
throw err;
});
const res = await app.request("/test");
expect(res.status).toBe(500);
const body: any = await res.json();
expect(body.error).toBe("InternalServerError");
expect(body.successful).toBe(false);
});
test("Prisma errors detected by class name", async () => {
const app = createAppWithErrorHandling();
app.get("/test", () => {
class PrismaClientError extends Error {
constructor() {
super("something");
this.name = "PrismaClientError";
}
}
throw new PrismaClientError();
});
const res = await app.request("/test");
expect(res.status).toBe(500);
});
test("ZodError returns 400 with error array", async () => {
const app = createAppWithErrorHandling();
app.get("/test", (c) => {
// In Zod v4, we need to use z.parse to generate a proper ZodError
const { z } = require("zod");
const schema = z.object({ name: z.string() });
schema.parse({}); // throws ZodError
return c.text("unreachable");
});
const res = await app.request("/test");
expect(res.status).toBe(400);
const body: any = await res.json();
expect(body.message).toBe("TypeError");
expect(body.successful).toBe(false);
expect(Array.isArray(body.error)).toBe(true);
});
test("GenericError returns custom status", async () => {
const app = createAppWithErrorHandling();
app.get("/test", () => {
throw new GenericError({
name: "NotFound",
message: "Resource not found",
status: 404,
});
});
const res = await app.request("/test");
expect(res.status).toBe(404);
const body: any = await res.json();
expect(body.error).toBe("NotFound");
expect(body.message).toBe("Resource not found");
expect(body.successful).toBe(false);
});
test("plain Error defaults to 400", async () => {
const app = createAppWithErrorHandling();
app.get("/test", () => {
throw new Error("Unexpected error");
});
const res = await app.request("/test");
expect(res.status).toBe(400);
const body: any = await res.json();
expect(body.successful).toBe(false);
});
});
+133
View File
@@ -0,0 +1,133 @@
import { describe, test, expect } from "bun:test";
import app from "../../src/api/server";
describe("API Server — Integration", () => {
// -------------------------------------------------------------------
// Teapot route (no auth required)
// -------------------------------------------------------------------
describe("GET /v1/teapot", () => {
test("returns 418 I'm not a teapot", async () => {
const res = await app.request("/v1/teapot");
expect(res.status).toBe(418);
const body: any = await res.json();
expect(body.status).toBe(418);
expect(body.message).toBe("I'm not a teapot");
expect(body.successful).toBe(true);
});
test("returns JSON content type", async () => {
const res = await app.request("/v1/teapot");
expect(res.headers.get("content-type")).toContain("application/json");
});
});
// -------------------------------------------------------------------
// Not Found
// -------------------------------------------------------------------
describe("Not Found handling", () => {
test("returns 404 for unknown routes", async () => {
const res = await app.request("/v1/nonexistent");
expect(res.status).toBe(404);
const body: any = await res.json();
expect(body.successful).toBe(false);
expect(body.error).toBe("NotFound");
});
test("includes method and path in message", async () => {
const res = await app.request("/v1/some/random/path", {
method: "POST",
});
const body: any = await res.json();
expect(body.message).toContain("POST");
expect(body.message).toContain("/v1/some/random/path");
});
test("returns 404 for root path", async () => {
const res = await app.request("/");
expect(res.status).toBe(404);
});
});
// -------------------------------------------------------------------
// CORS
// -------------------------------------------------------------------
describe("CORS", () => {
test("includes CORS headers", async () => {
const res = await app.request("/v1/teapot", {
headers: { Origin: "http://localhost:3000" },
});
// Hono's cors middleware should add access-control headers
const acaoHeader = res.headers.get("access-control-allow-origin");
expect(acaoHeader).toBeDefined();
});
test("handles OPTIONS preflight", async () => {
const res = await app.request("/v1/teapot", {
method: "OPTIONS",
headers: {
Origin: "http://localhost:3000",
"Access-Control-Request-Method": "GET",
},
});
// Should not be 404
expect(res.status).toBeLessThan(400);
});
});
// -------------------------------------------------------------------
// Auth-protected routes (should reject without auth)
// -------------------------------------------------------------------
describe("Protected routes require authorization", () => {
const protectedRoutes = [
{ method: "GET", path: "/v1/company/companies" },
{ method: "GET", path: "/v1/company/count" },
{ method: "GET", path: "/v1/credential/credentials/some-id" },
{ method: "POST", path: "/v1/credential/credentials" },
{ method: "GET", path: "/v1/role" },
{ method: "POST", path: "/v1/role" },
{ method: "GET", path: "/v1/user/users" },
];
test.each(protectedRoutes)(
"$method $path returns 401 without auth header",
async ({ method, path }) => {
const res = await app.request(path, { method });
expect(res.status).toBe(401);
const body: any = await res.json();
expect(body.successful).toBe(false);
},
);
test.each(protectedRoutes)(
"$method $path returns error with invalid auth header",
async ({ method, path }) => {
const res = await app.request(path, {
method,
headers: { Authorization: "invalid-format" },
});
const body: any = await res.json();
expect(body.successful).toBe(false);
},
);
});
// -------------------------------------------------------------------
// Error handling
// -------------------------------------------------------------------
describe("Error handling", () => {
test("ZodError returns 400 with error details", async () => {
// POST to credentials without proper body should trigger a Zod error
const res = await app.request("/v1/credential/credentials", {
method: "POST",
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer invalid.token.here",
},
});
// Will get auth error first, which is expected
const body: any = await res.json();
expect(body.successful).toBe(false);
});
});
});
+225
View File
@@ -0,0 +1,225 @@
/**
* Global test setup — mock heavy external dependencies so unit tests
* never touch real databases, APIs, or file-system keys.
*/
import { mock } from "bun:test";
// ---------------------------------------------------------------------------
// Mock the constants module — almost every source file imports from here.
// We provide safe defaults so modules can be imported without side-effects.
// ---------------------------------------------------------------------------
mock.module("../src/constants", () => ({
prisma: createMockPrisma(),
PORT: "3333",
API_BASE_URL: "http://localhost:3333",
sessionDuration: 30 * 24 * 60 * 60_000,
accessTokenDuration: "10min",
refreshTokenDuration: "30d",
accessTokenPrivateKey: "mock-access-private-key",
refreshTokenPrivateKey: "mock-refresh-private-key",
permissionsPrivateKey: "mock-permissions-private-key",
secureValuesPrivateKey: "mock-secure-values-private-key",
secureValuesPublicKey: "mock-secure-values-public-key",
msalClient: { acquireTokenByCode: mock(() => Promise.resolve({})) },
connectWiseApi: {
get: mock(() => Promise.resolve({ data: {} })),
post: mock(() => Promise.resolve({ data: {} })),
},
unifi: createMockUnifi(),
unifiControllerBaseUrl: "https://unifi.test.local",
unifiSite: "default",
unifiUsername: "admin",
unifiPassword: "test-pass",
io: { of: mock(() => ({ on: mock() })) },
engine: {},
}));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
export function createMockPrisma() {
const createModelProxy = () =>
new Proxy(
{},
{
get(_target, prop) {
return mock(() => Promise.resolve(null));
},
},
);
return new Proxy(
{},
{
get(_target, prop) {
if (prop === "$connect" || prop === "$disconnect")
return mock(() => Promise.resolve());
return createModelProxy();
},
},
);
}
export function createMockUnifi() {
return {
login: mock(() => Promise.resolve()),
getAllSites: mock(() => Promise.resolve([])),
getSiteOverview: mock(() => Promise.resolve({})),
getDevices: mock(() => Promise.resolve([])),
getWlanConf: mock(() => Promise.resolve([])),
updateWlanConf: mock(() => Promise.resolve({})),
getNetworks: mock(() => Promise.resolve([])),
createSite: mock(() =>
Promise.resolve({ name: "default", description: "Default" }),
),
getWlanGroups: mock(() => Promise.resolve([])),
createWlanGroup: mock(() => Promise.resolve({})),
getUserGroups: mock(() => Promise.resolve([])),
createUserGroup: mock(() => Promise.resolve({})),
getApGroups: mock(() => Promise.resolve([])),
createApGroup: mock(() => Promise.resolve({})),
updateApGroup: mock(() => Promise.resolve({})),
getAccessPoints: mock(() => Promise.resolve([])),
getWifiLimits: mock(() => Promise.resolve({})),
getPrivatePSKs: mock(() => Promise.resolve([])),
createPrivatePSK: mock(() => Promise.resolve({})),
};
}
/** Build a minimal Prisma-shaped User row for controller tests. */
export function buildMockUser(overrides: Record<string, any> = {}) {
return {
id: "user-1",
userId: "ms-uid-1",
name: "Test User",
login: "test@example.com",
email: "test@example.com",
emailVerified: null,
image: null,
token: "ms-token",
permissions: null,
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
roles: [],
...overrides,
};
}
/** Build a minimal Prisma-shaped Role row. */
export function buildMockRole(overrides: Record<string, any> = {}) {
return {
id: "role-1",
title: "Test Role",
moniker: "test-role",
permissions: "mock-permissions-token",
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
users: [],
...overrides,
};
}
/** Build a minimal Prisma-shaped Company row. */
export function buildMockCompany(overrides: Record<string, any> = {}) {
return {
id: "company-1",
name: "Test Company",
cw_Identifier: "TestCo",
cw_CompanyId: 123,
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
...overrides,
};
}
/** Build a minimal Session row. */
export function buildMockSession(overrides: Record<string, any> = {}) {
return {
id: "session-1",
sessionKey: "sk-abc123",
userId: "user-1",
expires: new Date(Date.now() + 30 * 24 * 60 * 60_000),
refreshedAt: null,
invalidatedAt: null,
refreshTokenGenerated: false,
...overrides,
};
}
/** Build a minimal CredentialType row. */
export function buildMockCredentialType(overrides: Record<string, any> = {}) {
return {
id: "ctype-1",
name: "Login Credential",
permissionScope: "credential.login",
icon: null,
fields: [
{
id: "username",
name: "Username",
required: true,
secure: false,
valueType: "plain_text",
},
{
id: "password",
name: "Password",
required: true,
secure: true,
valueType: "password",
},
],
credentials: [],
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
...overrides,
};
}
/** Build a minimal Credential row. */
export function buildMockCredential(overrides: Record<string, any> = {}) {
const ctype = buildMockCredentialType();
const company = buildMockCompany();
return {
id: "cred-1",
name: "Test Credential",
notes: null,
typeId: ctype.id,
companyId: company.id,
subCredentialOfId: null,
fields: { username: "admin" },
type: ctype,
company,
securevalues: [
{
id: "sv-1",
name: "password",
content: "encrypted-data",
hash: "BLAKE2s$abc$salt",
credentialId: "cred-1",
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
},
],
subCredentials: [],
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
...overrides,
};
}
/** Build a minimal UnifiSite row. */
export function buildMockUnifiSite(overrides: Record<string, any> = {}) {
return {
id: "usite-1",
name: "Main Office",
siteId: "default",
companyId: null,
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
...overrides,
};
}
+154
View File
@@ -0,0 +1,154 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { apiResponse } from "../../src/modules/api-utils/apiResponse";
import { z } from "zod";
import GenericError from "../../src/Errors/GenericError";
describe("apiResponse", () => {
// -------------------------------------------------------------------
// successful
// -------------------------------------------------------------------
describe("successful()", () => {
test("returns status 200 and successful: true", () => {
const res = apiResponse.successful("OK");
expect(res.status).toBe(200);
expect(res.successful).toBe(true);
expect(res.message).toBe("OK");
});
test("includes data when provided", () => {
const data = { id: 1, name: "Test" };
const res = apiResponse.successful("OK", data);
expect(res.data).toEqual(data);
});
test("data is undefined when not provided", () => {
const res = apiResponse.successful("OK");
expect(res.data).toBeUndefined();
});
test("includes meta.timestamp", () => {
const before = Date.now();
const res = apiResponse.successful("OK");
const after = Date.now();
expect(res.meta.timestamp).toBeGreaterThanOrEqual(before);
expect(res.meta.timestamp).toBeLessThanOrEqual(after);
});
test("accepts optional meta parameter (overridden by timestamp)", () => {
const res = apiResponse.successful("OK", null, { custom: true } as any);
// The implementation replaces meta entirely with { timestamp }
expect(res.meta.timestamp).toBeDefined();
});
});
// -------------------------------------------------------------------
// created
// -------------------------------------------------------------------
describe("created()", () => {
test("returns status 201 and successful: true", () => {
const res = apiResponse.created("Created");
expect(res.status).toBe(201);
expect(res.successful).toBe(true);
expect(res.message).toBe("Created");
});
test("includes data when provided", () => {
const res = apiResponse.created("Created", { id: "abc" });
expect(res.data).toEqual({ id: "abc" });
});
test("data is undefined when not provided", () => {
const res = apiResponse.created("Created");
expect(res.data).toBeUndefined();
});
test("includes meta.timestamp", () => {
const res = apiResponse.created("Created");
expect(res.meta.timestamp).toBeDefined();
expect(typeof res.meta.timestamp).toBe("number");
});
});
// -------------------------------------------------------------------
// error
// -------------------------------------------------------------------
describe("error()", () => {
test("reads status from error object", () => {
const err = new GenericError({
name: "Oops",
message: "bad",
status: 422,
});
const res = apiResponse.error(err);
expect(res.status).toBe(422);
expect(res.message).toBe("bad");
expect(res.error).toBe("Oops");
expect(res.successful).toBe(false);
});
test("defaults status to 400 when error has no status", () => {
const err = new Error("plain error");
const res = apiResponse.error(err);
expect(res.status).toBe(400);
});
test("includes meta.timestamp", () => {
const err = new Error("x");
const res = apiResponse.error(err);
expect(res.meta.timestamp).toBeDefined();
});
});
// -------------------------------------------------------------------
// internalError
// -------------------------------------------------------------------
describe("internalError()", () => {
test("returns status 500", () => {
const res = apiResponse.internalError();
expect(res.status).toBe(500);
expect(res.successful).toBe(false);
expect(res.error).toBe("InternalServerError");
expect(res.message).toContain("Internal Server Error");
});
test("includes meta.timestamp", () => {
const res = apiResponse.internalError();
expect(res.meta.timestamp).toBeDefined();
});
});
// -------------------------------------------------------------------
// zodError
// -------------------------------------------------------------------
describe("zodError()", () => {
test("returns status 400 with parsed error data", () => {
const schema = z.object({ name: z.string() });
let zodErr: z.ZodError;
try {
schema.parse({ name: 123 });
throw new Error("should not reach");
} catch (e) {
zodErr = e as z.ZodError;
}
const res = apiResponse.zodError(zodErr!);
expect(res.status).toBe(400);
expect(res.successful).toBe(false);
expect(res.message).toBe("TypeError");
expect(Array.isArray(res.error)).toBe(true);
expect(res.error[0].code).toBe("invalid_type");
});
test("includes meta.timestamp", () => {
const schema = z.object({ x: z.string() });
let zodErr: z.ZodError;
try {
schema.parse({});
throw new Error("should not reach");
} catch (e) {
zodErr = e as z.ZodError;
}
const res = apiResponse.zodError(zodErr!);
expect(res.meta.timestamp).toBeDefined();
});
});
});
@@ -0,0 +1,188 @@
import { describe, test, expect } from "bun:test";
import { CompanyController } from "../../../src/controllers/CompanyController";
import { buildMockCompany } from "../../setup";
const mockCwData = {
company: {
addressLine1: "123 Main St",
addressLine2: null,
city: "Springfield",
state: "IL",
zip: "62701",
country: { name: "United States" },
_info: { contacts_href: "" },
} as any,
defaultContact: {
id: 100,
firstName: "John",
lastName: "Doe",
inactiveFlag: false,
title: "CEO",
defaultPhoneNbr: "555-1234",
communicationItems: [
{ type: { name: "Email" }, value: "john@test.com" },
{ type: { name: "Phone" }, value: "555-1234" },
],
} as any,
allContacts: [] as any[],
};
describe("CompanyController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets public properties from company data", () => {
const data = buildMockCompany();
const ctrl = new CompanyController(data);
expect(ctrl.id).toBe("company-1");
expect(ctrl.name).toBe("Test Company");
expect(ctrl.cw_Identifier).toBe("TestCo");
expect(ctrl.cw_CompanyId).toBe(123);
});
test("accepts optional CW data", () => {
const data = buildMockCompany();
const ctrl = new CompanyController(data, mockCwData);
expect(ctrl.cw_Data).toBeDefined();
expect(ctrl.cw_Data?.company.city).toBe("Springfield");
});
test("cw_Data is undefined when not provided", () => {
const data = buildMockCompany();
const ctrl = new CompanyController(data);
expect(ctrl.cw_Data).toBeUndefined();
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns base fields without options", () => {
const ctrl = new CompanyController(buildMockCompany(), mockCwData);
const json = ctrl.toJson();
expect(json.id).toBe("company-1");
expect(json.name).toBe("Test Company");
expect(json.cw_Identifier).toBe("TestCo");
expect(json.cw_CompanyId).toBe(123);
});
test("excludes address when includeAddress is false", () => {
const ctrl = new CompanyController(buildMockCompany(), mockCwData);
const json = ctrl.toJson({
includeAddress: false,
includePrimaryContact: false,
});
expect(json.cw_Data.address).toBeUndefined();
});
test("includes address when includeAddress is true", () => {
const ctrl = new CompanyController(buildMockCompany(), mockCwData);
const json = ctrl.toJson({
includeAddress: true,
includePrimaryContact: false,
});
expect(json.cw_Data.address).toBeDefined();
expect(json.cw_Data.address!.city).toBe("Springfield");
expect(json.cw_Data.address!.state).toBe("IL");
});
test("includes primary contact when includePrimaryContact is true", () => {
const ctrl = new CompanyController(buildMockCompany(), mockCwData);
const json = ctrl.toJson({
includeAddress: false,
includePrimaryContact: true,
});
expect(json.cw_Data.primaryContact).toBeDefined();
expect(json.cw_Data.primaryContact!.firstName).toBe("John");
expect(json.cw_Data.primaryContact!.lastName).toBe("Doe");
expect(json.cw_Data.primaryContact!.email).toBe("john@test.com");
});
test("excludes primary contact when includePrimaryContact is false", () => {
const ctrl = new CompanyController(buildMockCompany(), mockCwData);
const json = ctrl.toJson({
includeAddress: false,
includePrimaryContact: false,
});
expect(json.cw_Data.primaryContact).toBeUndefined();
});
test("includes allContacts when includeAllContacts is true", () => {
const cwDataWithContacts = {
...mockCwData,
allContacts: [
{
id: 200,
firstName: "Jane",
lastName: "Smith",
inactiveFlag: false,
title: "CTO",
defaultPhoneNbr: "555-5678",
communicationItems: [
{ type: { name: "Email" }, value: "jane@test.com" },
],
} as any,
],
};
const ctrl = new CompanyController(
buildMockCompany(),
cwDataWithContacts,
);
const json = ctrl.toJson({
includeAddress: false,
includePrimaryContact: false,
includeAllContacts: true,
});
expect(json.cw_Data.allContacts).toBeDefined();
expect(json.cw_Data.allContacts).toHaveLength(1);
expect(json.cw_Data.allContacts![0]!.firstName).toBe("Jane");
});
test("email is null when no Email communication item", () => {
const noEmailCw = {
...mockCwData,
defaultContact: {
...mockCwData.defaultContact,
communicationItems: [{ type: { name: "Phone" }, value: "555" }],
},
};
const ctrl = new CompanyController(buildMockCompany(), noEmailCw);
const json = ctrl.toJson({
includeAddress: false,
includePrimaryContact: true,
});
expect(json.cw_Data.primaryContact!.email).toBeNull();
});
test("email is null when communicationItems is missing", () => {
const noCIData = {
...mockCwData,
defaultContact: {
...mockCwData.defaultContact,
communicationItems: undefined,
},
};
const ctrl = new CompanyController(buildMockCompany(), noCIData);
const json = ctrl.toJson({
includeAddress: false,
includePrimaryContact: true,
});
expect(json.cw_Data.primaryContact!.email).toBeNull();
});
test("country defaults to United States when null", () => {
const noCntry = {
...mockCwData,
company: { ...mockCwData.company, country: null },
};
const ctrl = new CompanyController(buildMockCompany(), noCntry);
const json = ctrl.toJson({
includeAddress: true,
includePrimaryContact: false,
});
expect(json.cw_Data.address!.country).toBe("United States");
});
});
});
@@ -0,0 +1,195 @@
import { describe, test, expect } from "bun:test";
import { CredentialController } from "../../../src/controllers/CredentialController";
import { buildMockCredential } from "../../setup";
import { ValueType } from "../../../src/modules/credentials/credentialTypeDefs";
describe("CredentialController", () => {
// -------------------------------------------------------------------
// Constructor & _buildFields
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets public properties from credential data", () => {
const data = buildMockCredential();
const ctrl = new CredentialController(data);
expect(ctrl.id).toBe("cred-1");
expect(ctrl.name).toBe("Test Credential");
expect(ctrl.notes).toBeNull();
expect(ctrl.typeId).toBe("ctype-1");
expect(ctrl.companyId).toBe("company-1");
expect(ctrl.subCredentialOfId).toBeNull();
});
test("builds fields from type definition", () => {
const data = buildMockCredential();
const ctrl = new CredentialController(data);
expect(Array.isArray(ctrl.fields)).toBe(true);
expect(ctrl.fields).toHaveLength(2);
});
test("plain fields have value from raw data", () => {
const data = buildMockCredential();
const ctrl = new CredentialController(data);
const usernameField = ctrl.fields.find((f: any) => f.id === "username");
expect(usernameField).toBeDefined();
expect(usernameField.value).toBe("admin");
expect(usernameField.secure).toBe(false);
});
test("secure fields reference secure value ID", () => {
const data = buildMockCredential();
const ctrl = new CredentialController(data);
const passwordField = ctrl.fields.find((f: any) => f.id === "password");
expect(passwordField).toBeDefined();
expect(passwordField.secure).toBe(true);
expect(passwordField.value).toBe("secure-sv-1");
});
test("handles sub-credentials in constructor", () => {
const subCred = buildMockCredential({
id: "sub-cred-1",
name: "Sub Cred",
subCredentialOfId: "cred-1",
type: {
id: "ctype-1",
name: "Login Credential",
permissionScope: "credential.login",
icon: null,
fields: [
{
id: "username",
name: "Username",
required: true,
secure: false,
valueType: "plain_text",
},
],
},
securevalues: [],
subCredentials: [],
});
const parent = buildMockCredential({
subCredentials: [subCred],
});
const ctrl = new CredentialController(parent);
// The parent should have the sub-credential processed
expect(ctrl.id).toBe("cred-1");
});
});
// -------------------------------------------------------------------
// getType / getCompany
// -------------------------------------------------------------------
describe("getType() / getCompany()", () => {
test("getType returns the credential type", () => {
const ctrl = new CredentialController(buildMockCredential());
const type = ctrl.getType();
expect(type.id).toBe("ctype-1");
expect(type.name).toBe("Login Credential");
});
test("getCompany returns the company", () => {
const ctrl = new CredentialController(buildMockCredential());
const company = ctrl.getCompany();
expect(company.id).toBe("company-1");
expect(company.name).toBe("Test Company");
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns structured JSON without secure field IDs by default", () => {
const ctrl = new CredentialController(buildMockCredential());
const json = ctrl.toJson();
expect(json.id).toBe("cred-1");
expect(json.name).toBe("Test Credential");
expect(json.typeId).toBe("ctype-1");
expect(json.companyId).toBe("company-1");
expect(json.type.id).toBe("ctype-1");
expect(json.company.id).toBe("company-1");
expect(json.secureFieldIds).toBeUndefined();
});
test("includes secure field IDs when includeSecureValues is true", () => {
const ctrl = new CredentialController(buildMockCredential());
const json = ctrl.toJson({ includeSecureValues: true });
expect(json.secureFieldIds).toBeDefined();
expect(json.secureFieldIds).toContain("password");
});
test("includes subCredentialOfId when present", () => {
const data = buildMockCredential({ subCredentialOfId: "parent-1" });
const ctrl = new CredentialController(data);
const json = ctrl.toJson();
expect(json.subCredentialOfId).toBe("parent-1");
});
test("excludes subCredentialOfId when null", () => {
const ctrl = new CredentialController(buildMockCredential());
const json = ctrl.toJson();
expect(json.subCredentialOfId).toBeUndefined();
});
test("includes timestamp fields", () => {
const ctrl = new CredentialController(buildMockCredential());
const json = ctrl.toJson();
expect(json.createdAt).toBeDefined();
expect(json.updatedAt).toBeDefined();
});
test("subCredentials is undefined when empty", () => {
const ctrl = new CredentialController(buildMockCredential());
const json = ctrl.toJson();
expect(json.subCredentials).toBeUndefined();
});
});
// -------------------------------------------------------------------
// Sub-credential field building
// -------------------------------------------------------------------
describe("sub-credential field building", () => {
test("builds fields differently for sub-credentials", () => {
const subData = buildMockCredential({
id: "sub-1",
subCredentialOfId: "parent-1",
fields: { sub_user: "jdoe" },
type: {
id: "ctype-1",
name: "Login",
permissionScope: "credential.login",
icon: null,
fields: [
{
id: "sub_user",
name: "Sub User",
required: true,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
],
},
securevalues: [
{
id: "sv-2",
name: "sub_pass",
content: "enc",
hash: "hash",
credentialId: "sub-1",
createdAt: new Date("2025-01-01"),
updatedAt: new Date("2025-01-01"),
},
],
});
const ctrl = new CredentialController(subData);
// Sub-credential fields are built as array with id/value/secure
expect(Array.isArray(ctrl.fields)).toBe(true);
const plainField = ctrl.fields.find((f: any) => f.id === "sub_user");
expect(plainField).toBeDefined();
expect(plainField.secure).toBe(false);
const secureField = ctrl.fields.find((f: any) => f.id === "sub_pass");
expect(secureField).toBeDefined();
expect(secureField.secure).toBe(true);
});
});
});
@@ -0,0 +1,144 @@
import { describe, test, expect } from "bun:test";
import { CredentialTypeController } from "../../../src/controllers/CredentialTypeController";
import { buildMockCredentialType } from "../../setup";
import { ValueType } from "../../../src/modules/credentials/credentialTypeDefs";
describe("CredentialTypeController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets public properties", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
expect(ctrl.id).toBe("ctype-1");
expect(ctrl.name).toBe("Login Credential");
expect(ctrl.permissionScope).toBe("credential.login");
expect(ctrl.icon).toBeNull();
expect(ctrl.fields).toHaveLength(2);
});
test("parses timestamps", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
expect(ctrl.createdAt).toBeInstanceOf(Date);
expect(ctrl.updatedAt).toBeInstanceOf(Date);
});
});
// -------------------------------------------------------------------
// getFieldDefinition
// -------------------------------------------------------------------
describe("getFieldDefinition()", () => {
test("returns matching field", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
const field = ctrl.getFieldDefinition("username");
expect(field).toBeDefined();
expect(field!.name).toBe("Username");
});
test("returns undefined for unknown field", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
expect(ctrl.getFieldDefinition("nonexistent")).toBeUndefined();
});
});
// -------------------------------------------------------------------
// getRequiredFields
// -------------------------------------------------------------------
describe("getRequiredFields()", () => {
test("returns only required fields", () => {
const data = buildMockCredentialType({
fields: [
{
id: "a",
name: "A",
required: true,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
{
id: "b",
name: "B",
required: false,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
{
id: "c",
name: "C",
required: true,
secure: true,
valueType: ValueType.PASSWORD,
},
],
});
const ctrl = new CredentialTypeController(data);
const required = ctrl.getRequiredFields();
expect(required).toHaveLength(2);
expect(required.map((f) => f.id)).toEqual(["a", "c"]);
});
});
// -------------------------------------------------------------------
// getSecureFields
// -------------------------------------------------------------------
describe("getSecureFields()", () => {
test("returns only secure fields", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
const secure = ctrl.getSecureFields();
expect(secure).toHaveLength(1);
expect(secure[0]!.id).toBe("password");
});
});
// -------------------------------------------------------------------
// countCredentials
// -------------------------------------------------------------------
describe("countCredentials()", () => {
test("returns 0 when no credentials", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
expect(ctrl.countCredentials()).toBe(0);
});
test("returns correct count", () => {
const data = buildMockCredentialType({
credentials: [{ id: "c1" }, { id: "c2" }, { id: "c3" }],
});
const ctrl = new CredentialTypeController(data);
expect(ctrl.countCredentials()).toBe(3);
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns base JSON without credential count by default", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
const json = ctrl.toJson();
expect(json.id).toBe("ctype-1");
expect(json.name).toBe("Login Credential");
expect(json.credentialCount).toBeUndefined();
});
test("includes credential count when option is set", () => {
const data = buildMockCredentialType({
credentials: [{ id: "c1" }, { id: "c2" }],
});
const ctrl = new CredentialTypeController(data);
const json = ctrl.toJson({ includeCredentialCount: true });
expect(json.credentialCount).toBe(2);
});
test("includes all expected keys", () => {
const ctrl = new CredentialTypeController(buildMockCredentialType());
const json = ctrl.toJson();
expect(json).toHaveProperty("id");
expect(json).toHaveProperty("name");
expect(json).toHaveProperty("permissionScope");
expect(json).toHaveProperty("icon");
expect(json).toHaveProperty("fields");
expect(json).toHaveProperty("createdAt");
expect(json).toHaveProperty("updatedAt");
});
});
});
@@ -0,0 +1,73 @@
import { describe, test, expect } from "bun:test";
import { RoleController } from "../../../src/controllers/RoleController";
import { buildMockRole, buildMockUser } from "../../setup";
describe("RoleController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets public properties from role data", () => {
const data = buildMockRole();
const ctrl = new RoleController(data);
expect(ctrl.id).toBe("role-1");
expect(ctrl.title).toBe("Test Role");
expect(ctrl.moniker).toBe("test-role");
expect(ctrl.deleted).toBe(false);
});
test("sets timestamps", () => {
const ctrl = new RoleController(buildMockRole());
expect(ctrl.createdAt).toBeInstanceOf(Date);
expect(ctrl.updatedAt).toBeInstanceOf(Date);
});
});
// -------------------------------------------------------------------
// getUsers
// -------------------------------------------------------------------
describe("getUsers()", () => {
test("returns empty collection when no users", () => {
const ctrl = new RoleController(buildMockRole({ users: [] }));
const users = ctrl.getUsers();
expect(users.size).toBe(0);
});
test("returns collection of UserController instances", () => {
const userData = buildMockUser({ id: "u-1" });
const ctrl = new RoleController(buildMockRole({ users: [userData] }));
const users = ctrl.getUsers();
expect(users.size).toBe(1);
expect(users.get("u-1")).toBeDefined();
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns base JSON without permissions or users by default", () => {
const ctrl = new RoleController(buildMockRole());
const json = ctrl.toJson();
expect(json.id).toBe("role-1");
expect(json.title).toBe("Test Role");
expect(json.moniker).toBe("test-role");
expect(json.permissions).toBeUndefined();
expect(json.users).toBeUndefined();
expect(json.createdAt).toBeDefined();
expect(json.updatedAt).toBeDefined();
});
test("includes users when viewUsers is true", () => {
const userData = buildMockUser({
id: "u-1",
roles: [{ id: "role-1", moniker: "test-role" }],
});
const ctrl = new RoleController(buildMockRole({ users: [userData] }));
const json = ctrl.toJson({ viewUsers: true });
expect(json.users).toBeDefined();
expect(json.users).toHaveLength(1);
expect(json.users![0]!.id).toBe("u-1");
});
});
});
@@ -0,0 +1,58 @@
import { describe, test, expect } from "bun:test";
import { SessionController } from "../../../src/controllers/SessionController";
import { buildMockSession } from "../../setup";
describe("SessionController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets all public properties from session data", () => {
const data = buildMockSession();
const ctrl = new SessionController(data);
expect(ctrl.id).toBe("session-1");
expect(ctrl.sessionKey).toBe("sk-abc123");
expect(ctrl.userId).toBe("user-1");
expect(ctrl.expires).toBeInstanceOf(Date);
expect(ctrl.refreshedAt).toBeNull();
expect(ctrl.invalidatedAt).toBeNull();
expect(ctrl.terminated).toBe(false);
});
test("sets custom values from overrides", () => {
const refreshDate = new Date("2025-06-01");
const ctrl = new SessionController(
buildMockSession({ refreshedAt: refreshDate }),
);
expect(ctrl.refreshedAt).toEqual(refreshDate);
});
});
// -------------------------------------------------------------------
// invalidate
// -------------------------------------------------------------------
describe("invalidate()", () => {
test("throws when session is already invalidated", async () => {
const ctrl = new SessionController(
buildMockSession({ invalidatedAt: new Date() }),
);
await expect(ctrl.invalidate()).rejects.toThrow(
"Session has already been invalidated",
);
});
});
// -------------------------------------------------------------------
// generateTokens
// -------------------------------------------------------------------
describe("generateTokens()", () => {
test("throws when tokens have already been generated", async () => {
const ctrl = new SessionController(
buildMockSession({ refreshTokenGenerated: true }),
);
await expect(ctrl.generateTokens()).rejects.toThrow(
"Tokens have alredy been generated",
);
});
});
});
@@ -0,0 +1,43 @@
import { describe, test, expect } from "bun:test";
import { UnifiSiteController } from "../../../src/controllers/UnifiSiteController";
import { buildMockUnifiSite } from "../../setup";
describe("UnifiSiteController", () => {
describe("constructor", () => {
test("sets all properties from site data", () => {
const ctrl = new UnifiSiteController(buildMockUnifiSite());
expect(ctrl.id).toBe("usite-1");
expect(ctrl.name).toBe("Main Office");
expect(ctrl.siteId).toBe("default");
expect(ctrl.companyId).toBeNull();
});
test("accepts non-null companyId", () => {
const ctrl = new UnifiSiteController(
buildMockUnifiSite({ companyId: "company-1" }),
);
expect(ctrl.companyId).toBe("company-1");
});
});
describe("toJson()", () => {
test("returns all properties", () => {
const ctrl = new UnifiSiteController(
buildMockUnifiSite({ companyId: "comp-abc" }),
);
const json = ctrl.toJson();
expect(json).toEqual({
id: "usite-1",
name: "Main Office",
siteId: "default",
companyId: "comp-abc",
});
});
test("companyId is null when unlinked", () => {
const ctrl = new UnifiSiteController(buildMockUnifiSite());
const json = ctrl.toJson();
expect(json.companyId).toBeNull();
});
});
});
@@ -0,0 +1,117 @@
import { describe, test, expect } from "bun:test";
import UserController from "../../../src/controllers/UserController";
import { buildMockUser } from "../../setup";
describe("UserController", () => {
// -------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------
describe("constructor", () => {
test("sets all public properties", () => {
const ctrl = new UserController(buildMockUser());
expect(ctrl.id).toBe("user-1");
expect(ctrl.name).toBe("Test User");
expect(ctrl.login).toBe("test@example.com");
expect(ctrl.email).toBe("test@example.com");
expect(ctrl.image).toBeNull();
});
test("sets timestamps", () => {
const ctrl = new UserController(buildMockUser());
expect(ctrl.createdAt).toBeInstanceOf(Date);
expect(ctrl.updatedAt).toBeInstanceOf(Date);
});
test("builds roles collection", () => {
const mockRole = {
id: "role-1",
title: "Admin",
moniker: "admin",
permissions: "tok",
createdAt: new Date(),
updatedAt: new Date(),
};
const ctrl = new UserController(buildMockUser({ roles: [mockRole] }));
// _roles is private, but we can verify via toJson
const json = ctrl.toJson();
expect(json.roles).toContain("admin");
});
});
// -------------------------------------------------------------------
// toJson
// -------------------------------------------------------------------
describe("toJson()", () => {
test("returns full JSON by default", () => {
const ctrl = new UserController(buildMockUser());
const json = ctrl.toJson();
expect(json.id).toBe("user-1");
expect(json.name).toBe("Test User");
expect(json.login).toBe("test@example.com");
expect(json.email).toBe("test@example.com");
expect(json.image).toBeNull();
expect(json.createdAt).toBeDefined();
expect(json.updatedAt).toBeDefined();
});
test("safeReturn hides sensitive fields", () => {
const ctrl = new UserController(buildMockUser());
const json = ctrl.toJson({ safeReturn: true });
expect(json.id).toBe("user-1");
expect(json.name).toBe("Test User");
expect(json.login).toBeUndefined();
expect(json.email).toBeUndefined();
expect(json.roles).toBeUndefined();
expect(json.permissions).toBeUndefined();
});
test("roles is undefined when user has no roles", () => {
const ctrl = new UserController(buildMockUser({ roles: [] }));
const json = ctrl.toJson();
// _roles.size == 0, so roles is undefined
expect(json.roles).toBeUndefined();
});
test("roles returns monikers when present", () => {
const mockRoles = [
{
id: "r1",
title: "Admin",
moniker: "admin",
permissions: "t",
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "r2",
title: "User",
moniker: "user",
permissions: "t",
createdAt: new Date(),
updatedAt: new Date(),
},
];
const ctrl = new UserController(buildMockUser({ roles: mockRoles }));
const json = ctrl.toJson();
expect(json.roles).toHaveLength(2);
expect(json.roles).toContain("admin");
expect(json.roles).toContain("user");
});
test("permissions returns empty array when user has no permissions token", () => {
const ctrl = new UserController(buildMockUser({ permissions: null }));
const json = ctrl.toJson();
expect(json.permissions).toEqual([]);
});
});
// -------------------------------------------------------------------
// readPermissions
// -------------------------------------------------------------------
describe("readPermissions()", () => {
test("returns empty array when permissions is null", () => {
const ctrl = new UserController(buildMockUser({ permissions: null }));
expect(ctrl.readPermissions()).toEqual([]);
});
});
});
+107
View File
@@ -0,0 +1,107 @@
import { describe, test, expect } from "bun:test";
import { createRoute } from "../../src/modules/api-utils/createRoute";
import { Hono } from "hono";
describe("createRoute", () => {
test("returns a Hono instance", () => {
const route = createRoute("get", ["/test"], (c) => c.text("ok"));
expect(route).toBeInstanceOf(Hono);
});
test("GET route responds correctly", async () => {
const route = createRoute("get", ["/hello"], (c) =>
c.json({ message: "Hello" }),
);
const res = await route.request("/hello");
expect(res.status).toBe(200);
const body: any = await res.json();
expect(body.message).toBe("Hello");
});
test("POST route responds correctly", async () => {
const route = createRoute("post", ["/items"], async (c) => {
const body = await c.req.json();
return c.json({ received: body });
});
const res = await route.request("/items", {
method: "POST",
body: JSON.stringify({ name: "test" }),
headers: { "Content-Type": "application/json" },
});
expect(res.status).toBe(200);
const data: any = await res.json();
expect(data.received.name).toBe("test");
});
test("supports multiple paths", async () => {
const route = createRoute("get", ["/a", "/b"], (c) =>
c.json({ path: c.req.path }),
);
const resA = await route.request("/a");
const resB = await route.request("/b");
expect(resA.status).toBe(200);
expect(resB.status).toBe(200);
});
test("applies middleware", async () => {
let middlewareRan = false;
const route = createRoute(
"get",
["/protected"],
(c) => c.json({ ok: true }),
async (c, next) => {
middlewareRan = true;
await next();
},
);
await route.request("/protected");
expect(middlewareRan).toBe(true);
});
test("middleware can block handler", async () => {
const route = createRoute(
"get",
["/blocked"],
(c) => c.json({ ok: true }),
async (c, _next) => {
return c.json({ blocked: true }, 403);
},
);
const res = await route.request("/blocked");
expect(res.status).toBe(403);
const body: any = await res.json();
expect(body.blocked).toBe(true);
});
test("supports multiple middleware functions", async () => {
const order: number[] = [];
const route = createRoute(
"get",
["/multi"],
(c) => c.json({ order }),
async (_c, next) => {
order.push(1);
await next();
},
async (_c, next) => {
order.push(2);
await next();
},
);
await route.request("/multi");
expect(order).toEqual([1, 2]);
});
test("returns 404 for unmatched paths", async () => {
const route = createRoute("get", ["/exists"], (c) => c.text("ok"));
const res = await route.request("/not-exists");
expect(res.status).toBe(404);
});
test("method mismatch returns 404 or 405", async () => {
const route = createRoute("get", ["/only-get"], (c) => c.text("ok"));
const res = await route.request("/only-get", { method: "POST" });
// Hono returns 404 for method mismatch by default
expect([404, 405]).toContain(res.status);
});
});
+56
View File
@@ -0,0 +1,56 @@
import { describe, test, expect } from "bun:test";
import { ValueType } from "../../src/modules/credentials/credentialTypeDefs";
import type { CredentialTypeField } from "../../src/modules/credentials/credentialTypeDefs";
describe("credentialTypeDefs", () => {
describe("ValueType enum", () => {
test("has expected values", () => {
expect(ValueType.PLAIN_TEXT as string).toBe("plain_text");
expect(ValueType.LICENSE_KEY as string).toBe("license_key");
expect(ValueType.IP_ADDRESS as string).toBe("ip_address");
expect(ValueType.GENERIC_SECRET as string).toBe("generic_secret");
expect(ValueType.BITLOCKER_KEY as string).toBe("bitlocker_key");
expect(ValueType.PASSWORD as string).toBe("password");
expect(ValueType.MULTI_CREDENTIAL as string).toBe("multi_credential");
});
test("has exactly 7 members", () => {
const values = Object.values(ValueType);
expect(values).toHaveLength(7);
});
});
describe("CredentialTypeField type", () => {
test("valid field shape satisfies interface", () => {
const field: CredentialTypeField = {
id: "test",
name: "Test Field",
required: true,
secure: false,
valueType: ValueType.PLAIN_TEXT,
};
expect(field.id).toBe("test");
expect(field.subFields).toBeUndefined();
});
test("field with subFields satisfies interface", () => {
const field: CredentialTypeField = {
id: "multi",
name: "Multi",
required: false,
secure: false,
valueType: ValueType.MULTI_CREDENTIAL,
subFields: [
{
id: "sub1",
name: "Sub 1",
required: true,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
],
};
expect(field.subFields).toHaveLength(1);
});
});
});
+266
View File
@@ -0,0 +1,266 @@
import { describe, test, expect } from "bun:test";
import GenericError from "../../src/Errors/GenericError";
import AuthenticationError from "../../src/Errors/AuthenticationError";
import AuthorizationError from "../../src/Errors/AuthorizationError";
import BodyError from "../../src/Errors/BodyError";
import InsufficientPermission from "../../src/Errors/InsufficientPermission";
import SessionError from "../../src/Errors/SessionError";
import SessionTokenError from "../../src/Errors/SessionTokenError";
import UserError from "../../src/Errors/UserError";
import RoleError from "../../src/Errors/RoleError";
import MissingBodyValue from "../../src/Errors/MissingBodyValue";
import ExpiredAccessTokenError from "../../src/Errors/ExpiredAccessTokenError";
import ExpiredRefreshTokenError from "../../src/Errors/ExpiredRefreshTokenError";
import PermissionsVerificationError from "../../src/Errors/PermissionsVerificationError";
// ---------------------------------------------------------------------------
// GenericError
// ---------------------------------------------------------------------------
describe("GenericError", () => {
test("sets name, message, status, and cause", () => {
const err = new GenericError({
name: "TestError",
message: "Something went wrong",
cause: "bad input",
status: 422,
});
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe("TestError");
expect(err.message).toBe("Something went wrong");
expect(err.cause).toBe("bad input");
expect(err.status).toBe(422);
});
test("defaults status to 400", () => {
const err = new GenericError({ name: "X", message: "Y" });
expect(err.status).toBe(400);
});
test("cause is optional", () => {
const err = new GenericError({ name: "X", message: "Y" });
expect(err.cause).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// AuthenticationError
// ---------------------------------------------------------------------------
describe("AuthenticationError", () => {
test("sets correct name and message", () => {
const err = new AuthenticationError("Invalid credentials");
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe("AuthenticationError");
expect(err.message).toBe("Invalid credentials");
});
test("accepts optional cause", () => {
const err = new AuthenticationError("Fail", "token expired");
expect(err.cause).toBe("token expired");
});
test("cause defaults to undefined", () => {
const err = new AuthenticationError("Fail");
expect(err.cause).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// AuthorizationError
// ---------------------------------------------------------------------------
describe("AuthorizationError", () => {
test("sets correct name and default status", () => {
const err = new AuthorizationError("Not authorized");
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe("AuthorizationError");
expect(err.message).toBe("Not authorized");
expect(err.status).toBe(401);
});
test("allows custom status", () => {
const err = new AuthorizationError("Forbidden", "nope", 403);
expect(err.status).toBe(403);
expect(err.cause).toBe("nope");
});
});
// ---------------------------------------------------------------------------
// BodyError
// ---------------------------------------------------------------------------
describe("BodyError", () => {
test("sets name and message", () => {
const err = new BodyError("Body is invalid");
expect(err.name).toBe("BodyError");
expect(err.message).toBe("Body is invalid");
});
test("accepts optional cause", () => {
const err = new BodyError("Bad", "missing field");
expect(err.cause).toBe("missing field");
});
});
// ---------------------------------------------------------------------------
// InsufficientPermission
// ---------------------------------------------------------------------------
describe("InsufficientPermission", () => {
test("always has status 403", () => {
const err = new InsufficientPermission("Nope");
expect(err.name).toBe("InsufficientPermission");
expect(err.status).toBe(403);
expect(err.message).toBe("Nope");
});
test("accepts optional cause", () => {
const err = new InsufficientPermission("Nope", "missing role");
expect(err.cause).toBe("missing role");
});
});
// ---------------------------------------------------------------------------
// SessionError
// ---------------------------------------------------------------------------
describe("SessionError", () => {
test("sets name and message", () => {
const err = new SessionError("Invalid session");
expect(err.name).toBe("SessionError");
expect(err.message).toBe("Invalid session");
});
});
// ---------------------------------------------------------------------------
// SessionTokenError
// ---------------------------------------------------------------------------
describe("SessionTokenError", () => {
test("sets name and message", () => {
const err = new SessionTokenError("Token invalid");
expect(err.name).toBe("SessionTokenError");
expect(err.message).toBe("Token invalid");
});
test("accepts cause", () => {
const err = new SessionTokenError("Bad", "expired");
expect(err.cause).toBe("expired");
});
});
// ---------------------------------------------------------------------------
// UserError
// ---------------------------------------------------------------------------
describe("UserError", () => {
test("sets name and message", () => {
const err = new UserError("User not found");
expect(err.name).toBe("UserError");
expect(err.message).toBe("User not found");
});
});
// ---------------------------------------------------------------------------
// RoleError
// ---------------------------------------------------------------------------
describe("RoleError", () => {
test("sets name and message", () => {
const err = new RoleError("Role conflict");
expect(err.name).toBe("RoleError");
expect(err.message).toBe("Role conflict");
});
test("accepts cause", () => {
const err = new RoleError("Conflict", "moniker taken");
expect(err.cause).toBe("moniker taken");
});
});
// ---------------------------------------------------------------------------
// MissingBodyValue
// ---------------------------------------------------------------------------
describe("MissingBodyValue", () => {
test("formats message with value name", () => {
const err = new MissingBodyValue("email");
expect(err.name).toBe("MissingBodyValue");
expect(err.message).toBe("Value 'email' is missing from the body.");
expect(err.cause).toBe(
"A value that was required by the body of this request is missing.",
);
});
test("works with different value names", () => {
const err = new MissingBodyValue("password");
expect(err.message).toContain("password");
});
});
// ---------------------------------------------------------------------------
// ExpiredAccessTokenError
// ---------------------------------------------------------------------------
describe("ExpiredAccessTokenError", () => {
test("sets fixed name and message", () => {
const err = new ExpiredAccessTokenError();
expect(err.name).toBe("ExpiredAccessTokenError");
expect(err.message).toBe("The provided access token has expired.");
});
test("accepts optional cause", () => {
const err = new ExpiredAccessTokenError("jwt expired");
expect(err.cause).toBe("jwt expired");
});
});
// ---------------------------------------------------------------------------
// ExpiredRefreshTokenError
// ---------------------------------------------------------------------------
describe("ExpiredRefreshTokenError", () => {
test("sets fixed name and message", () => {
const err = new ExpiredRefreshTokenError();
expect(err.name).toBe("ExpiredRefreshTokenError");
expect(err.message).toBe("The provided refresh token has expired.");
});
test("accepts optional cause", () => {
const err = new ExpiredRefreshTokenError("jwt expired");
expect(err.cause).toBe("jwt expired");
});
});
// ---------------------------------------------------------------------------
// PermissionsVerificationError
// ---------------------------------------------------------------------------
describe("PermissionsVerificationError", () => {
test("sets name and message", () => {
const err = new PermissionsVerificationError("Cannot verify");
expect(err.name).toBe("PermissionsVerificationError");
expect(err.message).toBe("Cannot verify");
});
test("accepts cause", () => {
const err = new PermissionsVerificationError("Fail", "key mismatch");
expect(err.cause).toBe("key mismatch");
});
});
// ---------------------------------------------------------------------------
// Cross-cutting: all errors are instanceof Error
// ---------------------------------------------------------------------------
describe("All errors extend Error", () => {
const errors = [
new GenericError({ name: "G", message: "g" }),
new AuthenticationError("a"),
new AuthorizationError("a"),
new BodyError("b"),
new InsufficientPermission("i"),
new SessionError("s"),
new SessionTokenError("st"),
new UserError("u"),
new RoleError("r"),
new MissingBodyValue("v"),
new ExpiredAccessTokenError(),
new ExpiredRefreshTokenError(),
new PermissionsVerificationError("p"),
];
test.each(errors.map((e) => [e.constructor.name, e]))(
"%s is instanceof Error",
(_name, err) => {
expect(err).toBeInstanceOf(Error);
},
);
});
+95
View File
@@ -0,0 +1,95 @@
import { describe, test, expect } from "bun:test";
import { ValueType } from "../../src/modules/credentials/credentialTypeDefs";
import { fieldValidator } from "../../src/modules/credentials/fieldValidator";
import type {
CredentialField,
CredentialTypeField,
} from "../../src/modules/credentials/credentialTypeDefs";
const baseAcceptableFields: CredentialTypeField[] = [
{
id: "username",
name: "Username",
required: true,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
{
id: "password",
name: "Password",
required: true,
secure: true,
valueType: ValueType.PASSWORD,
},
{
id: "notes",
name: "Notes",
required: false,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
];
describe("fieldValidator", () => {
test("validates correct fields and returns validated array", async () => {
const fields: CredentialField[] = [
{ fieldId: "username", value: "admin" },
{ fieldId: "password", value: "secret123" },
];
const result = await fieldValidator(fields, baseAcceptableFields);
expect(result).toHaveLength(2);
expect(result[0]!.fieldId).toBe("username");
expect(result[0]!.secure).toBe(false);
expect(result[1]!.fieldId).toBe("password");
expect(result[1]!.secure).toBe(true);
});
test("throws GenericError for unknown field ID", async () => {
const fields: CredentialField[] = [
{ fieldId: "nonexistent", value: "val" },
];
await expect(fieldValidator(fields, baseAcceptableFields)).rejects.toThrow(
"Invalid field ID: nonexistent",
);
});
test("handles optional fields", async () => {
const fields: CredentialField[] = [
{ fieldId: "notes", value: "some note" },
];
const result = await fieldValidator(fields, baseAcceptableFields);
expect(result).toHaveLength(1);
expect(result[0]!.secure).toBe(false);
});
test("handles empty fields array", async () => {
const result = await fieldValidator([], baseAcceptableFields);
expect(result).toEqual([]);
});
test("marks MULTI_CREDENTIAL fields correctly", async () => {
const acceptableFields: CredentialTypeField[] = [
{
id: "sub_creds",
name: "Sub Credentials",
required: false,
secure: false,
valueType: ValueType.MULTI_CREDENTIAL,
subFields: [
{
id: "sub_user",
name: "Sub User",
required: true,
secure: false,
valueType: ValueType.PLAIN_TEXT,
},
],
},
];
const fields: CredentialField[] = [{ fieldId: "sub_creds", value: "" }];
const result = await fieldValidator(fields, acceptableFields);
expect(result).toHaveLength(1);
expect(result[0]!.isMultiCredential).toBe(true);
expect(result[0]!.secure).toBe(false);
});
});
+19
View File
@@ -0,0 +1,19 @@
import { describe, test, expect } from "bun:test";
import { genImplicitPerm } from "../../src/modules/permission-utils/genImplicitPerm";
describe("genImplicitPerm", () => {
test("builds a dot-delimited implicit permission string", () => {
const result = genImplicitPerm("sessions", "sess-1", "user-1");
expect(result).toBe("resource.sessions.sess-1.user.user-1.implicit");
});
test("works with different resource types", () => {
const result = genImplicitPerm("roles", "role-abc", "user-xyz");
expect(result).toBe("resource.roles.role-abc.user.user-xyz.implicit");
});
test("handles IDs with special characters", () => {
const result = genImplicitPerm("keys", "key-123_abc", "u-1");
expect(result).toBe("resource.keys.key-123_abc.user.u-1.implicit");
});
});
+28
View File
@@ -0,0 +1,28 @@
import { describe, test, expect, mock } from "bun:test";
import { Eventra } from "@duxcore/eventra";
// We test the globalEvents module shape and the setupEventDebugger function.
// We import directly since the module has minimal side-effects.
import { events, setupEventDebugger } from "../../src/modules/globalEvents";
describe("globalEvents", () => {
test("events is an Eventra instance", () => {
expect(events).toBeDefined();
expect(typeof events.emit).toBe("function");
expect(typeof events.on).toBe("function");
});
test("setupEventDebugger registers a catch-all listener", () => {
// Calling setupEventDebugger should not throw
expect(() => setupEventDebugger()).not.toThrow();
});
test("can emit and receive events", () => {
let received = false;
events.on("api:started", () => {
received = true;
});
events.emit("api:started");
expect(received).toBe(true);
});
});
+60
View File
@@ -0,0 +1,60 @@
import { describe, test, expect } from "bun:test";
import { mergeArrays } from "../../src/modules/tools/mergeArrays";
describe("mergeArrays", () => {
test("merges two disjoint arrays", () => {
const result = mergeArrays([1, 2], [3, 4]);
expect(result).toEqual([1, 2, 3, 4]);
});
test("removes duplicates from second array", () => {
const result = mergeArrays([1, 2, 3], [2, 3, 4]);
expect(result).toEqual([1, 2, 3, 4]);
});
test("handles empty first array", () => {
const result = mergeArrays([], [1, 2]);
expect(result).toEqual([1, 2]);
});
test("handles empty second array", () => {
const result = mergeArrays([1, 2], []);
expect(result).toEqual([1, 2]);
});
test("handles both arrays empty", () => {
const result = mergeArrays([], []);
expect(result).toEqual([]);
});
test("works with strings", () => {
const result = mergeArrays(["a", "b"], ["b", "c"]);
expect(result).toEqual(["a", "b", "c"]);
});
test("does not mutate original arrays", () => {
const a = [1, 2];
const b = [2, 3];
const result = mergeArrays(a, b);
expect(a).toEqual([1, 2]);
expect(b).toEqual([2, 3]);
expect(result).toEqual([1, 2, 3]);
});
test("accepts custom predicate", () => {
const a = [{ id: 1, n: "a" }];
const b = [
{ id: 1, n: "b" },
{ id: 2, n: "c" },
];
const result = mergeArrays(a, b, (x: any, y: any) => x.id === y.id);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ id: 1, n: "a" });
expect(result[1]).toEqual({ id: 2, n: "c" });
});
test("keeps all elements when predicate never matches", () => {
const result = mergeArrays([1, 2], [3, 4], () => false);
expect(result).toEqual([1, 2, 3, 4]);
});
});
+164
View File
@@ -0,0 +1,164 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
import { Hono } from "hono";
// We test the authMiddleware in isolation by importing and mounting it on a
// minimal Hono app, without touching the real session/user layer.
// Mock the managers and modules that authMiddleware depends on
mock.module("../../../src/managers/sessions", () => ({
sessions: {
fetch: mock(),
},
}));
mock.module("../../../src/modules/globalEvents", () => ({
events: {
emit: mock(),
on: mock(),
any: mock(),
},
}));
import { authMiddleware } from "../../../src/api/middleware/authorization";
import { sessions } from "../../../src/managers/sessions";
import { apiResponse } from "../../../src/modules/api-utils/apiResponse";
function createTestApp(permParams?: Parameters<typeof authMiddleware>[0]) {
const app = new Hono();
app.onError((err, c) => {
const response = apiResponse.error(err);
return c.json(response, response.status);
});
app.use("*", authMiddleware(permParams));
app.get("/test", (c) => c.json({ ok: true }));
return app;
}
describe("authMiddleware", () => {
beforeEach(() => {
// Reset mocks
(sessions.fetch as any).mockReset?.();
});
// -------------------------------------------------------------------
// Missing authorization header
// -------------------------------------------------------------------
test("rejects requests without authorization header", async () => {
const app = createTestApp();
const res = await app.request("/test");
expect(res.status).toBe(401);
const body: any = await res.json();
expect(body.error).toBe("AuthorizationError");
expect(body.message).toContain("authorization");
});
// -------------------------------------------------------------------
// Malformed authorization header
// -------------------------------------------------------------------
test("rejects malformed authorization header", async () => {
const app = createTestApp();
const res = await app.request("/test", {
headers: { Authorization: "foobar" },
});
expect(res.status).toBe(401);
const body: any = await res.json();
expect(body.error).toBe("AuthorizationError");
expect(body.message).toContain("malformed");
});
test("rejects authorization missing token value", async () => {
const app = createTestApp();
const res = await app.request("/test", {
headers: { Authorization: "Bearer " },
});
expect(res.status).toBeGreaterThanOrEqual(400);
const body: any = await res.json();
expect(body.successful).toBe(false);
});
// -------------------------------------------------------------------
// Forbidden auth types
// -------------------------------------------------------------------
test("rejects forbidden auth types", async () => {
const app = createTestApp({ forbiddenAuthTypes: ["Key"] });
const res = await app.request("/test", {
headers: { Authorization: "Key aaa.bbb.ccc" },
});
expect(res.status).toBe(403);
const body: any = await res.json();
expect(body.error).toBe("NonpermittedAuthType");
});
// -------------------------------------------------------------------
// Valid token flow
// -------------------------------------------------------------------
test("calls sessions.fetch with access token", async () => {
const mockUser = {
hasPermission: mock(() => Promise.resolve(true)),
};
const mockSession = {
fetchUser: mock(() => Promise.resolve(mockUser)),
};
(sessions.fetch as any).mockResolvedValue?.(mockSession) ??
((sessions as any).fetch = mock(() => Promise.resolve(mockSession)));
const app = createTestApp();
const res = await app.request("/test", {
headers: { Authorization: "Bearer aaa.bbb.ccc" },
});
// If sessions.fetch resolves, the middleware should pass through
expect(res.status).toBe(200);
});
// -------------------------------------------------------------------
// Permission checking
// -------------------------------------------------------------------
test("rejects when user lacks required permission", async () => {
const mockUser = {
hasPermission: mock(() => Promise.resolve(false)),
};
const mockSession = {
fetchUser: mock(() => Promise.resolve(mockUser)),
};
(sessions as any).fetch = mock(() => Promise.resolve(mockSession));
const app = createTestApp({ permissions: ["admin.super"] });
const res = await app.request("/test", {
headers: { Authorization: "Bearer aaa.bbb.ccc" },
});
expect(res.status).toBe(403);
const body: any = await res.json();
expect(body.message).toContain("permission");
});
test("allows when user has all required permissions", async () => {
const mockUser = {
hasPermission: mock(() => Promise.resolve(true)),
};
const mockSession = {
fetchUser: mock(() => Promise.resolve(mockUser)),
};
(sessions as any).fetch = mock(() => Promise.resolve(mockSession));
const app = createTestApp({
permissions: ["company.fetch", "company.list"],
});
const res = await app.request("/test", {
headers: { Authorization: "Bearer aaa.bbb.ccc" },
});
expect(res.status).toBe(200);
});
test("passes through when no permissions required", async () => {
const mockUser = { hasPermission: mock(() => Promise.resolve(true)) };
const mockSession = { fetchUser: mock(() => Promise.resolve(mockUser)) };
(sessions as any).fetch = mock(() => Promise.resolve(mockSession));
const app = createTestApp();
const res = await app.request("/test", {
headers: { Authorization: "Bearer aaa.bbb.ccc" },
});
expect(res.status).toBe(200);
});
});
+108
View File
@@ -0,0 +1,108 @@
import { describe, test, expect } from "bun:test";
import Password from "../../src/modules/tools/Password";
describe("Password", () => {
// -------------------------------------------------------------------
// generateSalt
// -------------------------------------------------------------------
describe("generateSalt()", () => {
test("returns a string of default length 12", () => {
const salt = Password.generateSalt();
expect(typeof salt).toBe("string");
expect(salt.length).toBe(12);
});
test("returns a string of custom length", () => {
const salt = Password.generateSalt({ length: 24 });
expect(salt.length).toBe(24);
});
test("generates different salts each time", () => {
const s1 = Password.generateSalt();
const s2 = Password.generateSalt();
// Extremely unlikely to be equal
expect(s1).not.toBe(s2);
});
test("returns hex characters only", () => {
const salt = Password.generateSalt({ length: 20 });
expect(/^[0-9a-f]+$/.test(salt)).toBe(true);
});
});
// -------------------------------------------------------------------
// hash
// -------------------------------------------------------------------
describe("hash()", () => {
test("returns a BLAKE2s prefixed string", () => {
const hashed = Password.hash("mypassword");
expect(hashed.startsWith("BLAKE2s$")).toBe(true);
});
test("contains three dollar-sign separated parts", () => {
const hashed = Password.hash("mypassword", { overrideSalt: "testsalt" });
const parts = hashed.split("$");
expect(parts.length).toBe(3);
expect(parts[0]).toBe("BLAKE2s");
expect(parts[2]).toBe("testsalt");
});
test("same password + same salt produces same hash", () => {
const h1 = Password.hash("password", { overrideSalt: "salt123" });
const h2 = Password.hash("password", { overrideSalt: "salt123" });
expect(h1).toBe(h2);
});
test("different passwords produce different hashes", () => {
const h1 = Password.hash("password1", { overrideSalt: "salt" });
const h2 = Password.hash("password2", { overrideSalt: "salt" });
expect(h1).not.toBe(h2);
});
test("different salts produce different hashes", () => {
const h1 = Password.hash("password", { overrideSalt: "salt1" });
const h2 = Password.hash("password", { overrideSalt: "salt2" });
expect(h1).not.toBe(h2);
});
test("generates salt when saltOpts provided", () => {
const hashed = Password.hash("password", { saltOpts: { length: 16 } });
const parts = hashed.split("$");
// Should have a 16-char salt
expect(parts[2]!.length).toBe(16);
});
});
// -------------------------------------------------------------------
// validate
// -------------------------------------------------------------------
describe("validate()", () => {
test("returns true for matching password", () => {
const hashed = Password.hash("correctpassword", { overrideSalt: "salt" });
expect(Password.validate("correctpassword", hashed)).toBe(true);
});
test("returns false for wrong password", () => {
const hashed = Password.hash("correctpassword", { overrideSalt: "salt" });
// timingSafeEqual throws if buffers are different lengths, but since
// the hash output has the same length regardless, a wrong password
// with same-length output will return false.
// However if the buffers are different lengths it throws — in that
// case we just check the behaviour is consistent:
try {
const result = Password.validate("wrongpassword", hashed);
expect(result).toBe(false);
} catch {
// timingSafeEqual may throw on different lengths, which is acceptable
expect(true).toBe(true);
}
});
test("round-trips correctly with generated salt", () => {
const hashed = Password.hash("securePass123!", {
saltOpts: { length: 12 },
});
expect(Password.validate("securePass123!", hashed)).toBe(true);
});
});
});
+92
View File
@@ -0,0 +1,92 @@
import { describe, test, expect } from "bun:test";
/**
* Tests for the PermissionNodes type definitions and structure.
* We import the permission nodes and validate the shape of the data.
*/
import { PERMISSION_NODES } from "../../src/types/PermissionNodes";
import type {
PermissionNode,
PermissionCategory,
} from "../../src/types/PermissionNodes";
describe("PermissionNodes", () => {
test("PERMISSION_NODES is defined and is an object", () => {
expect(PERMISSION_NODES).toBeDefined();
expect(typeof PERMISSION_NODES).toBe("object");
});
test("has required top-level categories", () => {
expect(PERMISSION_NODES).toHaveProperty("global");
expect(PERMISSION_NODES).toHaveProperty("company");
expect(PERMISSION_NODES).toHaveProperty("credential");
});
test("each category has name, description, and permissions", () => {
for (const [key, category] of Object.entries(PERMISSION_NODES)) {
const cat = category as PermissionCategory;
expect(cat).toHaveProperty("name");
expect(typeof cat.name).toBe("string");
expect(cat).toHaveProperty("description");
expect(typeof cat.description).toBe("string");
expect(cat).toHaveProperty("permissions");
expect(Array.isArray(cat.permissions)).toBe(true);
}
});
test("each permission node has required fields", () => {
for (const [_key, category] of Object.entries(PERMISSION_NODES)) {
const cat = category as PermissionCategory;
for (const perm of cat.permissions) {
expect(perm).toHaveProperty("node");
expect(typeof perm.node).toBe("string");
expect(perm.node.length).toBeGreaterThan(0);
expect(perm).toHaveProperty("description");
expect(typeof perm.description).toBe("string");
expect(perm).toHaveProperty("usedIn");
expect(Array.isArray(perm.usedIn)).toBe(true);
}
}
});
test("global category contains the wildcard * node", () => {
const globalPerms = (PERMISSION_NODES.global as PermissionCategory)
.permissions;
const wildcard = globalPerms.find((p) => p.node === "*");
expect(wildcard).toBeDefined();
expect(wildcard!.description).toContain("Full access");
});
test("all permission nodes are non-empty strings", () => {
for (const [_key, category] of Object.entries(PERMISSION_NODES)) {
const cat = category as PermissionCategory;
for (const perm of cat.permissions) {
expect(typeof perm.node).toBe("string");
expect(perm.node.length).toBeGreaterThan(0);
}
}
});
test("dependencies reference existing permission nodes", () => {
// Collect all nodes
const allNodes = new Set<string>();
for (const [_key, category] of Object.entries(PERMISSION_NODES)) {
const cat = category as PermissionCategory;
for (const perm of cat.permissions) {
allNodes.add(perm.node);
}
}
// Check all dependencies point to real nodes
for (const [_key, category] of Object.entries(PERMISSION_NODES)) {
const cat = category as PermissionCategory;
for (const perm of cat.permissions) {
if (perm.dependencies) {
for (const dep of perm.dependencies) {
expect(allNodes.has(dep)).toBe(true);
}
}
}
}
});
});
+158
View File
@@ -0,0 +1,158 @@
import { describe, test, expect } from "bun:test";
import { permissionValidator } from "../../src/modules/permission-utils/permissionValidator";
describe("permissionValidator", () => {
// -------------------------------------------------------------------
// Exact match
// -------------------------------------------------------------------
describe("exact matches", () => {
test("returns true for exact permission match", () => {
expect(permissionValidator("company.fetch", ["company.fetch"])).toBe(
true,
);
});
test("returns false when no match", () => {
expect(permissionValidator("company.fetch", ["company.create"])).toBe(
false,
);
});
test("returns false for empty expressions", () => {
expect(permissionValidator("company.fetch", [])).toBe(false);
});
test("handles single string expression", () => {
expect(permissionValidator("company.fetch", "company.fetch")).toBe(true);
});
test("handles single string non-match", () => {
expect(permissionValidator("company.fetch", "company.create")).toBe(
false,
);
});
});
// -------------------------------------------------------------------
// Wildcard *
// -------------------------------------------------------------------
describe("wildcard (*)", () => {
test("* matches any single-segment permission", () => {
expect(permissionValidator("company", ["*"])).toBe(true);
});
test("* matches multi-segment permissions", () => {
expect(permissionValidator("company.fetch.many", ["*"])).toBe(true);
});
test("company.* matches company.fetch", () => {
expect(permissionValidator("company.fetch", ["company.*"])).toBe(true);
});
test("company.* matches company.fetch.many", () => {
expect(permissionValidator("company.fetch.many", ["company.*"])).toBe(
true,
);
});
test("*.fetch matches company.fetch", () => {
expect(permissionValidator("company.fetch", ["*.fetch"])).toBe(true);
});
test("company.fetch.* matches company.fetch.many", () => {
expect(
permissionValidator("company.fetch.many", ["company.fetch.*"]),
).toBe(true);
});
test("company.fetch.* does NOT match company.create", () => {
expect(permissionValidator("company.create", ["company.fetch.*"])).toBe(
false,
);
});
});
// -------------------------------------------------------------------
// Single-character wildcard ?
// -------------------------------------------------------------------
describe("single-character wildcard (?)", () => {
test("? matches exactly one character", () => {
expect(permissionValidator("company.a", ["company.?"])).toBe(true);
});
test("? does not match multiple characters", () => {
expect(permissionValidator("company.ab", ["company.?"])).toBe(false);
});
test("? does not match dot separator", () => {
expect(permissionValidator("company.a.b", ["company.?"])).toBe(false);
});
});
// -------------------------------------------------------------------
// Bracket groups [a,b,c]
// -------------------------------------------------------------------
describe("bracket groups [a,b,c]", () => {
test("matches first option in group", () => {
expect(
permissionValidator("company.fetch", ["company.[fetch,create]"]),
).toBe(true);
});
test("matches second option in group", () => {
expect(
permissionValidator("company.create", ["company.[fetch,create]"]),
).toBe(true);
});
test("does not match unlisted option", () => {
expect(
permissionValidator("company.delete", ["company.[fetch,create]"]),
).toBe(false);
});
});
// -------------------------------------------------------------------
// Multiple expressions
// -------------------------------------------------------------------
describe("multiple expressions", () => {
test("returns true if any expression matches", () => {
expect(
permissionValidator("role.create", [
"company.fetch",
"role.create",
"user.read",
]),
).toBe(true);
});
test("returns false if no expression matches", () => {
expect(
permissionValidator("role.delete", [
"company.fetch",
"role.create",
"user.read",
]),
).toBe(false);
});
});
// -------------------------------------------------------------------
// Complex patterns
// -------------------------------------------------------------------
describe("complex patterns", () => {
test("combined wildcard and bracket", () => {
expect(
permissionValidator("company.fetch.many", ["company.[fetch,create].*"]),
).toBe(true);
});
test("deeply nested permission with wildcard", () => {
expect(
permissionValidator("unifi.site.wifi.read.passphrase", [
"unifi.site.wifi.*",
]),
).toBe(true);
});
});
});
+117
View File
@@ -0,0 +1,117 @@
import { describe, test, expect, mock, beforeEach } from "bun:test";
// Mock the user controller's hasPermission
const mockHasPermission = mock(() => Promise.resolve(true));
const mockUserController = {
hasPermission: mockHasPermission,
};
describe("processObjectValuePerms", () => {
// Import after mock setup
const { processObjectValuePerms, processObjectPermMap } =
require("../../src/modules/permission-utils/processObjectPermissions") as typeof import("../../src/modules/permission-utils/processObjectPermissions");
beforeEach(() => {
mockHasPermission.mockReset();
});
test("returns only fields user has permission for", async () => {
let callCount = 0;
mockHasPermission.mockImplementation(() => {
callCount++;
// Allow field "name" but deny "secret"
return Promise.resolve(callCount === 1);
});
const obj = { name: "Test", secret: "hidden" };
const result = await processObjectValuePerms(
obj,
"scope",
mockUserController as any,
);
// First call: scope.name → true, second: scope.secret → false
expect(result.name).toBe("Test");
expect(result.secret).toBeUndefined();
});
test("returns empty object when user has no permissions", async () => {
mockHasPermission.mockResolvedValue(false);
const obj = { a: 1, b: 2, c: 3 };
const result = await processObjectValuePerms(
obj,
"test",
mockUserController as any,
);
expect(Object.keys(result)).toHaveLength(0);
});
test("returns full object when user has all permissions", async () => {
mockHasPermission.mockResolvedValue(true);
const obj = { x: "hello", y: 42 };
const result = await processObjectValuePerms(
obj,
"test",
mockUserController as any,
);
expect(result).toEqual({ x: "hello", y: 42 });
});
test("checks permission with correct scope.key pattern", async () => {
mockHasPermission.mockResolvedValue(true);
const obj = { fieldA: 1 };
await processObjectValuePerms(obj, "myScope", mockUserController as any);
expect(mockHasPermission).toHaveBeenCalledWith("myScope.fieldA");
});
});
describe("processObjectPermMap", () => {
const { processObjectPermMap } =
require("../../src/modules/permission-utils/processObjectPermissions") as typeof import("../../src/modules/permission-utils/processObjectPermissions");
beforeEach(() => {
mockHasPermission.mockReset();
});
test("returns boolean map for each key", async () => {
let idx = 0;
mockHasPermission.mockImplementation(() => {
idx++;
return Promise.resolve(idx % 2 === 1); // true, false, true, ...
});
const obj = { a: "x", b: "y", c: "z" };
const result = await processObjectPermMap(
obj,
"scope",
mockUserController as any,
);
expect(result.a).toBe(true);
expect(result.b).toBe(false);
expect(result.c).toBe(true);
});
test("all true when user has all permissions", async () => {
mockHasPermission.mockResolvedValue(true);
const obj = { foo: 1, bar: 2 };
const result = await processObjectPermMap(
obj,
"s",
mockUserController as any,
);
expect(result.foo).toBe(true);
expect(result.bar).toBe(true);
});
test("all false when user has no permissions", async () => {
mockHasPermission.mockResolvedValue(false);
const obj = { foo: 1, bar: 2 };
const result = await processObjectPermMap(
obj,
"s",
mockUserController as any,
);
expect(result.foo).toBe(false);
expect(result.bar).toBe(false);
});
});
+73
View File
@@ -0,0 +1,73 @@
import { describe, test, expect } from "bun:test";
import { Hono } from "hono";
/**
* Tests for the router aggregation pattern used throughout the app.
* Each router imports route modules and mounts them.
*/
describe("Router pattern", () => {
test("mounting multiple routes via Object.values pattern", () => {
// Simulate the pattern used in companyRouter.ts
const route1 = new Hono().get("/items", (c) => c.json({ route: "items" }));
const route2 = new Hono().get("/count", (c) => c.json({ route: "count" }));
const routes = { route1, route2 };
const router = new Hono();
Object.values(routes).map((r) => router.route("/", r));
// Mount router under a prefix
const app = new Hono();
app.route("/v1/resource", router);
return Promise.all([
(async () => {
const res = await app.request("/v1/resource/items");
expect(res.status).toBe(200);
const body: any = await res.json();
expect(body.route).toBe("items");
})(),
(async () => {
const res = await app.request("/v1/resource/count");
expect(res.status).toBe(200);
const body: any = await res.json();
expect(body.route).toBe("count");
})(),
]);
});
test("nested route parameters work correctly", async () => {
const route = new Hono().get("/items/:id", (c) =>
c.json({ id: c.req.param("id") }),
);
const router = new Hono();
router.route("/", route);
const app = new Hono();
app.route("/v1/test", router);
const res = await app.request("/v1/test/items/abc-123");
expect(res.status).toBe(200);
const body: any = await res.json();
expect(body.id).toBe("abc-123");
});
test("error propagation through router chain", async () => {
const route = new Hono().get("/fail", () => {
throw new Error("Boom!");
});
const router = new Hono();
router.route("/", route);
const app = new Hono();
app.onError((err, c) => c.json({ error: err.message, caught: true }, 500));
app.route("/v1", router);
const res = await app.request("/v1/fail");
expect(res.status).toBe(500);
const body: any = await res.json();
expect(body.caught).toBe(true);
expect(body.error).toBe("Boom!");
});
});
+25
View File
@@ -0,0 +1,25 @@
import { describe, test, expect } from "bun:test";
import teapot from "../../src/api/teapot";
describe("Teapot route handler", () => {
test("GET / returns 418 status", async () => {
const res = await teapot.request("/");
expect(res.status).toBe(418);
});
test("response body has correct shape", async () => {
const res = await teapot.request("/");
const body = await res.json();
expect(body).toEqual({
status: 418,
message: "I'm not a teapot",
successful: true,
});
});
test("returns JSON content type", async () => {
const res = await teapot.request("/");
const ct = res.headers.get("content-type");
expect(ct).toContain("application/json");
});
});
+38
View File
@@ -0,0 +1,38 @@
import { describe, test, expect } from "bun:test";
/**
* Tests for the HonoTypes and PermissionTypes type exports.
* These are mainly compile-time checks that ensure the types exist
* and can be used.
*/
import type { Variables } from "../../src/types/HonoTypes";
import type {
PermissionIssuers,
DecodedPermissionsBlock,
} from "../../src/types/PermissionTypes";
describe("HonoTypes", () => {
test("Variables type is usable", () => {
// If this compiles and runs, the type exists
const vars: Variables = { user: {} as any };
expect(vars).toBeDefined();
});
});
describe("PermissionTypes", () => {
test("PermissionIssuers type accepts valid values", () => {
const issuers: PermissionIssuers[] = ["roles", "user", "api_key"];
expect(issuers).toHaveLength(3);
});
test("DecodedPermissionsBlock shape is correct", () => {
const block: DecodedPermissionsBlock = {
permissions: ["admin.*"],
iat: 1234567890,
iss: "roles",
sub: "role-1",
};
expect(block.permissions).toEqual(["admin.*"]);
expect(block.iss).toBe("roles");
});
});