/* * 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 . */ import {createAPIApp} from '@fluxer/api/src/App'; import {Config} from '@fluxer/api/src/Config'; import {clearSqliteStore} from '@fluxer/api/src/database/SqliteKV'; import {MeilisearchSearchProvider} from '@fluxer/api/src/infrastructure/MeilisearchSearchProvider'; import {NullSearchProvider} from '@fluxer/api/src/infrastructure/NullSearchProvider'; import { setInjectedBlueskyOAuthService, setInjectedGatewayService, setInjectedKVProvider, setInjectedMediaService, setInjectedS3Service, setInjectedSearchProviderService, setInjectedWorkerService, } from '@fluxer/api/src/middleware/ServiceRegistry'; import type {ISearchProvider} from '@fluxer/api/src/search/ISearchProvider'; import {acquireMeilisearchTestServer} from '@fluxer/api/src/test/meilisearch/MeilisearchTestServer'; import {MockBlueskyOAuthService} from '@fluxer/api/src/test/mocks/MockBlueskyOAuthService'; import {MockKVProvider} from '@fluxer/api/src/test/mocks/MockKVProvider'; import {NoopLogger} from '@fluxer/api/src/test/mocks/NoopLogger'; import {NoopGatewayService} from '@fluxer/api/src/test/NoopGatewayService'; import {NoopWorkerService} from '@fluxer/api/src/test/NoopWorkerService'; import {TestMediaService} from '@fluxer/api/src/test/TestMediaService'; import {TestS3Service} from '@fluxer/api/src/test/TestS3Service'; import type {HonoApp} from '@fluxer/api/src/types/HonoEnv'; import {DEFAULT_SEARCH_CLIENT_TIMEOUT_MS} from '@fluxer/constants/src/Timeouts'; import type {IKVProvider} from '@fluxer/kv_client/src/IKVProvider'; export interface ApiTestHarness { app: HonoApp; kvProvider: IKVProvider; mockBlueskyOAuthService: MockBlueskyOAuthService; reset: () => Promise; resetData: () => Promise; shutdown: () => Promise; requestJson: (params: { path: string; method?: string; body?: unknown; headers?: Record; }) => Promise; } export interface CreateApiTestHarnessOptions { search?: 'disabled' | 'meilisearch'; } export async function createApiTestHarness(options: CreateApiTestHarnessOptions = {}): Promise { const kvProvider = new MockKVProvider(); setInjectedKVProvider(kvProvider); setInjectedGatewayService(new NoopGatewayService()); setInjectedWorkerService(new NoopWorkerService()); const s3Service = new TestS3Service(); await s3Service.initialize(); setInjectedS3Service(s3Service); const mediaService = new TestMediaService(); mediaService.setS3Service(s3Service); setInjectedMediaService(mediaService); const harnessLogger = new NoopLogger(); let searchProvider: ISearchProvider | null = null; let releaseMeilisearch: (() => Promise) | null = null; if (options.search === 'meilisearch') { const server = await acquireMeilisearchTestServer(); releaseMeilisearch = server.release; searchProvider = new MeilisearchSearchProvider({ config: { url: server.url, apiKey: server.apiKey, timeoutMs: DEFAULT_SEARCH_CLIENT_TIMEOUT_MS, taskWaitTimeoutMs: DEFAULT_SEARCH_CLIENT_TIMEOUT_MS, taskPollIntervalMs: 50, }, logger: harnessLogger, }); await searchProvider.initialize(); } else { searchProvider = new NullSearchProvider(); await searchProvider.initialize(); } setInjectedSearchProviderService(searchProvider); const mockBlueskyOAuthService = new MockBlueskyOAuthService(); setInjectedBlueskyOAuthService(mockBlueskyOAuthService); const { app, initialize: initializeApp, shutdown: shutdownApp, } = await createAPIApp({ config: Config, logger: harnessLogger, }); try { await initializeApp(); } catch (error) { console.error('Failed to initialize API app for tests:', error); throw error; } async function reset(): Promise { clearSqliteStore(); kvProvider.reset(); mockBlueskyOAuthService.reset(); } async function resetData(): Promise { kvProvider.reset(); } async function shutdown(): Promise { try { await shutdownApp(); } catch (_error) {} if (searchProvider) { try { await searchProvider.shutdown(); } catch (_error) {} } setInjectedWorkerService(undefined); setInjectedGatewayService(undefined); setInjectedKVProvider(undefined); setInjectedMediaService(undefined); setInjectedS3Service(undefined); setInjectedSearchProviderService(undefined); setInjectedBlueskyOAuthService(undefined); await s3Service.cleanup(); if (releaseMeilisearch) { await releaseMeilisearch(); } } async function requestJson(params: { path: string; method?: string; body?: unknown; headers?: Record; }): Promise { const {path, body, method = 'GET', headers} = params; const mergedHeaders = new Headers(headers); if (!mergedHeaders.has('content-type')) { mergedHeaders.set('content-type', 'application/json'); } if (!mergedHeaders.has('x-forwarded-for')) { mergedHeaders.set('x-forwarded-for', '127.0.0.1'); } const contentType = mergedHeaders.get('content-type'); let requestBody: string | undefined; if (body !== undefined) { if (typeof body === 'string') { requestBody = body; } else if (contentType === 'application/json') { requestBody = JSON.stringify(body); } else { requestBody = JSON.stringify(body); } } return app.request(path, { method, headers: mergedHeaders, body: requestBody, }); } return {app, kvProvider, mockBlueskyOAuthService, reset, resetData, shutdown, requestJson}; }