Files
fluxer/packages/api/src/test/msw/handlers/BlueskyApiHandlers.tsx
2026-02-17 12:22:36 +00:00

221 lines
5.8 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {
BlueskyPostEmbed,
BlueskyPostThread,
BlueskyProfile,
} from '@fluxer/api/src/unfurler/resolvers/bluesky/BlueskyTypes';
import {HttpResponse, http} from 'msw';
const API_BASES = ['https://api.bsky.app/xrpc', 'https://public.api.bsky.app/xrpc'];
export interface BlueskyApiMockConfig {
handles?: Map<string, string>;
posts?: Map<string, BlueskyPostThread>;
profiles?: Map<string, BlueskyProfile>;
profileDescriptions?: Map<string, string>;
error404Handles?: Set<string>;
error404Profiles?: Set<string>;
error500?: boolean;
}
export function createBlueskyApiHandlers(config: BlueskyApiMockConfig = {}) {
const handles = config.handles ?? new Map();
const posts = config.posts ?? new Map();
const profiles = config.profiles ?? new Map();
const profileDescriptions = config.profileDescriptions ?? new Map();
const error404Handles = config.error404Handles ?? new Set();
const error404Profiles = config.error404Profiles ?? new Set();
const error500 = config.error500 ?? false;
const handlers = [];
for (const API_BASE of API_BASES) {
handlers.push(
http.get(`${API_BASE}/com.atproto.identity.resolveHandle`, ({request}) => {
if (error500) {
return HttpResponse.json({error: 'Internal server error'}, {status: 500});
}
const url = new URL(request.url);
const handle = url.searchParams.get('handle');
if (!handle) {
return HttpResponse.json({error: 'Missing handle parameter'}, {status: 400});
}
if (error404Handles.has(handle)) {
return HttpResponse.json({error: 'Handle not found'}, {status: 404});
}
const did = handles.get(handle);
if (!did) {
return HttpResponse.json({error: 'Handle not found'}, {status: 404});
}
return HttpResponse.json({did});
}),
);
handlers.push(
http.get(`${API_BASE}/app.bsky.feed.getPostThread`, ({request}) => {
const url = new URL(request.url);
const uri = url.searchParams.get('uri');
if (!uri) {
return HttpResponse.json({error: 'Missing uri parameter'}, {status: 400});
}
const thread = posts.get(uri);
if (!thread) {
return HttpResponse.json({error: 'Post not found'}, {status: 404});
}
return HttpResponse.json(thread);
}),
);
handlers.push(
http.get(`${API_BASE}/app.bsky.actor.getProfile`, ({request}) => {
if (error500) {
return HttpResponse.json({error: 'Internal server error'}, {status: 500});
}
const url = new URL(request.url);
const actor = url.searchParams.get('actor');
if (!actor) {
return HttpResponse.json({error: 'Missing actor parameter'}, {status: 400});
}
if (error404Profiles.has(actor)) {
return HttpResponse.json({error: 'Profile not found'}, {status: 404});
}
let profile = profiles.get(actor);
if (!profile) {
const description = profileDescriptions.get(actor);
if (description) {
profile = {
did: actor,
handle: actor,
description,
};
} else {
return HttpResponse.json({error: 'Profile not found'}, {status: 404});
}
}
return HttpResponse.json(profile);
}),
);
}
handlers.push(
http.get('https://plc.directory/*', () => {
return HttpResponse.json({
service: [{type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://bsky.social'}],
});
}),
);
return handlers;
}
export function createBlueskyPost(options: {
uri: string;
did: string;
handle: string;
displayName?: string;
avatar?: string;
text: string;
createdAt?: string;
embed?: BlueskyPostEmbed;
replyCount?: number;
repostCount?: number;
likeCount?: number;
quoteCount?: number;
bookmarkCount?: number;
parent?: {
did: string;
handle: string;
displayName?: string;
uri: string;
text: string;
};
}): BlueskyPostThread {
const post: BlueskyPostThread['thread']['post'] = {
uri: options.uri,
author: {
did: options.did,
handle: options.handle,
displayName: options.displayName,
avatar: options.avatar,
},
record: {
text: options.text,
createdAt: options.createdAt ?? new Date().toISOString(),
reply: options.parent
? {
parent: {uri: options.parent.uri, cid: 'parent-cid'},
root: {uri: options.parent.uri, cid: 'root-cid'},
}
: undefined,
},
embed: options.embed,
indexedAt: options.createdAt ?? new Date().toISOString(),
replyCount: options.replyCount ?? 0,
repostCount: options.repostCount ?? 0,
likeCount: options.likeCount ?? 0,
quoteCount: options.quoteCount ?? 0,
bookmarkCount: options.bookmarkCount ?? 0,
};
const thread: BlueskyPostThread = {
thread: {
post,
parent: options.parent
? {
post: {
uri: options.parent.uri,
author: {
did: options.parent.did,
handle: options.parent.handle,
displayName: options.parent.displayName,
},
record: {
text: options.parent.text,
createdAt: new Date().toISOString(),
},
indexedAt: new Date().toISOString(),
replyCount: 0,
repostCount: 0,
likeCount: 0,
quoteCount: 0,
bookmarkCount: 0,
},
}
: undefined,
},
};
return thread;
}