fix: various fixes to sentry-reported errors and more
This commit is contained in:
@@ -750,6 +750,12 @@ export class NoopGatewayService extends IGatewayService {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
async getDiscoveryGuildCounts(
|
||||
_guildIds: Array<GuildID>,
|
||||
): Promise<Map<GuildID, {memberCount: number; onlineCount: number}>> {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
async getNodeStats(): Promise<{
|
||||
status: string;
|
||||
sessions: number;
|
||||
|
||||
@@ -3095,6 +3095,46 @@ export function TestHarnessController(app: HonoApp) {
|
||||
return ctx.json(result, result.success ? 200 : 409);
|
||||
});
|
||||
|
||||
app.post('/test/users/:userId/set-contact-info', async (ctx) => {
|
||||
ensureHarnessAccess(ctx);
|
||||
|
||||
const params = ctx.req.param() as {userId?: string};
|
||||
const userIdParam = params.userId;
|
||||
if (!userIdParam) {
|
||||
throw new Error('Missing userId parameter');
|
||||
}
|
||||
const userId = createUserID(BigInt(userIdParam));
|
||||
const body = await ctx.req.json();
|
||||
const {phone, email} = body as {phone?: string | null; email?: string | null};
|
||||
|
||||
const userRepository = new UserRepository();
|
||||
const user = await userRepository.findUnique(userId);
|
||||
if (!user) {
|
||||
throw new UnknownUserError();
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (phone !== undefined) {
|
||||
updates['phone'] = phone;
|
||||
}
|
||||
if (email !== undefined) {
|
||||
updates['email'] = email;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return ctx.json({success: true, updated: false});
|
||||
}
|
||||
|
||||
await userRepository.patchUpsert(userId, updates, user.toRow());
|
||||
|
||||
return ctx.json({
|
||||
success: true,
|
||||
updated: true,
|
||||
phone: updates['phone'] ?? user.phone,
|
||||
email: updates['email'] ?? user.email,
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/test/cache-clear', async (ctx) => {
|
||||
ensureHarnessAccess(ctx);
|
||||
const cacheService = ctx.get('cacheService');
|
||||
@@ -3107,4 +3147,10 @@ export function TestHarnessController(app: HonoApp) {
|
||||
Logger.info({totalDeleted}, 'Cleared KV cache via test harness');
|
||||
return ctx.json({cleared: true, deleted_count: totalDeleted});
|
||||
});
|
||||
|
||||
app.post('/test/rpc-session-init', async (ctx) => {
|
||||
const request = await ctx.req.json();
|
||||
const response = await ctx.get('rpcService').handleRpcRequest({request, requestCache: ctx.get('requestCache')});
|
||||
return ctx.json(response);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {clearSqliteStore} from '@fluxer/api/src/database/SqliteKV';
|
||||
import {Logger} from '@fluxer/api/src/Logger';
|
||||
import {resetSearchServices} from '@fluxer/api/src/SearchFactory';
|
||||
import type {IKVProvider} from '@fluxer/kv_client/src/IKVProvider';
|
||||
import type {QueueEngine} from '@fluxer/queue/src/engine/QueueEngine';
|
||||
import type {S3Service} from '@fluxer/s3/src/s3/S3Service';
|
||||
|
||||
export type TestHarnessResetHandler = () => Promise<void>;
|
||||
@@ -41,7 +40,6 @@ export async function resetTestHarnessState(): Promise<void> {
|
||||
|
||||
interface CreateTestHarnessResetOptions {
|
||||
kvProvider?: IKVProvider;
|
||||
queueEngine?: QueueEngine;
|
||||
s3Service?: S3Service;
|
||||
}
|
||||
|
||||
@@ -59,11 +57,6 @@ export function createTestHarnessResetHandler(options: CreateTestHarnessResetOpt
|
||||
}
|
||||
}
|
||||
|
||||
if (options.queueEngine) {
|
||||
Logger.info('Resetting queue engine');
|
||||
await options.queueEngine.resetState();
|
||||
}
|
||||
|
||||
if (options.s3Service) {
|
||||
Logger.info('Wiping S3 storage');
|
||||
await options.s3Service.clearAll();
|
||||
|
||||
60
packages/api/src/test/mocks/MockGatewayRpcTransport.tsx
Normal file
60
packages/api/src/test/mocks/MockGatewayRpcTransport.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 {GatewayRpcMethodError} from '@fluxer/api/src/infrastructure/GatewayRpcError';
|
||||
import type {IGatewayRpcTransport} from '@fluxer/api/src/infrastructure/IGatewayRpcTransport';
|
||||
import {vi} from 'vitest';
|
||||
|
||||
export class MockGatewayRpcTransport implements IGatewayRpcTransport {
|
||||
private methodErrors = new Map<string, string>();
|
||||
private methodResults = new Map<string, unknown>();
|
||||
|
||||
call = vi.fn(async (method: string, _params: Record<string, unknown>): Promise<unknown> => {
|
||||
const errorCode = this.methodErrors.get(method);
|
||||
if (errorCode !== undefined) {
|
||||
throw new GatewayRpcMethodError(errorCode);
|
||||
}
|
||||
|
||||
const result = this.methodResults.get(method);
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
destroy = vi.fn(async (): Promise<void> => {});
|
||||
|
||||
setMethodError(method: string, errorCode: string): void {
|
||||
this.methodErrors.set(method, errorCode);
|
||||
this.methodResults.delete(method);
|
||||
}
|
||||
|
||||
setMethodResult(method: string, result: unknown): void {
|
||||
this.methodResults.set(method, result);
|
||||
this.methodErrors.delete(method);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.methodErrors.clear();
|
||||
this.methodResults.clear();
|
||||
this.call.mockClear();
|
||||
this.destroy.mockClear();
|
||||
}
|
||||
}
|
||||
@@ -263,6 +263,11 @@ export class MockGatewayService implements IGatewayService {
|
||||
async getDiscoveryOnlineCounts(_guildIds: Array<GuildID>): Promise<Map<GuildID, number>> {
|
||||
return new Map();
|
||||
}
|
||||
async getDiscoveryGuildCounts(
|
||||
_guildIds: Array<GuildID>,
|
||||
): Promise<Map<GuildID, {memberCount: number; onlineCount: number}>> {
|
||||
return new Map();
|
||||
}
|
||||
async getNodeStats(): Promise<{
|
||||
status: string;
|
||||
sessions: number;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import type {Readable} from 'node:stream';
|
||||
import {S3ServiceException} from '@aws-sdk/client-s3';
|
||||
import type {IStorageService} from '@fluxer/api/src/infrastructure/IStorageService';
|
||||
@@ -33,6 +34,7 @@ export interface MockStorageServiceConfig {
|
||||
|
||||
export class MockStorageService implements IStorageService {
|
||||
private objects: Map<string, {data: Uint8Array; contentType?: string}> = new Map();
|
||||
private multipartUploads: Map<string, {parts: Map<number, Uint8Array>; key: string; bucket: string}> = new Map();
|
||||
private deletedObjects: Array<{bucket: string; key: string}> = [];
|
||||
private copiedObjects: Array<{
|
||||
sourceBucket: string;
|
||||
@@ -56,6 +58,10 @@ export class MockStorageService implements IStorageService {
|
||||
readonly deleteAvatarSpy = vi.fn();
|
||||
readonly listObjectsSpy = vi.fn();
|
||||
readonly deleteObjectsSpy = vi.fn();
|
||||
readonly createMultipartUploadSpy = vi.fn();
|
||||
readonly uploadPartSpy = vi.fn();
|
||||
readonly completeMultipartUploadSpy = vi.fn();
|
||||
readonly abortMultipartUploadSpy = vi.fn();
|
||||
|
||||
private config: MockStorageServiceConfig;
|
||||
|
||||
@@ -227,6 +233,62 @@ export class MockStorageService implements IStorageService {
|
||||
this.deleteObjectsSpy(_params);
|
||||
}
|
||||
|
||||
async createMultipartUpload(params: {
|
||||
bucket: string;
|
||||
key: string;
|
||||
contentType?: string;
|
||||
}): Promise<{uploadId: string}> {
|
||||
this.createMultipartUploadSpy(params);
|
||||
const uploadId = crypto.randomUUID();
|
||||
this.multipartUploads.set(uploadId, {parts: new Map(), key: params.key, bucket: params.bucket});
|
||||
return {uploadId};
|
||||
}
|
||||
|
||||
async uploadPart(params: {
|
||||
bucket: string;
|
||||
key: string;
|
||||
uploadId: string;
|
||||
partNumber: number;
|
||||
body: Uint8Array;
|
||||
}): Promise<{etag: string}> {
|
||||
this.uploadPartSpy(params);
|
||||
const upload = this.multipartUploads.get(params.uploadId);
|
||||
if (!upload) {
|
||||
throw new Error(`Mock: multipart upload ${params.uploadId} not found`);
|
||||
}
|
||||
upload.parts.set(params.partNumber, params.body);
|
||||
const etag = `"etag-${params.partNumber}"`;
|
||||
return {etag};
|
||||
}
|
||||
|
||||
async completeMultipartUpload(params: {
|
||||
bucket: string;
|
||||
key: string;
|
||||
uploadId: string;
|
||||
parts: Array<{partNumber: number; etag: string}>;
|
||||
}): Promise<void> {
|
||||
this.completeMultipartUploadSpy(params);
|
||||
const upload = this.multipartUploads.get(params.uploadId);
|
||||
if (!upload) {
|
||||
throw new Error(`Mock: multipart upload ${params.uploadId} not found`);
|
||||
}
|
||||
const sortedParts = [...upload.parts.entries()].sort(([a], [b]) => a - b);
|
||||
const totalSize = sortedParts.reduce((sum, [, data]) => sum + data.length, 0);
|
||||
const combined = new Uint8Array(totalSize);
|
||||
let offset = 0;
|
||||
for (const [, data] of sortedParts) {
|
||||
combined.set(data, offset);
|
||||
offset += data.length;
|
||||
}
|
||||
this.objects.set(upload.key, {data: combined});
|
||||
this.multipartUploads.delete(params.uploadId);
|
||||
}
|
||||
|
||||
async abortMultipartUpload(params: {bucket: string; key: string; uploadId: string}): Promise<void> {
|
||||
this.abortMultipartUploadSpy(params);
|
||||
this.multipartUploads.delete(params.uploadId);
|
||||
}
|
||||
|
||||
getDeletedObjects(): Array<{bucket: string; key: string}> {
|
||||
return [...this.deletedObjects];
|
||||
}
|
||||
@@ -246,6 +308,7 @@ export class MockStorageService implements IStorageService {
|
||||
|
||||
reset(): void {
|
||||
this.objects.clear();
|
||||
this.multipartUploads.clear();
|
||||
this.deletedObjects = [];
|
||||
this.copiedObjects = [];
|
||||
this.config = {};
|
||||
@@ -264,5 +327,9 @@ export class MockStorageService implements IStorageService {
|
||||
this.deleteAvatarSpy.mockClear();
|
||||
this.listObjectsSpy.mockClear();
|
||||
this.deleteObjectsSpy.mockClear();
|
||||
this.createMultipartUploadSpy.mockClear();
|
||||
this.uploadPartSpy.mockClear();
|
||||
this.completeMultipartUploadSpy.mockClear();
|
||||
this.abortMultipartUploadSpy.mockClear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* 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 {Config} from '@fluxer/api/src/Config';
|
||||
import {HttpResponse, http} from 'msw';
|
||||
|
||||
interface GatewayRpcRequest {
|
||||
method: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface GatewayRpcResponse {
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GatewayRpcRequestCapture {
|
||||
method: string;
|
||||
params: Record<string, unknown>;
|
||||
authorization?: string;
|
||||
}
|
||||
|
||||
export type GatewayRpcMockResponses = Map<string, unknown>;
|
||||
|
||||
export function createGatewayRpcHandler(
|
||||
mockResponses: GatewayRpcMockResponses = new Map(),
|
||||
requestCapture?: {current: GatewayRpcRequestCapture | null},
|
||||
) {
|
||||
const endpoint = `${Config.gateway.rpcEndpoint}/_rpc`;
|
||||
|
||||
return http.post(endpoint, async ({request}) => {
|
||||
const body = (await request.json()) as GatewayRpcRequest;
|
||||
const authorization = request.headers.get('authorization') ?? undefined;
|
||||
|
||||
if (requestCapture) {
|
||||
requestCapture.current = {
|
||||
method: body.method,
|
||||
params: body.params,
|
||||
authorization,
|
||||
};
|
||||
}
|
||||
|
||||
const mockResult = mockResponses.get(body.method);
|
||||
|
||||
if (mockResult === undefined) {
|
||||
const errorResponse: GatewayRpcResponse = {
|
||||
error: `No mock configured for method: ${body.method}`,
|
||||
};
|
||||
return HttpResponse.json(errorResponse, {status: 500});
|
||||
}
|
||||
|
||||
if (mockResult instanceof Error) {
|
||||
const errorResponse: GatewayRpcResponse = {
|
||||
error: mockResult.message,
|
||||
};
|
||||
return HttpResponse.json(errorResponse, {status: 500});
|
||||
}
|
||||
|
||||
const response: GatewayRpcResponse = {
|
||||
result: mockResult,
|
||||
};
|
||||
|
||||
return HttpResponse.json(response);
|
||||
});
|
||||
}
|
||||
|
||||
export function createGatewayRpcErrorHandler(status: number, errorMessage: string) {
|
||||
const endpoint = `${Config.gateway.rpcEndpoint}/_rpc`;
|
||||
|
||||
return http.post(endpoint, () => {
|
||||
const response: GatewayRpcResponse = {
|
||||
error: errorMessage,
|
||||
};
|
||||
return HttpResponse.json(response, {status});
|
||||
});
|
||||
}
|
||||
|
||||
export function createGatewayRpcMethodErrorHandler(method: string, errorMessage: string) {
|
||||
const endpoint = `${Config.gateway.rpcEndpoint}/_rpc`;
|
||||
|
||||
return http.post(endpoint, async ({request}) => {
|
||||
const body = (await request.json()) as GatewayRpcRequest;
|
||||
|
||||
if (body.method === method) {
|
||||
const response: GatewayRpcResponse = {
|
||||
error: errorMessage,
|
||||
};
|
||||
return HttpResponse.json(response, {status: 500});
|
||||
}
|
||||
|
||||
return HttpResponse.json({result: {}});
|
||||
});
|
||||
}
|
||||
|
||||
export function createGatewayRpcSequenceHandler(
|
||||
method: string,
|
||||
responses: Array<{result?: unknown; error?: string; status?: number}>,
|
||||
) {
|
||||
const endpoint = `${Config.gateway.rpcEndpoint}/_rpc`;
|
||||
let callCount = 0;
|
||||
|
||||
return http.post(endpoint, async ({request}) => {
|
||||
const body = (await request.json()) as GatewayRpcRequest;
|
||||
|
||||
if (body.method !== method) {
|
||||
return HttpResponse.json({result: {}});
|
||||
}
|
||||
|
||||
const responseConfig = responses[callCount] ?? responses[responses.length - 1];
|
||||
callCount++;
|
||||
|
||||
if (responseConfig.error) {
|
||||
const errorResponse: GatewayRpcResponse = {
|
||||
error: responseConfig.error,
|
||||
};
|
||||
return HttpResponse.json(errorResponse, {status: responseConfig.status ?? 500});
|
||||
}
|
||||
|
||||
const response: GatewayRpcResponse = {
|
||||
result: responseConfig.result,
|
||||
};
|
||||
|
||||
return HttpResponse.json(response, {status: responseConfig.status ?? 200});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user