/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { Test, TestingModule } from '@nestjs/testing'; import { describe, expect, beforeAll, afterAll, test } from 'vitest'; import type { MiUser } from '@/models/User.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { genAidx } from '@/misc/id/aidx.js'; import { BlockingsRepository, FollowingsRepository, FollowRequestsRepository, MiRole, MiUserProfile, MutingsRepository, RenoteMutingsRepository, RoleAssignmentsRepository, RolesRepository, UserMemoRepository, UserProfilesRepository, UsersRepository, } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; import { RoleService } from '@/core/RoleService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { IdService } from '@/core/IdService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { CacheService } from '@/core/CacheService.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; import { MfmService } from '@/core/MfmService.js'; import { HashtagService } from '@/core/HashtagService.js'; import UsersChart from '@/core/chart/charts/users.js'; import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { ChatService } from '@/core/ChatService.js'; process.env.NODE_ENV = 'test'; describe('UserEntityService', () => { describe('pack/packMany', () => { let app: TestingModule; let service: UserEntityService; let usersRepository: UsersRepository; let userProfileRepository: UserProfilesRepository; let userMemosRepository: UserMemoRepository; let followingRepository: FollowingsRepository; let followingRequestRepository: FollowRequestsRepository; let blockingRepository: BlockingsRepository; let mutingRepository: MutingsRepository; let renoteMutingsRepository: RenoteMutingsRepository; let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; let roleService: RoleService; type RoleServiceCacheController = { rolesCache: { delete(): void }; roleAssignmentByUserIdCache: { delete(userId: MiUser['id']): void }; }; async function createUser(userData: Partial = {}, profileData: Partial = {}) { const un = secureRndstr(16); const user = await usersRepository .insert({ ...userData, id: genAidx(Date.now()), username: un, usernameLower: un.toLowerCase(), }) .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); await userProfileRepository.insert({ ...profileData, userId: user.id, }); return user; } async function createRole(roleData: Partial = {}) { const role = await rolesRepository .insert({ id: genAidx(Date.now()), updatedAt: new Date(), lastUsedAt: new Date(), name: '', description: '', ...roleData, }) .then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); (roleService as unknown as RoleServiceCacheController).rolesCache.delete(); return role; } async function assignRole(user: MiUser, role: MiRole) { await roleAssignmentsRepository.insert({ id: genAidx(Date.now()), userId: user.id, roleId: role.id, }); (roleService as unknown as RoleServiceCacheController).roleAssignmentByUserIdCache.delete(user.id); } async function memo(writer: MiUser, target: MiUser, memo: string) { await userMemosRepository.insert({ id: genAidx(Date.now()), userId: writer.id, targetUserId: target.id, memo, }); } async function follow(follower: MiUser, followee: MiUser) { await followingRepository.insert({ id: genAidx(Date.now()), followerId: follower.id, followeeId: followee.id, }); } async function requestFollow(requester: MiUser, requestee: MiUser) { await followingRequestRepository.insert({ id: genAidx(Date.now()), followerId: requester.id, followeeId: requestee.id, }); } async function block(blocker: MiUser, blockee: MiUser) { await blockingRepository.insert({ id: genAidx(Date.now()), blockerId: blocker.id, blockeeId: blockee.id, }); } async function mute(mutant: MiUser, mutee: MiUser) { await mutingRepository.insert({ id: genAidx(Date.now()), muterId: mutant.id, muteeId: mutee.id, }); } async function muteRenote(mutant: MiUser, mutee: MiUser) { await renoteMutingsRepository.insert({ id: genAidx(Date.now()), muterId: mutant.id, muteeId: mutee.id, }); } function randomIntRange(weight = 10) { return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx); } beforeAll(async () => { const services = [ UserEntityService, ApPersonService, NoteEntityService, PageEntityService, CustomEmojiService, AnnouncementService, RoleService, FederatedInstanceService, IdService, AvatarDecorationService, UtilityService, EmojiEntityService, ModerationLogService, GlobalEventService, DriveFileEntityService, MetaService, FetchInstanceMetadataService, CacheService, ApResolverService, ApNoteService, ApImageService, ApMfmService, MfmService, HashtagService, UsersChart, ChartLoggerService, InstanceChart, ApLoggerService, AccountMoveService, ReactionService, ReactionsBufferingService, NotificationService, ChatService, ]; app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], providers: [ ...services, ...services.map(x => ({ provide: x.name, useExisting: x })), ], }).compile(); await app.init(); app.enableShutdownHooks(); service = app.get(UserEntityService); usersRepository = app.get(DI.usersRepository); userProfileRepository = app.get(DI.userProfilesRepository); userMemosRepository = app.get(DI.userMemosRepository); followingRepository = app.get(DI.followingsRepository); followingRequestRepository = app.get(DI.followRequestsRepository); blockingRepository = app.get(DI.blockingsRepository); mutingRepository = app.get(DI.mutingsRepository); renoteMutingsRepository = app.get(DI.renoteMutingsRepository); rolesRepository = app.get(DI.rolesRepository); roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository); roleService = app.get(RoleService); }); afterAll(async () => { await app.close(); }); test('UserLite', async() => { const me = await createUser(); const who = await createUser(); await memo(me, who, 'memo'); const actual = await service.pack(who, me, { schema: 'UserLite' }) as any; // no detail expect(actual.memo).toBeUndefined(); // no detail and me expect(actual.birthday).toBeUndefined(); // no detail and me expect(actual.achievements).toBeUndefined(); }); test('UserDetailedNotMe', async() => { const me = await createUser(); const who = await createUser({}, { birthday: '2000-01-01' }); await memo(me, who, 'memo'); const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any; // is detail expect(actual.memo).toBe('memo'); // is detail expect(actual.birthday).toBe('2000-01-01'); // no detail and me expect(actual.achievements).toBeUndefined(); }); test('MeDetailed', async() => { const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }]; const me = await createUser({}, { birthday: '2000-01-01', achievements: achievements, }); await memo(me, me, 'memo'); const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any; // is detail expect(actual.memo).toBe('memo'); // is detail expect(actual.birthday).toBe('2000-01-01'); // is detail and me expect(actual.achievements).toEqual(achievements); }); test('MeDetailed hiddenRoleIds are sanitized to assigned public non-forced roles in stored order', async() => { const me = await createUser(); const visibleRole = await createRole({ name: 'visible', isPublic: true }); const secondVisibleRole = await createRole({ name: 'visible2', isPublic: true }); const privateRole = await createRole({ name: 'private', isPublic: false }); const forcedRole = await createRole({ name: 'forced', isPublic: true, isPublicDisplayRequired: true }); const unassignedRole = await createRole({ name: 'unassigned', isPublic: true }); await assignRole(me, visibleRole); await assignRole(me, secondVisibleRole); await assignRole(me, privateRole); await assignRole(me, forcedRole); await usersRepository.update(me.id, { hiddenRoleIds: [ secondVisibleRole.id, 'unknownroleid', unassignedRole.id, privateRole.id, forcedRole.id, visibleRole.id, secondVisibleRole.id, ], }); const updatedMe = await usersRepository.findOneByOrFail({ id: me.id }); const actual = await service.pack(updatedMe, updatedMe, { schema: 'MeDetailed' }); expect(actual.hiddenRoleIds).toEqual([secondVisibleRole.id, visibleRole.id]); }); test('UserDetailed filters hidden display roles for normal viewers, preserves forced roles, and bypasses for moderators', async() => { const [viewer, moderator, target] = await Promise.all([createUser(), createUser(), createUser()]); const hiddenRole = await createRole({ name: 'hidden', isPublic: true, asBadge: true, displayOrder: 30 }); const visibleRole = await createRole({ name: 'visible', isPublic: true, asBadge: true, displayOrder: 20 }); const forcedRole = await createRole({ name: 'forced', isPublic: true, isPublicDisplayRequired: true, asBadge: true, displayOrder: 10 }); const moderatorRole = await createRole({ name: 'moderator', isModerator: true }); await assignRole(target, hiddenRole); await assignRole(target, visibleRole); await assignRole(target, forcedRole); await assignRole(moderator, moderatorRole); await usersRepository.update(target.id, { hiddenRoleIds: [hiddenRole.id, forcedRole.id] }); const updatedTarget = await usersRepository.findOneByOrFail({ id: target.id }); const normalView = await service.pack(updatedTarget, viewer, { schema: 'UserDetailed' }); const moderatorView = await service.pack(updatedTarget, moderator, { schema: 'UserDetailed' }); expect(normalView.roles.map(role => role.id)).toEqual([visibleRole.id, forcedRole.id]); expect(normalView.badgeRoles?.map(role => role.id)).toEqual([visibleRole.id, forcedRole.id]); expect(normalView.badgeRoles?.every(role => typeof role.id === 'string')).toBe(true); expect(moderatorView.roles.map(role => role.id)).toEqual([hiddenRole.id, visibleRole.id, forcedRole.id]); expect(moderatorView.badgeRoles?.map(role => role.id)).toEqual([hiddenRole.id, visibleRole.id, forcedRole.id]); }); test('alsoKnownAs as string does not throw', async () => { const me = await createUser(); const who = await createUser(); const whoWithStringAlsoKnownAs: MiUser = { ...who, alsoKnownAs: 'https://remote.example.com/users/alice' as any }; const actual = await service.pack(whoWithStringAlsoKnownAs, me, { schema: 'UserDetailedNotMe' }) as any; expect(Array.isArray(actual.alsoKnownAs)).toBe(true); }); describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { test('no-preload', async() => { const me = await createUser(); // meがフォローしてる人たち const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); for (const who of followeeMe) { await follow(me, who); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(true); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } // meをフォローしてる人たち const followerMe = await Promise.all(randomIntRange().map(() => createUser())); for (const who of followerMe) { await follow(who, me); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(true); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } // meがフォローリクエストを送った人たち const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); for (const who of requestsFromYou) { await requestFollow(me, who); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(true); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } // meにフォローリクエストを送った人たち const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); for (const who of requestsToYou) { await requestFollow(who, me); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(true); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } // meがブロックしてる人たち const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); for (const who of blockingYou) { await block(me, who); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(true); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } // meをブロックしてる人たち const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); for (const who of blockingMe) { await block(who, me); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(true); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } // meがミュートしてる人たち const muters = await Promise.all(randomIntRange().map(() => createUser())); for (const who of muters) { await mute(me, who); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(true); expect(actual.isRenoteMuted).toBe(false); } // meがリノートミュートしてる人たち const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); for (const who of renoteMuters) { await muteRenote(me, who); const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(true); } }); test('preload', async() => { const me = await createUser(); { // meがフォローしてる人たち const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); for (const who of followeeMe) { await follow(me, who); } const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(true); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } } { // meをフォローしてる人たち const followerMe = await Promise.all(randomIntRange().map(() => createUser())); for (const who of followerMe) { await follow(who, me); } const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(true); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } } { // meがフォローリクエストを送った人たち const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); for (const who of requestsFromYou) { await requestFollow(me, who); } const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(true); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } } { // meにフォローリクエストを送った人たち const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); for (const who of requestsToYou) { await requestFollow(who, me); } const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(true); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } } { // meがブロックしてる人たち const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); for (const who of blockingYou) { await block(me, who); } const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(true); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } } { // meをブロックしてる人たち const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); for (const who of blockingMe) { await block(who, me); } const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(true); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(false); } } { // meがミュートしてる人たち const muters = await Promise.all(randomIntRange().map(() => createUser())); for (const who of muters) { await mute(me, who); } const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(true); expect(actual.isRenoteMuted).toBe(false); } } { // meがリノートミュートしてる人たち const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); for (const who of renoteMuters) { await muteRenote(me, who); } const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any; for (const actual of actualList) { expect(actual.isFollowing).toBe(false); expect(actual.isFollowed).toBe(false); expect(actual.hasPendingFollowRequestFromYou).toBe(false); expect(actual.hasPendingFollowRequestToYou).toBe(false); expect(actual.isBlocking).toBe(false); expect(actual.isBlocked).toBe(false); expect(actual.isMuted).toBe(false); expect(actual.isRenoteMuted).toBe(true); } } }); }); }); });