Files
fluxer/fluxer_api/src/api/user/UserHelpers.test.ts
T

291 lines
13 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {Config} from '@app/api/Config';
import type {User} from '@app/api/models/User';
import {setInjectedAccountPolicyEvaluator} from '@app/api/risk/AccountPolicyService';
import {setCachedDeferredPhoneGateEnabled} from '@app/api/risk/DeferredPhoneGateCache';
import {
createCurrentBehaviorTestAccountPolicyEvaluator,
TEST_POLICY_CONTACT_DOMAIN,
TEST_POLICY_CONTACT_SUBDOMAIN,
} from '@app/api/test/AccountPolicyTestEvaluator';
import {
checkIsPremium,
getEffectivePremiumUntil,
getEffectiveSuspiciousFlags,
getRequiredActions,
} from '@app/api/user/UserHelpers';
import {
DEFERRED_PHONE_ON_COMMUNITY_JOIN,
imposePhoneRequirements,
PHONE_GATE_PROMOTED_FROM_DEFERRAL,
PremiumFlags,
SuspiciousActivityFlags,
UserPremiumTypes,
} from '@fluxer/constants/src/UserConstants';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
function createUser(
overrides: Partial<Pick<User, 'email' | 'emailVerified' | 'hasVerifiedPhone' | 'suspiciousActivityFlags'>> = {},
): User {
return {
email: '[email protected]',
emailVerified: false,
hasVerifiedPhone: false,
suspiciousActivityFlags: 0,
...overrides,
} as User;
}
describe('deferred phone gate marker', () => {
beforeEach(() => {
setInjectedAccountPolicyEvaluator(createCurrentBehaviorTestAccountPolicyEvaluator());
setCachedDeferredPhoneGateEnabled(true);
});
afterEach(() => {
setInjectedAccountPolicyEvaluator(undefined);
setCachedDeferredPhoneGateEnabled(false);
});
it('does not suppress anything until a policy read has proven the gate is on', () => {
setCachedDeferredPhoneGateEnabled(false);
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_PHONE']);
});
it('keeps a deferral suppressed when the gate reads off but phone flagging is disabled', () => {
setCachedDeferredPhoneGateEnabled(false);
const original = {...Config.abusePolicy.phoneFlagging};
Config.abusePolicy.phoneFlagging = {enabled: false, exemptCountryCodes: []};
try {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
} finally {
Config.abusePolicy.phoneFlagging = original;
}
});
it('suppresses a deferred phone requirement so the account is not locked out', () => {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
});
it('never lets an inbound-SMS requirement be suppressed, since that tier is never deferred', () => {
const user = createUser({
suspiciousActivityFlags:
SuspiciousActivityFlags.REQUIRE_INBOUND_PHONE_VERIFICATION | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_PHONE', 'REQUIRE_INBOUND_PHONE_VERIFICATION']);
});
it('keeps non-phone requirements active while a phone requirement is deferred', () => {
const user = createUser({
suspiciousActivityFlags:
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL |
SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE |
DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_EMAIL']);
expect(getEffectiveSuspiciousFlags(user)).toBe(SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL);
});
it('applies the phone requirement in full once the marker is cleared', () => {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_PHONE']);
expect(getEffectiveSuspiciousFlags(user)).toBe(SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE);
});
it('leaves an account carrying only the marker completely unrestricted', () => {
const user = createUser({suspiciousActivityFlags: DEFERRED_PHONE_ON_COMMUNITY_JOIN});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
});
it('re-arms stored phone requirements as soon as the gate is switched off', () => {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN,
});
expect(getRequiredActions(user)).toEqual([]);
setCachedDeferredPhoneGateEnabled(false);
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_PHONE']);
});
it('stops suppressing once another subsystem imposes the phone requirement directly', () => {
const deferred = SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN;
const imposed = imposePhoneRequirements(deferred, SuspiciousActivityFlags.REQUIRE_REVERIFIED_PHONE);
expect(imposed & DEFERRED_PHONE_ON_COMMUNITY_JOIN).toBe(0);
const user = createUser({suspiciousActivityFlags: imposed});
expect(getRequiredActions(user)).toEqual(['REQUIRE_REVERIFIED_PHONE']);
});
it('keeps the marker when a non-phone requirement is imposed', () => {
const deferred = SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | DEFERRED_PHONE_ON_COMMUNITY_JOIN;
const imposed = imposePhoneRequirements(deferred, SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL);
expect(imposed & DEFERRED_PHONE_ON_COMMUNITY_JOIN).not.toBe(0);
expect(getRequiredActions(createUser({suspiciousActivityFlags: imposed}))).toEqual(['REQUIRE_VERIFIED_EMAIL']);
});
it('revokes escape eligibility once another subsystem imposes the phone requirement directly', () => {
const promoted = SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | PHONE_GATE_PROMOTED_FROM_DEFERRAL;
const imposed = imposePhoneRequirements(promoted, SuspiciousActivityFlags.REQUIRE_REVERIFIED_PHONE);
expect(imposed & PHONE_GATE_PROMOTED_FROM_DEFERRAL).toBe(0);
expect(imposed & DEFERRED_PHONE_ON_COMMUNITY_JOIN).toBe(0);
expect(getRequiredActions(createUser({suspiciousActivityFlags: imposed}))).toEqual(['REQUIRE_REVERIFIED_PHONE']);
});
it('keeps escape eligibility when a non-phone requirement is imposed', () => {
const promoted = SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | PHONE_GATE_PROMOTED_FROM_DEFERRAL;
const imposed = imposePhoneRequirements(promoted, SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL);
expect(imposed & PHONE_GATE_PROMOTED_FROM_DEFERRAL).not.toBe(0);
expect(getRequiredActions(createUser({suspiciousActivityFlags: imposed}))).toEqual([
'REQUIRE_VERIFIED_EMAIL',
'REQUIRE_VERIFIED_PHONE',
]);
});
it('keeps both bookkeeping bits when a non-phone requirement is imposed on a still-deferred account', () => {
const deferred =
SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE |
DEFERRED_PHONE_ON_COMMUNITY_JOIN |
PHONE_GATE_PROMOTED_FROM_DEFERRAL;
const imposed = imposePhoneRequirements(deferred, SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL);
expect(imposed & DEFERRED_PHONE_ON_COMMUNITY_JOIN).not.toBe(0);
expect(imposed & PHONE_GATE_PROMOTED_FROM_DEFERRAL).not.toBe(0);
});
it('never turns the promotion bit into a requirement or an enforceable flag of its own', () => {
const promotedOnly = createUser({suspiciousActivityFlags: PHONE_GATE_PROMOTED_FROM_DEFERRAL});
expect(getRequiredActions(promotedOnly)).toEqual([]);
expect(getEffectiveSuspiciousFlags(promotedOnly)).toBe(0);
const promoted = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE | PHONE_GATE_PROMOTED_FROM_DEFERRAL,
});
expect(getRequiredActions(promoted)).toEqual(['REQUIRE_VERIFIED_PHONE']);
expect(getEffectiveSuspiciousFlags(promoted)).toBe(SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE);
});
it('yields no enforceable requirement for an account without an email, so the gate must not promote it', () => {
const user = createUser({
email: null,
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE,
});
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
});
});
describe('getRequiredActions', () => {
beforeEach(() => {
setInjectedAccountPolicyEvaluator(createCurrentBehaviorTestAccountPolicyEvaluator());
});
afterEach(() => {
setInjectedAccountPolicyEvaluator(undefined);
});
it('keeps verified-email requirements active when the account email is unverified', () => {
const user = createUser({
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_VERIFIED_EMAIL']);
expect(getEffectiveSuspiciousFlags(user)).toBe(SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL);
});
it('masks verified-email requirements when the account has a verified email', () => {
const user = createUser({
emailVerified: true,
suspiciousActivityFlags:
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL |
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE,
});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
});
it('drops weaker redundant clauses while preserving the stronger canonical action', () => {
const user = createUser({
suspiciousActivityFlags:
SuspiciousActivityFlags.REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE |
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE']);
expect(getEffectiveSuspiciousFlags(user)).toBe(SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE);
});
it('retains incomparable combinations so the client can complete them sequentially', () => {
const user = createUser({
suspiciousActivityFlags:
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL | SuspiciousActivityFlags.REQUIRE_REVERIFIED_PHONE,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_REVERIFIED_EMAIL', 'REQUIRE_REVERIFIED_PHONE']);
expect(getEffectiveSuspiciousFlags(user)).toBe(
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL | SuspiciousActivityFlags.REQUIRE_REVERIFIED_PHONE,
);
});
it('masks verified-phone requirements after the stored phone has been removed', () => {
const user = createUser({
hasVerifiedPhone: true,
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_VERIFIED_PHONE,
});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
});
it('masks all suspicious activity requirements for policy-exempt contact domains', () => {
const user = createUser({
email: `builder@${TEST_POLICY_CONTACT_DOMAIN}`,
suspiciousActivityFlags:
SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL |
SuspiciousActivityFlags.REQUIRE_REVERIFIED_PHONE |
SuspiciousActivityFlags.REQUIRE_INBOUND_PHONE_VERIFICATION,
});
expect(getRequiredActions(user)).toEqual([]);
expect(getEffectiveSuspiciousFlags(user)).toBe(0);
});
it('does not mask suspicious activity requirements for non-matching subdomains', () => {
const user = createUser({
email: `builder@${TEST_POLICY_CONTACT_SUBDOMAIN}`,
suspiciousActivityFlags: SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL,
});
expect(getRequiredActions(user)).toEqual(['REQUIRE_REVERIFIED_EMAIL']);
expect(getEffectiveSuspiciousFlags(user)).toBe(SuspiciousActivityFlags.REQUIRE_REVERIFIED_EMAIL);
});
});
describe('checkIsPremium', () => {
it('uses the later gift extension as the effective premium end', () => {
const premiumUntil = new Date(Date.now() + 60_000);
const premiumGiftExtensionEndsAt = new Date(Date.now() + 120_000);
expect(getEffectivePremiumUntil({premiumUntil, premiumGiftExtensionEndsAt})?.toISOString()).toBe(
premiumGiftExtensionEndsAt.toISOString(),
);
});
it('treats a future gift extension as active after the subscription period ended', () => {
const user = {
isBot: false,
premiumType: UserPremiumTypes.SUBSCRIPTION,
premiumUntil: new Date(Date.now() - 60_000),
premiumGiftExtensionEndsAt: new Date(Date.now() + 60_000),
premiumGraceEndsAt: null,
premiumWillCancel: false,
flags: 0n,
premiumFlags: 0,
};
expect(checkIsPremium(user)).toBe(true);
});
it('lets the perks-disabled flag override an active paid subscription', () => {
const user = {
isBot: false,
premiumType: UserPremiumTypes.SUBSCRIPTION,
premiumUntil: new Date(Date.now() + 60_000),
premiumGiftExtensionEndsAt: null,
premiumGraceEndsAt: null,
premiumWillCancel: false,
flags: 0n,
premiumFlags: PremiumFlags.PERKS_DISABLED,
};
expect(checkIsPremium(user)).toBe(false);
});
it('lets the perks-disabled flag override backend premium override', () => {
const user = {
isBot: false,
premiumType: UserPremiumTypes.NONE,
premiumUntil: null,
premiumGiftExtensionEndsAt: null,
premiumGraceEndsAt: null,
premiumWillCancel: false,
flags: 0n,
premiumFlags: PremiumFlags.ENABLED_OVERRIDE | PremiumFlags.PERKS_DISABLED,
};
expect(checkIsPremium(user)).toBe(false);
});
});