Files
cinny/src/app/pages/auth/oidc/oidcState.test.ts
T
jaredandClaude Opus 4.8 a50d3e7ca7 feat(auth): OIDC phase 2 — login initiation (discover/register/authorize)
- oidc/oidcState.ts (pure, +3 tests): dynamic-registration cache (by issuer +
  redirectUri, corrupt-tolerant) and parseOidcCallbackParams (success/error/invalid).
- oidc/oidcLoginUtil.ts: getOrRegisterClientId (cache + registerOidcClient) and
  startOidcLogin (discoverAndValidateOIDCIssuerWellKnown -> generateOidcAuthorization
  Url -> redirect; invalidates the cache on failure). redirectUri is the
  deterministic getOidcCallbackUrl(), and the SDK returns clientId/issuer on
  callback, so no hand-rolled transient state is needed.
- login/OidcLogin.tsx: native-OIDC button mirroring SSOLogin + TokenLogin async/error.
- login/Login.tsx: issuer-gated — when discovery advertises an issuer, render
  OidcLogin and suppress password/legacy-SSO; non-OIDC servers unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:01:35 -04:00

63 lines
2.1 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
getCachedClientId,
cacheClientId,
invalidateCachedClient,
parseOidcCallbackParams,
} from './oidcState';
const installStorage = (): Map<string, string> => {
const store = new Map<string, string>();
(globalThis as { localStorage?: unknown }).localStorage = {
getItem: (k: string) => (store.has(k) ? store.get(k) : null),
setItem: (k: string, v: string) => {
store.set(k, String(v));
},
removeItem: (k: string) => {
store.delete(k);
},
};
return store;
};
test('registration cache: get / put / invalidate, scoped by issuer + redirectUri', () => {
installStorage();
assert.equal(getCachedClientId('iss', 'rd'), undefined);
cacheClientId('iss', 'client-1', 'rd');
assert.equal(getCachedClientId('iss', 'rd'), 'client-1');
assert.equal(getCachedClientId('iss', 'other-redirect'), undefined); // redirect mismatch = miss
assert.equal(getCachedClientId('iss2', 'rd'), undefined); // different issuer = miss
invalidateCachedClient('iss');
assert.equal(getCachedClientId('iss', 'rd'), undefined);
invalidateCachedClient('absent'); // no-op, no throw
});
test('registration cache tolerates corrupt storage', () => {
const store = installStorage();
store.set('cinny_oidc_dynamic_clients', '{ not json');
assert.equal(getCachedClientId('iss', 'rd'), undefined);
cacheClientId('iss', 'c', 'rd');
assert.equal(getCachedClientId('iss', 'rd'), 'c');
});
test('parseOidcCallbackParams classifies success / error / invalid', () => {
assert.deepEqual(parseOidcCallbackParams('?code=abc&state=xyz'), {
kind: 'success',
code: 'abc',
state: 'xyz',
});
assert.deepEqual(parseOidcCallbackParams('?error=access_denied&error_description=nope'), {
kind: 'error',
error: 'access_denied',
errorDescription: 'nope',
});
assert.deepEqual(parseOidcCallbackParams('?error=bad'), {
kind: 'error',
error: 'bad',
errorDescription: undefined,
});
assert.deepEqual(parseOidcCallbackParams('?code=only'), { kind: 'invalid' });
assert.deepEqual(parseOidcCallbackParams(''), { kind: 'invalid' });
});