import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
getAccessToken,
clearTokenCache,
hasValidToken,
} from "../../src/auth/ionAuth.js";
import type { IonApiConfig } from "../../src/config/ionapi.js";
// Mock fetch globally
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
describe("ionAuth", () => {
const mockConfig: IonApiConfig = {
ti: "TEST_TENANT",
cn: "TestConnection",
dt: "Browser",
ci: "test-client-id",
cs: "test-client-secret",
iu: "https://api.example.com",
pu: "https://auth.example.com",
oa: "/oauth/authorize",
ot: "/oauth/token",
or: "/oauth/revoke",
ev: "1.0",
v: "1.0",
saak: "test-access-key",
sask: "test-secret-key",
};
const mockTokenResponse = {
access_token: "mock-access-token",
token_type: "Bearer",
expires_in: 3600,
};
beforeEach(() => {
vi.clearAllMocks();
clearTokenCache();
});
afterEach(() => {
clearTokenCache();
});
describe("getAccessToken", () => {
it("should request a new token when cache is empty", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockTokenResponse,
});
const token = await getAccessToken(mockConfig);
expect(token).toBe("mock-access-token");
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it("should return cached token on subsequent calls", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockTokenResponse,
});
const token1 = await getAccessToken(mockConfig);
const token2 = await getAccessToken(mockConfig);
expect(token1).toBe("mock-access-token");
expect(token2).toBe("mock-access-token");
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it("should throw error when token request fails", async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
text: async () => "Invalid credentials",
});
await expect(getAccessToken(mockConfig)).rejects.toThrow(
"Failed to obtain access token: 401 Unauthorized"
);
});
it("should send correct request body", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockTokenResponse,
});
await getAccessToken(mockConfig);
expect(mockFetch).toHaveBeenCalledWith(
"https://auth.example.com/oauth/token",
expect.objectContaining({
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
);
// Check the body contains required parameters
const callArgs = mockFetch.mock.calls[0];
const body = callArgs[1].body;
expect(body).toContain("grant_type=password");
expect(body).toContain("username=test-access-key");
expect(body).toContain("password=test-secret-key");
expect(body).toContain("client_id=test-client-id");
expect(body).toContain("client_secret=test-client-secret");
});
});
describe("clearTokenCache", () => {
it("should clear cache for specific tenant", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockTokenResponse,
});
await getAccessToken(mockConfig);
expect(hasValidToken(mockConfig)).toBe(true);
clearTokenCache(mockConfig.ti);
expect(hasValidToken(mockConfig)).toBe(false);
});
it("should clear all cache when no tenant specified", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockTokenResponse,
});
await getAccessToken(mockConfig);
const config2 = { ...mockConfig, ti: "TENANT2" };
await getAccessToken(config2);
clearTokenCache();
expect(hasValidToken(mockConfig)).toBe(false);
expect(hasValidToken(config2)).toBe(false);
});
});
describe("hasValidToken", () => {
it("should return false when no token is cached", () => {
expect(hasValidToken(mockConfig)).toBe(false);
});
it("should return true when valid token is cached", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockTokenResponse,
});
await getAccessToken(mockConfig);
expect(hasValidToken(mockConfig)).toBe(true);
});
});
});