feat: add Keycloak authentication
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { parseRuntimeConfig } from "@/shared/config/env";
|
||||
import {
|
||||
initializeAuthentication,
|
||||
toAuthenticatedUser,
|
||||
toKeycloakConfig
|
||||
} from "./keycloak-auth";
|
||||
|
||||
const requiredConfig = parseRuntimeConfig({
|
||||
TJWATER_AUTH_MODE: "required",
|
||||
TJWATER_KEYCLOAK_ISSUER: "https://auth.example.test/auth/realms/tjwater",
|
||||
TJWATER_KEYCLOAK_CLIENT_ID: "next-tjwater"
|
||||
});
|
||||
|
||||
describe("Keycloak authentication", () => {
|
||||
it("derives the Keycloak server and Realm from the issuer", () => {
|
||||
expect(toKeycloakConfig(requiredConfig.TJWATER_KEYCLOAK_ISSUER, "next-tjwater")).toEqual({
|
||||
url: "https://auth.example.test/auth",
|
||||
realm: "tjwater",
|
||||
clientId: "next-tjwater"
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes the public SPA flow and exposes the refreshed token", async () => {
|
||||
const client = {
|
||||
token: "access-token",
|
||||
idTokenParsed: {
|
||||
sub: "user-1",
|
||||
name: "张调度",
|
||||
email: "operator@example.test"
|
||||
},
|
||||
init: vi.fn().mockResolvedValue(true),
|
||||
login: vi.fn().mockResolvedValue(undefined),
|
||||
logout: vi.fn().mockResolvedValue(undefined),
|
||||
updateToken: vi.fn().mockResolvedValue(false)
|
||||
};
|
||||
|
||||
const authentication = await initializeAuthentication(requiredConfig, () => client);
|
||||
|
||||
expect(client.init).toHaveBeenCalledWith({
|
||||
onLoad: "login-required",
|
||||
flow: "standard",
|
||||
pkceMethod: "S256"
|
||||
});
|
||||
expect(authentication.user).toEqual({
|
||||
name: "张调度",
|
||||
role: "operator@example.test"
|
||||
});
|
||||
await expect(authentication.getAccessToken()).resolves.toBe("access-token");
|
||||
expect(client.updateToken).toHaveBeenCalledWith(30);
|
||||
|
||||
await authentication.logout?.();
|
||||
expect(client.logout).toHaveBeenCalledWith({ redirectUri: "http://localhost:3000/" });
|
||||
});
|
||||
|
||||
it("uses stable user claim fallbacks", () => {
|
||||
expect(toAuthenticatedUser({ sub: "user-2", preferred_username: "dispatcher" })).toEqual({
|
||||
name: "dispatcher",
|
||||
role: "统一认证用户"
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to Keycloak instead of using a token after refresh failure", async () => {
|
||||
const refreshError = new Error("refresh failed");
|
||||
const client = {
|
||||
token: "expired-token",
|
||||
tokenParsed: { sub: "user-3" },
|
||||
init: vi.fn().mockResolvedValue(true),
|
||||
login: vi.fn().mockResolvedValue(undefined),
|
||||
logout: vi.fn().mockResolvedValue(undefined),
|
||||
updateToken: vi.fn().mockRejectedValue(refreshError)
|
||||
};
|
||||
const authentication = await initializeAuthentication(requiredConfig, () => client);
|
||||
|
||||
await expect(authentication.getAccessToken()).rejects.toBe(refreshError);
|
||||
expect(client.login).toHaveBeenCalledWith({ redirectUri: "http://localhost:3000/" });
|
||||
});
|
||||
|
||||
it("keeps authentication inert when explicitly disabled", async () => {
|
||||
const createClient = vi.fn();
|
||||
const authentication = await initializeAuthentication(
|
||||
parseRuntimeConfig({ TJWATER_AUTH_MODE: "disabled" }),
|
||||
createClient
|
||||
);
|
||||
|
||||
expect(authentication.enabled).toBe(false);
|
||||
await expect(authentication.getAccessToken()).resolves.toBeNull();
|
||||
expect(createClient).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user