A client that could not authorize against a foreign signal server was redirected into a dead end with no way to retry, so servers joined from another signal route became unreachable. The home server now stores a per-user provision secret, clients keep it in their own store, and a recovery service records why authorization failed per server URL. Invite, server browser, and chat room surface that reason and offer a retry instead of silently redirecting.
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import {
|
|
describe,
|
|
it,
|
|
expect
|
|
} from 'vitest';
|
|
import {
|
|
ProvisionUsernameCollisionError,
|
|
buildProvisionPlan,
|
|
buildProvisionUsernameCandidates,
|
|
shortHomeUserId,
|
|
shouldAdoptCanonicalSecret
|
|
} from './signal-server-provision.rules';
|
|
|
|
const HOME_USER_ID = 'a3f2b1c4-5678-90ab-cdef-1234567890ab';
|
|
|
|
function plan(canonical: string | null, deviceLocal: string | null) {
|
|
return buildProvisionPlan({
|
|
preferredUsername: 'alice',
|
|
homeUserId: HOME_USER_ID,
|
|
secrets: { canonical, deviceLocal }
|
|
}).map((attempt) => `${attempt.kind}:${attempt.username}:${attempt.secretSource}`);
|
|
}
|
|
|
|
describe('signal-server-provision.rules', () => {
|
|
it('derives a stable short id from a home user uuid', () => {
|
|
expect(shortHomeUserId('a3f2b1c4-5678-90ab-cdef-1234567890ab')).toBe('a3f2b1');
|
|
});
|
|
|
|
it('orders username candidates with preferred first then suffixed fallback', () => {
|
|
expect(
|
|
buildProvisionUsernameCandidates('alice', 'a3f2b1c4-5678-90ab-cdef-1234567890ab')
|
|
).toEqual(['alice', 'alice-a3f2b1']);
|
|
});
|
|
|
|
it('deduplicates candidates when suffix would repeat preferred username', () => {
|
|
expect(
|
|
buildProvisionUsernameCandidates('alice-a3f2b1', 'a3f2b1c4-5678-90ab-cdef-1234567890ab')
|
|
).toEqual(['alice-a3f2b1']);
|
|
});
|
|
|
|
it('signs in to the preferred username before ever trying the suffixed one', () => {
|
|
expect(plan('canonical-secret', null)).toEqual([
|
|
'register:alice:canonical',
|
|
'login:alice:canonical',
|
|
'register:alice-a3f2b1:canonical',
|
|
'login:alice-a3f2b1:canonical'
|
|
]);
|
|
});
|
|
|
|
it('tries the legacy device secret before giving the username up as someone else\'s', () => {
|
|
expect(plan('canonical-secret', 'legacy-secret')).toEqual([
|
|
'register:alice:canonical',
|
|
'login:alice:canonical',
|
|
'login:alice:device-local',
|
|
'register:alice-a3f2b1:canonical',
|
|
'login:alice-a3f2b1:canonical',
|
|
'login:alice-a3f2b1:device-local'
|
|
]);
|
|
});
|
|
|
|
it('never registers a suffixed duplicate without a canonical secret', () => {
|
|
expect(plan(null, 'legacy-secret')).toEqual([
|
|
'register:alice:device-local',
|
|
'login:alice:device-local',
|
|
'login:alice-a3f2b1:device-local'
|
|
]);
|
|
});
|
|
|
|
it('produces no attempts when no secret is available at all', () => {
|
|
expect(plan(null, null)).toEqual([]);
|
|
});
|
|
|
|
it('does not repeat the canonical secret as a legacy attempt', () => {
|
|
expect(plan('same-secret', 'same-secret')).toEqual([
|
|
'register:alice:canonical',
|
|
'login:alice:canonical',
|
|
'register:alice-a3f2b1:canonical',
|
|
'login:alice-a3f2b1:canonical'
|
|
]);
|
|
});
|
|
|
|
it('adopts the canonical secret only after a legacy login', () => {
|
|
expect(shouldAdoptCanonicalSecret({ kind: 'login', secretSource: 'device-local' }, 'canonical')).toBe(true);
|
|
expect(shouldAdoptCanonicalSecret({ kind: 'login', secretSource: 'canonical' }, 'canonical')).toBe(false);
|
|
expect(shouldAdoptCanonicalSecret({ kind: 'register', secretSource: 'device-local' }, 'canonical')).toBe(false);
|
|
expect(shouldAdoptCanonicalSecret({ kind: 'login', secretSource: 'device-local' }, null)).toBe(false);
|
|
});
|
|
|
|
it('exposes attempted usernames on collision errors', () => {
|
|
const error = new ProvisionUsernameCollisionError('https://signal.example.com', ['alice', 'alice-a3f2b1']);
|
|
|
|
expect(error.name).toBe('ProvisionUsernameCollisionError');
|
|
expect(error.serverUrl).toBe('https://signal.example.com');
|
|
expect(error.attemptedUsernames).toEqual(['alice', 'alice-a3f2b1']);
|
|
});
|
|
});
|