fix(api): raise coded errors for prerequisites and bounds (#2502)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-06 15:29:36 +02:00
1 parent f06d65db54
commit cc110b9f5a
7 files changed
+109 -28

No files matched your search

+8 -6
View File
@@ -16,6 +16,7 @@ import {PhoneVerificationRequiredError} from '@fluxer/errors/src/domains/auth/Ph
import {SmsVerificationUnavailableError} from '@fluxer/errors/src/domains/auth/SmsVerificationUnavailableError';
import {CaptchaVerificationRequiredError} from '@fluxer/errors/src/domains/core/CaptchaVerificationRequiredError';
import {RateLimitError} from '@fluxer/errors/src/domains/core/RateLimitError';
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
import type {FluxerError} from '@fluxer/errors/src/FluxerError';
import {PHONE_E164_REGEX} from '@fluxer/schema/src/primitives/UserValidators';
import type {RateLimitResult, RateLimitScope} from '@pkgs/rate_limit/src/IRateLimitService';
@@ -74,15 +75,16 @@ function reuseStoreFor(ctx: ApiContext): PhoneVerificationReuseStore {
export async function startInboundPhoneChallenge(ctx: ApiContext, userId: UserID): Promise<IssuedChallenge> {
const {inboundSmsChallenge, users, config} = ctx.services;
if (!inboundSmsChallenge) {
throw new Error('Inbound SMS challenge flow is not configured on this instance');
}
const ourNumber = config.sms.inboundChallengeNumber;
if (!ourNumber) {
throw new Error('Config.sms.inboundChallengeNumber is required for the inbound SMS challenge flow');
if (!inboundSmsChallenge || !ourNumber) {
Logger.warn(
{userId: String(userId)},
'Inbound SMS challenge requested but FLUXER_SMS_INBOUND_CHALLENGE_NUMBER is unset',
);
throw new SmsVerificationUnavailableError();
}
const user = await users.findUnique(userId);
if (!user) throw new Error('User not found');
if (!user) throw new UnknownUserError();
assertNonBotUser(user);
return inboundSmsChallenge.issueChallenge({userId, ourNumber});
}
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
import {getConfig} from '../../Config';
import type {ApiTestHarness} from '../../test/ApiTestHarness';
import {createBuilder} from '../../test/TestRequestBuilder';
import {createAuthHarness, createTestAccount} from './AuthTestUtils';
describe('Inbound SMS challenge without a receiving number', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createAuthHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('rejects the challenge with SMS_VERIFICATION_UNAVAILABLE when no number is configured', async () => {
const account = await createTestAccount(harness);
const config = getConfig();
const previousInboundNumber = config.sms.inboundChallengeNumber;
config.sms.inboundChallengeNumber = undefined;
try {
const {response, json} = await createBuilder(harness, account.token)
.post('/users/@me/phone/inbound-challenge')
.executeRaw();
expect(response.status).toBe(400);
expect(json).toMatchObject({code: 'SMS_VERIFICATION_UNAVAILABLE'});
} finally {
config.sms.inboundChallengeNumber = previousInboundNumber;
}
});
it('issues the challenge once a receiving number is configured', async () => {
const account = await createTestAccount(harness);
const config = getConfig();
const previousInboundNumber = config.sms.inboundChallengeNumber;
config.sms.inboundChallengeNumber = '+15551234567';
try {
const response = await createBuilder<{
challenge_code: string;
our_number: string;
expires_at: string;
}>(harness, account.token)
.post('/users/@me/phone/inbound-challenge')
.expect(200)
.execute();
expect(response.our_number).toBe('+15551234567');
expect(response.challenge_code).toMatch(/^\d{6}$/);
} finally {
config.sms.inboundChallengeNumber = previousInboundNumber;
}
});
});