test: share plugin auth and ui storage fixtures

This commit is contained in:
Peter Steinberger
2026-03-26 16:54:27 +00:00
parent d748ea9361
commit c12623a857
7 changed files with 100 additions and 250 deletions

View File

@@ -89,6 +89,66 @@ function createJwt(payload: Record<string, unknown>): string {
return `${header}.${body}.signature`;
}
function getOpenAICodexProvider() {
return requireProvider(registerProviders(openAIPlugin), "openai-codex");
}
function buildOpenAICodexOAuthResult(params: {
profileId: string;
access: string;
refresh: string;
expires: number;
email?: string;
}) {
return {
profiles: [
{
profileId: params.profileId,
credential: {
type: "oauth" as const,
provider: "openai-codex",
access: params.access,
refresh: params.refresh,
expires: params.expires,
...(params.email ? { email: params.email } : {}),
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
};
}
async function expectOpenAICodexStableFallbackProfile(params: {
access: string;
profileId: string;
}) {
const provider = getOpenAICodexProvider();
loginOpenAICodexOAuthMock.mockResolvedValueOnce({
refresh: "refresh-token",
access: params.access,
expires: 1_700_000_000_000,
});
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual(
buildOpenAICodexOAuthResult({
profileId: params.profileId,
access: params.access,
refresh: "refresh-token",
expires: 1_700_000_000_000,
}),
);
}
describe("provider auth contract", () => {
let authStore: AuthProfileStore;
@@ -113,7 +173,7 @@ describe("provider auth contract", () => {
});
it("keeps OpenAI Codex OAuth auth results provider-owned", async () => {
const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex");
const provider = getOpenAICodexProvider();
loginOpenAICodexOAuthMock.mockResolvedValueOnce({
email: "user@example.com",
refresh: "refresh-token",
@@ -123,36 +183,19 @@ describe("provider auth contract", () => {
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual({
profiles: [
{
profileId: "openai-codex:user@example.com",
credential: {
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires: 1_700_000_000_000,
email: "user@example.com",
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
});
expect(result).toEqual(
buildOpenAICodexOAuthResult({
profileId: "openai-codex:user@example.com",
access: "access-token",
refresh: "refresh-token",
expires: 1_700_000_000_000,
email: "user@example.com",
}),
);
});
it("backfills OpenAI Codex OAuth email from the JWT profile claim", async () => {
const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex");
const provider = getOpenAICodexProvider();
const access = createJwt({
"https://api.openai.com/profile": {
email: "jwt-user@example.com",
@@ -166,79 +209,31 @@ describe("provider auth contract", () => {
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual({
profiles: [
{
profileId: "openai-codex:jwt-user@example.com",
credential: {
type: "oauth",
provider: "openai-codex",
access,
refresh: "refresh-token",
expires: 1_700_000_000_000,
email: "jwt-user@example.com",
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
});
expect(result).toEqual(
buildOpenAICodexOAuthResult({
profileId: "openai-codex:jwt-user@example.com",
access,
refresh: "refresh-token",
expires: 1_700_000_000_000,
email: "jwt-user@example.com",
}),
);
});
it("uses a stable fallback id when OpenAI Codex JWT email is missing", async () => {
const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex");
const access = createJwt({
"https://api.openai.com/auth": {
chatgpt_account_user_id: "user-123__acct-456",
},
});
const expectedStableId = Buffer.from("user-123__acct-456", "utf8").toString("base64url");
loginOpenAICodexOAuthMock.mockResolvedValueOnce({
refresh: "refresh-token",
await expectOpenAICodexStableFallbackProfile({
access,
expires: 1_700_000_000_000,
});
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual({
profiles: [
{
profileId: `openai-codex:id-${expectedStableId}`,
credential: {
type: "oauth",
provider: "openai-codex",
access,
refresh: "refresh-token",
expires: 1_700_000_000_000,
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
profileId: `openai-codex:id-${expectedStableId}`,
});
});
it("uses iss and sub to build a stable fallback id when auth claims are missing", async () => {
const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex");
const access = createJwt({
iss: "https://accounts.openai.com",
sub: "user-abc",
@@ -246,84 +241,25 @@ describe("provider auth contract", () => {
const expectedStableId = Buffer.from("https://accounts.openai.com|user-abc").toString(
"base64url",
);
loginOpenAICodexOAuthMock.mockResolvedValueOnce({
refresh: "refresh-token",
await expectOpenAICodexStableFallbackProfile({
access,
expires: 1_700_000_000_000,
});
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual({
profiles: [
{
profileId: `openai-codex:id-${expectedStableId}`,
credential: {
type: "oauth",
provider: "openai-codex",
access,
refresh: "refresh-token",
expires: 1_700_000_000_000,
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
profileId: `openai-codex:id-${expectedStableId}`,
});
});
it("uses sub alone to build a stable fallback id when iss is missing", async () => {
const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex");
const access = createJwt({
sub: "user-abc",
});
const expectedStableId = Buffer.from("user-abc").toString("base64url");
loginOpenAICodexOAuthMock.mockResolvedValueOnce({
refresh: "refresh-token",
await expectOpenAICodexStableFallbackProfile({
access,
expires: 1_700_000_000_000,
});
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual({
profiles: [
{
profileId: `openai-codex:id-${expectedStableId}`,
credential: {
type: "oauth",
provider: "openai-codex",
access,
refresh: "refresh-token",
expires: 1_700_000_000_000,
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
profileId: `openai-codex:id-${expectedStableId}`,
});
});
it("falls back to the default OpenAI Codex profile when JWT parsing yields no identity", async () => {
const provider = requireProvider(registerProviders(openAIPlugin), "openai-codex");
const provider = getOpenAICodexProvider();
loginOpenAICodexOAuthMock.mockResolvedValueOnce({
refresh: "refresh-token",
access: "not-a-jwt-token",
@@ -332,31 +268,14 @@ describe("provider auth contract", () => {
const result = await provider.auth[0]?.run(buildAuthContext() as never);
expect(result).toEqual({
profiles: [
{
profileId: "openai-codex:default",
credential: {
type: "oauth",
provider: "openai-codex",
access: "not-a-jwt-token",
refresh: "refresh-token",
expires: 1_700_000_000_000,
},
},
],
configPatch: {
agents: {
defaults: {
models: {
"openai-codex/gpt-5.4": {},
},
},
},
},
defaultModel: "openai-codex/gpt-5.4",
notes: undefined,
});
expect(result).toEqual(
buildOpenAICodexOAuthResult({
profileId: "openai-codex:default",
access: "not-a-jwt-token",
refresh: "refresh-token",
expires: 1_700_000_000_000,
}),
);
});
it("keeps OpenAI Codex OAuth failures non-fatal at the provider layer", async () => {

View File

@@ -1,34 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorageMock } from "../../test-helpers/storage.ts";
import { pt_BR } from "../locales/pt-BR.ts";
import { zh_CN } from "../locales/zh-CN.ts";
import { zh_TW } from "../locales/zh-TW.ts";
type TranslateModule = typeof import("../lib/translate.ts");
function createStorageMock(): Storage {
const store = new Map<string, string>();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key: string) {
return store.get(key) ?? null;
},
key(index: number) {
return Array.from(store.keys())[index] ?? null;
},
removeItem(key: string) {
store.delete(key);
},
setItem(key: string, value: string) {
store.set(key, String(value));
},
};
}
describe("i18n", () => {
let translate: TranslateModule;

View File

@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorageMock } from "../test-helpers/storage.ts";
import {
applyResolvedTheme,
applySettings,
@@ -7,7 +8,6 @@ import {
setTabFromRoute,
syncThemeWithSettings,
} from "./app-settings.ts";
import { createStorageMock } from "./test-helpers/storage.ts";
import type { ThemeMode, ThemeName } from "./theme.ts";
type Tab =

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorageMock } from "../test-helpers/storage.ts";
import { loadDeviceAuthToken, storeDeviceAuthToken } from "./device-auth.ts";
import type { DeviceIdentity } from "./device-identity.ts";
import { createStorageMock } from "./test-helpers/storage.ts";
const wsInstances = vi.hoisted((): MockWebSocket[] => []);
const loadOrCreateDeviceIdentityMock = vi.hoisted(() =>

View File

@@ -1,31 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorageMock } from "../test-helpers/storage.ts";
type NavigationModule = typeof import("./navigation.ts");
function createStorageMock(): Storage {
const store = new Map<string, string>();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key: string) {
return store.get(key) ?? null;
},
key(index: number) {
return Array.from(store.keys())[index] ?? null;
},
removeItem(key: string) {
store.delete(key);
},
setItem(key: string, value: string) {
store.set(key, String(value));
},
};
}
describe("TAB_GROUPS", () => {
let navigation: NavigationModule;

View File

@@ -1,28 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
function createStorageMock(): Storage {
const store = new Map<string, string>();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key: string) {
return store.get(key) ?? null;
},
key(index: number) {
return Array.from(store.keys())[index] ?? null;
},
removeItem(key: string) {
store.delete(key);
},
setItem(key: string, value: string) {
store.set(key, String(value));
},
};
}
import { createStorageMock } from "../test-helpers/storage.ts";
function setTestLocation(params: { protocol: string; host: string; pathname: string }) {
vi.stubGlobal("location", {