mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	test(backend): Add tests for users (#10546)
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
		
							parent
							
								
									49749b46c4
								
							
						
					
					
						commit
						5c3a4a8224
					
				
					 2 changed files with 903 additions and 4 deletions
				
			
		
							
								
								
									
										868
									
								
								packages/backend/test/e2e/users.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										868
									
								
								packages/backend/test/e2e/users.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,868 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { inspect } from 'node:util';
 | 
			
		||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { 
 | 
			
		||||
	signup, 
 | 
			
		||||
	post, 
 | 
			
		||||
	page,
 | 
			
		||||
	role,
 | 
			
		||||
	startServer, 
 | 
			
		||||
	api,
 | 
			
		||||
	successfulApiCall, 
 | 
			
		||||
	failedApiCall,
 | 
			
		||||
	uploadFile,
 | 
			
		||||
} from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
describe('ユーザー', () => {
 | 
			
		||||
	// エンティティとしてのユーザーを主眼においたテストを記述する
 | 
			
		||||
	// (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする)
 | 
			
		||||
 | 
			
		||||
	const stripUndefined = <T extends { [key: string]: any }, >(orig: T): Partial<T> => {
 | 
			
		||||
		return Object.entries({ ...orig })
 | 
			
		||||
			.filter(([, value]) => value !== undefined)
 | 
			
		||||
			.reduce((obj: Partial<T>, [key, value]) => {
 | 
			
		||||
				obj[key as keyof T] = value;
 | 
			
		||||
				return obj;
 | 
			
		||||
			}, {});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// FIXME: 足りないキーがたくさんある
 | 
			
		||||
	type UserLite = misskey.entities.UserLite & {
 | 
			
		||||
		badgeRoles: any[],
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	type UserDetailedNotMe = UserLite & 
 | 
			
		||||
	misskey.entities.UserDetailed & {
 | 
			
		||||
		roles: any[],
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	type MeDetailed = UserDetailedNotMe & 
 | 
			
		||||
		misskey.entities.MeDetailed & {
 | 
			
		||||
		showTimelineReplies: boolean,
 | 
			
		||||
		achievements: object[],
 | 
			
		||||
		loggedInDays: number,
 | 
			
		||||
		policies: object,
 | 
			
		||||
	};
 | 
			
		||||
	
 | 
			
		||||
	type User = MeDetailed & { token: string };	
 | 
			
		||||
 | 
			
		||||
	const show = async (id: string, me = alice): Promise<MeDetailed | UserDetailedNotMe> => {
 | 
			
		||||
		return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const userLite = (user: User): Partial<UserLite> => {
 | 
			
		||||
		return stripUndefined({
 | 
			
		||||
			id: user.id,
 | 
			
		||||
			name: user.name,
 | 
			
		||||
			username: user.username,
 | 
			
		||||
			host: user.host,
 | 
			
		||||
			avatarUrl: user.avatarUrl,
 | 
			
		||||
			avatarBlurhash: user.avatarBlurhash,
 | 
			
		||||
			isBot: user.isBot,
 | 
			
		||||
			isCat: user.isCat,
 | 
			
		||||
			instance: user.instance,
 | 
			
		||||
			emojis: user.emojis,
 | 
			
		||||
			onlineStatus: user.onlineStatus,
 | 
			
		||||
			badgeRoles: user.badgeRoles,
 | 
			
		||||
 | 
			
		||||
			// BUG isAdmin/isModeratorはUserLiteではなくMeDetailedOnlyに含まれる。
 | 
			
		||||
			isAdmin: undefined,
 | 
			
		||||
			isModerator: undefined,
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const userDetailedNotMe = (user: User): Partial<UserDetailedNotMe> => {
 | 
			
		||||
		return stripUndefined({
 | 
			
		||||
			...userLite(user),
 | 
			
		||||
			url: user.url,
 | 
			
		||||
			uri: user.uri,
 | 
			
		||||
			movedToUri: user.movedToUri,
 | 
			
		||||
			alsoKnownAs: user.alsoKnownAs,
 | 
			
		||||
			createdAt: user.createdAt,
 | 
			
		||||
			updatedAt: user.updatedAt,
 | 
			
		||||
			lastFetchedAt: user.lastFetchedAt,
 | 
			
		||||
			bannerUrl: user.bannerUrl,
 | 
			
		||||
			bannerBlurhash: user.bannerBlurhash,
 | 
			
		||||
			isLocked: user.isLocked,
 | 
			
		||||
			isSilenced: user.isSilenced,
 | 
			
		||||
			isSuspended: user.isSuspended,
 | 
			
		||||
			description: user.description,
 | 
			
		||||
			location: user.location,
 | 
			
		||||
			birthday: user.birthday,
 | 
			
		||||
			lang: user.lang,
 | 
			
		||||
			fields: user.fields,
 | 
			
		||||
			followersCount: user.followersCount,
 | 
			
		||||
			followingCount: user.followingCount,
 | 
			
		||||
			notesCount: user.notesCount,
 | 
			
		||||
			pinnedNoteIds: user.pinnedNoteIds,
 | 
			
		||||
			pinnedNotes: user.pinnedNotes,
 | 
			
		||||
			pinnedPageId: user.pinnedPageId,
 | 
			
		||||
			pinnedPage: user.pinnedPage,
 | 
			
		||||
			publicReactions: user.publicReactions,
 | 
			
		||||
			ffVisibility: user.ffVisibility,
 | 
			
		||||
			twoFactorEnabled: user.twoFactorEnabled,
 | 
			
		||||
			usePasswordLessLogin: user.usePasswordLessLogin,
 | 
			
		||||
			securityKeys: user.securityKeys,
 | 
			
		||||
			roles: user.roles,
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const userDetailedNotMeWithRelations = (user: User): Partial<UserDetailedNotMe> => {
 | 
			
		||||
		return stripUndefined({
 | 
			
		||||
			...userDetailedNotMe(user),
 | 
			
		||||
			isFollowing: user.isFollowing ?? false,
 | 
			
		||||
			isFollowed: user.isFollowed ?? false,
 | 
			
		||||
			hasPendingFollowRequestFromYou: user.hasPendingFollowRequestFromYou ?? false,
 | 
			
		||||
			hasPendingFollowRequestToYou: user.hasPendingFollowRequestToYou ?? false,
 | 
			
		||||
			isBlocking: user.isBlocking ?? false,
 | 
			
		||||
			isBlocked: user.isBlocked ?? false,
 | 
			
		||||
			isMuted: user.isMuted ?? false,
 | 
			
		||||
			isRenoteMuted: user.isRenoteMuted ?? false,
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const meDetailed = (user: User, security = false): Partial<MeDetailed> => {
 | 
			
		||||
		return stripUndefined({
 | 
			
		||||
			...userDetailedNotMe(user),
 | 
			
		||||
			avatarId: user.avatarId,
 | 
			
		||||
			bannerId: user.bannerId,
 | 
			
		||||
			isModerator: user.isModerator,
 | 
			
		||||
			isAdmin: user.isAdmin,
 | 
			
		||||
			injectFeaturedNote: user.injectFeaturedNote,
 | 
			
		||||
			receiveAnnouncementEmail: user.receiveAnnouncementEmail,
 | 
			
		||||
			alwaysMarkNsfw: user.alwaysMarkNsfw,
 | 
			
		||||
			autoSensitive: user.autoSensitive,
 | 
			
		||||
			carefulBot: user.carefulBot,
 | 
			
		||||
			autoAcceptFollowed: user.autoAcceptFollowed,
 | 
			
		||||
			noCrawle: user.noCrawle,
 | 
			
		||||
			isExplorable: user.isExplorable,
 | 
			
		||||
			isDeleted: user.isDeleted,
 | 
			
		||||
			hideOnlineStatus: user.hideOnlineStatus,
 | 
			
		||||
			hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes,
 | 
			
		||||
			hasUnreadMentions: user.hasUnreadMentions,
 | 
			
		||||
			hasUnreadAnnouncement: user.hasUnreadAnnouncement,
 | 
			
		||||
			hasUnreadAntenna: user.hasUnreadAntenna,
 | 
			
		||||
			hasUnreadChannel: user.hasUnreadChannel,
 | 
			
		||||
			hasUnreadNotification: user.hasUnreadNotification,
 | 
			
		||||
			hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
 | 
			
		||||
			mutedWords: user.mutedWords,
 | 
			
		||||
			mutedInstances: user.mutedInstances,
 | 
			
		||||
			mutingNotificationTypes: user.mutingNotificationTypes,
 | 
			
		||||
			emailNotificationTypes: user.emailNotificationTypes,
 | 
			
		||||
			showTimelineReplies: user.showTimelineReplies,
 | 
			
		||||
			achievements: user.achievements, 
 | 
			
		||||
			loggedInDays: user.loggedInDays,
 | 
			
		||||
			policies: user.policies,
 | 
			
		||||
			...(security ? {
 | 
			
		||||
				email: user.email,
 | 
			
		||||
				emailVerified: user.emailVerified,
 | 
			
		||||
				securityKeysList: user.securityKeysList,
 | 
			
		||||
			} : {}),
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let root: User;
 | 
			
		||||
	let alice: User;
 | 
			
		||||
	let aliceNote: misskey.entities.Note;
 | 
			
		||||
	let alicePage: misskey.entities.Page;
 | 
			
		||||
	let aliceList: misskey.entities.UserList;
 | 
			
		||||
 | 
			
		||||
	let bob: User;
 | 
			
		||||
	let bobNote: misskey.entities.Note;
 | 
			
		||||
 | 
			
		||||
	let carol: User;
 | 
			
		||||
	let dave: User;
 | 
			
		||||
	let ellen: User;
 | 
			
		||||
	let frank: User;
 | 
			
		||||
 | 
			
		||||
	let usersReplying: User[];
 | 
			
		||||
 | 
			
		||||
	let userNoNote: User;
 | 
			
		||||
	let userNotExplorable: User;
 | 
			
		||||
	let userLocking: User;
 | 
			
		||||
	let userAdmin: User;
 | 
			
		||||
	let roleAdmin: any;
 | 
			
		||||
	let userModerator: User;
 | 
			
		||||
	let roleModerator: any;
 | 
			
		||||
	let userRolePublic: User;
 | 
			
		||||
	let rolePublic: any;
 | 
			
		||||
	let userRoleBadge: User;
 | 
			
		||||
	let roleBadge: any;
 | 
			
		||||
	let userSilenced: User;
 | 
			
		||||
	let roleSilenced: any;
 | 
			
		||||
	let userSuspended: User;
 | 
			
		||||
	let userDeletedBySelf: User;
 | 
			
		||||
	let userDeletedByAdmin: User;
 | 
			
		||||
	let userFollowingAlice: User;
 | 
			
		||||
	let userFollowedByAlice: User;
 | 
			
		||||
	let userBlockingAlice: User;
 | 
			
		||||
	let userBlockedByAlice: User;
 | 
			
		||||
	let userMutingAlice: User;
 | 
			
		||||
	let userMutedByAlice: User;
 | 
			
		||||
	let userRnMutingAlice: User;
 | 
			
		||||
	let userRnMutedByAlice: User;
 | 
			
		||||
	let userFollowRequesting: User;
 | 
			
		||||
	let userFollowRequested: User;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		root = await signup({ username: 'alice' });
 | 
			
		||||
		alice = root;
 | 
			
		||||
		aliceNote = await post(alice, { text: 'test' }) as any; 
 | 
			
		||||
		alicePage = await page(alice);
 | 
			
		||||
		aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		bobNote = await post(bob, { text: 'test' }) as any; 
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
		dave = await signup({ username: 'dave' });
 | 
			
		||||
		ellen = await signup({ username: 'ellen' });
 | 
			
		||||
		frank = await signup({ username: 'frank' });
 | 
			
		||||
 | 
			
		||||
		// @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする
 | 
			
		||||
		usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => {
 | 
			
		||||
			const u = await signup({ username: `replying${i}` });
 | 
			
		||||
			for (let j = 0; j < 10 - i; j++) {
 | 
			
		||||
				const p = await post(u, { text: `test${j}` }); 
 | 
			
		||||
				await post(alice, { text: `@${u.username} test${j}`, replyId: p.id });
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			return (await acc).concat(u);
 | 
			
		||||
		}, Promise.resolve([] as User[]));
 | 
			
		||||
 | 
			
		||||
		userNoNote = await signup({ username: 'userNoNote' });
 | 
			
		||||
		userNotExplorable = await signup({ username: 'userNotExplorable' });
 | 
			
		||||
		await post(userNotExplorable, { text: 'test' });
 | 
			
		||||
		await api('i/update', { isExplorable: false }, userNotExplorable);
 | 
			
		||||
		userLocking = await signup({ username: 'userLocking' });
 | 
			
		||||
		await post(userLocking, { text: 'test' });
 | 
			
		||||
		await api('i/update', { isLocked: true }, userLocking);
 | 
			
		||||
		userAdmin = await signup({ username: 'userAdmin' });
 | 
			
		||||
		roleAdmin = await role(root, { isAdministrator: true, name: 'Admin Role' });
 | 
			
		||||
		await api('admin/roles/assign', { userId: userAdmin.id, roleId: roleAdmin.id }, root);
 | 
			
		||||
		userModerator = await signup({ username: 'userModerator' });
 | 
			
		||||
		roleModerator = await role(root, { isModerator: true, name: 'Moderator Role' });
 | 
			
		||||
		await api('admin/roles/assign', { userId: userModerator.id, roleId: roleModerator.id }, root);
 | 
			
		||||
		userRolePublic = await signup({ username: 'userRolePublic' });
 | 
			
		||||
		rolePublic = await role(root, { isPublic: true, name: 'Public Role' });
 | 
			
		||||
		await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root);
 | 
			
		||||
		userRoleBadge = await signup({ username: 'userRoleBadge' });
 | 
			
		||||
		roleBadge = await role(root, { asBadge: true, name: 'Badge Role' });
 | 
			
		||||
		await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
 | 
			
		||||
		userSilenced = await signup({ username: 'userSilenced' });
 | 
			
		||||
		await post(userSilenced, { text: 'test' });
 | 
			
		||||
		roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
 | 
			
		||||
		await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
 | 
			
		||||
		userSuspended = await signup({ username: 'userSuspended' });
 | 
			
		||||
		await post(userSuspended, { text: 'test' });
 | 
			
		||||
		await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended });
 | 
			
		||||
		await api('admin/suspend-user', { userId: userSuspended.id }, root);
 | 
			
		||||
		userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' });
 | 
			
		||||
		await post(userDeletedBySelf, { text: 'test' });
 | 
			
		||||
		await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
 | 
			
		||||
		userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
 | 
			
		||||
		await post(userDeletedByAdmin, { text: 'test' });
 | 
			
		||||
		await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
 | 
			
		||||
		userFollowingAlice = await signup({ username: 'userFollowingAlice' });
 | 
			
		||||
		await post(userFollowingAlice, { text: 'test' });
 | 
			
		||||
		await api('following/create', { userId: alice.id }, userFollowingAlice);
 | 
			
		||||
		userFollowedByAlice = await signup({ username: 'userFollowedByAlice' });
 | 
			
		||||
		await post(userFollowedByAlice, { text: 'test' });
 | 
			
		||||
		await api('following/create', { userId: userFollowedByAlice.id }, alice);
 | 
			
		||||
		userBlockingAlice = await signup({ username: 'userBlockingAlice' });
 | 
			
		||||
		await post(userBlockingAlice, { text: 'test' });
 | 
			
		||||
		await api('blocking/create', { userId: alice.id }, userBlockingAlice);
 | 
			
		||||
		userBlockedByAlice = await signup({ username: 'userBlockedByAlice' });
 | 
			
		||||
		await post(userBlockedByAlice, { text: 'test' });
 | 
			
		||||
		await api('blocking/create', { userId: userBlockedByAlice.id }, alice);
 | 
			
		||||
		userMutingAlice = await signup({ username: 'userMutingAlice' });
 | 
			
		||||
		await post(userMutingAlice, { text: 'test' });
 | 
			
		||||
		await api('mute/create', { userId: alice.id }, userMutingAlice);
 | 
			
		||||
		userMutedByAlice = await signup({ username: 'userMutedByAlice' });
 | 
			
		||||
		await post(userMutedByAlice, { text: 'test' });
 | 
			
		||||
		await api('mute/create', { userId: userMutedByAlice.id }, alice);
 | 
			
		||||
		userRnMutingAlice = await signup({ username: 'userRnMutingAlice' });
 | 
			
		||||
		await post(userRnMutingAlice, { text: 'test' });
 | 
			
		||||
		await api('renote-mute/create', { userId: alice.id }, userRnMutingAlice);
 | 
			
		||||
		userRnMutedByAlice = await signup({ username: 'userRnMutedByAlice' });
 | 
			
		||||
		await post(userRnMutedByAlice, { text: 'test' });
 | 
			
		||||
		await api('renote-mute/create', { userId: userRnMutedByAlice.id }, alice);
 | 
			
		||||
		userFollowRequesting = await signup({ username: 'userFollowRequesting' });
 | 
			
		||||
		await post(userFollowRequesting, { text: 'test' });
 | 
			
		||||
		userFollowRequested = userLocking;
 | 
			
		||||
		await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting);
 | 
			
		||||
	}, 1000 * 60 * 10);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	beforeEach(async () => {
 | 
			
		||||
		alice = {
 | 
			
		||||
			...alice,
 | 
			
		||||
			...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any,
 | 
			
		||||
		};
 | 
			
		||||
		aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice });
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#region サインアップ(signup)
 | 
			
		||||
 | 
			
		||||
	test('が作れる。(作りたての状態で自分のユーザー情報が取れる)', async () => {
 | 
			
		||||
		// SignupApiService.ts
 | 
			
		||||
		const response = await successfulApiCall({
 | 
			
		||||
			endpoint: 'signup',
 | 
			
		||||
			parameters: { username: 'zoe', password: 'password' },
 | 
			
		||||
			user: undefined,
 | 
			
		||||
		}) as unknown as User; // BUG MeDetailedに足りないキーがある
 | 
			
		||||
 | 
			
		||||
		// signupの時はtokenが含まれる特別なMeDetailedが返ってくる
 | 
			
		||||
		assert.match(response.token, /[a-zA-Z0-9]{16}/);
 | 
			
		||||
 | 
			
		||||
		// UserLite
 | 
			
		||||
		assert.match(response.id, /[0-9a-z]{10}/);
 | 
			
		||||
		assert.strictEqual(response.name, null);
 | 
			
		||||
		assert.strictEqual(response.username, 'zoe');
 | 
			
		||||
		assert.strictEqual(response.host, null);
 | 
			
		||||
		assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
 | 
			
		||||
		assert.strictEqual(response.avatarBlurhash, null);
 | 
			
		||||
		assert.strictEqual(response.isBot, false);
 | 
			
		||||
		assert.strictEqual(response.isCat, false);
 | 
			
		||||
		assert.strictEqual(response.instance, undefined);
 | 
			
		||||
		assert.deepStrictEqual(response.emojis, {});
 | 
			
		||||
		assert.strictEqual(response.onlineStatus, 'unknown');
 | 
			
		||||
		assert.deepStrictEqual(response.badgeRoles, []);
 | 
			
		||||
		// UserDetailedNotMeOnly
 | 
			
		||||
		assert.strictEqual(response.url, null);
 | 
			
		||||
		assert.strictEqual(response.uri, null);
 | 
			
		||||
		assert.strictEqual(response.movedToUri, null);
 | 
			
		||||
		assert.strictEqual(response.alsoKnownAs, null);
 | 
			
		||||
		assert.strictEqual(response.createdAt, new Date(response.createdAt).toISOString());
 | 
			
		||||
		assert.strictEqual(response.updatedAt, null);
 | 
			
		||||
		assert.strictEqual(response.lastFetchedAt, null);
 | 
			
		||||
		assert.strictEqual(response.bannerUrl, null);
 | 
			
		||||
		assert.strictEqual(response.bannerBlurhash, null);
 | 
			
		||||
		assert.strictEqual(response.isLocked, false);
 | 
			
		||||
		assert.strictEqual(response.isSilenced, false);
 | 
			
		||||
		assert.strictEqual(response.isSuspended, false);
 | 
			
		||||
		assert.strictEqual(response.description, null);
 | 
			
		||||
		assert.strictEqual(response.location, null);
 | 
			
		||||
		assert.strictEqual(response.birthday, null);
 | 
			
		||||
		assert.strictEqual(response.lang, null);
 | 
			
		||||
		assert.deepStrictEqual(response.fields, []);
 | 
			
		||||
		assert.strictEqual(response.followersCount, 0);
 | 
			
		||||
		assert.strictEqual(response.followingCount, 0);
 | 
			
		||||
		assert.strictEqual(response.notesCount, 0);
 | 
			
		||||
		assert.deepStrictEqual(response.pinnedNoteIds, []);
 | 
			
		||||
		assert.deepStrictEqual(response.pinnedNotes, []);
 | 
			
		||||
		assert.strictEqual(response.pinnedPageId, null);
 | 
			
		||||
		assert.strictEqual(response.pinnedPage, null);
 | 
			
		||||
		assert.strictEqual(response.publicReactions, false);
 | 
			
		||||
		assert.strictEqual(response.ffVisibility, 'public');
 | 
			
		||||
		assert.strictEqual(response.twoFactorEnabled, false);
 | 
			
		||||
		assert.strictEqual(response.usePasswordLessLogin, false);
 | 
			
		||||
		assert.strictEqual(response.securityKeys, false);
 | 
			
		||||
		assert.deepStrictEqual(response.roles, []);
 | 
			
		||||
		
 | 
			
		||||
		// MeDetailedOnly
 | 
			
		||||
		assert.strictEqual(response.avatarId, null);
 | 
			
		||||
		assert.strictEqual(response.bannerId, null);
 | 
			
		||||
		assert.strictEqual(response.isModerator, false);
 | 
			
		||||
		assert.strictEqual(response.isAdmin, false);
 | 
			
		||||
		assert.strictEqual(response.injectFeaturedNote, true);
 | 
			
		||||
		assert.strictEqual(response.receiveAnnouncementEmail, true);
 | 
			
		||||
		assert.strictEqual(response.alwaysMarkNsfw, false);
 | 
			
		||||
		assert.strictEqual(response.autoSensitive, false);
 | 
			
		||||
		assert.strictEqual(response.carefulBot, false);
 | 
			
		||||
		assert.strictEqual(response.autoAcceptFollowed, true);
 | 
			
		||||
		assert.strictEqual(response.noCrawle, false);
 | 
			
		||||
		assert.strictEqual(response.isExplorable, true);
 | 
			
		||||
		assert.strictEqual(response.isDeleted, false);
 | 
			
		||||
		assert.strictEqual(response.hideOnlineStatus, false);
 | 
			
		||||
		assert.strictEqual(response.hasUnreadSpecifiedNotes, false);
 | 
			
		||||
		assert.strictEqual(response.hasUnreadMentions, false);
 | 
			
		||||
		assert.strictEqual(response.hasUnreadAnnouncement, false);
 | 
			
		||||
		assert.strictEqual(response.hasUnreadAntenna, false);
 | 
			
		||||
		assert.strictEqual(response.hasUnreadChannel, false);
 | 
			
		||||
		assert.strictEqual(response.hasUnreadNotification, false);
 | 
			
		||||
		assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
 | 
			
		||||
		assert.deepStrictEqual(response.mutedWords, []);
 | 
			
		||||
		assert.deepStrictEqual(response.mutedInstances, []);
 | 
			
		||||
		assert.deepStrictEqual(response.mutingNotificationTypes, []);
 | 
			
		||||
		assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
 | 
			
		||||
		assert.strictEqual(response.showTimelineReplies, false);
 | 
			
		||||
		assert.deepStrictEqual(response.achievements, []);
 | 
			
		||||
		assert.deepStrictEqual(response.loggedInDays, 0);
 | 
			
		||||
		assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); 
 | 
			
		||||
		assert.notStrictEqual(response.email, undefined);
 | 
			
		||||
		assert.strictEqual(response.emailVerified, false);
 | 
			
		||||
		assert.deepStrictEqual(response.securityKeysList, []);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region 自分の情報(i)
 | 
			
		||||
 | 
			
		||||
	test('を読み取ることができる。(自分)', async () => {
 | 
			
		||||
		const response = await successfulApiCall({
 | 
			
		||||
			endpoint: 'i',
 | 
			
		||||
			parameters: {},
 | 
			
		||||
			user: userNoNote,
 | 
			
		||||
		});
 | 
			
		||||
		const expected = meDetailed(userNoNote, true);
 | 
			
		||||
		expected.loggedInDays = 1; // iはloggedInDaysを更新する
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region 自分の情報の更新(i/update)
 | 
			
		||||
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ parameters: (): object => ({ name: null }) },
 | 
			
		||||
		{ parameters: (): object => ({ name: 'x'.repeat(50) }) },
 | 
			
		||||
		{ parameters: (): object => ({ name: 'x' }) },
 | 
			
		||||
		{ parameters: (): object => ({ name: 'My name' }) },
 | 
			
		||||
		{ parameters: (): object => ({ description: null }) },
 | 
			
		||||
		{ parameters: (): object => ({ description: 'x'.repeat(1500) }) },
 | 
			
		||||
		{ parameters: (): object => ({ description: 'x' }) },
 | 
			
		||||
		{ parameters: (): object => ({ description: 'My description' }) },
 | 
			
		||||
		{ parameters: (): object => ({ location: null }) },
 | 
			
		||||
		{ parameters: (): object => ({ location: 'x'.repeat(50) }) },
 | 
			
		||||
		{ parameters: (): object => ({ location: 'x' }) },
 | 
			
		||||
		{ parameters: (): object => ({ location: 'My location' }) },
 | 
			
		||||
		{ parameters: (): object => ({ birthday: '0000-00-00' }) },
 | 
			
		||||
		{ parameters: (): object => ({ birthday: '9999-99-99' }) },
 | 
			
		||||
		{ parameters: (): object => ({ lang: 'en-US' }) },
 | 
			
		||||
		{ parameters: (): object => ({ fields: [] }) },
 | 
			
		||||
		{ parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) },
 | 
			
		||||
		{ parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない
 | 
			
		||||
		{ parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) },
 | 
			
		||||
		{ parameters: (): object => ({ isLocked: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ isLocked: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ isExplorable: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ isExplorable: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ hideOnlineStatus: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ hideOnlineStatus: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ publicReactions: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ publicReactions: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ autoAcceptFollowed: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ autoAcceptFollowed: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ noCrawle: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ noCrawle: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ isBot: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ isBot: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ isCat: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ isCat: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ showTimelineReplies: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ showTimelineReplies: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ injectFeaturedNote: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ injectFeaturedNote: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ receiveAnnouncementEmail: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ alwaysMarkNsfw: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ alwaysMarkNsfw: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ autoSensitive: true }) },
 | 
			
		||||
		{ parameters: (): object => ({ autoSensitive: false }) },
 | 
			
		||||
		{ parameters: (): object => ({ ffVisibility: 'private' }) },
 | 
			
		||||
		{ parameters: (): object => ({ ffVisibility: 'followers' }) },
 | 
			
		||||
		{ parameters: (): object => ({ ffVisibility: 'public' }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedWords: [] }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutedInstances: [] }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
 | 
			
		||||
		{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
 | 
			
		||||
		{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
 | 
			
		||||
		{ parameters: (): object => ({ emailNotificationTypes: [] }) },
 | 
			
		||||
	] as const)('を書き換えることができる($#)', async ({ parameters }) => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice });
 | 
			
		||||
		const expected = { ...meDetailed(alice, true), ...parameters() };
 | 
			
		||||
		assert.deepStrictEqual(response, expected, inspect(parameters()));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('を書き換えることができる(Avatar)', async () => {
 | 
			
		||||
		const aliceFile = (await uploadFile(alice)).body;
 | 
			
		||||
		const parameters = { avatarId: aliceFile.id };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
 | 
			
		||||
		assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
 | 
			
		||||
		assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/);
 | 
			
		||||
		const expected = { 
 | 
			
		||||
			...meDetailed(alice, true), 
 | 
			
		||||
			avatarId: aliceFile.id,
 | 
			
		||||
			avatarBlurhash: response.avatarBlurhash,
 | 
			
		||||
			avatarUrl: response.avatarUrl,
 | 
			
		||||
		};
 | 
			
		||||
		assert.deepStrictEqual(response, expected, inspect(parameters));
 | 
			
		||||
 | 
			
		||||
		if (1) return; // BUG 521eb95 以降アバターのリセットができない。
 | 
			
		||||
		const parameters2 = { avatarId: null };
 | 
			
		||||
		const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice });
 | 
			
		||||
		const expected2 = { 
 | 
			
		||||
			...meDetailed(alice, true), 
 | 
			
		||||
			avatarId: null,
 | 
			
		||||
			avatarBlurhash: null,
 | 
			
		||||
			avatarUrl: alice.avatarUrl, // 解除した場合、identiconになる
 | 
			
		||||
		};
 | 
			
		||||
		assert.deepStrictEqual(response2, expected2, inspect(parameters));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('を書き換えることができる(Banner)', async () => {
 | 
			
		||||
		const aliceFile = (await uploadFile(alice)).body;
 | 
			
		||||
		const parameters = { bannerId: aliceFile.id };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice });
 | 
			
		||||
		assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/);
 | 
			
		||||
		assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/);
 | 
			
		||||
		const expected = { 
 | 
			
		||||
			...meDetailed(alice, true), 
 | 
			
		||||
			bannerId: aliceFile.id,
 | 
			
		||||
			bannerBlurhash: response.bannerBlurhash,
 | 
			
		||||
			bannerUrl: response.bannerUrl,
 | 
			
		||||
		};
 | 
			
		||||
		assert.deepStrictEqual(response, expected, inspect(parameters));
 | 
			
		||||
 | 
			
		||||
		if (1) return; // BUG 521eb95 以降バナーのリセットができない。
 | 
			
		||||
		const parameters2 = { bannerId: null };
 | 
			
		||||
		const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice });
 | 
			
		||||
		const expected2 = { 
 | 
			
		||||
			...meDetailed(alice, true), 
 | 
			
		||||
			bannerId: null,
 | 
			
		||||
			bannerBlurhash: null,
 | 
			
		||||
			bannerUrl: null,
 | 
			
		||||
		};
 | 
			
		||||
		assert.deepStrictEqual(response2, expected2, inspect(parameters));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region 自分の情報の更新(i/pin, i/unpin)
 | 
			
		||||
 | 
			
		||||
	test('を書き換えることができる(ピン止めノート)', async () => {
 | 
			
		||||
		const parameters = { noteId: aliceNote.id };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'i/pin', parameters, user: alice });
 | 
			
		||||
		const expected = { ...meDetailed(alice, false), pinnedNoteIds: [aliceNote.id], pinnedNotes: [aliceNote] };
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
		
 | 
			
		||||
		const response2 = await successfulApiCall({ endpoint: 'i/unpin', parameters, user: alice });
 | 
			
		||||
		const expected2 = meDetailed(alice, false);
 | 
			
		||||
		assert.deepStrictEqual(response2, expected2);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region ユーザー(users)
 | 
			
		||||
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id },
 | 
			
		||||
		{ label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
 | 
			
		||||
		{ label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
 | 
			
		||||
		{ label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
 | 
			
		||||
		{ label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
 | 
			
		||||
		{ label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
 | 
			
		||||
		{ label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
 | 
			
		||||
	] as const)('をリスト形式で取得することができる($label)', async ({ parameters, selector }) => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
 | 
			
		||||
 | 
			
		||||
		// 結果の並びを事前にアサートするのは困難なので返ってきたidに対応するユーザーが返っており、ソート順が正しいことだけを検証する
 | 
			
		||||
		const users = await Promise.all(response.map(u => show(u.id)));
 | 
			
		||||
		const expected = users.sort((x, y) => {
 | 
			
		||||
			const index = (selector(x) < selector(y)) ? -1 : (selector(x) > selector(y)) ? 1 : 0;
 | 
			
		||||
			return index * (parameters.sort?.startsWith('+') ? -1 : 1);
 | 
			
		||||
		});
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true },
 | 
			
		||||
		{ label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true },
 | 
			
		||||
		{ label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true },
 | 
			
		||||
		{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true },
 | 
			
		||||
		{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
 | 
			
		||||
		{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
 | 
			
		||||
		{ label: 'サスペンドユーザーが含まれる', user: (): User => userSuspended },
 | 
			
		||||
		{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
 | 
			
		||||
		{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
 | 
			
		||||
	] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
 | 
			
		||||
		const parameters = { limit: 100 };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
 | 
			
		||||
		const expected = (excluded ?? false) ? [] : [await show(user().id)];
 | 
			
		||||
		assert.deepStrictEqual(response.filter((u) => u.id === user().id), expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.todo('をリスト形式で取得することができる(リモート, hostname指定)');
 | 
			
		||||
	test.todo('をリスト形式で取得することができる(pagenation)');
 | 
			
		||||
	
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region ユーザー情報(users/show)
 | 
			
		||||
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed },
 | 
			
		||||
		{ label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations },
 | 
			
		||||
		{ label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe },
 | 
			
		||||
		{ label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed },
 | 
			
		||||
		{ label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations },
 | 
			
		||||
		{ label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe },
 | 
			
		||||
	] as const)('を取得することができる($label)', async ({ parameters, user, type }) => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() });
 | 
			
		||||
		const expected = type(alice);
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin },
 | 
			
		||||
		{ label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined },
 | 
			
		||||
		{ label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator },
 | 
			
		||||
		{ label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined },
 | 
			
		||||
		{ label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced },
 | 
			
		||||
		{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended },
 | 
			
		||||
		{ label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted },
 | 
			
		||||
		{ label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
 | 
			
		||||
		{ label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted },
 | 
			
		||||
		{ label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
 | 
			
		||||
		{ label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing },
 | 
			
		||||
		{ label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed },
 | 
			
		||||
		{ label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking },
 | 
			
		||||
		{ label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked },
 | 
			
		||||
		{ label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted },
 | 
			
		||||
		{ label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted },
 | 
			
		||||
		{ label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou },
 | 
			
		||||
		{ label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou },
 | 
			
		||||
	] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice });
 | 
			
		||||
		assert.strictEqual(selector(response), (expected ?? ((): true => true))());
 | 
			
		||||
	});
 | 
			
		||||
	test('を取得することができ、Publicなロールがセットされていること', async () => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice });
 | 
			
		||||
		assert.deepStrictEqual(response.badgeRoles, []);
 | 
			
		||||
		assert.deepStrictEqual(response.roles, [{
 | 
			
		||||
			id: rolePublic.id,
 | 
			
		||||
			name: rolePublic.name,
 | 
			
		||||
			color: rolePublic.color,
 | 
			
		||||
			iconUrl: rolePublic.iconUrl,
 | 
			
		||||
			description: rolePublic.description,
 | 
			
		||||
			isModerator: rolePublic.isModerator,
 | 
			
		||||
			isAdministrator: rolePublic.isAdministrator,
 | 
			
		||||
			displayOrder: rolePublic.displayOrder,
 | 
			
		||||
		}]);
 | 
			
		||||
	});
 | 
			
		||||
	test('を取得することができ、バッヂロールがセットされていること', async () => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
 | 
			
		||||
		assert.deepStrictEqual(response.badgeRoles, [{
 | 
			
		||||
			name: roleBadge.name,
 | 
			
		||||
			iconUrl: roleBadge.iconUrl,
 | 
			
		||||
			displayOrder: roleBadge.displayOrder,
 | 
			
		||||
		}]);
 | 
			
		||||
		assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない
 | 
			
		||||
	});
 | 
			
		||||
	test('をID指定のリスト形式で取得することができる(空)', async () => {
 | 
			
		||||
		const parameters = { userIds: [] };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice });
 | 
			
		||||
		const expected: [] = [];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test('をID指定のリスト形式で取得することができる', async() => {
 | 
			
		||||
		const parameters = { userIds: [bob.id, alice.id, carol.id] };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice });
 | 
			
		||||
		const expected = [
 | 
			
		||||
			await successfulApiCall({ endpoint: 'users/show', parameters: { userId: bob.id }, user: alice }), 
 | 
			
		||||
			await successfulApiCall({ endpoint: 'users/show', parameters: { userId: alice.id }, user: alice }), 
 | 
			
		||||
			await successfulApiCall({ endpoint: 'users/show', parameters: { userId: carol.id }, user: alice }), 
 | 
			
		||||
		];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
 | 
			
		||||
		{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
 | 
			
		||||
		{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
 | 
			
		||||
		{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
 | 
			
		||||
		{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
 | 
			
		||||
		{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
 | 
			
		||||
		{ label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root },
 | 
			
		||||
		// BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる
 | 
			
		||||
		//{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true },
 | 
			
		||||
		{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
 | 
			
		||||
		{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },	
 | 
			
		||||
	] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => {
 | 
			
		||||
		const parameters = { userIds: [user().id] };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice });
 | 
			
		||||
		const expected = (excluded ?? false) ? [] : [await show(user().id, me?.() ?? alice)];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.todo('をID指定のリスト形式で取得することができる(リモート)');
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region 検索(users/search)
 | 
			
		||||
 | 
			
		||||
	test('を検索することができる', async () => {
 | 
			
		||||
		const parameters = { query: 'carol', limit: 10 };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
 | 
			
		||||
		const expected = [await show(carol.id)];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test('を検索することができる(UserLite)', async () => {
 | 
			
		||||
		const parameters = { query: 'carol', detail: false, limit: 10 };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
 | 
			
		||||
		const expected = [userLite(await show(carol.id))];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
 | 
			
		||||
		{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
 | 
			
		||||
		{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
 | 
			
		||||
		{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
 | 
			
		||||
		{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
 | 
			
		||||
		{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
 | 
			
		||||
		{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
 | 
			
		||||
		{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
 | 
			
		||||
		{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },	
 | 
			
		||||
	] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
 | 
			
		||||
		const parameters = { query: user().username, limit: 1 };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
 | 
			
		||||
		const expected = (excluded ?? false) ? [] : [await show(user().id)];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.todo('を検索することができる(リモート)');
 | 
			
		||||
	test.todo('を検索することができる(pagenation)');
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region ID指定検索(users/search-by-username-and-host)
 | 
			
		||||
 | 
			
		||||
	test.each([ 
 | 
			
		||||
		{ label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] },
 | 
			
		||||
		{ label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] },
 | 
			
		||||
		{ label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] },
 | 
			
		||||
		{ label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] },
 | 
			
		||||
		{ label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] },
 | 
			
		||||
		{ label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] },
 | 
			
		||||
		{ label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] },
 | 
			
		||||
		{ label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] },
 | 
			
		||||
		{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] },
 | 
			
		||||
	])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
 | 
			
		||||
		const expected = await Promise.all(user().map(u => show(u.id)));
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
 | 
			
		||||
		{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
 | 
			
		||||
		{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
 | 
			
		||||
		{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
 | 
			
		||||
		{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
 | 
			
		||||
		{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
 | 
			
		||||
		{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
 | 
			
		||||
		{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
 | 
			
		||||
		{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
 | 
			
		||||
	] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
 | 
			
		||||
		const parameters = { username: user().username };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
 | 
			
		||||
		const expected = (excluded ?? false) ? [] : [await show(user().id)];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.todo('をID&ホスト指定で検索できる(リモート)');
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region ID指定検索(users/get-frequently-replied-users)
 | 
			
		||||
 | 
			
		||||
	test('がよくリプライをするユーザーのリストを取得できる', async () => {
 | 
			
		||||
		const parameters = { userId: alice.id, limit: 5 };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice });
 | 
			
		||||
		const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({ 
 | 
			
		||||
			user: await show(s.id),
 | 
			
		||||
			weight: (usersReplying.length - i) / usersReplying.length,
 | 
			
		||||
		})));
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
 | 
			
		||||
		{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
 | 
			
		||||
		{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
 | 
			
		||||
		{ label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true },
 | 
			
		||||
		{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
 | 
			
		||||
		{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
 | 
			
		||||
		{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended },
 | 
			
		||||
		{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
 | 
			
		||||
		{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
 | 
			
		||||
	] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
 | 
			
		||||
		const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0];
 | 
			
		||||
		await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
 | 
			
		||||
		const parameters = { userId: alice.id, limit: 100 };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice });
 | 
			
		||||
		const expected = (excluded ?? false) ? [] : [await show(user().id)];
 | 
			
		||||
		assert.deepStrictEqual(response.map(s => s.user).filter((u) => u.id === user().id), expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region ハッシュタグ(hashtags/users)
 | 
			
		||||
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
 | 
			
		||||
		{ label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) },
 | 
			
		||||
		{ label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
 | 
			
		||||
		{ label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt },
 | 
			
		||||
		{ label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
 | 
			
		||||
		{ label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) },
 | 
			
		||||
	] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => {
 | 
			
		||||
		const hashtag = 'test_hashtag';
 | 
			
		||||
		await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
 | 
			
		||||
		const parameters = { tag: hashtag, limit: 5, ...sort };
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'hashtags/users', parameters, user: alice });
 | 
			
		||||
		const users = await Promise.all(response.map(u => show(u.id)));
 | 
			
		||||
		const expected = users.sort((x, y) => {
 | 
			
		||||
			const index = (selector(x) < selector(y)) ? -1 : (selector(x) > selector(y)) ? 1 : 0;
 | 
			
		||||
			return index * (parameters.sort.startsWith('+') ? -1 : 1);
 | 
			
		||||
		});
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.each([
 | 
			
		||||
		{ label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable },
 | 
			
		||||
		{ label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice },
 | 
			
		||||
		{ label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice },
 | 
			
		||||
		{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
 | 
			
		||||
		{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
 | 
			
		||||
		{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
 | 
			
		||||
		{ label: 'サスペンドユーザーが含まれる', user: (): User => userSuspended },
 | 
			
		||||
		{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
 | 
			
		||||
		{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
 | 
			
		||||
	] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user }) => {
 | 
			
		||||
		const hashtag = `user_test${user().username}`;
 | 
			
		||||
		if (user() !== userSuspended) {
 | 
			
		||||
			// サスペンドユーザーはupdateできない。
 | 
			
		||||
			await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: user() });
 | 
			
		||||
		}
 | 
			
		||||
		const parameters = { tag: hashtag, limit: 100, sort: '-follower' } as const;
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'hashtags/users', parameters, user: alice });
 | 
			
		||||
		const expected = [await show(user().id)];
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
	test.todo('をハッシュタグ指定で取得することができる(リモート)');
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region オススメユーザー(users/recommendation)
 | 
			
		||||
 | 
			
		||||
	// BUG users/recommendationは壊れている? > QueryFailedError: missing FROM-clause entry for table "note"
 | 
			
		||||
	test.skip('のオススメを取得することができる', async () => {
 | 
			
		||||
		const parameters = {};
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'users/recommendation', parameters, user: alice });
 | 
			
		||||
		const expected = await Promise.all(response.map(u => show(u.id)));
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
	//#region ピン止めユーザー(pinned-users)
 | 
			
		||||
 | 
			
		||||
	test('のピン止めユーザーを取得することができる', async () => {
 | 
			
		||||
		await successfulApiCall({ endpoint: 'admin/update-meta', parameters: { pinnedUsers: [bob.username, `@${carol.username}`] }, user: root });
 | 
			
		||||
		const parameters = {} as const;
 | 
			
		||||
		const response = await successfulApiCall({ endpoint: 'pinned-users', parameters, user: alice });
 | 
			
		||||
		const expected = await Promise.all([bob, carol].map(u => show(u.id)));
 | 
			
		||||
		assert.deepStrictEqual(response, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	test.todo('を管理人として確認することができる(admin/show-user)');
 | 
			
		||||
	test.todo('を管理人として確認することができる(admin/show-users)');
 | 
			
		||||
	test.todo('をサーバー向けに取得することができる(federation/users)');
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import WebSocket from 'ws';
 | 
			
		|||
import fetch, { Blob, File, RequestInit } from 'node-fetch';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
import { JSDOM } from 'jsdom';
 | 
			
		||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 | 
			
		||||
import { entities } from '../src/postgres.js';
 | 
			
		||||
import { loadConfig } from '../src/config.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
| 
						 | 
				
			
			@ -31,12 +32,12 @@ export type ApiRequest = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const successfulApiCall = async <T, >(request: ApiRequest, assertion: {
 | 
			
		||||
	status: number,
 | 
			
		||||
} = { status: 200 }): Promise<T> => {
 | 
			
		||||
	status?: number,
 | 
			
		||||
} = {}): Promise<T> => {
 | 
			
		||||
	const { endpoint, parameters, user } = request;
 | 
			
		||||
	const { status } = assertion;
 | 
			
		||||
	const res = await api(endpoint, parameters, user);
 | 
			
		||||
	assert.strictEqual(res.status, status, inspect(res.body));
 | 
			
		||||
	const status = assertion.status ?? (res.body == null ? 204 : 200);
 | 
			
		||||
	assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true }));
 | 
			
		||||
	return res.body;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -188,6 +189,36 @@ export const channel = async (user: any, channel: any = {}): Promise<any> => {
 | 
			
		|||
	return res.body;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const role = async (user: any, role: any = {}, policies: any = {}): Promise<any> => {
 | 
			
		||||
	const res = await api('admin/roles/create', {
 | 
			
		||||
		asBadge: false,
 | 
			
		||||
		canEditMembersByModerator: false,
 | 
			
		||||
		color: null,
 | 
			
		||||
		condFormula: {
 | 
			
		||||
			id: 'ebef1684-672d-49b6-ad82-1b3ec3784f85',
 | 
			
		||||
			type: 'isRemote',
 | 
			
		||||
		},
 | 
			
		||||
		description: '',
 | 
			
		||||
		displayOrder: 0,
 | 
			
		||||
		iconUrl: null,
 | 
			
		||||
		isAdministrator: false,
 | 
			
		||||
		isModerator: false,
 | 
			
		||||
		isPublic: false,
 | 
			
		||||
		name: 'New Role',
 | 
			
		||||
		target: 'manual',
 | 
			
		||||
		policies: { 
 | 
			
		||||
			...Object.entries(DEFAULT_POLICIES).map(([k, v]) => [k, { 
 | 
			
		||||
				priority: 0,
 | 
			
		||||
				useDefault: true,
 | 
			
		||||
				value: v,
 | 
			
		||||
			}]),
 | 
			
		||||
			...policies,
 | 
			
		||||
		},
 | 
			
		||||
		...role,
 | 
			
		||||
	}, user);
 | 
			
		||||
	return res.body;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface UploadOptions {
 | 
			
		||||
	/** Optional, absolute path or relative from ./resources/ */
 | 
			
		||||
	path?: string | URL;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue