Update Roxy session cleanup, bounded proxy retries and Team authorization selection. Persist mailbox export usage and strengthen scoped member removal. Update isolated Roxy fixtures; all 808 tests pass.
278 lines
21 KiB
JavaScript
278 lines
21 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { isTeamJoinCandidate, teamAuthorizationState, teamAutoJsonState, teamAccountWorkflows, teamInviteCandidateState, teamMemberRemovalState, teamAutoJsonCandidates, teamAutoJsonRowTarget } from '../shared/team-workflow-ui.mjs';
|
|
|
|
const teamId = 'fixture-workspace';
|
|
const mother = { accountId: teamId, email: 'owner@fixture.test' };
|
|
const account = { id: 'fixture-child', email: 'member@fixture.test', status: 'ready', credentialsStatus: { canAcquire: true, hasPassword: true, hasTotp: true } };
|
|
const activeMembership = { team: teamId, status: 'active', workspaceTokenStatus: 'pending', joinStatus: 'member_confirmed_token_pending' };
|
|
|
|
test('member removal follows the exact active Team without requiring OAuth or blocking banned seats', () => {
|
|
const target = { ...mother, id: 'mother-removal' };
|
|
const joined = { ...account, status: 'banned', team: 'other-team', joinedTeams: [activeMembership] };
|
|
assert.equal(teamMemberRemovalState(joined, target).selectable, true);
|
|
assert.equal(teamMemberRemovalState(joined, { ...target, accountId: 'absent-team' }).selectable, false);
|
|
assert.equal(teamMemberRemovalState({ ...account, teamInvitations: [{ motherId: target.id, workspaceId: teamId, status: 'invited' }] }, target).selectable, false);
|
|
assert.equal(teamMemberRemovalState({ ...account, joinedTeams: [{ ...activeMembership, status: 'kicked' }] }, target).selectable, false);
|
|
});
|
|
|
|
test('member removal protects a fixed primary owner but preserves existing rotating and ordinary owner policy', () => {
|
|
const target = { ...mother, id: 'mother-removal', rotationMode: 'fixed' };
|
|
const owner = { ...account, email: mother.email, joinedTeams: [activeMembership] };
|
|
assert.equal(teamMemberRemovalState(owner, target).selectable, false);
|
|
assert.equal(teamMemberRemovalState(owner, { ...target, rotationMode: 'rotating' }).selectable, true);
|
|
assert.equal(teamMemberRemovalState({ ...owner, email: 'another-owner@fixture.test' }, target).selectable, true);
|
|
});
|
|
|
|
test('already joined members with ready pool status are not candidates and remain counted as pending Team authorization', () => {
|
|
const joined = { ...account, workspaceHistory: [activeMembership], joinedTeams: [{ team: teamId, status: 'active' }] };
|
|
assert.equal(isTeamJoinCandidate(joined, mother), false);
|
|
assert.equal(teamAuthorizationState(joined, teamId)?.pending, true);
|
|
assert.equal(teamAuthorizationState(joined, 'another-workspace'), null);
|
|
});
|
|
|
|
test('an email/password/2FA-only account may join without first manually obtaining Free JSON', () => {
|
|
assert.equal(isTeamJoinCandidate(account, mother), true);
|
|
assert.equal(isTeamJoinCandidate(account, mother, 'request'), true);
|
|
assert.equal(isTeamJoinCandidate({ ...account, email: 'member@another.test' }, mother, 'request'), false);
|
|
assert.equal(isTeamJoinCandidate({ ...account, email: 'spaces@business.test' }, { ...mother, email: 'owner@business.test' }, 'request'), true);
|
|
assert.equal(isTeamJoinCandidate({ ...account, email: 'member @fixture.test' }, mother, 'request'), false);
|
|
assert.equal(isTeamJoinCandidate({ ...account, workspaceHistory: [{ team: 'another-workspace', status: 'active' }] }, mother), true);
|
|
});
|
|
|
|
test('waiting browser verification, bans, remote members, and unexpired rejoin cooldowns are not ready candidates', () => {
|
|
assert.equal(isTeamJoinCandidate({ ...account, status: 'login_pending', login: { browserRequired: true } }, mother), false);
|
|
assert.equal(isTeamJoinCandidate({ ...account, banStatus: 'banned' }, mother), false);
|
|
assert.equal(isTeamJoinCandidate(account, { ...mother, members: [{ email: account.email, role: 'standard-user' }] }), false);
|
|
const cooldown = { ...account, workspaceHistory: [{ team: teamId, status: 'cooldown', retryAfter: '2099-01-01T00:00:00.000Z' }] };
|
|
assert.equal(isTeamJoinCandidate(cooldown, mother, 'invite', Date.parse('2026-10-08')), false);
|
|
assert.equal(isTeamJoinCandidate(cooldown, mother, 'invite', Date.parse('2100-01-01')), true);
|
|
});
|
|
|
|
test('a Team authorization action requires the same workspace and its independent browser-required official URL', () => {
|
|
const freeUrl = 'https://auth.openai.com/oauth/authorize?state=fixture-free';
|
|
const teamUrl = 'https://auth.openai.com/oauth/authorize?state=fixture-team';
|
|
const joined = { ...account, workspaceHistory: [activeMembership], login: { browserRequired: true, authUrl: freeUrl }, teamLogin: { workspaceId: teamId, status: 'browser_verification_required', browserRequired: true, needsInput: true, authUrl: teamUrl } };
|
|
assert.equal(teamAuthorizationState(joined, teamId)?.authUrl, teamUrl);
|
|
assert.equal(teamAuthorizationState({ ...joined, teamLogin: null }, teamId)?.authUrl, null, 'the Free URL is never substituted');
|
|
assert.equal(teamAuthorizationState(joined, 'another-workspace'), null);
|
|
assert.equal(teamAuthorizationState({ ...joined, teamLogin: { ...joined.teamLogin, browserRequired: false } }, teamId)?.authUrl, null);
|
|
assert.equal(teamAuthorizationState({ ...joined, teamLogin: { ...joined.teamLogin, status: 'ready', browserRequired: false, authUrl: null } }, teamId)?.pending, false);
|
|
});
|
|
|
|
test('Team authorization rejects executable URLs, external hosts, malformed URLs, and URLs after account suspension', () => {
|
|
for (const authUrl of ['javascript:alert(1)', 'https://external.example/authorize', 'not-a-url', 'https://auth.openai.com@external.example/authorize']) {
|
|
assert.equal(teamAuthorizationState({ ...account, teamLogin: { workspaceId: teamId, status: 'verification_required', browserRequired: true, authUrl } }, teamId)?.authUrl, null);
|
|
}
|
|
assert.equal(teamAuthorizationState({ ...account, banStatus: 'banned', teamLogin: { workspaceId: teamId, status: 'verification_required', browserRequired: true, authUrl: 'https://auth.openai.com/oauth/authorize?state=fixture' } }, teamId)?.authUrl, null);
|
|
});
|
|
|
|
const invitationMother = { ...mother, id: 'mother-fixture' };
|
|
const invitation = { motherId: invitationMother.id, workspaceId: teamId, status: 'invited' };
|
|
const authorization = { motherId: invitationMother.id, workspaceId: teamId, ready: true, hasAccessToken: true, hasRefreshToken: true, expiresAt: '2099-01-01T00:00:00.000Z' };
|
|
|
|
test('administrator invitation permits recipients without credentials and recipients waiting for login verification', () => {
|
|
const recipient = { id: account.id, email: account.email };
|
|
assert.deepEqual(teamInviteCandidateState(recipient, invitationMother), { selectable: true, reason: '', alreadyMember: false, invited: false });
|
|
for (const login of [{ status: 'waiting_code' }, { status: 'phone_verification_required' }, { status: 'verification_required', browserRequired: true }]) {
|
|
assert.equal(teamInviteCandidateState({ ...recipient, status: 'login_pending', login }, invitationMother).selectable, true);
|
|
}
|
|
});
|
|
|
|
test('invitation selection rejects existing target members and pending invites without restricting other Teams', () => {
|
|
assert.equal(teamInviteCandidateState({ ...account, joinedTeams: [{ team: teamId, status: 'active' }] }, invitationMother).alreadyMember, true);
|
|
assert.equal(teamInviteCandidateState(account, { ...invitationMother, members: [{ email: account.email }] }).selectable, false);
|
|
assert.equal(teamInviteCandidateState(account, { ...invitationMother, members: [{ email: account.email, deactivatedTime: '2026-01-01' }] }).selectable, true);
|
|
const otherMembership = { ...account, joinedTeams: [{ team: 'another-workspace', status: 'active' }] };
|
|
assert.equal(teamInviteCandidateState(otherMembership, invitationMother).selectable, true);
|
|
const invited = teamInviteCandidateState({ ...account, teamInvitations: [invitation] }, invitationMother);
|
|
assert.equal(invited.invited, true);
|
|
assert.equal(invited.selectable, false);
|
|
for (const pending of [{ ...invitation, workspaceId: 'another-workspace' }, { ...invitation, motherId: 'another-mother' }, { ...invitation, status: 'joined' }]) {
|
|
assert.equal(teamInviteCandidateState({ ...account, teamInvitations: [pending] }, invitationMother).selectable, true);
|
|
}
|
|
});
|
|
|
|
test('invitation selection enforces bans, valid email and only the target Team rejoin cooldown', () => {
|
|
for (const email of ['', 'invalid', 'space @fixture.test', 'member@@fixture.test']) {
|
|
assert.equal(teamInviteCandidateState({ ...account, email }, invitationMother).selectable, false);
|
|
}
|
|
for (const ban of [{ status: 'banned' }, { banStatus: 'banned' }]) {
|
|
assert.equal(teamInviteCandidateState({ ...account, ...ban }, invitationMother).selectable, false);
|
|
}
|
|
const nowMs = Date.parse('2026-10-09T00:00:00Z');
|
|
const cooldown = { team: teamId, status: 'cooldown', retryAfter: '2026-10-10T00:00:00Z' };
|
|
assert.equal(teamInviteCandidateState({ ...account, joinedTeams: [cooldown] }, invitationMother, nowMs).selectable, false);
|
|
assert.equal(teamInviteCandidateState({ ...account, joinedTeams: [{ ...cooldown, team: 'another-workspace' }] }, invitationMother, nowMs).selectable, true);
|
|
assert.equal(teamInviteCandidateState({ ...account, joinedTeams: [{ ...cooldown, retryAfter: '2026-10-08T00:00:00Z' }] }, invitationMother, nowMs).selectable, true);
|
|
const unknownReset = { team: teamId, status: 'kicked', reason: 'quota_7d' };
|
|
assert.equal(teamInviteCandidateState({ ...account, joinedTeams: [unknownReset] }, invitationMother, nowMs).selectable, false);
|
|
assert.equal(teamInviteCandidateState({ ...account, joinedTeams: [{ ...unknownReset, rejoinEligible: true }] }, invitationMother, nowMs).selectable, true);
|
|
});
|
|
|
|
test('Team JSON readiness does not inherit Free credentials or another workspace or mother authorization', () => {
|
|
const joined = { ...account, joinedTeams: [activeMembership], hasAccessToken: true, sub2apiStatus: { exportable: true }, credentialsStatus: { hasAccessToken: true, hasRefreshToken: true }, login: { status: 'ready' } };
|
|
assert.equal(teamAutoJsonState(joined, invitationMother).ready, false);
|
|
for (const other of [{ ...authorization, workspaceId: 'another-workspace' }, { ...authorization, motherId: 'another-mother' }]) {
|
|
const state = teamAutoJsonState({ ...joined, teamAuthorizations: [other] }, invitationMother);
|
|
assert.equal(state.ready, false);
|
|
assert.equal(state.authorization, null);
|
|
}
|
|
assert.equal(teamAutoJsonState({ ...joined, teamAuthorizations: [authorization] }, invitationMother).ready, true);
|
|
});
|
|
|
|
test('Team JSON ready requires complete unexpired target credentials and actual membership', () => {
|
|
const joined = { ...account, joinedTeams: [activeMembership] };
|
|
for (const partial of [{ ready: false }, { hasAccessToken: false }, { hasRefreshToken: false }, { expiresAt: '' }, { expiresAt: 'invalid' }, { expiresAt: '2020-01-01T00:00:00Z' }]) {
|
|
assert.equal(teamAutoJsonState({ ...joined, teamAuthorizations: [{ ...authorization, ...partial }] }, invitationMother).ready, false);
|
|
}
|
|
const pending = teamAutoJsonState({ ...account, teamInvitations: [invitation], teamAuthorizations: [authorization] }, invitationMother);
|
|
assert.equal(pending.invited, true);
|
|
assert.equal(pending.related, true);
|
|
assert.equal(pending.member, false);
|
|
assert.equal(pending.ready, false, 'an unaccepted invitation cannot expose stale Team credentials as ready');
|
|
assert.equal(teamAutoJsonState({ ...account, teamAuthorizations: [authorization] }, { ...invitationMother, members: [{ email: account.email }] }).ready, true);
|
|
});
|
|
|
|
test('old joined invitations and removed membership history do not stay related to a target Team', () => {
|
|
const removed = { ...account, team: teamId, joinedTeams: [{ team: teamId, status: 'kicked' }], teamInvitations: [{ ...invitation, status: 'joined' }], teamAuthorizations: [authorization] };
|
|
const state = teamAutoJsonState(removed, invitationMother);
|
|
assert.equal(state.member, false);
|
|
assert.equal(state.invited, false);
|
|
assert.equal(state.related, false);
|
|
assert.equal(state.ready, false);
|
|
for (const pending of [{ ...invitation, workspaceId: 'another-workspace' }, { ...invitation, motherId: 'another-mother' }]) {
|
|
assert.equal(teamAutoJsonState({ ...account, teamInvitations: [pending] }, invitationMother).related, false);
|
|
}
|
|
});
|
|
|
|
test('Team login state comes exclusively from target authorization metadata', () => {
|
|
const login = { workspaceId: teamId, status: 'verification_required', browserRequired: true, authUrl: 'https://auth.openai.com/oauth/authorize?state=fixture-team' };
|
|
const target = { ...account, teamInvitations: [invitation], teamLogin: login, login: { status: 'ready' } };
|
|
assert.equal(teamAutoJsonState(target, invitationMother).login, null);
|
|
assert.deepEqual(teamAutoJsonState({ ...target, teamAuthorizations: [{ ...authorization, login }] }, invitationMother).login, login);
|
|
assert.equal(teamAutoJsonState({ ...target, teamAuthorizations: [{ ...authorization, login: { ...login, workspaceId: 'another-workspace' } }] }, invitationMother).login, null);
|
|
assert.equal(teamAutoJsonState(null, null).related, false);
|
|
assert.equal(teamInviteCandidateState(null, null).selectable, false);
|
|
});
|
|
|
|
test('stale ready login metadata cannot claim complete Team authorization with expired or partial JSON', () => {
|
|
const login = { workspaceId: teamId, status: 'ready', code: 'ready', message: '之前已授权' };
|
|
const joined = { ...account, joinedTeams: [activeMembership] };
|
|
for (const invalid of [{ hasRefreshToken: false }, { hasAccessToken: false }, { expiresAt: '2020-01-01T00:00:00Z' }, { ready: false }]) {
|
|
const state = teamAutoJsonState({ ...joined, teamAuthorizations: [{ ...authorization, ...invalid, login }] }, invitationMother);
|
|
assert.equal(state.ready, false);
|
|
assert.equal(state.login.status, 'login_required');
|
|
assert.equal(state.login.code, 'team_json_incomplete');
|
|
assert.equal(state.login.authUrl, null);
|
|
assert.equal(teamAuthorizationState({ ...joined, teamLogin: state.login }, teamId)?.pending, true);
|
|
assert.equal(login.status, 'ready', 'normalization must not mutate server metadata');
|
|
}
|
|
const valid = teamAutoJsonState({ ...joined, teamAuthorizations: [{ ...authorization, login }] }, invitationMother);
|
|
assert.equal(valid.login.status, 'ready');
|
|
const challenge = { workspaceId: teamId, status: 'browser_verification_required', browserRequired: true, needsInput: true, authUrl: 'https://auth.openai.com/oauth/authorize?state=fixture-team' };
|
|
assert.deepEqual(teamAutoJsonState({ ...joined, teamAuthorizations: [{ ...authorization, ready: false, login: challenge }] }, invitationMother).login, challenge);
|
|
});
|
|
|
|
test('multi-Team overview preserves independent ready and pending authorization for the same account', () => {
|
|
const secondMother = { id: 'mother-second', accountId: 'workspace-second', email: 'owner2@fixture.test' };
|
|
const pendingLogin = { workspaceId: secondMother.accountId, status: 'verification_required', browserRequired: true, authUrl: 'https://auth.openai.com/oauth/authorize?state=second' };
|
|
const joined = {
|
|
...account,
|
|
joinedTeams: [activeMembership, { team: secondMother.accountId, status: 'active' }],
|
|
teamAuthorizations: [authorization, { motherId: secondMother.id, workspaceId: secondMother.accountId, ready: false, hasAccessToken: false, hasRefreshToken: false, login: pendingLogin }],
|
|
};
|
|
const workflows = teamAccountWorkflows(joined, [invitationMother, secondMother]);
|
|
assert.deepEqual(workflows.map(({ mother: target, workspaceId, ready }) => [target.id, workspaceId, ready]), [
|
|
[invitationMother.id, teamId, true],
|
|
[secondMother.id, secondMother.accountId, false],
|
|
]);
|
|
assert.equal(workflows[0].login, null);
|
|
assert.equal(workflows[1].login, pendingLogin);
|
|
assert.deepEqual(teamAccountWorkflows(joined, [invitationMother, secondMother], secondMother.id), [workflows[1]]);
|
|
});
|
|
|
|
test('multi-Team overview includes invitations and removed history without showing stale authorization as ready', () => {
|
|
const removedMother = { id: 'mother-removed', accountId: 'workspace-removed' };
|
|
const unrelatedMother = { id: 'mother-unrelated', accountId: 'workspace-unrelated' };
|
|
const historicalMother = { id: 'mother-history', accountId: 'workspace-history' };
|
|
const recipient = {
|
|
...account,
|
|
teamInvitations: [invitation],
|
|
joinedTeams: [{ team: removedMother.accountId, status: 'kicked' }, { team: historicalMother.accountId, status: 'cooldown' }],
|
|
teamAuthorizations: [{ ...authorization, motherId: removedMother.id, workspaceId: removedMother.accountId }],
|
|
};
|
|
const workflows = teamAccountWorkflows(recipient, [invitationMother, removedMother, unrelatedMother, historicalMother]);
|
|
assert.deepEqual(workflows.map((workflow) => workflow.mother.id), [invitationMother.id, removedMother.id, historicalMother.id]);
|
|
assert.equal(workflows[0].invited, true);
|
|
assert.equal(workflows[0].related, true);
|
|
assert.equal(workflows[1].ready, false);
|
|
assert.equal(workflows[1].related, false);
|
|
assert.equal(workflows[1].membership.status, 'kicked');
|
|
assert.equal(workflows[2].membership.status, 'cooldown');
|
|
assert.equal(workflows[2].authorization, null);
|
|
});
|
|
|
|
test('explicit Team selection includes uninvited accounts while absent or unknown targets never choose a first Team', () => {
|
|
const unrelatedMother = { id: 'mother-unrelated', accountId: 'workspace-unrelated' };
|
|
assert.deepEqual(teamAccountWorkflows(account, [invitationMother, unrelatedMother]), []);
|
|
const selected = teamAccountWorkflows(account, [invitationMother, unrelatedMother], unrelatedMother.id);
|
|
assert.equal(selected.length, 1);
|
|
assert.equal(selected[0].mother, unrelatedMother);
|
|
assert.equal(selected[0].related, false);
|
|
assert.equal(selected[0].ready, false);
|
|
assert.deepEqual(teamAccountWorkflows(account, [invitationMother, unrelatedMother], 'missing-mother'), []);
|
|
assert.deepEqual(teamAccountWorkflows(account), []);
|
|
assert.deepEqual(teamAccountWorkflows(account, null), []);
|
|
});
|
|
|
|
test('Free credentials and unassociated Team metadata cannot create ready authorization in a multi-Team overview', () => {
|
|
const secondMother = { id: 'mother-second', accountId: 'workspace-second' };
|
|
const pooled = {
|
|
...account,
|
|
hasAccessToken: true,
|
|
credentialsStatus: { hasAccessToken: true, hasRefreshToken: true },
|
|
sub2apiStatus: { exportable: true },
|
|
login: { status: 'ready' },
|
|
teamInvitations: [invitation],
|
|
teamAuthorizations: [{ ...authorization, motherId: secondMother.id, workspaceId: secondMother.accountId }],
|
|
};
|
|
const workflows = teamAccountWorkflows(pooled, [invitationMother, secondMother]);
|
|
assert.equal(workflows.length, 2, 'authorization-only metadata remains visible for inspection');
|
|
assert.equal(workflows.every((workflow) => !workflow.ready), true);
|
|
assert.equal(workflows[0].authorization, null, 'another Team authorization cannot replace invited Team authorization');
|
|
assert.equal(workflows[1].related, false, 'credentials alone cannot prove current membership');
|
|
});
|
|
|
|
test('automatic batch count excludes successful JSON and keeps only pending target-Team accounts', () => {
|
|
const pending = { ...account, id: 'pending', teamInvitations: [invitation] };
|
|
const complete = { ...account, id: 'complete', joinedTeams: [activeMembership], teamAuthorizations: [authorization] };
|
|
const other = { ...pending, id: 'other', teamInvitations: [{ ...invitation, motherId: 'other-mother', workspaceId: 'other-team' }] };
|
|
const banned = { ...pending, id: 'banned', banStatus: 'banned' };
|
|
const accounts = [complete, pending, other, banned, { ...pending }];
|
|
assert.deepEqual(teamAutoJsonCandidates(accounts, invitationMother).map((item) => item.id), ['pending']);
|
|
assert.deepEqual(teamAutoJsonCandidates(accounts, null), []);
|
|
});
|
|
|
|
test('single-row automatic selection never widens an empty, ready, missing or explicit account id to the batch', () => {
|
|
const first = { ...account, id: 'first', teamInvitations: [invitation] };
|
|
const second = { ...account, id: 'second', joinedTeams: [activeMembership], teamAuthorizations: [authorization] };
|
|
const third = { ...first, id: 'third' };
|
|
const accounts = [first, second, third];
|
|
assert.deepEqual(teamAutoJsonCandidates(accounts, invitationMother, ['third']).map((item) => item.id), ['third']);
|
|
for (const ids of [[], ['missing'], ['second']]) assert.deepEqual(teamAutoJsonCandidates(accounts, invitationMother, ids), []);
|
|
assert.deepEqual(teamAutoJsonCandidates(accounts, invitationMother, ['third', 'third', 'second']).map((item) => item.id), ['third']);
|
|
});
|
|
|
|
test('row target follows its sole association in overview and never substitutes an explicitly chosen Team', () => {
|
|
const secondMother = { id: 'mother-second', accountId: 'workspace-second' };
|
|
const invited = { ...account, teamInvitations: [invitation] };
|
|
const mothers = [secondMother, invitationMother];
|
|
assert.equal(teamAutoJsonRowTarget(invited, mothers).mother.id, invitationMother.id);
|
|
assert.equal(teamAutoJsonRowTarget(invited, mothers, secondMother.id), null);
|
|
assert.equal(teamAutoJsonRowTarget(invited, mothers, 'deleted-mother'), null);
|
|
const both = { ...invited, teamInvitations: [invitation, { motherId: secondMother.id, workspaceId: secondMother.accountId, status: 'invited' }] };
|
|
assert.equal(teamAutoJsonRowTarget(both, mothers), null);
|
|
assert.equal(teamAutoJsonRowTarget(both, mothers, secondMother.id).mother.id, secondMother.id);
|
|
});
|