Files
Toju/toju-app/src/app/domains/authentication/application/services/signal-server-authorize.service.spec.ts
T
myxelium f9e8538c80 feat(auth): recover cross-signal authorization with provision secrets
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.
2026-08-14 03:19:29 +02:00

119 lines
4.4 KiB
TypeScript

import '@angular/compiler';
import { Injector, runInInjectionContext } from '@angular/core';
import {
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { ServerDirectoryFacade } from '../../../server-directory';
import { SignalServerAuthService } from './signal-server-auth.service';
import { SignalServerAuthorizeService } from './signal-server-authorize.service';
const HOME_URL = 'https://signal.toju.app';
const FOREIGN_URL = 'https://signal-sweden.toju.app';
const homeUser = {
id: 'user-1',
username: 'alice',
displayName: 'Alice',
homeSignalServerUrl: HOME_URL
};
describe('SignalServerAuthorizeService', () => {
let router: { navigate: ReturnType<typeof vi.fn>; url: string };
let store: { select: ReturnType<typeof vi.fn> };
let serverDirectory: {
ensureServerEndpoint: ReturnType<typeof vi.fn>;
findServerByUrl: ReturnType<typeof vi.fn>;
servers: ReturnType<typeof vi.fn>;
testServer: ReturnType<typeof vi.fn>;
};
let signalServerAuth: {
ensureProvisioned: ReturnType<typeof vi.fn>;
hasValidCredential: ReturnType<typeof vi.fn>;
migrateHomeCredential: ReturnType<typeof vi.fn>;
};
let service: SignalServerAuthorizeService;
beforeEach(() => {
router = { navigate: vi.fn(() => Promise.resolve(true)), url: '/room/room-1' };
store = { select: vi.fn(() => of(homeUser)) };
serverDirectory = {
ensureServerEndpoint: vi.fn(() => ({ id: 'endpoint-1' })),
findServerByUrl: vi.fn(() => ({ id: 'endpoint-1', status: 'online' })),
servers: vi.fn(() => []),
testServer: vi.fn(() => Promise.resolve())
};
signalServerAuth = {
ensureProvisioned: vi.fn(() => Promise.resolve({ kind: 'skipped', reason: 'no-home-user' })),
hasValidCredential: vi.fn(() => false),
migrateHomeCredential: vi.fn()
};
const injector = Injector.create({
providers: [
SignalServerAuthorizeService,
{ provide: Router, useValue: router },
{ provide: Store, useValue: store },
{ provide: ServerDirectoryFacade, useValue: serverDirectory },
{ provide: SignalServerAuthService, useValue: signalServerAuth }
]
});
service = runInInjectionContext(injector, () => injector.get(SignalServerAuthorizeService));
});
it('returns true immediately when a valid credential already exists', async () => {
signalServerAuth.hasValidCredential.mockReturnValue(true);
await expect(service.ensureCredentialForServerUrl(FOREIGN_URL)).resolves.toBe(true);
expect(signalServerAuth.ensureProvisioned).not.toHaveBeenCalled();
});
it('never provisions or opens the authorize login page for the home signal server', async () => {
await expect(service.ensureCredentialForServerUrl(HOME_URL)).resolves.toBe(false);
expect(signalServerAuth.ensureProvisioned).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
it('restores the home credential from the legacy token store instead of provisioning', async () => {
signalServerAuth.hasValidCredential
.mockReturnValueOnce(false)
.mockReturnValue(true);
await expect(service.ensureCredentialForServerUrl(HOME_URL)).resolves.toBe(true);
expect(signalServerAuth.migrateHomeCredential).toHaveBeenCalledWith(homeUser);
expect(signalServerAuth.ensureProvisioned).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
it('treats scheme variants of the home url as the home server', async () => {
await expect(service.ensureCredentialForServerUrl('wss://signal.toju.app/')).resolves.toBe(false);
expect(signalServerAuth.ensureProvisioned).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
it('keeps the home session active when automatic foreign provisioning cannot recover', async () => {
await expect(service.ensureCredentialForServerUrl(FOREIGN_URL)).resolves.toBe(false);
expect(signalServerAuth.ensureProvisioned).toHaveBeenCalledWith(FOREIGN_URL, homeUser);
expect(router.navigate).not.toHaveBeenCalled();
});
it('returns true when foreign provisioning succeeds', async () => {
signalServerAuth.ensureProvisioned.mockResolvedValue({ kind: 'provisioned', result: {} });
await expect(service.ensureCredentialForServerUrl(FOREIGN_URL)).resolves.toBe(true);
expect(router.navigate).not.toHaveBeenCalled();
});
});