// SPDX-License-Identifier: AGPL-3.0-or-later import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createFriendship} from '@app/api/channel/tests/ChannelTestUtils'; import {acceptInvite, createChannelInvite, createGuild, getChannel} from '@app/api/guild/tests/GuildTestUtils'; import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import { ProfileFieldPrivacyFlags, type ProfilePrivacyLevel, ProfilePrivacyLevels, } from '@fluxer/constants/src/UserConstants'; import {getCurrentTimeZoneOffsetMinutes} from '@fluxer/date_utils/src/TimeZoneUtils'; import {DEFAULT_PROFILE_TIMEZONE_CONFIG} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas'; import type {UserPrivateResponse, UserProfileFullResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; const TEST_TIMEZONE = 'America/New_York'; const TEST_TIMEZONE_OFFSET = getCurrentTimeZoneOffsetMinutes(TEST_TIMEZONE); if (TEST_TIMEZONE_OFFSET == null) { throw new Error(`Expected ${TEST_TIMEZONE} to have a current UTC offset`); } async function updateProfileTimezone( harness: ApiTestHarness, token: string, data: {timezone?: string | null; timezone_privacy_flags?: number}, ): Promise { return createBuilder(harness, token).patch('/users/@me').body(data).execute(); } async function setProfileTimezoneUsers(userIds: Array): Promise { await getInstanceConfigRepository().setProfileTimezoneConfig({ ...DEFAULT_PROFILE_TIMEZONE_CONFIG, enabled: true, included_user_ids: userIds, }); } async function updateProfilePrivacy( harness: ApiTestHarness, token: string, profilePrivacy: ProfilePrivacyLevel, ): Promise { await createBuilder(harness, token).patch('/users/@me/settings').body({profile_privacy: profilePrivacy}).execute(); } async function getUserProfile( harness: ApiTestHarness, token: string, userId: string, ): Promise { return createBuilder(harness, token).get(`/users/${userId}/profile`).execute(); } async function createSharedGuild(harness: ApiTestHarness, ownerToken: string, memberToken: string): Promise { const guild = await createGuild(harness, ownerToken, 'Timezone Test Guild'); const systemChannel = await getChannel(harness, ownerToken, guild.system_channel_id!); const invite = await createChannelInvite(harness, ownerToken, systemChannel.id); await acceptInvite(harness, memberToken, invite.code); } describe('User Profile Timezone Visibility', () => { let harness: ApiTestHarness; beforeAll(async () => { harness = await createApiTestHarness(); }); beforeEach(async () => { await harness.reset(); }); afterAll(async () => { await harness?.shutdown(); }); it('defaults timezone visibility to everyone when a timezone is set', async () => { const targetAccount = await createTestAccount(harness); const viewerAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE}); expect(updated.timezone).toBe(TEST_TIMEZONE); expect(updated.timezone_privacy_flags).toBe(ProfileFieldPrivacyFlags.EVERYONE); await createFriendship(harness, targetAccount, viewerAccount); const profile = await getUserProfile(harness, viewerAccount.token, targetAccount.userId); expect(profile.timezone_offset).toBe(TEST_TIMEZONE_OFFSET); }); it('restores default timezone visibility when a timezone is set again without explicit flags', async () => { const targetAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); await updateProfileTimezone(harness, targetAccount.token, { timezone: TEST_TIMEZONE, timezone_privacy_flags: 0, }); await updateProfileTimezone(harness, targetAccount.token, {timezone: null}); const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE}); expect(updated.timezone_privacy_flags).toBe(ProfileFieldPrivacyFlags.EVERYONE); }); it('hides timezone from the public profile when privacy flags are unset', async () => { const targetAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); await updateProfileTimezone(harness, targetAccount.token, { timezone: TEST_TIMEZONE, timezone_privacy_flags: 0, }); const profile = await getUserProfile(harness, targetAccount.token, targetAccount.userId); expect(profile.timezone_offset).toBeNull(); }); it('shows timezone to friends only when the friends flag is set', async () => { const targetAccount = await createTestAccount(harness); const friendAccount = await createTestAccount(harness); const guildMemberAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); await updateProfileTimezone(harness, targetAccount.token, { timezone: TEST_TIMEZONE, timezone_privacy_flags: ProfileFieldPrivacyFlags.FRIENDS, }); await createFriendship(harness, targetAccount, friendAccount); await createSharedGuild(harness, targetAccount.token, guildMemberAccount.token); const friendProfile = await getUserProfile(harness, friendAccount.token, targetAccount.userId); const guildMemberProfile = await getUserProfile(harness, guildMemberAccount.token, targetAccount.userId); expect(friendProfile.timezone_offset).toBe(TEST_TIMEZONE_OFFSET); expect(guildMemberProfile.timezone_offset).toBeNull(); }); it('shows timezone to mutual community members only when the mutual communities flag is set', async () => { const targetAccount = await createTestAccount(harness); const friendAccount = await createTestAccount(harness); const guildMemberAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); await updateProfileTimezone(harness, targetAccount.token, { timezone: TEST_TIMEZONE, timezone_privacy_flags: ProfileFieldPrivacyFlags.MUTUAL_GUILDS, }); await createFriendship(harness, targetAccount, friendAccount); await createSharedGuild(harness, targetAccount.token, guildMemberAccount.token); const friendProfile = await getUserProfile(harness, friendAccount.token, targetAccount.userId); const guildMemberProfile = await getUserProfile(harness, guildMemberAccount.token, targetAccount.userId); expect(friendProfile.timezone_offset).toBeNull(); expect(guildMemberProfile.timezone_offset).toBe(TEST_TIMEZONE_OFFSET); }); it('hides timezone when full profile privacy restricts the viewer', async () => { const targetAccount = await createTestAccount(harness); const guildMemberAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE}); await updateProfilePrivacy(harness, targetAccount.token, ProfilePrivacyLevels.FRIENDS_ONLY); await createSharedGuild(harness, targetAccount.token, guildMemberAccount.token); const profile = await getUserProfile(harness, guildMemberAccount.token, targetAccount.userId); expect(profile.profile_limited).toBe(true); expect(profile.timezone_offset).toBeNull(); }); it('ignores profile timezone updates from users outside the experiment', async () => { const targetAccount = await createTestAccount(harness, {skipEmailVerification: true}); const updated = await updateProfileTimezone(harness, targetAccount.token, { timezone: TEST_TIMEZONE, timezone_privacy_flags: ProfileFieldPrivacyFlags.FRIENDS, }); expect(updated.timezone).toBeNull(); expect(updated.timezone_privacy_flags).toBe(ProfileFieldPrivacyFlags.EVERYONE); }); it('ignores profile timezone updates from users excluded from a full rollout', async () => { const targetAccount = await createTestAccount(harness, {skipEmailVerification: true}); await getInstanceConfigRepository().setProfileTimezoneConfig({ ...DEFAULT_PROFILE_TIMEZONE_CONFIG, enabled: true, rollout_basis_points: 10000, excluded_user_ids: [targetAccount.userId], }); const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE}); expect(updated.timezone).toBeNull(); }); it('lets members of an included guild set and show a timezone', async () => { const targetAccount = await createTestAccount(harness); const viewerAccount = await createTestAccount(harness); const guild = await createGuild(harness, targetAccount.token, 'Timezone Rollout Guild'); await getInstanceConfigRepository().setProfileTimezoneConfig({ ...DEFAULT_PROFILE_TIMEZONE_CONFIG, enabled: true, included_guild_ids: [guild.id], }); const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE}); expect(updated.timezone).toBe(TEST_TIMEZONE); await createFriendship(harness, targetAccount, viewerAccount); const profile = await getUserProfile(harness, viewerAccount.token, targetAccount.userId); expect(profile.timezone_offset).toBe(TEST_TIMEZONE_OFFSET); }); it('hides stored profile timezone after the user leaves the experiment', async () => { const targetAccount = await createTestAccount(harness); const viewerAccount = await createTestAccount(harness); await setProfileTimezoneUsers([targetAccount.userId]); await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE}); await setProfileTimezoneUsers([]); await createFriendship(harness, targetAccount, viewerAccount); const profile = await getUserProfile(harness, viewerAccount.token, targetAccount.userId); expect(profile.timezone_offset).toBeNull(); }); });