mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-10-18 06:17:23 +08:00
f61608df00
Closes #23328
102 lines
5.1 KiB
TypeScript
102 lines
5.1 KiB
TypeScript
import type { DurableObjectState } from '@cloudflare/workers-types';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { BrowserSession } from './browser-session.worker';
|
|
|
|
const mocks = vi.hoisted(() => ({ acquire: vi.fn() }));
|
|
vi.mock('@cloudflare/playwright', () => ({ acquire: mocks.acquire }));
|
|
|
|
let storage: Map<string, string>;
|
|
const binding = { fetch: vi.fn() };
|
|
|
|
function createObject() {
|
|
const state = {
|
|
blockConcurrencyWhile: (callback: () => Promise<void>) => callback(),
|
|
storage: {
|
|
get: vi.fn((key) => Promise.resolve(storage.get(key))),
|
|
put: vi.fn((key, value) => {
|
|
storage.set(key, value);
|
|
return Promise.resolve();
|
|
}),
|
|
delete: vi.fn((key) => Promise.resolve(storage.delete(key))),
|
|
},
|
|
};
|
|
return { object: new BrowserSession(state as unknown as DurableObjectState, { BROWSER: binding }), state };
|
|
}
|
|
|
|
function request(invalidSessionId?: string) {
|
|
return new Request('https://browser-session/session', { method: 'POST', body: JSON.stringify(invalidSessionId ? { invalidSessionId } : {}) });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.acquire.mockReset().mockResolvedValue({ sessionId: 'first-session' });
|
|
storage = new Map();
|
|
});
|
|
|
|
describe('Browser Run session coordinator', () => {
|
|
it('acquires once and reuses the session for subsequent requests', async () => {
|
|
const { object } = createObject();
|
|
expect(await (await object.fetch(request())).json()).toEqual({ sessionId: 'first-session' });
|
|
expect(await (await object.fetch(request())).json()).toEqual({ sessionId: 'first-session' });
|
|
expect(mocks.acquire).toHaveBeenCalledOnce();
|
|
expect(mocks.acquire).toHaveBeenCalledWith(binding, { keep_alive: 60000 });
|
|
expect(storage.get('sessionId')).toBe('first-session');
|
|
});
|
|
|
|
it('coalesces simultaneous acquisitions while the browser is starting', async () => {
|
|
const acquisition = Promise.withResolvers<{ sessionId: string }>();
|
|
mocks.acquire.mockReturnValueOnce(acquisition.promise);
|
|
const { object } = createObject();
|
|
const pending = Promise.all([object.fetch(request()), object.fetch(request()), object.fetch(request())]);
|
|
await vi.waitFor(() => expect(mocks.acquire).toHaveBeenCalledOnce());
|
|
acquisition.resolve({ sessionId: 'concurrent-session' });
|
|
const responses = await pending;
|
|
expect(await Promise.all(responses.map((response) => response.json()))).toEqual(Array.from({ length: 3 }, () => ({ sessionId: 'concurrent-session' })));
|
|
expect(mocks.acquire).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('restores the session ID after the Durable Object is evicted', async () => {
|
|
await createObject().object.fetch(request());
|
|
const restarted = createObject();
|
|
expect(await (await restarted.object.fetch(request())).json()).toEqual({ sessionId: 'first-session' });
|
|
expect(mocks.acquire).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('coalesces concurrent invalidations of the same expired session', async () => {
|
|
storage.set('sessionId', 'expired');
|
|
mocks.acquire.mockResolvedValueOnce({ sessionId: 'replacement' });
|
|
const { object, state } = createObject();
|
|
const responses = await Promise.all([object.fetch(request('expired')), object.fetch(request('expired'))]);
|
|
expect(await Promise.all(responses.map((response) => response.json()))).toEqual([{ sessionId: 'replacement' }, { sessionId: 'replacement' }]);
|
|
expect(mocks.acquire).toHaveBeenCalledOnce();
|
|
expect(state.storage.delete).toHaveBeenCalledOnce();
|
|
expect(storage.get('sessionId')).toBe('replacement');
|
|
});
|
|
|
|
it('does not invalidate a newer session when an old request reports failure', async () => {
|
|
storage.set('sessionId', 'current-session');
|
|
const { object } = createObject();
|
|
expect(await (await object.fetch(request('old-session'))).json()).toEqual({ sessionId: 'current-session' });
|
|
expect(mocks.acquire).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('recovers from an acquisition failure without poisoning queued requests', async () => {
|
|
mocks.acquire.mockRejectedValueOnce(new Error('Quota temporarily exhausted'));
|
|
const { object } = createObject();
|
|
const results = await Promise.allSettled([object.fetch(request()), object.fetch(request())]);
|
|
expect(results[0].status).toBe('rejected');
|
|
expect(results[1].status).toBe('fulfilled');
|
|
expect(mocks.acquire).toHaveBeenCalledTimes(2);
|
|
expect(storage.get('sessionId')).toBe('first-session');
|
|
});
|
|
|
|
it('rejects invalid methods and invalidation payloads before acquisition', async () => {
|
|
const { object } = createObject();
|
|
expect((await object.fetch(new Request('https://browser-session/session'))).status).toBe(405);
|
|
const invalidRequest = new Request('https://browser-session/session', { method: 'POST', body: JSON.stringify({ invalidSessionId: 123 }) });
|
|
expect((await object.fetch(invalidRequest)).status).toBe(400);
|
|
expect(mocks.acquire).not.toHaveBeenCalled();
|
|
});
|
|
});
|