Files
world/tests/iam-address.test.mjs
zeekayandHanzo Dev c2254e1dba world: read orgs and projects from the collections that serve them
The org switcher asked IAM for its two lists at verb-noun addresses. Those
addresses now answer 410 naming their successor, and both call sites turn any
failure into a usable default — one org, one project — so the switcher went on
rendering a plausible screen.

  /v1/iam/get-organizations         -> GET /v1/iam/organizations
  /v1/iam/get-organization-projects -> GET /v1/iam/projects

A typed collection answers the list under its own key rather than in an
envelope, so each reader takes `organizations` / `projects`; reading `data` or a
bare array yields [] against the real response, which is the same shape a
genuinely empty answer has. Organizations scope themselves to the bearer and
take no owner, so that parameter goes; projects keep theirs, which is how a
member reads an org other than their home one.

The defaults stay — a switcher that throws is a switcher nobody can use — but
each now says it fell back, because a lone org and a lone Default are also what
the real thing looks like.

tests/iam-address.test.mjs pins both addresses and the key each list arrives
under.

Co-authored-by: Hanzo Dev <[email protected]>
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-24 14:59:54 -07:00

58 lines
2.3 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const read = (p) => readFileSync(resolve(__dirname, p), 'utf-8');
const iam = read('../src/services/iam.ts');
const orgScope = read('../src/services/org-scope.ts');
// IAM serves its organizations and projects as typed collections. The verb-noun
// spellings that used to reach them answer 410 with the successor in the body,
// and both call sites degrade to a usable default on failure — so a call left at
// a retired address renders a plausible screen (one org, one project) and nothing
// says otherwise. The address is therefore worth pinning on its own.
const RETIRED = [
'/v1/iam/get-organizations',
'/v1/iam/get-organization-projects',
'/v1/iam/get-organization',
'/v1/iam/get-user',
'/v1/iam/update-user',
];
describe('IAM addresses', () => {
it('reads organizations from the collection', () => {
assert.match(iam, /organizations: '\/v1\/iam\/organizations'/);
});
it('reads projects from the collection', () => {
assert.match(orgScope, /const IAM_PROJECTS = '\/v1\/iam\/projects'/);
});
it('names no retired address', () => {
for (const path of RETIRED) {
for (const [name, src] of [['iam.ts', iam], ['org-scope.ts', orgScope]]) {
assert.equal(src.includes(path), false, `${name} still names ${path}`);
}
}
});
// A typed collection answers the list under its own key. Reading `data` or a
// bare array yields [] against the real response, which both callers turn into
// the default — the same shape a genuine empty answer has.
it('reads each list from the key its collection answers under', () => {
assert.match(iam, /Array\.isArray\(data\?\.organizations\)/);
assert.match(orgScope, /Array\.isArray\(data\?\.projects\)/);
});
// Both lists fall back to a default the user cannot tell from a real answer,
// so the fallback has to say it happened.
it('reports a degraded list rather than only degrading', () => {
assert.match(iam, /console\.warn\([^)]*showing the home org only/);
assert.match(orgScope, /console\.warn\([^)]*showing Default only/);
});
});