fix: various fixes to sentry-reported errors and more

This commit is contained in:
Hampus Kraft
2026-02-18 15:38:51 +00:00
parent 302c0d2a0c
commit 0517a966a3
357 changed files with 25420 additions and 16281 deletions

View File

@@ -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;

View File

@@ -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);
});
}

View File

@@ -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();

View 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();
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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});
});
}