Merge d72119fd08
into 218070eb13
This commit is contained in:
commit
baba89330c
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FollowingIsFollowerSuspended1752410859370 {
|
||||
name = 'FollowingIsFollowerSuspended1752410859370'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
|
||||
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerSuspended" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_1896254b78a41a50e0396fdabd" ON "following" ("followeeId", "followerHost", "isFollowerSuspended", "isFollowerHibernated") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d2b8dbf0b772042f4fe241a29d" ON "following" ("followerId", "followeeId", "isFollowerSuspended") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d2b8dbf0b772042f4fe241a29d"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_1896254b78a41a50e0396fdabd"`);
|
||||
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerSuspended"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class FollowingIsFollowerSuspendedCopySuspendedState1752410900000 {
|
||||
name = 'FollowingIsFollowerSuspendedCopySuspendedState1752410900000'
|
||||
|
||||
async up(queryRunner) {
|
||||
// Update existing records based on user suspension status
|
||||
await queryRunner.query(`
|
||||
UPDATE "following"
|
||||
SET "isFollowerSuspended" = "user"."isSuspended"
|
||||
FROM "user"
|
||||
WHERE "following"."followerId" = "user"."id"
|
||||
`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
}
|
||||
}
|
|
@ -549,6 +549,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
// TODO: キャッシュ
|
||||
this.followingsRepository.findBy({
|
||||
followeeId: user.id,
|
||||
isFollowerSuspended: false,
|
||||
notify: 'normal',
|
||||
}).then(async followings => {
|
||||
if (note.visibility !== 'specified') {
|
||||
|
@ -854,6 +855,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
where: {
|
||||
followeeId: user.id,
|
||||
followerHost: IsNull(),
|
||||
isFollowerSuspended: false,
|
||||
isFollowerHibernated: false,
|
||||
},
|
||||
select: ['followerId', 'withReplies'],
|
||||
|
|
|
@ -229,9 +229,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followee: {
|
||||
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
|
||||
},
|
||||
follower: {
|
||||
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
|
||||
},
|
||||
follower: MiUser,
|
||||
silent = false,
|
||||
withReplies?: boolean,
|
||||
): Promise<void> {
|
||||
|
@ -244,6 +242,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
withReplies: withReplies,
|
||||
isFollowerSuspended: follower.isSuspended,
|
||||
|
||||
// 非正規化
|
||||
followerHost: follower.host,
|
||||
|
@ -734,6 +733,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
return this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: userId })
|
||||
.andWhere('following.isFollowerSuspended = false')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
|
@ -743,6 +743,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
where: {
|
||||
followerId,
|
||||
followeeId,
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import { DI } from '@/di-symbols.js';
|
|||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RelationshipJobData } from '@/queue/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -49,8 +48,8 @@ export class UserSuspendService {
|
|||
});
|
||||
|
||||
(async () => {
|
||||
await this.postSuspend(user).catch(e => {});
|
||||
await this.unFollowAll(user).catch(e => {});
|
||||
await this.postSuspend(user).catch((e: any) => {});
|
||||
await this.suspendFollowings(user).catch((e: any) => {});
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -67,7 +66,8 @@ export class UserSuspendService {
|
|||
});
|
||||
|
||||
(async () => {
|
||||
await this.postUnsuspend(user).catch(e => {});
|
||||
await this.postUnsuspend(user).catch((e: any) => {});
|
||||
await this.restoreFollowings(user).catch((e: any) => {});
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -139,24 +139,27 @@ export class UserSuspendService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async unFollowAll(follower: MiUser) {
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: {
|
||||
private async suspendFollowings(follower: MiUser) {
|
||||
await this.followingsRepository.update(
|
||||
{
|
||||
followerId: follower.id,
|
||||
followeeId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const jobs: RelationshipJobData[] = [];
|
||||
for (const following of followings) {
|
||||
if (following.followeeId && following.followerId) {
|
||||
jobs.push({
|
||||
from: { id: following.followerId },
|
||||
to: { id: following.followeeId },
|
||||
silent: true,
|
||||
});
|
||||
{
|
||||
isFollowerSuspended: true,
|
||||
}
|
||||
}
|
||||
this.queueService.createUnfollowJob(jobs);
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async restoreFollowings(follower: MiUser) {
|
||||
// フォロー関係を復元(isFollowerSuspended: false)に変更
|
||||
await this.followingsRepository.update(
|
||||
{
|
||||
followerId: follower.id,
|
||||
},
|
||||
{
|
||||
isFollowerSuspended: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ class DeliverManager {
|
|||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
|
|
|
@ -9,7 +9,8 @@ import { MiUser } from './User.js';
|
|||
|
||||
@Entity('following')
|
||||
@Index(['followerId', 'followeeId'], { unique: true })
|
||||
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
|
||||
@Index(['followerId', 'followeeId', 'isFollowerSuspended'])
|
||||
@Index(['followeeId', 'followerHost', 'isFollowerSuspended', 'isFollowerHibernated'])
|
||||
export class MiFollowing {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
@ -45,6 +46,11 @@ export class MiFollowing {
|
|||
})
|
||||
public isFollowerHibernated: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isFollowerSuspended: boolean;
|
||||
|
||||
// タイムラインにその人のリプライまで含めるかどうか
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
|
|
@ -50,7 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followeeHost = :host', { host: ps.host });
|
||||
.andWhere('following.followeeHost = :host', { host: ps.host })
|
||||
.andWhere('following.isFollowerSuspended = false');
|
||||
|
||||
const followings = await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -50,7 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followerHost = :host', { host: ps.host });
|
||||
.andWhere('following.followerHost = :host', { host: ps.host })
|
||||
.andWhere('following.isFollowerSuspended = false');
|
||||
|
||||
const followings = await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -94,11 +94,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.followingsRepository.count({
|
||||
where: {
|
||||
followeeHost: Not(IsNull()),
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
}),
|
||||
this.followingsRepository.count({
|
||||
where: {
|
||||
followerHost: Not(IsNull()),
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -125,6 +125,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
|
@ -136,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.isFollowerSuspended = false')
|
||||
.innerJoinAndSelect('following.follower', 'follower');
|
||||
|
||||
const followings = await query
|
||||
|
|
|
@ -133,6 +133,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
isFollowerSuspended: false,
|
||||
},
|
||||
});
|
||||
if (!isFollowing) {
|
||||
|
@ -144,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.isFollowerSuspended = false')
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
if (ps.birthday) {
|
||||
|
|
|
@ -68,7 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
.where('following.followerId = :followerId', { followerId: me.id })
|
||||
.andWhere('following.isFollowerSuspended = false');
|
||||
|
||||
query
|
||||
.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
|
||||
|
|
|
@ -0,0 +1,413 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import {
|
||||
MiFollowing,
|
||||
MiUser,
|
||||
FollowingsRepository,
|
||||
FollowRequestsRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { randomString } from '../utils.js';
|
||||
|
||||
function genHost() {
|
||||
return randomString() + '.example.com';
|
||||
}
|
||||
|
||||
describe('UserSuspendService', () => {
|
||||
let app: TestingModule;
|
||||
let userSuspendService: UserSuspendService;
|
||||
let usersRepository: UsersRepository;
|
||||
let followingsRepository: FollowingsRepository;
|
||||
let followRequestsRepository: FollowRequestsRepository;
|
||||
let userEntityService: jest.Mocked<UserEntityService>;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
let globalEventService: jest.Mocked<GlobalEventService>;
|
||||
let apRendererService: jest.Mocked<ApRendererService>;
|
||||
let moderationLogService: jest.Mocked<ModerationLogService>;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}): Promise<MiUser> {
|
||||
const user = {
|
||||
id: secureRndstr(16),
|
||||
username: secureRndstr(16),
|
||||
usernameLower: secureRndstr(16).toLowerCase(),
|
||||
host: null,
|
||||
isSuspended: false,
|
||||
...data,
|
||||
} as MiUser;
|
||||
|
||||
await usersRepository.insert(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createFollowing(follower: MiUser, followee: MiUser, data: Partial<MiFollowing> = {}): Promise<MiFollowing> {
|
||||
const following = {
|
||||
id: secureRndstr(16),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
isFollowerSuspended: false,
|
||||
isFollowerHibernated: false,
|
||||
withReplies: false,
|
||||
notify: null,
|
||||
followerHost: follower.host,
|
||||
followerInbox: null,
|
||||
followerSharedInbox: null,
|
||||
followeeHost: followee.host,
|
||||
followeeInbox: null,
|
||||
followeeSharedInbox: null,
|
||||
...data,
|
||||
} as MiFollowing;
|
||||
|
||||
await followingsRepository.insert(following);
|
||||
return following;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [GlobalModule],
|
||||
providers: [
|
||||
UserSuspendService,
|
||||
{
|
||||
provide: UserEntityService,
|
||||
useFactory: () => ({
|
||||
isLocalUser: jest.fn(),
|
||||
genLocalUserUri: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: QueueService,
|
||||
useFactory: () => ({
|
||||
deliver: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: GlobalEventService,
|
||||
useFactory: () => ({
|
||||
publishInternalEvent: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ApRendererService,
|
||||
useFactory: () => ({
|
||||
addContext: jest.fn(),
|
||||
renderDelete: jest.fn(),
|
||||
renderUndo: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: ModerationLogService,
|
||||
useFactory: () => ({
|
||||
log: jest.fn(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app.enableShutdownHooks();
|
||||
|
||||
userSuspendService = app.get<UserSuspendService>(UserSuspendService);
|
||||
usersRepository = app.get<UsersRepository>(DI.usersRepository);
|
||||
followingsRepository = app.get<FollowingsRepository>(DI.followingsRepository);
|
||||
followRequestsRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository);
|
||||
userEntityService = app.get<UserEntityService>(UserEntityService) as jest.Mocked<UserEntityService>;
|
||||
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
|
||||
globalEventService = app.get<GlobalEventService>(GlobalEventService) as jest.Mocked<GlobalEventService>;
|
||||
apRendererService = app.get<ApRendererService>(ApRendererService) as jest.Mocked<ApRendererService>;
|
||||
moderationLogService = app.get<ModerationLogService>(ModerationLogService) as jest.Mocked<ModerationLogService>;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('suspend', () => {
|
||||
test('should suspend user and update database', async () => {
|
||||
const user = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
|
||||
// ユーザーが凍結されているかチェック
|
||||
const suspendedUser = await usersRepository.findOneBy({ id: user.id });
|
||||
expect(suspendedUser?.isSuspended).toBe(true);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'suspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
});
|
||||
|
||||
test('should mark follower relationships as suspended', async () => {
|
||||
const user = await createUser();
|
||||
const followee1 = await createUser();
|
||||
const followee2 = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
// ユーザーがフォローしている関係を作成
|
||||
await createFollowing(user, followee1);
|
||||
await createFollowing(user, followee2);
|
||||
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// フォロー関係が論理削除されているかチェック
|
||||
const followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should publish internal event for suspension', async () => {
|
||||
const user = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 内部イベントが発行されているかチェック(非同期処理のため少し待つ)
|
||||
await setTimeout(100);
|
||||
|
||||
expect(globalEventService.publishInternalEvent).toHaveBeenCalledWith(
|
||||
'userChangeSuspendedState',
|
||||
{ id: user.id, isSuspended: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuspend', () => {
|
||||
test('should unsuspend user and update database', async () => {
|
||||
const user = await createUser({ isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.unsuspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ユーザーの凍結が解除されているかチェック
|
||||
const unsuspendedUser = await usersRepository.findOneBy({ id: user.id });
|
||||
expect(unsuspendedUser?.isSuspended).toBe(false);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'unsuspend', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
});
|
||||
|
||||
test('should restore follower relationships', async () => {
|
||||
const user = await createUser({ isSuspended: true });
|
||||
const followee1 = await createUser();
|
||||
const followee2 = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
// 凍結状態のフォロー関係を作成
|
||||
await createFollowing(user, followee1, { isFollowerSuspended: true });
|
||||
await createFollowing(user, followee2, { isFollowerSuspended: true });
|
||||
|
||||
await userSuspendService.unsuspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// フォロー関係が復元されているかチェック
|
||||
const followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should publish internal event for unsuspension', async () => {
|
||||
const user = await createUser({ isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.unsuspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 内部イベントが発行されているかチェック(非同期処理のため少し待つ)
|
||||
await setTimeout(100);
|
||||
|
||||
expect(globalEventService.publishInternalEvent).toHaveBeenCalledWith(
|
||||
'userChangeSuspendedState',
|
||||
{ id: user.id, isSuspended: false },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test: suspend and unsuspend cycle', () => {
|
||||
test('should preserve follow relationships through suspend/unsuspend cycle', async () => {
|
||||
const user = await createUser();
|
||||
const followee1 = await createUser();
|
||||
const followee2 = await createUser();
|
||||
const moderator = await createUser();
|
||||
|
||||
// 初期のフォロー関係を作成
|
||||
await createFollowing(user, followee1);
|
||||
await createFollowing(user, followee2);
|
||||
|
||||
// 初期状態の確認
|
||||
let followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(false);
|
||||
});
|
||||
|
||||
// 凍結
|
||||
await userSuspendService.suspend(user, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 凍結後の状態確認
|
||||
followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(true);
|
||||
});
|
||||
|
||||
// 凍結解除
|
||||
const suspendedUser = await usersRepository.findOneByOrFail({ id: user.id });
|
||||
await userSuspendService.unsuspend(suspendedUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// 凍結解除後の状態確認
|
||||
followings = await followingsRepository.find({
|
||||
where: { followerId: user.id },
|
||||
});
|
||||
expect(followings).toHaveLength(2);
|
||||
followings.forEach(following => {
|
||||
expect(following.isFollowerSuspended).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActivityPub delivery', () => {
|
||||
test('should deliver Delete activity on suspend of local user', async () => {
|
||||
const localUser = await createUser({ host: null });
|
||||
const moderator = await createUser();
|
||||
|
||||
userEntityService.isLocalUser.mockReturnValue(true);
|
||||
userEntityService.genLocalUserUri.mockReturnValue(`https://example.com/users/${localUser.id}`);
|
||||
apRendererService.renderDelete.mockReturnValue({ type: 'Delete' } as any);
|
||||
apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Delete' } as any);
|
||||
|
||||
await userSuspendService.suspend(localUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ActivityPub配信が呼ばれているかチェック
|
||||
expect(userEntityService.isLocalUser).toHaveBeenCalledWith(localUser);
|
||||
expect(apRendererService.renderDelete).toHaveBeenCalled();
|
||||
expect(apRendererService.addContext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should deliver Undo Delete activity on unsuspend of local user', async () => {
|
||||
const localUser = await createUser({ host: null, isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
userEntityService.isLocalUser.mockReturnValue(true);
|
||||
userEntityService.genLocalUserUri.mockReturnValue(`https://example.com/users/${localUser.id}`);
|
||||
apRendererService.renderDelete.mockReturnValue({ type: 'Delete' } as any);
|
||||
apRendererService.renderUndo.mockReturnValue({ type: 'Undo' } as any);
|
||||
apRendererService.addContext.mockReturnValue({ '@context': '...', type: 'Undo' } as any);
|
||||
|
||||
await userSuspendService.unsuspend(localUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ActivityPub配信が呼ばれているかチェック
|
||||
expect(userEntityService.isLocalUser).toHaveBeenCalledWith(localUser);
|
||||
expect(apRendererService.renderDelete).toHaveBeenCalled();
|
||||
expect(apRendererService.renderUndo).toHaveBeenCalled();
|
||||
expect(apRendererService.addContext).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not deliver any activity on suspend of remote user', async () => {
|
||||
const remoteUser = await createUser({ host: 'remote.example.com' });
|
||||
const moderator = await createUser();
|
||||
|
||||
userEntityService.isLocalUser.mockReturnValue(false);
|
||||
|
||||
await userSuspendService.suspend(remoteUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ActivityPub配信が呼ばれていないことをチェック
|
||||
expect(userEntityService.isLocalUser).toHaveBeenCalledWith(remoteUser);
|
||||
expect(apRendererService.renderDelete).not.toHaveBeenCalled();
|
||||
expect(queueService.deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote user suspension', () => {
|
||||
test('should suspend remote user without AP delivery', async () => {
|
||||
const remoteUser = await createUser({ host: genHost() });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.suspend(remoteUser, moderator);
|
||||
await setTimeout(250);
|
||||
|
||||
// ユーザーが凍結されているかチェック
|
||||
const suspendedUser = await usersRepository.findOneBy({ id: remoteUser.id });
|
||||
expect(suspendedUser?.isSuspended).toBe(true);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'suspend', {
|
||||
userId: remoteUser.id,
|
||||
userUsername: remoteUser.username,
|
||||
userHost: remoteUser.host,
|
||||
});
|
||||
|
||||
// ActivityPub配信が呼ばれていないことを確認
|
||||
expect(queueService.deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remote user unsuspension', () => {
|
||||
test('should unsuspend remote user without AP delivery', async () => {
|
||||
const remoteUser = await createUser({ host: genHost(), isSuspended: true });
|
||||
const moderator = await createUser();
|
||||
|
||||
await userSuspendService.unsuspend(remoteUser, moderator);
|
||||
|
||||
await setTimeout(250);
|
||||
|
||||
// ユーザーの凍結が解除されているかチェック
|
||||
const unsuspendedUser = await usersRepository.findOneBy({ id: remoteUser.id });
|
||||
expect(unsuspendedUser?.isSuspended).toBe(false);
|
||||
|
||||
// モデレーションログが記録されているかチェック
|
||||
expect(moderationLogService.log).toHaveBeenCalledWith(moderator, 'unsuspend', {
|
||||
userId: remoteUser.id,
|
||||
userUsername: remoteUser.username,
|
||||
userHost: remoteUser.host,
|
||||
});
|
||||
|
||||
// ActivityPub配信が呼ばれていないことを確認
|
||||
expect(queueService.deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue