Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
createDeniedIsolationError,
|
||||
expectJsonResponse,
|
||||
expectStatusLog,
|
||||
isolationAdminSession,
|
||||
isolationAdminUser,
|
||||
isolationApiKeyLogA,
|
||||
isolationApiKeyLogB,
|
||||
isolationInactiveApiKey,
|
||||
isolationInactiveApiKeyLog,
|
||||
isolationInvalidBearerToken,
|
||||
isolationRevokedBearerSession,
|
||||
isolationTenantA,
|
||||
isolationTenantARelationship,
|
||||
isolationTenantARow,
|
||||
isolationTenantB,
|
||||
isolationTenantBRelationship,
|
||||
isolationTenantBRow,
|
||||
makeAdminHostGetRequest,
|
||||
makeInactiveApiKeyGetRequest,
|
||||
makeInvalidBearerSessionGetRequest,
|
||||
makeRevokedBearerSessionGetRequest,
|
||||
makeTenantASessionJsonRequest,
|
||||
resetIsolationMocks,
|
||||
type IsolationMocks,
|
||||
} from "./user-session-isolation";
|
||||
|
||||
describe("user-session isolation fixtures", () => {
|
||||
it("keeps tenant A and tenant B rows and relationships distinct", () => {
|
||||
expect(isolationTenantARow.tenantId).toBe(isolationTenantA.id);
|
||||
expect(isolationTenantBRow.tenantId).toBe(isolationTenantB.id);
|
||||
expect(isolationTenantARow.tenantId).not.toBe(isolationTenantBRow.tenantId);
|
||||
expect(isolationTenantARelationship.parent.item.tenantId).toBe(isolationTenantA.id);
|
||||
expect(isolationTenantBRelationship.parent.item.tenantId).toBe(isolationTenantB.id);
|
||||
});
|
||||
|
||||
it("keeps admin fixtures tenant-neutral and cross-tenant capable", () => {
|
||||
expect(isolationAdminUser.tenantId).toBe(null);
|
||||
expect(isolationAdminSession.tenantId).toBe(null);
|
||||
expect(isolationAdminSession.isSuperAdmin).toBe(true);
|
||||
expect(isolationAdminSession.canAccessTenants).toEqual([isolationTenantA.id, isolationTenantB.id]);
|
||||
});
|
||||
|
||||
it("keeps inactive API-key fixtures distinct from active tenant keys", () => {
|
||||
expect(isolationInactiveApiKey.active).toBe(false);
|
||||
expect(isolationInactiveApiKey.id).toBe(isolationInactiveApiKeyLog.apiKeyId);
|
||||
expect(isolationInactiveApiKeyLog.id).not.toBe(isolationApiKeyLogA.id);
|
||||
expect(isolationInactiveApiKeyLog.id).not.toBe(isolationApiKeyLogB.id);
|
||||
expect(isolationInactiveApiKeyLog.apiKeyId).not.toBe(isolationApiKeyLogA.apiKeyId);
|
||||
expect(isolationInactiveApiKeyLog.apiKeyId).not.toBe(isolationApiKeyLogB.apiKeyId);
|
||||
});
|
||||
|
||||
it("builds tenant/session JSON requests and parses JSON responses", async () => {
|
||||
const request = makeTenantASessionJsonRequest("POST", "/api/parents", { name: "Tenant A" });
|
||||
|
||||
expect(request.method).toBe("POST");
|
||||
expect(request.url).toBe("https://example.test/api/parents");
|
||||
await expect(request.json()).resolves.toEqual({ name: "Tenant A" });
|
||||
|
||||
const body = await expectJsonResponse<{ ok: true }>(Response.json({ ok: true }, { status: 201, headers: { "Server-Timing": "unit" } }), 201);
|
||||
expect(body).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("builds admin-host and revoked credential requests with synthetic headers", () => {
|
||||
const adminRequest = makeAdminHostGetRequest("/api-keys/logs");
|
||||
const inactiveApiKeyRequest = makeInactiveApiKeyGetRequest("/api/parents");
|
||||
const revokedBearerRequest = makeRevokedBearerSessionGetRequest("/api/parents");
|
||||
const invalidBearerRequest = makeInvalidBearerSessionGetRequest("/api/parents");
|
||||
|
||||
expect(adminRequest.url).toBe("https://admin.example.test/api-keys/logs");
|
||||
expect(inactiveApiKeyRequest.headers.get("Authorization")).toBe(`Bearer ${isolationInactiveApiKey.key}`);
|
||||
expect(revokedBearerRequest.headers.get("Cookie")).toBe(`session=${isolationRevokedBearerSession.token}`);
|
||||
expect(invalidBearerRequest.headers.get("Cookie")).toBe(`session=${isolationInvalidBearerToken}`);
|
||||
expect(isolationRevokedBearerSession.token).not.toBe(isolationInvalidBearerToken);
|
||||
});
|
||||
|
||||
it("resets shared mocks and supports denied access status assertions", async () => {
|
||||
const mocks = {
|
||||
createMetrics: vi.fn(),
|
||||
getTranslations: vi.fn(),
|
||||
setApiKeyLogStatus: vi.fn(),
|
||||
reportUsage: vi.fn(),
|
||||
} satisfies IsolationMocks;
|
||||
|
||||
resetIsolationMocks(mocks);
|
||||
const metrics = (await mocks.createMetrics()) as { getServerTimingHeader: () => Record<string, string> };
|
||||
const deniedError = createDeniedIsolationError("API Key does not have access to tenant B row", 403, isolationApiKeyLogA);
|
||||
|
||||
await mocks.setApiKeyLogStatus(isolationApiKeyLogA.id, { status: deniedError.status, error: deniedError.message, startTime: performance.now() });
|
||||
|
||||
expect(metrics.getServerTimingHeader()).toEqual({ "Server-Timing": "unit;dur=1" });
|
||||
await expect(mocks.getTranslations()).resolves.toEqual({ t: expect.any(Function) });
|
||||
expectStatusLog(mocks.setApiKeyLogStatus, isolationApiKeyLogA.id, 403, deniedError.message);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
type MockFunction = {
|
||||
(...args: unknown[]): unknown;
|
||||
mockResolvedValue(value: unknown): MockFunction;
|
||||
};
|
||||
|
||||
declare const expect: {
|
||||
(actual: unknown): {
|
||||
toBe(expected: unknown): void;
|
||||
toBeTruthy(): void;
|
||||
toHaveBeenCalledWith(...expected: unknown[]): void;
|
||||
};
|
||||
any(constructor: unknown): unknown;
|
||||
objectContaining(value: Record<string, unknown>): unknown;
|
||||
};
|
||||
|
||||
declare const vi: {
|
||||
fn(implementation?: (...args: unknown[]) => unknown): MockFunction;
|
||||
clearAllMocks(): void;
|
||||
resetModules(): void;
|
||||
};
|
||||
|
||||
export const isolationTenantA = {
|
||||
id: "tenant_session_iso_a",
|
||||
name: "Tenant Session Isolation A",
|
||||
slug: "tenant-session-isolation-a",
|
||||
};
|
||||
|
||||
export const isolationTenantB = {
|
||||
id: "tenant_session_iso_b",
|
||||
name: "Tenant Session Isolation B",
|
||||
slug: "tenant-session-isolation-b",
|
||||
};
|
||||
|
||||
export const isolationUserA = {
|
||||
id: "user_session_iso_a",
|
||||
email: "user-a@example.test",
|
||||
tenantId: isolationTenantA.id,
|
||||
};
|
||||
|
||||
export const isolationUserB = {
|
||||
id: "user_session_iso_b",
|
||||
email: "user-b@example.test",
|
||||
tenantId: isolationTenantB.id,
|
||||
};
|
||||
|
||||
export const isolationAdminUser = {
|
||||
id: "user_session_iso_admin",
|
||||
email: "admin@example.test",
|
||||
tenantId: null,
|
||||
admin: true,
|
||||
};
|
||||
|
||||
export const isolationAdminSession = {
|
||||
userId: isolationAdminUser.id,
|
||||
tenantId: null,
|
||||
isSuperAdmin: true,
|
||||
canAccessTenants: [isolationTenantA.id, isolationTenantB.id],
|
||||
};
|
||||
|
||||
export const isolationApiKeyLogA = {
|
||||
id: "api_key_log_session_iso_a",
|
||||
apiKeyId: "api_key_session_iso_a",
|
||||
};
|
||||
|
||||
export const isolationApiKeyLogB = {
|
||||
id: "api_key_log_session_iso_b",
|
||||
apiKeyId: "api_key_session_iso_b",
|
||||
};
|
||||
|
||||
export const isolationInactiveApiKeyLog = {
|
||||
id: "api_key_log_session_iso_inactive",
|
||||
apiKeyId: "api_key_session_iso_inactive",
|
||||
};
|
||||
|
||||
export const isolationInactiveApiKey = {
|
||||
id: isolationInactiveApiKeyLog.apiKeyId,
|
||||
key: "sk_test_session_iso_inactive",
|
||||
tenantId: isolationTenantA.id,
|
||||
active: false,
|
||||
};
|
||||
|
||||
export const isolationRevokedBearerSession = {
|
||||
id: "session_session_iso_revoked",
|
||||
userId: isolationUserA.id,
|
||||
tenantId: isolationTenantA.id,
|
||||
token: "bearer_session_iso_revoked",
|
||||
revokedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
export const isolationInvalidBearerToken = "bearer_session_iso_invalid";
|
||||
|
||||
export const isolationTiming = {
|
||||
startTime: 1_700_000_000_000,
|
||||
serverTiming: "unit;dur=1",
|
||||
};
|
||||
|
||||
export const isolationParentEntity = {
|
||||
id: "entity_session_iso_parent",
|
||||
name: "Parent",
|
||||
slug: "parents",
|
||||
title: "Parent",
|
||||
titlePlural: "Parents",
|
||||
properties: [],
|
||||
};
|
||||
|
||||
export const isolationChildEntity = {
|
||||
id: "entity_session_iso_child",
|
||||
name: "Child",
|
||||
slug: "children",
|
||||
title: "Child",
|
||||
titlePlural: "Children",
|
||||
properties: [],
|
||||
};
|
||||
|
||||
export const isolationTenantARow = {
|
||||
id: "row_session_iso_a",
|
||||
entityId: isolationParentEntity.id,
|
||||
tenantId: isolationTenantA.id,
|
||||
createdByUserId: isolationUserA.id,
|
||||
values: [{ name: "name", textValue: "Tenant A row" }],
|
||||
};
|
||||
|
||||
export const isolationTenantBRow = {
|
||||
id: "row_session_iso_b",
|
||||
entityId: isolationParentEntity.id,
|
||||
tenantId: isolationTenantB.id,
|
||||
createdByUserId: isolationUserB.id,
|
||||
values: [{ name: "name", textValue: "Tenant B row" }],
|
||||
};
|
||||
|
||||
export const isolationTenantAChildRow = {
|
||||
id: "row_session_iso_a_child",
|
||||
entityId: isolationChildEntity.id,
|
||||
tenantId: isolationTenantA.id,
|
||||
createdByUserId: isolationUserA.id,
|
||||
values: [{ name: "name", textValue: "Tenant A child row" }],
|
||||
};
|
||||
|
||||
export const isolationTenantBChildRow = {
|
||||
id: "row_session_iso_b_child",
|
||||
entityId: isolationChildEntity.id,
|
||||
tenantId: isolationTenantB.id,
|
||||
createdByUserId: isolationUserB.id,
|
||||
values: [{ name: "name", textValue: "Tenant B child row" }],
|
||||
};
|
||||
|
||||
export const isolationTenantARelationship = {
|
||||
id: "relationship_session_iso_a",
|
||||
parentId: isolationTenantARow.id,
|
||||
childId: isolationTenantAChildRow.id,
|
||||
tenantId: isolationTenantA.id,
|
||||
parent: { entity: isolationParentEntity, item: isolationTenantARow },
|
||||
child: { entity: isolationChildEntity, item: isolationTenantAChildRow },
|
||||
};
|
||||
|
||||
export const isolationTenantBRelationship = {
|
||||
id: "relationship_session_iso_b",
|
||||
parentId: isolationTenantBRow.id,
|
||||
childId: isolationTenantBChildRow.id,
|
||||
tenantId: isolationTenantB.id,
|
||||
parent: { entity: isolationParentEntity, item: isolationTenantBRow },
|
||||
child: { entity: isolationChildEntity, item: isolationTenantBChildRow },
|
||||
};
|
||||
|
||||
export const isolationUsage = {
|
||||
title: "api.usage",
|
||||
value: 50,
|
||||
remaining: 10,
|
||||
};
|
||||
|
||||
export const isolationAccessTenantASession = {
|
||||
tenant: isolationTenantA,
|
||||
userId: isolationUserA.id,
|
||||
entity: isolationParentEntity,
|
||||
apiKeyLog: isolationApiKeyLogA,
|
||||
usage: isolationUsage,
|
||||
};
|
||||
|
||||
export const isolationAccessTenantBSession = {
|
||||
tenant: isolationTenantB,
|
||||
userId: isolationUserB.id,
|
||||
entity: isolationParentEntity,
|
||||
apiKeyLog: isolationApiKeyLogB,
|
||||
usage: isolationUsage,
|
||||
};
|
||||
|
||||
export const isolationRelationshipAccessTenantASession = {
|
||||
tenant: isolationTenantA,
|
||||
userId: isolationUserA.id,
|
||||
apiKeyLog: isolationApiKeyLogA,
|
||||
parentRow: isolationTenantARow,
|
||||
childRow: isolationTenantAChildRow,
|
||||
};
|
||||
|
||||
export const isolationRelationshipAccessTenantBSession = {
|
||||
tenant: isolationTenantB,
|
||||
userId: isolationUserB.id,
|
||||
apiKeyLog: isolationApiKeyLogB,
|
||||
parentRow: isolationTenantBRow,
|
||||
childRow: isolationTenantBChildRow,
|
||||
};
|
||||
|
||||
export type IsolationMocks = {
|
||||
createMetrics: ReturnType<typeof vi.fn>;
|
||||
getTranslations: ReturnType<typeof vi.fn>;
|
||||
setApiKeyLogStatus: ReturnType<typeof vi.fn>;
|
||||
reportUsage: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
export function makeJsonRequest(method: string, url: string, body?: unknown): Request {
|
||||
return new Request(url, {
|
||||
method,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
headers: body === undefined ? undefined : { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
export function makeTenantASessionJsonRequest(method: string, path: string, body?: unknown): Request {
|
||||
return makeJsonRequest(method, `https://example.test${path}`, body);
|
||||
}
|
||||
|
||||
export function makeTenantBSessionJsonRequest(method: string, path: string, body?: unknown): Request {
|
||||
return makeJsonRequest(method, `https://example.test${path}`, body);
|
||||
}
|
||||
|
||||
export function makeGetRequest(path: string): Request {
|
||||
return new Request(`https://example.test${path}`, { method: "GET" });
|
||||
}
|
||||
|
||||
export function makeAdminHostGetRequest(path: string): Request {
|
||||
return new Request(`https://admin.example.test${path}`, { method: "GET" });
|
||||
}
|
||||
|
||||
export function makeApiKeyGetRequest(path: string, apiKey: string): Request {
|
||||
return new Request(`https://example.test${path}`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
}
|
||||
|
||||
export function makeInactiveApiKeyGetRequest(path: string): Request {
|
||||
return makeApiKeyGetRequest(path, isolationInactiveApiKey.key);
|
||||
}
|
||||
|
||||
export function makeBearerSessionGetRequest(path: string, token: string): Request {
|
||||
return new Request(`https://example.test${path}`, {
|
||||
method: "GET",
|
||||
headers: { Cookie: `session=${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
export function makeRevokedBearerSessionGetRequest(path: string): Request {
|
||||
return makeBearerSessionGetRequest(path, isolationRevokedBearerSession.token);
|
||||
}
|
||||
|
||||
export function makeInvalidBearerSessionGetRequest(path: string): Request {
|
||||
return makeBearerSessionGetRequest(path, isolationInvalidBearerToken);
|
||||
}
|
||||
|
||||
export function makeTenantASessionGetRequest(path: string): Request {
|
||||
return makeGetRequest(path);
|
||||
}
|
||||
|
||||
export function makeTenantBSessionGetRequest(path: string): Request {
|
||||
return makeGetRequest(path);
|
||||
}
|
||||
|
||||
export async function expectJsonResponse<TBody = unknown>(response: Response, status: number): Promise<TBody> {
|
||||
expect(response.status).toBe(status);
|
||||
expect(response.headers.get("Server-Timing")).toBeTruthy();
|
||||
return (await response.json()) as TBody;
|
||||
}
|
||||
|
||||
export function expectStatusLog(setApiKeyLogStatus: ReturnType<typeof vi.fn>, logId: string, status: number, error?: string): void {
|
||||
expect(setApiKeyLogStatus).toHaveBeenCalledWith(
|
||||
logId,
|
||||
expect.objectContaining({
|
||||
status,
|
||||
startTime: expect.any(Number),
|
||||
...(error === undefined ? {} : { error }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function createDeniedIsolationError(message: string, status = 403, apiKeyLog = isolationApiKeyLogA): Error & { status: number; apiKeyLog: typeof isolationApiKeyLogA } {
|
||||
return Object.assign(new Error(message), { status, apiKeyLog });
|
||||
}
|
||||
|
||||
export function resetIsolationMocks(mocks: IsolationMocks): void {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
mocks.createMetrics.mockResolvedValue({
|
||||
time: vi.fn((value) => value),
|
||||
getServerTimingHeader: () => ({ "Server-Timing": isolationTiming.serverTiming }),
|
||||
});
|
||||
mocks.getTranslations.mockResolvedValue({ t: (value: string) => value });
|
||||
mocks.setApiKeyLogStatus.mockResolvedValue(undefined);
|
||||
mocks.reportUsage.mockResolvedValue(undefined);
|
||||
}
|
||||
Reference in new issue
Block a user