mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
185 lines
6.6 KiB
TypeScript
185 lines
6.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { mockSecureFetch } = vi.hoisted(() => ({
|
|
mockSecureFetch: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../client-cert', () => ({
|
|
secureFetch: mockSecureFetch,
|
|
}));
|
|
|
|
import {
|
|
normalizeServerUrl,
|
|
emailDomain,
|
|
isEmailAddress,
|
|
serverCandidates,
|
|
probeJmapServer,
|
|
discoverServerForEmail,
|
|
} from '../server-discovery';
|
|
|
|
const SESSION_DOC = { capabilities: {}, apiUrl: '/jmap/', primaryAccounts: {} };
|
|
|
|
/**
|
|
* Answers `status` for the listed base URLs and rejects everything else. A 200
|
|
* yields a JMAP session document unless a body is given explicitly.
|
|
*/
|
|
function respondFor(statuses: Record<string, number | { status: number; body: unknown }>) {
|
|
mockSecureFetch.mockImplementation(async (url: string) => {
|
|
const base = url.replace(/\/\.well-known\/jmap$/, '');
|
|
const entry = statuses[base];
|
|
if (entry === undefined) throw new TypeError('Network request failed');
|
|
const { status, body } =
|
|
typeof entry === 'number' ? { status: entry, body: SESSION_DOC } : entry;
|
|
return { status, json: async () => body } as Response;
|
|
});
|
|
}
|
|
|
|
describe('normalizeServerUrl', () => {
|
|
it('adds https:// to a bare host', () => {
|
|
expect(normalizeServerUrl('mail.example.com')).toBe('https://mail.example.com');
|
|
});
|
|
|
|
it('strips trailing slashes', () => {
|
|
expect(normalizeServerUrl('https://mail.example.com//')).toBe('https://mail.example.com');
|
|
});
|
|
|
|
it('strips a pasted JMAP session endpoint', () => {
|
|
expect(normalizeServerUrl('https://mail.example.com/.well-known/jmap')).toBe(
|
|
'https://mail.example.com',
|
|
);
|
|
expect(normalizeServerUrl('mail.example.com/jmap')).toBe('https://mail.example.com');
|
|
});
|
|
|
|
it('keeps an explicit http scheme and port', () => {
|
|
expect(normalizeServerUrl('http://localhost:8080')).toBe('http://localhost:8080');
|
|
});
|
|
|
|
it('rejects non-http schemes', () => {
|
|
expect(normalizeServerUrl('ftp://mail.example.com')).toBeNull();
|
|
expect(normalizeServerUrl('bulwarkmail://connect')).toBeNull();
|
|
});
|
|
|
|
it('rejects input that is not a host', () => {
|
|
expect(normalizeServerUrl('')).toBeNull();
|
|
expect(normalizeServerUrl(' ')).toBeNull();
|
|
expect(normalizeServerUrl('not a host')).toBeNull();
|
|
expect(normalizeServerUrl('ada@example.com')).toBeNull();
|
|
// A single label can't be a public host — except localhost.
|
|
expect(normalizeServerUrl('example')).toBeNull();
|
|
expect(normalizeServerUrl('localhost')).toBe('https://localhost');
|
|
});
|
|
});
|
|
|
|
describe('emailDomain', () => {
|
|
it('extracts and lowercases the domain', () => {
|
|
expect(emailDomain('Ada@Example.COM')).toBe('example.com');
|
|
});
|
|
|
|
it('rejects anything that is not an address', () => {
|
|
expect(emailDomain('ada')).toBeNull();
|
|
expect(emailDomain('ada@localhost')).toBeNull();
|
|
expect(emailDomain('a@b@example.com')).toBeNull();
|
|
expect(isEmailAddress('ada@example.com')).toBe(true);
|
|
expect(isEmailAddress('ada')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('serverCandidates', () => {
|
|
it('orders the bare domain ahead of the conventional subdomains', () => {
|
|
expect(serverCandidates('example.com')).toEqual([
|
|
'https://example.com',
|
|
'https://mail.example.com',
|
|
'https://webmail.example.com',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('probeJmapServer', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('treats 200 as a hit', async () => {
|
|
respondFor({ 'https://mail.example.com': 200 });
|
|
await expect(probeJmapServer('https://mail.example.com')).resolves.toBe(true);
|
|
});
|
|
|
|
it('treats 401 as a hit — an unauthenticated probe is meant to be rejected', async () => {
|
|
respondFor({ 'https://mail.example.com': 401 });
|
|
await expect(probeJmapServer('https://mail.example.com')).resolves.toBe(true);
|
|
});
|
|
|
|
it('treats 404 and transport failures as a miss', async () => {
|
|
respondFor({ 'https://mail.example.com': 404 });
|
|
await expect(probeJmapServer('https://mail.example.com')).resolves.toBe(false);
|
|
await expect(probeJmapServer('https://nothing.example.com')).resolves.toBe(false);
|
|
});
|
|
|
|
it('rejects a 200 that is not a session document', async () => {
|
|
// A catch-all web server or captive portal answering 200 for every path.
|
|
respondFor({ 'https://example.com': { status: 200, body: '<!doctype html>' } });
|
|
await expect(probeJmapServer('https://example.com')).resolves.toBe(false);
|
|
});
|
|
|
|
it('rejects a 200 whose body is not JSON at all', async () => {
|
|
mockSecureFetch.mockResolvedValue({
|
|
status: 200,
|
|
json: async () => {
|
|
throw new SyntaxError('Unexpected token <');
|
|
},
|
|
} as unknown as Response);
|
|
await expect(probeJmapServer('https://example.com')).resolves.toBe(false);
|
|
});
|
|
|
|
it('gives up at the deadline instead of hanging', async () => {
|
|
mockSecureFetch.mockImplementation(
|
|
() => new Promise(() => {/* never settles */}),
|
|
);
|
|
await expect(probeJmapServer('https://slow.example.com', 20)).resolves.toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('discoverServerForEmail', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('returns a known account server for the same domain without probing', async () => {
|
|
respondFor({});
|
|
const found = await discoverServerForEmail('ada@example.com', {
|
|
knownServerUrls: ['https://mail.example.com'],
|
|
});
|
|
expect(found).toBe('https://mail.example.com');
|
|
expect(mockSecureFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('prefers the bare domain when several hosts answer', async () => {
|
|
respondFor({ 'https://example.com': 401, 'https://mail.example.com': 200 });
|
|
await expect(discoverServerForEmail('ada@example.com')).resolves.toBe('https://example.com');
|
|
});
|
|
|
|
it('falls back to mail.<domain> when the bare domain is not a JMAP server', async () => {
|
|
respondFor({ 'https://example.com': 404, 'https://mail.example.com': 401 });
|
|
await expect(discoverServerForEmail('ada@example.com')).resolves.toBe(
|
|
'https://mail.example.com',
|
|
);
|
|
});
|
|
|
|
it('skips a bare domain that only serves a marketing site', async () => {
|
|
respondFor({
|
|
'https://example.com': { status: 200, body: '<!doctype html>' },
|
|
'https://mail.example.com': 200,
|
|
});
|
|
await expect(discoverServerForEmail('ada@example.com')).resolves.toBe(
|
|
'https://mail.example.com',
|
|
);
|
|
});
|
|
|
|
it('returns null when nothing answers, so the caller can ask', async () => {
|
|
respondFor({});
|
|
await expect(discoverServerForEmail('ada@example.org')).resolves.toBeNull();
|
|
});
|
|
|
|
it('returns null without touching the network for a non-address', async () => {
|
|
respondFor({});
|
|
await expect(discoverServerForEmail('ada')).resolves.toBeNull();
|
|
expect(mockSecureFetch).not.toHaveBeenCalled();
|
|
});
|
|
});
|