mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
chore(workspace): tighten quality gates (#2230)
This commit is contained in:
1 parent
e6e4c6f7b5
commit
5c2dca1c51
123 files changed
+4925
-5640
No files matched your search
@@ -40,8 +40,7 @@ export class DMPermissionValidator {
|
||||
if (isBugHunterBotUser(senderUser)) {
|
||||
return;
|
||||
}
|
||||
const isShadowbannedSpammer =
|
||||
!senderUser.isBot && (senderUser.flags & UserFlags.SPAMMER) === UserFlags.SPAMMER;
|
||||
const isShadowbannedSpammer = !senderUser.isBot && (senderUser.flags & UserFlags.SPAMMER) === UserFlags.SPAMMER;
|
||||
const [senderBlockedTarget, targetBlockedSender, areFriends, targetSettings, senderSettings] = await Promise.all([
|
||||
this.deps.userRepository.getRelationship(senderId, recipientId, RelationshipTypes.BLOCKED),
|
||||
this.deps.userRepository.getRelationship(recipientId, senderId, RelationshipTypes.BLOCKED),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {Logger} from '@fluxer/logger/src/Logger';
|
||||
import {beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
|
||||
import type {MediaProxyMetadataRequest} from '../../infrastructure/IMediaService';
|
||||
import {setInjectedMediaService} from '../../middleware/ServiceRegistry';
|
||||
@@ -71,6 +72,7 @@ describe('Attachment Upload Validation', () => {
|
||||
const guild = await createGuild(harness, account.token, 'Storage Unavailable Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
|
||||
harness.storageService.configure({shouldFailUpload: true});
|
||||
try {
|
||||
const payload = {
|
||||
@@ -83,8 +85,25 @@ describe('Attachment Upload Validation', () => {
|
||||
const body = JSON.parse(text) as {code?: string};
|
||||
expect(response.status).toBe(HTTP_STATUS.SERVICE_UNAVAILABLE);
|
||||
expect(body.code).toBe(APIErrorCodes.SERVICE_UNAVAILABLE);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
err: expect.objectContaining({
|
||||
cause: expect.objectContaining({message: 'Mock storage upload failure'}),
|
||||
code: APIErrorCodes.SERVICE_UNAVAILABLE,
|
||||
message: 'Attachment storage is temporarily unavailable',
|
||||
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
}),
|
||||
method: 'POST',
|
||||
path: `/channels/${channelId}/messages`,
|
||||
requestId: expect.any(String),
|
||||
status: HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
},
|
||||
'Request failed',
|
||||
);
|
||||
} finally {
|
||||
harness.storageService.configure({shouldFailUpload: false});
|
||||
errorLoggerSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -160,12 +179,9 @@ describe('Attachment Upload Validation', () => {
|
||||
content: 'Filename mismatch test',
|
||||
attachments: [{id: 0, filename: 'expected.txt'}],
|
||||
};
|
||||
const {response, text, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'different.txt', data: fileData},
|
||||
]);
|
||||
if (response.status !== 200) {
|
||||
console.log('Error response:', text);
|
||||
}
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
|
||||
@@ -798,8 +798,7 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
const corpus = new Map<string, Array<Row>>();
|
||||
const shapeCache = new Map<string, Array<Shape>>();
|
||||
const unstableShapes: Array<string> = [];
|
||||
const reproducibleOrders: Array<string> = [];
|
||||
const traffic = {legacy: 0, next: 0, shapes: 0, improved: 0};
|
||||
const traffic = {legacy: 0, next: 0, shapes: 0};
|
||||
const keysetOrderDeltas: Array<string> = [];
|
||||
const legacyThrowDeltas: Array<string> = [];
|
||||
|
||||
@@ -906,7 +905,6 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
executor: LegacyPostgresKvQueryExecutor,
|
||||
tableName: string,
|
||||
shape: Shape,
|
||||
candidate: string,
|
||||
): Promise<boolean> {
|
||||
const observed = new Set<string>();
|
||||
for (const ordering of HEAP_ORDERINGS) {
|
||||
@@ -914,7 +912,6 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
}
|
||||
await restoreOne(kv, tableName, HEAP_ORDERINGS[0]!);
|
||||
if (observed.size === 1) return false;
|
||||
if (observed.has(candidate)) reproducibleOrders.push(`${tableName} ${shape.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -950,7 +947,6 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
traffic.legacy += left.rowsRead;
|
||||
traffic.next += right.rowsRead;
|
||||
traffic.shapes += 1;
|
||||
if (right.rowsRead < left.rowsRead) traffic.improved += 1;
|
||||
if (left.fingerprint === right.fingerprint) {
|
||||
if (right.rowsRead > left.rowsRead) {
|
||||
mismatches.push({
|
||||
@@ -976,7 +972,7 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!(await legacyIsOrderUnstable(kv, executor, tableName, shape, right.fingerprint))) {
|
||||
if (!(await legacyIsOrderUnstable(kv, executor, tableName, shape))) {
|
||||
mismatches.push({
|
||||
kind: sameMultiset ? 'order' : 'rowset',
|
||||
table: tableName,
|
||||
@@ -1159,7 +1155,6 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
}
|
||||
}
|
||||
expect(shapeCount).toBeGreaterThan(3000);
|
||||
console.log(`select sweep: tables=${specs.length} shapes=${shapeCount}`);
|
||||
expect(mismatches.length, report(mismatches)).toBe(0);
|
||||
}, 1_800_000);
|
||||
|
||||
@@ -1210,7 +1205,6 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
traffic.legacy += left.rowsRead;
|
||||
traffic.next += right.rowsRead;
|
||||
traffic.shapes += 1;
|
||||
if (right.rowsRead < left.rowsRead) traffic.improved += 1;
|
||||
if (right.rowsRead > left.rowsRead) {
|
||||
mismatches.push({
|
||||
kind: 'rowsread',
|
||||
@@ -1309,7 +1303,7 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
keysetOrderDeltas.push(`${spec.name} ${label}`);
|
||||
continue;
|
||||
}
|
||||
if (await legacyIsOrderUnstable(NEXT_TABLE, readLegacy, spec.name, shape, fingerprint(rightAll))) {
|
||||
if (await legacyIsOrderUnstable(NEXT_TABLE, readLegacy, spec.name, shape)) {
|
||||
unstableShapes.push(`${spec.name} ${label}`);
|
||||
continue;
|
||||
}
|
||||
@@ -1454,14 +1448,5 @@ suite('PostgresKvQueryExecutor differential', () => {
|
||||
}
|
||||
expect(traffic.shapes).toBeGreaterThan(3000);
|
||||
expect(traffic.next).toBeLessThan(traffic.legacy);
|
||||
console.log(
|
||||
`row payload traffic: shapes=${traffic.shapes} legacyRows=${traffic.legacy} nextRows=${traffic.next} reduction=${(100 - (traffic.next / traffic.legacy) * 100).toFixed(1)}% shapesImproved=${traffic.improved}`,
|
||||
);
|
||||
console.log(
|
||||
`accepted deltas: order-unstable=${unstableShapes.length} (legacy-reproducible=${reproducibleOrders.length}) keyset-order=${keysetOrderDeltas.length} legacy-throw=${legacyThrowDeltas.length}`,
|
||||
);
|
||||
console.log(`order-unstable sample: ${unstableShapes.slice(0, 5).join(' | ')}`);
|
||||
console.log(`keyset-order sample: ${keysetOrderDeltas.slice(0, 5).join(' | ')}`);
|
||||
console.log(`legacy-throw: ${[...new Set(legacyThrowDeltas.map((entry) => entry.split(': ')[1]))].join(' | ')}`);
|
||||
});
|
||||
});
|
||||
@@ -210,9 +210,9 @@ suite('postgres kv upgrade safety', () => {
|
||||
if (pages > 200) throw new Error('runaway paging loop');
|
||||
}
|
||||
} catch (error) {
|
||||
return {keys, error: (error as Error).message, pages};
|
||||
return {keys, error: (error as Error).message};
|
||||
}
|
||||
return {keys, error: null, pages};
|
||||
return {keys, error: null};
|
||||
}
|
||||
|
||||
it('pages consistently with only the old image running', async () => {
|
||||
@@ -232,27 +232,18 @@ suite('postgres kv upgrade safety', () => {
|
||||
it('ROLLING RESTART: an old replica consumes a token minted by the new one', async () => {
|
||||
const first = await next.executePagedQuery(usersScan(), {pageSize: 7, pageState: null});
|
||||
expect(first.pageState).toBeTruthy();
|
||||
const token = JSON.parse(Buffer.from(first.pageState!, 'base64url').toString('utf8'));
|
||||
let failure: string | null = null;
|
||||
let rows = 0;
|
||||
try {
|
||||
const second = await legacy.executePagedQuery(usersScan(), {pageSize: 7, pageState: first.pageState});
|
||||
rows = second.rows.length;
|
||||
} catch (error) {
|
||||
failure = (error as Error).message;
|
||||
}
|
||||
console.log(`NEW->OLD token=${JSON.stringify(token)} legacy=${failure ?? `${rows} rows`}`);
|
||||
expect(failure).toBeNull();
|
||||
const second = await legacy.executePagedQuery(usersScan(), {pageSize: 7, pageState: first.pageState});
|
||||
expect(second.rows).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('ROLLING RESTART: alternating old and new replicas drain the scan', async () => {
|
||||
const a = await drain(['next', 'legacy'], 7);
|
||||
const b = await drain(['legacy', 'next'], 7);
|
||||
console.log(`new-first err=${a.error} pages=${a.pages} total=${a.keys.length} unique=${new Set(a.keys).size}`);
|
||||
console.log(`old-first err=${b.error} pages=${b.pages} total=${b.keys.length} unique=${new Set(b.keys).size}`);
|
||||
expect(a.error).toBeNull();
|
||||
expect(a.keys).toHaveLength(40);
|
||||
expect(new Set(a.keys).size).toBe(40);
|
||||
expect(b.error).toBeNull();
|
||||
expect(b.keys).toHaveLength(40);
|
||||
expect(new Set(b.keys).size).toBe(40);
|
||||
});
|
||||
|
||||
@@ -262,9 +253,9 @@ suite('postgres kv upgrade safety', () => {
|
||||
const dual = Buffer.from(JSON.stringify({offset: first.rows.length, after: cursor.after})).toString('base64url');
|
||||
const viaOld = await legacy.executePagedQuery(usersScan(), {pageSize: 7, pageState: dual});
|
||||
const viaNew = await next.executePagedQuery(usersScan(), {pageSize: 7, pageState: dual});
|
||||
console.log(`dual token -> old replica: ${viaOld.rows.length} rows, new replica: ${viaNew.rows.length} rows`);
|
||||
expect(viaOld.rows.length).toBe(7);
|
||||
expect(viaNew.rows.length).toBe(7);
|
||||
expect(viaOld.rows).toHaveLength(7);
|
||||
expect(viaNew.rows).toHaveLength(7);
|
||||
expect(fp(viaNew.rows)).toBe(fp(viaOld.rows));
|
||||
});
|
||||
|
||||
it('deletes exactly the same rows as the old image', async () => {
|
||||
@@ -282,8 +273,7 @@ suite('postgres kv upgrade safety', () => {
|
||||
const left = await raw.query<{row_key: string}>(
|
||||
`SELECT row_key FROM ${KV} WHERE table_name = 'read_states' AND row_key LIKE '%400%' ORDER BY row_key`,
|
||||
);
|
||||
console.log(`rows left after legacy delete + new delete: ${left.rows.length}`);
|
||||
expect(left.rows.length).toBe(0);
|
||||
expect(left.rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('writes byte-identical row_key and partition_key to the old image', async () => {
|
||||
@@ -333,8 +323,6 @@ suite('postgres kv upgrade safety', () => {
|
||||
const cIndexes = await raw.query<{tablename: string}>(
|
||||
`SELECT tablename FROM pg_indexes WHERE indexname IN ('${OLD}_row_key_c_idx', '${NEW}_row_key_c_idx')`,
|
||||
);
|
||||
console.log(`key diffs=${diff.rows[0]!.n} index-definition diffs=${schemaDiff.rows[0]!.n}`);
|
||||
console.log(collations.rows.map((r) => `${r.tablename}.${r.attname}=${r.collname}`).join(' '));
|
||||
expect(Number(diff.rows[0]!.n)).toBe(0);
|
||||
expect(Number(schemaDiff.rows[0]!.n)).toBe(0);
|
||||
expect(collations.rows.filter((r) => r.tablename === OLD).map((r) => r.collname)).toEqual(['default', 'default']);
|
||||
@@ -358,7 +346,6 @@ suite('postgres kv upgrade safety', () => {
|
||||
const indexes = await raw.query<{indexname: string}>(
|
||||
`SELECT indexname FROM pg_indexes WHERE tablename = '${OLD}' ORDER BY indexname`,
|
||||
);
|
||||
console.log(`after boot on a legacy table: ${indexes.rows.map((r) => r.indexname).join(', ')}`);
|
||||
expect(collations.rows.map((r) => r.collname)).toEqual(['default', 'default']);
|
||||
expect(indexes.rows.map((r) => r.indexname)).toContain(`${OLD}_row_key_c_idx`);
|
||||
});
|
||||
@@ -371,10 +358,11 @@ suite('postgres kv upgrade safety', () => {
|
||||
ensurePostgresKvSchema(new TableClient(raw, BOOT)),
|
||||
ensurePostgresKvSchema(new TableClient(raw, BOOT)),
|
||||
]);
|
||||
const failures = results.filter((r) => r.status === 'rejected');
|
||||
console.log(`concurrent boots rejected: ${failures.length}`);
|
||||
for (const f of failures) console.log(` ${(f as PromiseRejectedResult).reason}`);
|
||||
expect(failures.length).toBe(0);
|
||||
expect(results).toEqual([
|
||||
{status: 'fulfilled', value: undefined},
|
||||
{status: 'fulfilled', value: undefined},
|
||||
{status: 'fulfilled', value: undefined},
|
||||
]);
|
||||
}, 120_000);
|
||||
|
||||
it('backfills the messages partition key once and never scans for it again', async () => {
|
||||
@@ -416,7 +404,6 @@ suite('postgres kv upgrade safety', () => {
|
||||
await insertLegacy('m2');
|
||||
await ensurePostgresKvSchema(backfillClient);
|
||||
const skipped = await partitionOf('m2');
|
||||
console.log(`second boot left the new legacy row alone: partition_key=${skipped === legacyKey('m2')}`);
|
||||
expect(skipped).toBe(legacyKey('m2'));
|
||||
expect(await partitionOf('m1')).toBe(`"c"${SEP}"b"`);
|
||||
await raw.query(`DROP TABLE IF EXISTS ${BACKFILL}`);
|
||||
@@ -432,7 +419,6 @@ suite('postgres kv upgrade safety', () => {
|
||||
);
|
||||
const after = await next.executeQuery(q);
|
||||
await raw.query(`UPDATE pg_index SET indisvalid = true WHERE indexrelid = '${KV}_row_key_c_idx'::regclass`);
|
||||
console.log(`index valid after boot: ${check.rows[0]?.indisvalid}`);
|
||||
expect(String(after.map((r) => Object.values(r).map(String).join(',')))).toBe(
|
||||
String(before.map((r) => Object.values(r).map(String).join(','))),
|
||||
);
|
||||
@@ -441,7 +427,6 @@ suite('postgres kv upgrade safety', () => {
|
||||
|
||||
describe('multi-range plan shape on a populated upgraded database', () => {
|
||||
const PERF = 'kv_ranges_perf';
|
||||
const SEP = String.fromCharCode(31);
|
||||
const BASE = 1456074443984486400n;
|
||||
let perf: IPostgresClient;
|
||||
|
||||
@@ -481,6 +466,7 @@ FROM generate_series($1::bigint, $1::bigint + 19999) u, generate_series(1, 3) s`
|
||||
`SELECT kv.row_key, kv.row_data FROM ${PERF} kv WHERE kv.table_name = $1 AND (kv.expires_at IS NULL OR kv.expires_at > now())`,
|
||||
['push_subscriptions'],
|
||||
);
|
||||
expect(Number.isFinite(scan.ms)).toBe(true);
|
||||
const deltas: Array<string> = [];
|
||||
for (const size of [5, 10, 25, 50, 100]) {
|
||||
const params = {user_ids: Array.from({length: size}, (_, i) => BASE + BigInt(i * 3))};
|
||||
@@ -494,9 +480,10 @@ FROM generate_series($1::bigint, $1::bigint + 19999) u, generate_series(1, 3) s`
|
||||
`SELECT kv.row_key, kv.row_data FROM ${PERF} kv WHERE kv.table_name = $1${fragments.predicate} AND (kv.expires_at IS NULL OR kv.expires_at > now())`,
|
||||
['push_subscriptions', ...fragments.params],
|
||||
);
|
||||
const verdict = `n=${size} pushdown=${pushed.ms.toFixed(2)}ms scan=${scan.ms.toFixed(2)}ms`;
|
||||
console.log(verdict);
|
||||
if (pushed.ms > scan.ms) deltas.push(`${verdict}\n${pushed.plan}`);
|
||||
expect(Number.isFinite(pushed.ms)).toBe(true);
|
||||
if (pushed.ms > scan.ms) {
|
||||
deltas.push(`n=${size} pushdown=${pushed.ms.toFixed(2)}ms scan=${scan.ms.toFixed(2)}ms\n${pushed.plan}`);
|
||||
}
|
||||
}
|
||||
expect(deltas.join('\n')).toBe('');
|
||||
const overCap = buildCandidatePlan(meta, {
|
||||
@@ -506,17 +493,13 @@ FROM generate_series($1::bigint, $1::bigint + 19999) u, generate_series(1, 3) s`
|
||||
expect(overCap.exact).toBe(true);
|
||||
const overCapGroups = planFragmentGroups(overCap.candidates);
|
||||
expect(overCapGroups.map((group) => group.params.length)).toEqual([512, 512, 376]);
|
||||
let chunkedMs = 0;
|
||||
for (const fragments of overCapGroups) {
|
||||
const pushed = await explain(
|
||||
`SELECT kv.row_key, kv.row_data FROM ${PERF} kv WHERE kv.table_name = $1${fragments.predicate} AND (kv.expires_at IS NULL OR kv.expires_at > now())`,
|
||||
['push_subscriptions', ...fragments.params],
|
||||
);
|
||||
expect(pushed.plan).not.toContain('Seq Scan');
|
||||
chunkedMs += pushed.ms;
|
||||
}
|
||||
console.log(`n=700 chunked=${chunkedMs.toFixed(2)}ms scan=${scan.ms.toFixed(2)}ms`);
|
||||
expect(SEP).toBe('\u001f');
|
||||
}, 300_000);
|
||||
});
|
||||
});
|
||||
@@ -547,8 +530,11 @@ describe('candidate plan shapes reached by real queries', () => {
|
||||
: plan.candidates.kind === 'rowKeys'
|
||||
? plan.candidates.rowKeys.length
|
||||
: 0;
|
||||
console.log(`${c.name} -> kind=${plan.candidates.kind} n=${size} exact=${plan.exact}`);
|
||||
expect({kind: plan.candidates.kind, size, exact: plan.exact}, c.name).toEqual({
|
||||
kind: 'ranges',
|
||||
size: 100,
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -859,9 +859,7 @@ export class GuildOperationsService {
|
||||
const channels = await this.channelRepository.listGuildChannels(guildId);
|
||||
await Promise.all(channels.map((channel) => this.channelRepository.deleteAllChannelMessages(channel.id)));
|
||||
await Promise.all(
|
||||
channels.map((channel) =>
|
||||
deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}}),
|
||||
),
|
||||
channels.map((channel) => deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}})),
|
||||
);
|
||||
await Promise.all(channels.map((channel) => this.channelService.attachments.purgeChannelAttachments(channel)));
|
||||
const discoveryRow = await this.discoveryRepository.findByGuildId(guildId);
|
||||
|
||||
@@ -171,7 +171,6 @@ export async function stripNonJpegImageMetadataForUpload(
|
||||
return {body: await image.gif().toBuffer(), contentType: 'image/gif'};
|
||||
case 'webp':
|
||||
return {body: await image.webp({lossless: true}).toBuffer(), contentType: 'image/webp'};
|
||||
case 'avif':
|
||||
case 'heif':
|
||||
return {body: await image.avif().toBuffer(), contentType: 'image/avif'};
|
||||
case 'tiff':
|
||||
@@ -460,7 +459,6 @@ async function stripNonJpegImageFileToFile(
|
||||
case 'webp':
|
||||
await image.webp({lossless: true}).toFile(outputPath);
|
||||
return {contentType: 'image/webp', ...dimensions};
|
||||
case 'avif':
|
||||
case 'heif':
|
||||
await image.avif().toFile(outputPath);
|
||||
return {contentType: 'image/avif', ...dimensions};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {AppErrorHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
|
||||
import {ServiceUnavailableError} from '@fluxer/errors/src/domains/core/ServiceUnavailableError';
|
||||
import {Logger} from '@fluxer/logger/src/Logger';
|
||||
import {Hono} from 'hono';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {describe, expect, test, vi} from 'vitest';
|
||||
import type {HonoEnv} from '../../types/HonoEnv';
|
||||
import {ConcurrencyLimitMiddleware} from '../ConcurrencyLimitMiddleware';
|
||||
|
||||
@@ -22,6 +24,7 @@ function createDeferred(): Deferred {
|
||||
function buildApp(maxInflightRequests: number) {
|
||||
const release = createDeferred();
|
||||
const entered = createDeferred();
|
||||
const boomError = new Error('boom');
|
||||
const routes = new Hono<HonoEnv>({strict: true});
|
||||
routes.use(ConcurrencyLimitMiddleware({maxInflightRequests}));
|
||||
routes.get('/_health', (ctx) => ctx.text('OK'));
|
||||
@@ -33,65 +36,132 @@ function buildApp(maxInflightRequests: number) {
|
||||
return ctx.text('slow');
|
||||
});
|
||||
routes.get('/boom', () => {
|
||||
throw new Error('boom');
|
||||
throw boomError;
|
||||
});
|
||||
routes.onError(AppErrorHandler);
|
||||
const app = new Hono<HonoEnv>({strict: true});
|
||||
app.route('/v1', routes);
|
||||
app.route('/', routes);
|
||||
app.onError(AppErrorHandler);
|
||||
return {app, release, entered};
|
||||
return {app, release, entered, boomError};
|
||||
}
|
||||
|
||||
describe('ConcurrencyLimitMiddleware', () => {
|
||||
test('sheds with 503 once the in-flight ceiling is reached', async () => {
|
||||
const {app, release, entered} = buildApp(1);
|
||||
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
|
||||
const inflight = app.request('/slow');
|
||||
await entered.promise;
|
||||
try {
|
||||
await entered.promise;
|
||||
|
||||
const shed = await app.request('/fast');
|
||||
const shed = await app.request('/fast');
|
||||
|
||||
expect(shed.status).toBe(503);
|
||||
expect(shed.headers.get('Retry-After')).toBe('1');
|
||||
expect(await shed.json()).toMatchObject({code: 'SERVICE_UNAVAILABLE'});
|
||||
expect(shed.status).toBe(503);
|
||||
expect(shed.headers.get('Retry-After')).toBe('1');
|
||||
expect(await shed.json()).toMatchObject({code: 'SERVICE_UNAVAILABLE'});
|
||||
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
err: expect.any(ServiceUnavailableError),
|
||||
method: 'GET',
|
||||
path: '/fast',
|
||||
requestId: undefined,
|
||||
status: 503,
|
||||
},
|
||||
'Request failed',
|
||||
);
|
||||
|
||||
release.resolve();
|
||||
expect((await inflight).status).toBe(200);
|
||||
release.resolve();
|
||||
expect((await inflight).status).toBe(200);
|
||||
} finally {
|
||||
release.resolve();
|
||||
errorLoggerSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('admits probe paths while shedding everything else', async () => {
|
||||
const {app, release, entered} = buildApp(1);
|
||||
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
|
||||
const inflight = app.request('/slow');
|
||||
await entered.promise;
|
||||
try {
|
||||
await entered.promise;
|
||||
|
||||
expect((await app.request('/_health')).status).toBe(200);
|
||||
expect((await app.request('/_metrics')).status).toBe(200);
|
||||
expect((await app.request('/v1/_health')).status).toBe(200);
|
||||
expect((await app.request('/fast')).status).toBe(503);
|
||||
expect((await app.request('/_health')).status).toBe(200);
|
||||
expect((await app.request('/_metrics')).status).toBe(200);
|
||||
expect((await app.request('/v1/_health')).status).toBe(200);
|
||||
expect((await app.request('/fast')).status).toBe(503);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
err: expect.any(ServiceUnavailableError),
|
||||
method: 'GET',
|
||||
path: '/fast',
|
||||
requestId: undefined,
|
||||
status: 503,
|
||||
},
|
||||
'Request failed',
|
||||
);
|
||||
|
||||
release.resolve();
|
||||
await inflight;
|
||||
release.resolve();
|
||||
await inflight;
|
||||
} finally {
|
||||
release.resolve();
|
||||
errorLoggerSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('releases the slot when the handler throws', async () => {
|
||||
const {app} = buildApp(1);
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
expect((await app.request('/boom')).status).toBe(500);
|
||||
const {app, boomError} = buildApp(1);
|
||||
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
|
||||
try {
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
expect((await app.request('/boom')).status).toBe(500);
|
||||
}
|
||||
expect((await app.request('/fast')).status).toBe(200);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledTimes(3);
|
||||
for (let callIndex = 1; callIndex <= 3; callIndex += 1) {
|
||||
expect(errorLoggerSpy).toHaveBeenNthCalledWith(
|
||||
callIndex,
|
||||
{err: boomError, method: 'GET', path: '/boom', requestId: undefined, status: 500},
|
||||
'Unhandled error occurred',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
errorLoggerSpy.mockRestore();
|
||||
}
|
||||
expect((await app.request('/fast')).status).toBe(200);
|
||||
});
|
||||
|
||||
test('releases the slot taken by a shed request', async () => {
|
||||
const {app, release, entered} = buildApp(1);
|
||||
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
|
||||
const inflight = app.request('/slow');
|
||||
await entered.promise;
|
||||
try {
|
||||
await entered.promise;
|
||||
|
||||
expect((await app.request('/fast')).status).toBe(503);
|
||||
expect((await app.request('/fast')).status).toBe(503);
|
||||
expect((await app.request('/fast')).status).toBe(503);
|
||||
expect((await app.request('/fast')).status).toBe(503);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledTimes(2);
|
||||
for (let callIndex = 1; callIndex <= 2; callIndex += 1) {
|
||||
expect(errorLoggerSpy).toHaveBeenNthCalledWith(
|
||||
callIndex,
|
||||
{
|
||||
err: expect.any(ServiceUnavailableError),
|
||||
method: 'GET',
|
||||
path: '/fast',
|
||||
requestId: undefined,
|
||||
status: 503,
|
||||
},
|
||||
'Request failed',
|
||||
);
|
||||
}
|
||||
|
||||
release.resolve();
|
||||
await inflight;
|
||||
release.resolve();
|
||||
await inflight;
|
||||
|
||||
expect((await app.request('/fast')).status).toBe(200);
|
||||
expect((await app.request('/fast')).status).toBe(200);
|
||||
} finally {
|
||||
release.resolve();
|
||||
errorLoggerSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Logger} from '@fluxer/logger/src/Logger';
|
||||
import {SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {Hono} from 'hono';
|
||||
import {afterAll, beforeEach, describe, expect, test} from 'vitest';
|
||||
import {afterAll, beforeEach, describe, expect, test, vi} from 'vitest';
|
||||
import {z} from 'zod';
|
||||
import {Config} from '../../Config';
|
||||
import type {HonoEnv} from '../../types/HonoEnv';
|
||||
@@ -52,10 +53,26 @@ describe('ResponseTypeMiddleware', () => {
|
||||
test('rejects mismatching responses while validation is enabled', async () => {
|
||||
const app = new Hono<HonoEnv>();
|
||||
app.get('/snowflake', ResponseType(SnowflakeResponse), (ctx) => ctx.json({id: 'not-a-snowflake'}));
|
||||
const errorLoggerSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});
|
||||
|
||||
const response = await app.request('/snowflake');
|
||||
try {
|
||||
const response = await app.request('/snowflake');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.status).toBe(500);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorLoggerSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
body: {id: 'not-a-snowflake'},
|
||||
method: 'GET',
|
||||
path: '/snowflake',
|
||||
status: 200,
|
||||
validationErrors: [{message: 'INVALID_SNOWFLAKE_FORMAT', path: 'id'}],
|
||||
},
|
||||
'Response validation failed',
|
||||
);
|
||||
} finally {
|
||||
errorLoggerSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('passes the response through untouched while validation is disabled', async () => {
|
||||
|
||||
@@ -9,8 +9,7 @@ export function channelRequiresAgeVerification(
|
||||
channelsById: ReadonlyMap<string, Channel>,
|
||||
guildNsfw: boolean,
|
||||
): boolean {
|
||||
const parentCategory =
|
||||
channel.parentId != null ? (channelsById.get(channel.parentId.toString()) ?? null) : null;
|
||||
const parentCategory = channel.parentId != null ? (channelsById.get(channel.parentId.toString()) ?? null) : null;
|
||||
return computeEffectiveChannelNsfw(
|
||||
channelToContentWarningView(channel),
|
||||
parentCategory ? channelToContentWarningView(parentCategory) : null,
|
||||
|
||||
@@ -49,6 +49,20 @@ describe('Stripe Webhook Refund', () => {
|
||||
.body(payload)
|
||||
.execute();
|
||||
}
|
||||
function useRefundListHandler(chargeId: string): void {
|
||||
server.use(
|
||||
http.get(
|
||||
({request}) => request.url === `https://api.stripe.com/v1/refunds?charge=${chargeId}&limit=100`,
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
object: 'list',
|
||||
data: [],
|
||||
has_more: false,
|
||||
url: '/v1/refunds',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
describe('charge.refunded', () => {
|
||||
test('revokes premium and records first refund', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
@@ -73,6 +87,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
payment_intent_id: paymentIntentId,
|
||||
completed_at: new Date(),
|
||||
});
|
||||
useRefundListHandler('ch_test_refund_123');
|
||||
await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
@@ -121,6 +136,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
payment_intent_id: paymentIntentId,
|
||||
completed_at: new Date(),
|
||||
});
|
||||
useRefundListHandler('ch_test_refund_456');
|
||||
await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
@@ -150,6 +166,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
{stripe_customer_id: stripeCustomerId},
|
||||
(await userRepository.findUnique(userId))!.toRow(),
|
||||
);
|
||||
useRefundListHandler('ch_test_refund_sub_789');
|
||||
await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
@@ -182,6 +199,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
subscriptionCurrentPeriodEnd: null,
|
||||
subscriptionCancelAt: null,
|
||||
});
|
||||
useRefundListHandler('ch_test_refund_donation_101');
|
||||
const result = await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
@@ -234,6 +252,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
premium_until: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
})
|
||||
.execute();
|
||||
useRefundListHandler('ch_test_refund_gift_multi_123');
|
||||
await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
@@ -287,6 +306,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
.get('/users/@me')
|
||||
.execute();
|
||||
expect(redeemerBefore.premium_type).toBe(UserPremiumTypes.SUBSCRIPTION);
|
||||
useRefundListHandler('ch_test_refund_stripe_cus_only');
|
||||
await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
@@ -325,6 +345,7 @@ describe('Stripe Webhook Refund', () => {
|
||||
premium_type: UserPremiumTypes.LIFETIME,
|
||||
})
|
||||
.execute();
|
||||
useRefundListHandler('ch_test_refund_gift_override_123');
|
||||
await sendWebhook({
|
||||
type: 'charge.refunded',
|
||||
data: {
|
||||
|
||||
@@ -119,12 +119,8 @@ export class UserRelationshipService {
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Relationship> {
|
||||
const requesterUser = await this.userRepository.findUnique(userId);
|
||||
const requesterIsStaff =
|
||||
requesterUser != null && (requesterUser.flags & UserFlags.STAFF) === UserFlags.STAFF;
|
||||
if (
|
||||
!requesterIsStaff &&
|
||||
(await getInstanceConfigRepository().getInstancePolicyConfig()).direct_messages_disabled
|
||||
) {
|
||||
const requesterIsStaff = requesterUser != null && (requesterUser.flags & UserFlags.STAFF) === UserFlags.STAFF;
|
||||
if (!requesterIsStaff && (await getInstanceConfigRepository().getInstancePolicyConfig()).direct_messages_disabled) {
|
||||
throw new DirectMessagesDisabledError();
|
||||
}
|
||||
if (staffForceAccept && requesterIsStaff) {
|
||||
|
||||
Reference in new issue
Block a user