install token now shown in dashboard until install completes.
This commit is contained in:
18 files changed
+1977
-15
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const queryRawMock = vi.fn();
|
||||
vi.mock("@/lib/db/prisma", () => ({
|
||||
prisma: { $queryRaw: (...args: unknown[]) => queryRawMock(...args) },
|
||||
}));
|
||||
|
||||
const getInstallationStateMock = vi.fn();
|
||||
vi.mock("@/lib/installation/installation-state", () => ({
|
||||
getInstallationState: () => getInstallationStateMock(),
|
||||
}));
|
||||
|
||||
import { GET } from "../route";
|
||||
|
||||
/**
|
||||
* This is a public, unauthenticated endpoint (see the route's own header)
|
||||
* — the one contract that matters is that it never leaks anything beyond
|
||||
* status/database/installed/installedAt, in particular never the
|
||||
* installation token or any other tenant data. Manager (a separate app
|
||||
* with no database access of its own) relies on `installed`/`installedAt`
|
||||
* here to decide whether to still show that business's installation
|
||||
* token, so those two fields also need to behave correctly.
|
||||
*/
|
||||
describe("GET /api/health", () => {
|
||||
beforeEach(() => {
|
||||
queryRawMock.mockReset();
|
||||
getInstallationStateMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns unhealthy with 503 when the database is unreachable, without calling installation-state", async () => {
|
||||
queryRawMock.mockRejectedValueOnce(new Error("connection refused"));
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
const body = await response.json();
|
||||
expect(body).toEqual({ status: "unhealthy", database: "unreachable" });
|
||||
expect(getInstallationStateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports installed: false and installedAt: null before installation", async () => {
|
||||
queryRawMock.mockResolvedValueOnce([{ 1: 1 }]);
|
||||
getInstallationStateMock.mockResolvedValueOnce({ installed: false, status: "NOT_INSTALLED", installedAt: null });
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body).toEqual({ status: "healthy", database: "reachable", installed: false, installedAt: null });
|
||||
});
|
||||
|
||||
it("reports installed: true and an ISO installedAt once installed", async () => {
|
||||
queryRawMock.mockResolvedValueOnce([{ 1: 1 }]);
|
||||
const installedAt = new Date("2026-01-15T10:30:00.000Z");
|
||||
getInstallationStateMock.mockResolvedValueOnce({ installed: true, status: "INSTALLED", installedAt });
|
||||
|
||||
const response = await GET();
|
||||
|
||||
const body = await response.json();
|
||||
expect(body).toEqual({
|
||||
status: "healthy",
|
||||
database: "reachable",
|
||||
installed: true,
|
||||
installedAt: "2026-01-15T10:30:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to installed: false, installedAt: null rather than throwing if installation-state itself errors", async () => {
|
||||
queryRawMock.mockResolvedValueOnce([{ 1: 1 }]);
|
||||
getInstallationStateMock.mockRejectedValueOnce(new Error("boom"));
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body).toEqual({ status: "healthy", database: "reachable", installed: false, installedAt: null });
|
||||
});
|
||||
|
||||
it("never returns anything beyond status/database/installed/installedAt — no token, no other tenant data", async () => {
|
||||
queryRawMock.mockResolvedValueOnce([{ 1: 1 }]);
|
||||
getInstallationStateMock.mockResolvedValueOnce({ installed: true, status: "INSTALLED", installedAt: new Date() });
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(Object.keys(body).sort()).toEqual(["database", "installed", "installedAt", "status"]);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,14 @@ import { getInstallationState } from "@/lib/installation/installation-state";
|
||||
* show the installer), not unhealthy. Only a database connectivity
|
||||
* failure returns a non-200 status. Never returns secrets or a stack
|
||||
* trace — just enough for a health probe to act on.
|
||||
*
|
||||
* `installed`/`installedAt` are also this deployment's only externally
|
||||
* observable installation-status signal — NextChair Manager (a separate
|
||||
* app with no database access of its own) polls this over the internal
|
||||
* Docker network to decide whether to still show that business's
|
||||
* installation token, rather than Manager needing its own authenticated
|
||||
* status endpoint. Deliberately still just a boolean + a timestamp: never
|
||||
* the installation token itself, never anything else about the tenant.
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -17,7 +25,12 @@ export async function GET() {
|
||||
return NextResponse.json({ status: "unhealthy", database: "unreachable" }, { status: 503 });
|
||||
}
|
||||
|
||||
const { installed } = await getInstallationState().catch(() => ({ installed: false }));
|
||||
const { installed, installedAt } = await getInstallationState().catch(() => ({ installed: false, installedAt: null }));
|
||||
|
||||
return NextResponse.json({ status: "healthy", database: "reachable", installed });
|
||||
return NextResponse.json({
|
||||
status: "healthy",
|
||||
database: "reachable",
|
||||
installed,
|
||||
installedAt: installedAt ? installedAt.toISOString() : null,
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export const INSTALLATION_SINGLETON_ID = "singleton";
|
||||
export interface InstallationState {
|
||||
installed: boolean;
|
||||
status: InstallationStatus;
|
||||
/** When `status === INSTALLED`, else null. Surfaced by GET /api/health so an external tool (e.g. NextChair Manager) can show it without ever needing DB access of its own — see that route's own header. */
|
||||
installedAt: Date | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,10 +27,10 @@ export interface InstallationState {
|
||||
export const getInstallationState = cache(async (): Promise<InstallationState> => {
|
||||
const row = await prisma.applicationInstallation.findUnique({
|
||||
where: { id: INSTALLATION_SINGLETON_ID },
|
||||
select: { status: true },
|
||||
select: { status: true, installedAt: true },
|
||||
});
|
||||
const status = row?.status ?? InstallationStatus.NOT_INSTALLED;
|
||||
return { installed: status === InstallationStatus.INSTALLED, status };
|
||||
return { installed: status === InstallationStatus.INSTALLED, status, installedAt: row?.installedAt ?? null };
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user