feat(server): add fetchSpaceInviteSnapshot helper for cross-instance preview fetch
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { fetchSpaceInviteSnapshot } from './spaceInviteSnapshot';
|
||||||
|
|
||||||
|
describe('fetchSpaceInviteSnapshot', () => {
|
||||||
|
const originalFetch = global.fetch;
|
||||||
|
beforeEach(() => { global.fetch = vi.fn() as any; });
|
||||||
|
afterEach(() => { global.fetch = originalFetch; });
|
||||||
|
|
||||||
|
it('returns snapshot when preview endpoint succeeds', async () => {
|
||||||
|
(global.fetch as any).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
spaceId: 'S1',
|
||||||
|
spaceName: 'Aether',
|
||||||
|
description: 'desc',
|
||||||
|
icon: null,
|
||||||
|
avatarColor: 'mint',
|
||||||
|
memberCount: 12,
|
||||||
|
instanceName: 'Backspace',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const snap = await fetchSpaceInviteSnapshot('https://z.example', 'abc123');
|
||||||
|
expect(snap).toEqual({
|
||||||
|
spaceId: 'S1',
|
||||||
|
spaceName: 'Aether',
|
||||||
|
description: 'desc',
|
||||||
|
icon: null,
|
||||||
|
avatarColor: 'mint',
|
||||||
|
memberCount: 12,
|
||||||
|
instanceName: 'Backspace',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when preview returns 404', async () => {
|
||||||
|
(global.fetch as any).mockResolvedValueOnce({ ok: false, status: 404, json: async () => ({}) });
|
||||||
|
const snap = await fetchSpaceInviteSnapshot('https://z.example', 'badcode');
|
||||||
|
expect(snap).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when fetch throws (network error)', async () => {
|
||||||
|
(global.fetch as any).mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
||||||
|
const snap = await fetchSpaceInviteSnapshot('https://z.example', 'abc123');
|
||||||
|
expect(snap).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aborts after timeout', async () => {
|
||||||
|
(global.fetch as any).mockImplementationOnce((_url: string, opts: any) => {
|
||||||
|
return new Promise((_resolve, reject) => {
|
||||||
|
opts.signal?.addEventListener('abort', () => reject(new Error('aborted')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const snap = await fetchSpaceInviteSnapshot('https://z.example', 'abc', 50);
|
||||||
|
expect(snap).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { AvatarColor } from '@backspace/shared';
|
||||||
|
|
||||||
|
export interface SpaceInviteSnapshot {
|
||||||
|
spaceId: string;
|
||||||
|
spaceName: string;
|
||||||
|
description: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
avatarColor: AvatarColor | null;
|
||||||
|
memberCount: number;
|
||||||
|
instanceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a space invite preview from a (possibly remote) instance.
|
||||||
|
* Returns null on 4xx, network error, or timeout — caller should treat as
|
||||||
|
* "invite no longer valid".
|
||||||
|
*
|
||||||
|
* Uses a 5s default timeout so a slow/unreachable Z does not stall the
|
||||||
|
* caller-instance request.
|
||||||
|
*/
|
||||||
|
export async function fetchSpaceInviteSnapshot(
|
||||||
|
spaceInstanceOrigin: string,
|
||||||
|
inviteCode: string,
|
||||||
|
timeoutMs = 5000,
|
||||||
|
): Promise<SpaceInviteSnapshot | null> {
|
||||||
|
const url = `${spaceInstanceOrigin}/api/spaces/invite/${encodeURIComponent(inviteCode)}/preview`;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { signal: controller.signal });
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json() as Partial<SpaceInviteSnapshot>;
|
||||||
|
if (typeof data?.spaceId !== 'string' || typeof data?.spaceName !== 'string') return null;
|
||||||
|
return {
|
||||||
|
spaceId: data.spaceId,
|
||||||
|
spaceName: data.spaceName,
|
||||||
|
description: data.description ?? null,
|
||||||
|
icon: data.icon ?? null,
|
||||||
|
avatarColor: data.avatarColor ?? null,
|
||||||
|
memberCount: typeof data.memberCount === 'number' ? data.memberCount : 0,
|
||||||
|
instanceName: data.instanceName ?? 'Backspace',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user