Skip to content

Commit 5484d7e

Browse files
authored
Merge pull request #812 from Sun1090/fix/oauth-excluded-null-payload
🐛 fix(web): preserve explicit null oauth excluded wrappers
2 parents 2d31035 + 1173102 commit 5484d7e

4 files changed

Lines changed: 108 additions & 2 deletions

File tree

apps/web/src/services/api/authFiles.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,62 @@ beforeEach(() => {
4242
mocks.delete.mockReset();
4343
});
4444

45+
describe('authFilesApi OAuth excluded model normalization', () => {
46+
it.each([
47+
{ 'oauth-excluded-models': null },
48+
{ 'oauth-excluded-models': {} },
49+
{ items: null },
50+
{ items: {} },
51+
{},
52+
null,
53+
])('returns an empty map for %j', async (payload) => {
54+
mocks.get.mockResolvedValue(payload);
55+
56+
await expect(authFilesApi.getOauthExcludedModels()).resolves.toEqual({});
57+
expect(mocks.get).toHaveBeenCalledWith('/oauth-excluded-models');
58+
});
59+
60+
it.each([
61+
{ 'oauth-excluded-models': { ' Codex ': [' model-a ', 'MODEL-A', 'model-b'] } },
62+
{ items: { ' Codex ': [' model-a ', 'MODEL-A', 'model-b'] } },
63+
{ ' Codex ': [' model-a ', 'MODEL-A', 'model-b'] },
64+
{ codex: ' model-a, MODEL-A\nmodel-b ' },
65+
])('preserves supported response formats and normalization for %j', async (payload) => {
66+
mocks.get.mockResolvedValue(payload);
67+
68+
await expect(authFilesApi.getOauthExcludedModels()).resolves.toEqual({
69+
codex: ['model-a', 'model-b'],
70+
});
71+
});
72+
73+
it('does not fall through an explicit null wrapper to items or provider keys', async () => {
74+
mocks.get.mockResolvedValue({
75+
'oauth-excluded-models': null,
76+
items: { codex: ['model-a'] },
77+
codex: ['model-b'],
78+
});
79+
80+
await expect(authFilesApi.getOauthExcludedModels()).resolves.toEqual({});
81+
});
82+
83+
it('does not fall through an explicit null items wrapper to provider keys', async () => {
84+
mocks.get.mockResolvedValue({ items: null, codex: ['model-a'] });
85+
86+
await expect(authFilesApi.getOauthExcludedModels()).resolves.toEqual({});
87+
});
88+
89+
it('prefers the canonical wrapper over items', async () => {
90+
mocks.get.mockResolvedValue({
91+
'oauth-excluded-models': { codex: ['model-a'] },
92+
items: { codex: ['model-b'] },
93+
});
94+
95+
await expect(authFilesApi.getOauthExcludedModels()).resolves.toEqual({
96+
codex: ['model-a'],
97+
});
98+
});
99+
});
100+
45101
describe('authFilesApi OAuth model alias normalization', () => {
46102
it('preserves display-name and force-mapping returned by CPA', async () => {
47103
mocks.get.mockResolvedValue({

apps/web/src/services/api/authFiles.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,12 @@ const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]
962962
if (!payload || typeof payload !== 'object') return {};
963963

964964
const record = payload as Record<string, unknown>;
965-
const source = record['oauth-excluded-models'] ?? record.items ?? payload;
965+
// An explicit null wrapper means no exclusions, not a bare provider map.
966+
const source = Object.prototype.hasOwnProperty.call(record, 'oauth-excluded-models')
967+
? record['oauth-excluded-models']
968+
: Object.prototype.hasOwnProperty.call(record, 'items')
969+
? record.items
970+
: payload;
966971
if (!source || typeof source !== 'object') return {};
967972

968973
const result: Record<string, string[]> = {};

apps/web/src/services/api/transformers.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,43 @@ describe('normalizeConfigResponse Claude fingerprint profile', () => {
104104
expect(config.claudeApiKeys?.[0]).not.toHaveProperty('fingerprintProfile');
105105
});
106106
});
107+
108+
describe('normalizeConfigResponse OAuth excluded models', () => {
109+
it.each([{}, { 'oauth-excluded-models': null }])(
110+
'leaves missing or null config fields unset: %j',
111+
(raw) => {
112+
expect(normalizeConfigResponse(raw).oauthExcludedModels).toBeUndefined();
113+
}
114+
);
115+
116+
it.each([
117+
{ 'oauth-excluded-models': null },
118+
{ items: null },
119+
{ 'oauth-excluded-models': null, items: { codex: ['model-a'] } },
120+
{ items: null, codex: ['model-a'] },
121+
])('does not interpret an empty wrapper as a provider: %j', (payload) => {
122+
const config = normalizeConfigResponse({ 'oauth-excluded-models': payload });
123+
124+
expect(config.oauthExcludedModels).toBeUndefined();
125+
});
126+
127+
it.each([{}, { 'oauth-excluded-models': {} }, { items: {} }])(
128+
'preserves empty maps: %j',
129+
(payload) => {
130+
expect(
131+
normalizeConfigResponse({ 'oauth-excluded-models': payload }).oauthExcludedModels
132+
).toEqual({});
133+
}
134+
);
135+
136+
it.each([
137+
{ codex: ['model-a'] },
138+
{ 'oauth-excluded-models': { codex: ['model-a'] } },
139+
{ items: { codex: ['model-a'] } },
140+
{ 'oauth-excluded-models': { codex: ['model-a'] }, items: { codex: ['model-b'] } },
141+
])('preserves supported maps and wrapper precedence: %j', (payload) => {
142+
expect(
143+
normalizeConfigResponse({ 'oauth-excluded-models': payload }).oauthExcludedModels
144+
).toEqual({ codex: ['model-a'] });
145+
});
146+
});

apps/web/src/services/api/transformers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,12 @@ const normalizeOpenAIProvider = (provider: unknown): OpenAIProviderConfig | null
379379

380380
const normalizeOauthExcluded = (payload: unknown): Record<string, string[]> | undefined => {
381381
if (!isRecord(payload)) return undefined;
382-
const source = payload['oauth-excluded-models'] ?? payload.items ?? payload;
382+
// An explicit null wrapper means no exclusions, not a bare provider map.
383+
const source = Object.prototype.hasOwnProperty.call(payload, 'oauth-excluded-models')
384+
? payload['oauth-excluded-models']
385+
: Object.prototype.hasOwnProperty.call(payload, 'items')
386+
? payload.items
387+
: payload;
383388
if (!isRecord(source)) return undefined;
384389
const map: Record<string, string[]> = {};
385390
Object.entries(source).forEach(([provider, models]) => {

0 commit comments

Comments
 (0)