Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions lib/__tests__/config-route.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtempSync, unlink, writeFileSync } from "fs";
import { mkdtempSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand All @@ -8,7 +8,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// state on the developer's machine can't leak into these env-driven
// assertions. Must happen before the first GET, because the config manager
// singleton loads the directory once and caches it.
process.env.ADMIN_CONFIG_DIR = mkdtempSync(path.join(tmpdir(), 'bw-config-route-'));
const TEMP_DIR = mkdtempSync(path.join(tmpdir(), 'bw-config-route-'));
process.env.ADMIN_CONFIG_DIR = TEMP_DIR;

/**
* Backing file for the SESSION_SECRET_FILE tests. Kept in the temp dir rather
* than the working directory, which test runs share.
*/
const SECRET_FILE = path.join(TEMP_DIR, 'session-secret');

// Mock NextResponse before importing the route
vi.mock('next/server', () => ({
Expand Down Expand Up @@ -50,6 +57,7 @@ describe('config API route', () => {

afterEach(() => {
process.env = { ...originalEnv };
rmSync(SECRET_FILE, { force: true });
});

function mockRequest(headers: Record<string, string> = {}): unknown {
Expand Down Expand Up @@ -156,15 +164,11 @@ describe('config API route', () => {
});

it('should enable rememberMe when SESSION_SECRET_FILE is set', async () => {
writeFileSync('./session-secret', 'test-secret');
process.env.SESSION_SECRET_FILE = './session-secret';
writeFileSync(SECRET_FILE, 'test-secret');
process.env.SESSION_SECRET_FILE = SECRET_FILE;

const config = await getConfig();

unlink('./session-secret', (err) => {
if (err) throw err;
});

expect(config.rememberMeEnabled).toBe(true);
});

Expand All @@ -183,15 +187,11 @@ describe('config API route', () => {
const config1 = await getConfig();
expect(config1.settingsSyncEnabled).toBe(false);

writeFileSync('./session-secret', 'test-secret');
process.env.SESSION_SECRET_FILE = './session-secret';
writeFileSync(SECRET_FILE, 'test-secret');
process.env.SESSION_SECRET_FILE = SECRET_FILE;

const config2 = await getConfig();

unlink('./session-secret', (err) => {
if (err) throw err;
});

expect(config2.settingsSyncEnabled).toBe(true);
});

Expand Down
20 changes: 18 additions & 2 deletions lib/__tests__/jmap-client-resilience.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,29 @@ function mockFetchResponseWithHeaders(status: number, headers: Record<string, st
});
}

/**
* A mock answering every call with its own Response, for the tests that cannot
* say how many calls to expect. A body reads once, so a single shared Response
* would leave the second reader with "Body has already been read".
*/
function respondEveryTime(status: number, body?: unknown): () => Promise<Response> {
return () => Promise.resolve(mockFetchResponse(status, body));
}

describe('JMAPClient resilience', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;
const connectedClients: JMAPClient[] = [];

beforeEach(() => {
fetchSpy = vi.spyOn(globalThis, 'fetch');
vi.useFakeTimers({ shouldAdvanceTime: true });
});

afterEach(() => {
// Connecting starts a keep-alive interval that outlives the test unless it
// is stopped here, and its ping then consumes a later test's mocked fetch.
connectedClients.forEach((client) => client.disconnect());
connectedClients.length = 0;
fetchSpy.mockRestore();
vi.useRealTimers();
});
Comment on lines 53 to 60
Expand All @@ -57,6 +71,7 @@ describe('JMAPClient resilience', () => {
const client = new JMAPClient('https://mail.example.com', 'user@test.com', 'pass123');
await client.connect();
fetchSpy.mockReset();
connectedClients.push(client);
return client;
}

Expand All @@ -65,6 +80,7 @@ describe('JMAPClient resilience', () => {
const client = JMAPClient.withBearer('https://mail.example.com', 'token123', 'user@test.com');
await client.connect();
fetchSpy.mockReset();
connectedClients.push(client);
return client;
}

Expand Down Expand Up @@ -238,7 +254,7 @@ describe('JMAPClient resilience', () => {
client.onConnectionChange(callback);

const echoResponse = { methodResponses: [['Core/echo', { ping: 'pong' }, '0']] };
fetchSpy.mockResolvedValue(mockFetchResponse(200, echoResponse));
fetchSpy.mockImplementation(respondEveryTime(200, echoResponse));

// Advance past keep-alive interval (30s)
await vi.advanceTimersByTimeAsync(30_000);
Expand Down Expand Up @@ -342,7 +358,7 @@ describe('JMAPClient resilience', () => {
client.disconnect();

// Advancing timers should not trigger any ping
fetchSpy.mockResolvedValue(mockFetchResponse(200, { methodResponses: [['Core/echo', { ping: 'pong' }, '0']] }));
fetchSpy.mockImplementation(respondEveryTime(200, { methodResponses: [['Core/echo', { ping: 'pong' }, '0']] }));
await vi.advanceTimersByTimeAsync(60_000);

expect(callback).not.toHaveBeenCalled();
Expand Down