mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-15 23:18:39 +09:00
408 lines
20 KiB
TypeScript
408 lines
20 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils';
|
|
import {createGuild} from '@app/api/channel/tests/ChannelTestUtils';
|
|
import {getUserActivityBuffer} from '@app/api/middleware/ServiceSingletons';
|
|
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
|
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
|
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
|
import type {GuildAdminResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
|
import type {UserAdminResponse} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
|
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
|
|
|
interface UserSearchResponse {
|
|
users: Array<UserAdminResponse>;
|
|
total: number;
|
|
}
|
|
|
|
interface GuildSearchResponse {
|
|
guilds: Array<GuildAdminResponse>;
|
|
total: number;
|
|
}
|
|
|
|
async function setContactInfo(
|
|
harness: ApiTestHarness,
|
|
userId: string,
|
|
data: {
|
|
has_verified_phone?: boolean;
|
|
email?: string | null;
|
|
},
|
|
): Promise<void> {
|
|
await createBuilderWithoutAuth(harness)
|
|
.post(`/test/users/${userId}/set-contact-info`)
|
|
.body(data)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
}
|
|
|
|
async function setLastActiveIp(harness: ApiTestHarness, token: string, ip: string): Promise<void> {
|
|
await createBuilder(harness, `${token}`)
|
|
.get('/users/@me')
|
|
.header('x-forwarded-for', ip)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
await getUserActivityBuffer().drainAndFlush();
|
|
}
|
|
|
|
describe('Admin Search Field Coverage', () => {
|
|
let harness: ApiTestHarness;
|
|
beforeEach(async () => {
|
|
harness = await createApiTestHarness({search: 'enabled'});
|
|
});
|
|
afterEach(async () => {
|
|
await harness.shutdown();
|
|
});
|
|
describe('user search by email', () => {
|
|
test('searching by exact email returns only the matching user', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup', 'user:view:email']);
|
|
const uniqueEmail = `precise-email-${Date.now()}@searchtest.example`;
|
|
const targetUser = await createTestAccount(harness, {email: uniqueEmail});
|
|
await createTestAccount(harness, {email: `other-user-${Date.now()}@different.example`});
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(uniqueEmail)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const found = result.users.find((u) => u.id === targetUser.userId);
|
|
expect(found).toBeDefined();
|
|
expect(found!.email).toBe(uniqueEmail);
|
|
});
|
|
test('searching by email does not return users with different emails', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const emailA = `alpha-${Date.now()}@emailsearch.example`;
|
|
const emailB = `bravo-${Date.now()}@emailsearch.example`;
|
|
const userA = await createTestAccount(harness, {email: emailA});
|
|
const userB = await createTestAccount(harness, {email: emailB});
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(emailA)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const foundA = result.users.find((u) => u.id === userA.userId);
|
|
const foundB = result.users.find((u) => u.id === userB.userId);
|
|
expect(foundA).toBeDefined();
|
|
expect(foundB).toBeUndefined();
|
|
});
|
|
test('searching by partial email domain returns matching users', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const domain = `partialdomain${Date.now()}.example`;
|
|
const user = await createTestAccount(harness, {email: `user@${domain}`});
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(domain)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const found = result.users.find((u) => u.id === user.userId);
|
|
expect(found).toBeDefined();
|
|
});
|
|
});
|
|
describe('user search by username', () => {
|
|
test('searching by exact username returns only the matching user', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const uniqueUsername = `xuniq_${Date.now()}`;
|
|
const targetUser = await createTestAccount(harness, {username: uniqueUsername});
|
|
await createTestAccount(harness, {username: `yother_${Date.now()}`});
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(uniqueUsername)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const found = result.users.find((u) => u.id === targetUser.userId);
|
|
expect(found).toBeDefined();
|
|
expect(found!.username).toBe(uniqueUsername);
|
|
});
|
|
test('searching by username does not return users with completely different usernames', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const ts = Date.now();
|
|
const usernameA = `zephyrfox_${ts}`;
|
|
const usernameB = `quasarmoon_${ts}`;
|
|
const userA = await createTestAccount(harness, {username: usernameA});
|
|
const userB = await createTestAccount(harness, {username: usernameB});
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(usernameA)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const foundA = result.users.find((u) => u.id === userA.userId);
|
|
const foundB = result.users.find((u) => u.id === userB.userId);
|
|
expect(foundA).toBeDefined();
|
|
expect(foundB).toBeUndefined();
|
|
});
|
|
});
|
|
describe('user search by user ID', () => {
|
|
test('searching by exact user ID returns only the matching user', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const targetUser = await createTestAccount(harness);
|
|
await createTestAccount(harness);
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(targetUser.userId)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const found = result.users.find((u) => u.id === targetUser.userId);
|
|
expect(found).toBeDefined();
|
|
expect(found!.id).toBe(targetUser.userId);
|
|
});
|
|
test('user ID search returns the user even when the search index has no match via direct DB lookup', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const targetUser = await createTestAccount(harness);
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(targetUser.userId)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const found = result.users.find((u) => u.id === targetUser.userId);
|
|
expect(found).toBeDefined();
|
|
});
|
|
test('user ID search does not duplicate user when found by both search index and direct lookup', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const targetUser = await createTestAccount(harness);
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(targetUser.userId)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const matchingUsers = result.users.filter((u) => u.id === targetUser.userId);
|
|
expect(matchingUsers).toHaveLength(1);
|
|
});
|
|
});
|
|
describe('user search by last active IP', () => {
|
|
test('searching by last active IP returns users that share that IP', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup', 'user:view:ip']);
|
|
const matchingIp = '198.51.100.24';
|
|
const nonMatchingIp = '203.0.113.24';
|
|
const targetUser = await createTestAccount(harness);
|
|
const otherUser = await createTestAccount(harness);
|
|
await setLastActiveIp(harness, targetUser.token, matchingIp);
|
|
await setLastActiveIp(harness, otherUser.token, nonMatchingIp);
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?last_active_ip=${encodeURIComponent(matchingIp)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
expect(result.users.find((u) => u.id === targetUser.userId)).toBeDefined();
|
|
expect(result.users.find((u) => u.id === otherUser.userId)).toBeUndefined();
|
|
});
|
|
});
|
|
describe('user search response fields', () => {
|
|
test('user search response includes all expected admin fields', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup', 'user:view:email']);
|
|
const email = `fields-check-${Date.now()}@fieldtest.example`;
|
|
const username = `fieldcheck_${Date.now()}`;
|
|
const targetUser = await createTestAccount(harness, {email, username});
|
|
await setContactInfo(harness, targetUser.userId, {has_verified_phone: true});
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(email)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const found = result.users.find((u) => u.id === targetUser.userId);
|
|
expect(found).toBeDefined();
|
|
expect(found!.id).toBe(targetUser.userId);
|
|
expect(found!.username).toBe(username);
|
|
expect(found!.email).toBe(email);
|
|
expect(found!.has_verified_phone).toBe(true);
|
|
expect(found!).toHaveProperty('discriminator');
|
|
expect(found!).toHaveProperty('global_name');
|
|
expect(found!).toHaveProperty('flags');
|
|
expect(found!).toHaveProperty('email_verified');
|
|
expect(found!).toHaveProperty('email_bounced');
|
|
expect(found!).toHaveProperty('premium_type');
|
|
expect(found!).toHaveProperty('acls');
|
|
expect(found!).toHaveProperty('suspicious_activity_flags');
|
|
});
|
|
});
|
|
describe('user search isolation across fields', () => {
|
|
test('each searchable field returns only the correct user', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const ts = Date.now();
|
|
const emailA = `vortexfind-${ts}@alphadomain.example`;
|
|
const emailB = `nebulaseek-${ts}@betadomain.example`;
|
|
const usernameA = `vortexfox_${ts}`;
|
|
const usernameB = `nebulawolf_${ts}`;
|
|
const userA = await createTestAccount(harness, {email: emailA, username: usernameA});
|
|
const userB = await createTestAccount(harness, {email: emailB, username: usernameB});
|
|
const searchByEmailA = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(emailA)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(searchByEmailA.users.find((u) => u.id === userA.userId)).toBeDefined();
|
|
expect(searchByEmailA.users.find((u) => u.id === userB.userId)).toBeUndefined();
|
|
const searchByUsernameB = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(usernameB)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(searchByUsernameB.users.find((u) => u.id === userB.userId)).toBeDefined();
|
|
expect(searchByUsernameB.users.find((u) => u.id === userA.userId)).toBeUndefined();
|
|
const searchByIdB = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent(userB.userId)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(searchByIdB.users.find((u) => u.id === userB.userId)).toBeDefined();
|
|
expect(searchByIdB.users.find((u) => u.id === userA.userId)).toBeUndefined();
|
|
});
|
|
});
|
|
describe('guild search by name', () => {
|
|
test('searching by exact guild name returns only the matching guild', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const ts = Date.now();
|
|
const nameA = `Zephyrion Stronghold ${ts}`;
|
|
const nameB = `Quasarwave Citadel ${ts}`;
|
|
const guildA = await createGuild(harness, admin.token, nameA);
|
|
const guildB = await createGuild(harness, admin.token, nameB);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(nameA)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const foundA = result.guilds.find((g) => g.id === guildA.id);
|
|
const foundB = result.guilds.find((g) => g.id === guildB.id);
|
|
expect(foundA).toBeDefined();
|
|
expect(foundA!.name).toBe(nameA);
|
|
expect(foundB).toBeUndefined();
|
|
});
|
|
test('searching by guild name does not return guilds with different names', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const ts = Date.now();
|
|
const uniqueNameA = `Xanthium ${ts}`;
|
|
const uniqueNameB = `Ytterbium ${ts}`;
|
|
const guildA = await createGuild(harness, admin.token, uniqueNameA);
|
|
const guildB = await createGuild(harness, admin.token, uniqueNameB);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(uniqueNameA)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const foundA = result.guilds.find((g) => g.id === guildA.id);
|
|
const foundB = result.guilds.find((g) => g.id === guildB.id);
|
|
expect(foundA).toBeDefined();
|
|
expect(foundB).toBeUndefined();
|
|
});
|
|
});
|
|
describe('guild search by ID', () => {
|
|
test('searching by exact guild ID returns only the matching guild', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const guildA = await createGuild(harness, admin.token, `ID Search A ${Date.now()}`);
|
|
const guildB = await createGuild(harness, admin.token, `ID Search B ${Date.now()}`);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(guildA.id)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
const foundA = result.guilds.find((g) => g.id === guildA.id);
|
|
const foundB = result.guilds.find((g) => g.id === guildB.id);
|
|
expect(foundA).toBeDefined();
|
|
expect(foundA!.id).toBe(guildA.id);
|
|
expect(foundB).toBeUndefined();
|
|
});
|
|
test('guild ID search at non-zero offset does not perform direct DB lookup', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const guild = await createGuild(harness, admin.token, `Offset Guild ${Date.now()}`);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(guild.id)}&limit=10&offset=1`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.guilds).toEqual([]);
|
|
expect(result.total).toBe(0);
|
|
});
|
|
});
|
|
describe('guild search response fields', () => {
|
|
test('guild search response includes all expected admin fields', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const guildName = `Field Check Guild ${Date.now()}`;
|
|
const guild = await createGuild(harness, admin.token, guildName);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(guildName)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const found = result.guilds.find((g) => g.id === guild.id);
|
|
expect(found).toBeDefined();
|
|
expect(found!.id).toBe(guild.id);
|
|
expect(found!.name).toBe(guildName);
|
|
expect(found!.owner_id).toBe(admin.userId);
|
|
expect(found!).toHaveProperty('features');
|
|
expect(found!).toHaveProperty('icon');
|
|
expect(found!).toHaveProperty('banner');
|
|
expect(found!).toHaveProperty('member_count');
|
|
expect(found!.member_count).toBeGreaterThanOrEqual(1);
|
|
});
|
|
test('guild search returns correct owner_id for each guild', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const otherOwner = await createTestAccount(harness);
|
|
const ts = Date.now();
|
|
const guildByAdmin = await createGuild(harness, admin.token, `Admin Owned ${ts}`);
|
|
const guildByOther = await createGuild(harness, otherOwner.token, `Other Owned ${ts}`);
|
|
const resultAdmin = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(`Admin Owned ${ts}`)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const foundAdmin = resultAdmin.guilds.find((g) => g.id === guildByAdmin.id);
|
|
expect(foundAdmin).toBeDefined();
|
|
expect(foundAdmin!.owner_id).toBe(admin.userId);
|
|
const resultOther = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/guilds?q=${encodeURIComponent(`Other Owned ${ts}`)}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
const foundOther = resultOther.guilds.find((g) => g.id === guildByOther.id);
|
|
expect(foundOther).toBeDefined();
|
|
expect(foundOther!.owner_id).toBe(otherOwner.userId);
|
|
});
|
|
});
|
|
describe('omitted and no-match queries', () => {
|
|
test('omitted query on user search returns users', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
await createTestAccount(harness);
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
expect(result.users.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
test('omitted query on guild search returns guilds', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
await createGuild(harness, admin.token, `Omitted Query Guild ${Date.now()}`);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get('/admin/guilds?limit=10&offset=0')
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.total).toBeGreaterThanOrEqual(1);
|
|
expect(result.guilds.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
test('completely unrelated query returns no users', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'user:lookup']);
|
|
const result = await createBuilder<UserSearchResponse>(harness, `${admin.token}`)
|
|
.get(`/admin/users?q=${encodeURIComponent('zzz-impossible-match-query-xyzzy-99999')}&limit=10&offset=0`)
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.users).toEqual([]);
|
|
expect(result.total).toBe(0);
|
|
});
|
|
test('completely unrelated query returns no guilds', async () => {
|
|
const admin = await createTestAccount(harness);
|
|
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:lookup']);
|
|
const result = await createBuilder<GuildSearchResponse>(harness, `${admin.token}`)
|
|
.get('/admin/guilds?q=zzz-impossible-match-query-xyzzy-99999&limit=10&offset=0')
|
|
.expect(HTTP_STATUS.OK)
|
|
.execute();
|
|
expect(result.guilds).toEqual([]);
|
|
expect(result.total).toBe(0);
|
|
});
|
|
});
|
|
});
|