fix: register the app's own client on Stalwart 1.0 behind another public address and say when the server can't be reached

This commit is contained in:
Linus Rath
2026-10-29 02:44:00 +01:00
parent 28d928f6e9
commit a2caa4cca5
4 changed files with 115 additions and 19 deletions
+31
View File
@@ -102,6 +102,37 @@ describe('loginWithPkce on Stalwart', () => {
expect(redirectUri).toBe(REGISTERED_REDIRECT_URI);
});
it('registers its client on 1.0 when the metadata names another public address', async () => {
const registered: string[] = [];
fetchMock.mockImplementation(async (url: string) => {
if (url.startsWith('https://mail.example.com/api/auth/client')) return answer({ error: 'invalid_client' });
if (url.endsWith('/auth/register')) {
registered.push(url);
return answer({ client_id: 'swc1.pub' }, 201);
}
return answer({}, 404);
});
const advertised: OAuthMetadata = {
...stalwart,
issuer: 'https://stalwart.internal.example.net',
authorization_endpoint: 'https://stalwart.internal.example.net/login',
token_endpoint: 'https://stalwart.internal.example.net/auth/token',
registration_endpoint: 'https://stalwart.internal.example.net/auth/register',
};
const { params } = await openedSession(advertised);
expect(params.get('client_id')).toBe('swc1.pub');
expect(registered).toEqual(['https://stalwart.internal.example.net/auth/register']);
});
it('says the server is unreachable instead of opening the browser with a client it may not know', async () => {
fetchMock.mockRejectedValue(new TypeError('Network request failed'));
const err = await loginWithPkce('https://mail.example.com', stalwart).catch((e) => e);
expect(err.name).toBe('NetworkError');
expect(mockOpenAuthSession).not.toHaveBeenCalled();
});
it('keeps the webmail client and the app scheme on 0.16', async () => {
fetchMock.mockImplementation(async () => answer({ status: 404 }, 404));
const { params, redirectUri } = await openedSession(stalwart);
+34 -1
View File
@@ -219,10 +219,43 @@ describe('resolveLoginClient', () => {
resetLoginClients();
});
it('keeps the webmail client when the server is unreachable, without remembering it', async () => {
it('says the server is unreachable, not that it refused the app, and remembers nothing', async () => {
fetchMock.mockRejectedValueOnce(new TypeError('Network request failed'));
const err = await exchangePasswordForTokens(BASE, 'u', 'pw', '1').catch((e) => e);
expect(err.name).toBe('NetworkError');
expect(describeLoginError(err, { serverUrl: BASE }).title).toBe("Can't reach mail.example.com");
expect(fetchMock.mock.calls.some(([url]) => url === `${BASE}/api/auth`)).toBe(false);
fakeServer('1.0');
expect((await resolveLoginClient(BASE)).clientId).toBe('swc1.1');
});
it.each([503, 429])('does not remember the webmail client after a %i from the client check', async (status) => {
fetchMock.mockResolvedValueOnce(json({ error: 'busy' }, status));
expect((await resolveLoginClient(BASE)).clientId).toBe('bulwark-webmail');
fakeServer('1.0');
expect((await resolveLoginClient(BASE)).clientId).toBe('swc1.1');
});
it('remembers the webmail client on a server without the client check', async () => {
fakeServer('0.16');
await resolveLoginClient(BASE);
const calls = fakeServer('0.16');
expect((await resolveLoginClient(BASE)).clientId).toBe('bulwark-webmail');
expect(calls).toHaveLength(0);
});
it('registers at the endpoint the OAuth metadata names', async () => {
const urls: string[] = [];
fetchMock.mockImplementation(async (url: string) => {
urls.push(url.split('?')[0]);
if (url.startsWith(`${BASE}/api/auth/client`)) return json({ error: 'invalid_client' });
if (url === 'https://public.example.com/auth/register') return json({ client_id: 'swc1.pub' }, 201);
return json({}, 404);
});
const client = await resolveLoginClient(BASE, { registrationEndpoint: 'https://public.example.com/auth/register' });
expect(client.clientId).toBe('swc1.pub');
expect(urls).toEqual([`${BASE}/api/auth/client`, 'https://public.example.com/auth/register']);
});
});