632 lines
22 KiB
TypeScript
632 lines
22 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { jest } from '@jest/globals';
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import ms from 'ms';
|
|
import {
|
|
type MiNote,
|
|
type MiUser,
|
|
type NotesRepository,
|
|
type NoteFavoritesRepository,
|
|
type UserNotePiningsRepository,
|
|
type UsersRepository,
|
|
type UserProfilesRepository,
|
|
MiMeta,
|
|
} from '@/models/_.js';
|
|
import { CleanRemoteNotesProcessorService } from '@/queue/processors/CleanRemoteNotesProcessorService.js';
|
|
import { DI } from '@/di-symbols.js';
|
|
import { IdService } from '@/core/IdService.js';
|
|
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
|
import { GlobalModule } from '@/GlobalModule.js';
|
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
|
|
|
describe('CleanRemoteNotesProcessorService', () => {
|
|
let app: TestingModule;
|
|
let service: CleanRemoteNotesProcessorService;
|
|
let idService: IdService;
|
|
let notesRepository: NotesRepository;
|
|
let noteFavoritesRepository: NoteFavoritesRepository;
|
|
let userNotePiningsRepository: UserNotePiningsRepository;
|
|
let usersRepository: UsersRepository;
|
|
let userProfilesRepository: UserProfilesRepository;
|
|
|
|
// Local user
|
|
let alice: MiUser;
|
|
// Remote user 1
|
|
let bob: MiUser;
|
|
// Remote user 2
|
|
let carol: MiUser;
|
|
|
|
const meta = new MiMeta();
|
|
|
|
// Mock job object
|
|
const createMockJob = () => ({
|
|
log: jest.fn(),
|
|
updateProgress: jest.fn(),
|
|
});
|
|
|
|
async function createUser(data: Partial<MiUser> = {}) {
|
|
const id = idService.gen();
|
|
const un = data.username || secureRndstr(16);
|
|
const user = await usersRepository
|
|
.insert({
|
|
id,
|
|
username: un,
|
|
usernameLower: un.toLowerCase(),
|
|
...data,
|
|
})
|
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
|
|
|
await userProfilesRepository.save({
|
|
userId: id,
|
|
});
|
|
|
|
return user;
|
|
}
|
|
|
|
async function createNote(data: Partial<MiNote>, user: MiUser, time?: number): Promise<MiNote> {
|
|
const id = idService.gen(time);
|
|
const note = await notesRepository
|
|
.insert({
|
|
id: id,
|
|
text: `note_${id}`,
|
|
userId: user.id,
|
|
userHost: user.host,
|
|
visibility: 'public',
|
|
...data,
|
|
})
|
|
.then(x => notesRepository.findOneByOrFail(x.identifiers[0]));
|
|
return note;
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
app = await Test
|
|
.createTestingModule({
|
|
imports: [
|
|
GlobalModule,
|
|
],
|
|
providers: [
|
|
CleanRemoteNotesProcessorService,
|
|
IdService,
|
|
{
|
|
provide: QueueLoggerService,
|
|
useFactory: () => ({
|
|
logger: {
|
|
createSubLogger: () => ({
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
succ: jest.fn(),
|
|
}),
|
|
},
|
|
}),
|
|
},
|
|
],
|
|
})
|
|
.overrideProvider(DI.meta).useFactory({ factory: () => meta })
|
|
.compile();
|
|
|
|
service = app.get(CleanRemoteNotesProcessorService);
|
|
idService = app.get(IdService);
|
|
notesRepository = app.get(DI.notesRepository);
|
|
noteFavoritesRepository = app.get(DI.noteFavoritesRepository);
|
|
userNotePiningsRepository = app.get(DI.userNotePiningsRepository);
|
|
usersRepository = app.get(DI.usersRepository);
|
|
userProfilesRepository = app.get(DI.userProfilesRepository);
|
|
|
|
alice = await createUser({ username: 'alice', host: null });
|
|
bob = await createUser({ username: 'bob', host: 'remote1.example.com' });
|
|
carol = await createUser({ username: 'carol', host: 'remote2.example.com' });
|
|
|
|
app.enableShutdownHooks();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Reset mocks
|
|
jest.clearAllMocks();
|
|
|
|
// Set default meta values
|
|
meta.enableRemoteNotesCleaning = true;
|
|
meta.remoteNotesCleaningMaxProcessingDurationInMinutes = 0.3;
|
|
meta.remoteNotesCleaningExpiryDaysForEachNotes = 30;
|
|
}, 60 * 1000);
|
|
|
|
afterEach(async () => {
|
|
// Clean up test data
|
|
await Promise.all([
|
|
notesRepository.createQueryBuilder().delete().execute(),
|
|
userNotePiningsRepository.createQueryBuilder().delete().execute(),
|
|
noteFavoritesRepository.createQueryBuilder().delete().execute(),
|
|
]);
|
|
}, 60 * 1000);
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('basic', () => {
|
|
test('should skip cleaning when enableRemoteNotesCleaning is false', async () => {
|
|
meta.enableRemoteNotesCleaning = false;
|
|
const job = createMockJob();
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result).toEqual({
|
|
deletedCount: 0,
|
|
oldest: null,
|
|
newest: null,
|
|
skipped: true,
|
|
});
|
|
});
|
|
|
|
test('should return success result when enableRemoteNotesCleaning is true and no notes to clean', async () => {
|
|
const job = createMockJob();
|
|
|
|
await createNote({}, alice);
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result).toEqual({
|
|
deletedCount: 0,
|
|
oldest: null,
|
|
newest: null,
|
|
skipped: false,
|
|
});
|
|
}, 3000);
|
|
|
|
test('should clean remote notes and return stats', async () => {
|
|
// Remote notes
|
|
const remoteNotes = await Promise.all([
|
|
createNote({}, bob),
|
|
createNote({}, carol),
|
|
createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000),
|
|
createNote({}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000), // Note older than expiry
|
|
]);
|
|
|
|
// Local notes
|
|
const localNotes = await Promise.all([
|
|
createNote({}, alice),
|
|
createNote({}, alice, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000),
|
|
]);
|
|
|
|
const job = createMockJob();
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result).toEqual({
|
|
deletedCount: 2,
|
|
oldest: expect.any(Number),
|
|
newest: expect.any(Number),
|
|
skipped: false,
|
|
});
|
|
|
|
// Check side-by-side from all notes
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.length).toBe(4);
|
|
expect(remainingNotes.some(n => n.id === remoteNotes[0].id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === remoteNotes[1].id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === remoteNotes[2].id)).toBe(false);
|
|
expect(remainingNotes.some(n => n.id === remoteNotes[3].id)).toBe(false);
|
|
expect(remainingNotes.some(n => n.id === localNotes[0].id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === localNotes[1].id)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('advanced', () => {
|
|
// お気に入り
|
|
test('should not delete note that is favorited by any user', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Favorite the note
|
|
await noteFavoritesRepository.save({
|
|
id: idService.gen(),
|
|
userId: alice.id,
|
|
noteId: olderRemoteNote.id,
|
|
});
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0);
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNote = await notesRepository.findOneBy({ id: olderRemoteNote.id });
|
|
expect(remainingNote).not.toBeNull();
|
|
});
|
|
|
|
// ピン留め
|
|
test('should not delete note that is pinned by the user', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Pin the note by the user who created it
|
|
await userNotePiningsRepository.save({
|
|
id: idService.gen(),
|
|
userId: bob.id, // Same user as the note creator
|
|
noteId: olderRemoteNote.id,
|
|
});
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0);
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNote = await notesRepository.findOneBy({ id: olderRemoteNote.id });
|
|
expect(remainingNote).not.toBeNull();
|
|
});
|
|
|
|
// クリップ
|
|
test('should not delete note that is clipped', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that is clipped
|
|
const clippedNote = await createNote({
|
|
clippedCount: 1, // Clipped
|
|
}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0);
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNote = await notesRepository.findOneBy({ id: clippedNote.id });
|
|
expect(remainingNote).not.toBeNull();
|
|
});
|
|
|
|
// 古いreply, renoteが含まれている時の挙動
|
|
test('should handle reply/renote relationships correctly', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote notes with reply/renote relationships
|
|
const originalNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
const replyNote = await createNote({
|
|
replyId: originalNote.id,
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
|
const renoteNote = await createNote({
|
|
renoteId: originalNote.id,
|
|
}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 3000);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
// Should delete all three notes as they are all old and remote
|
|
expect(result.deletedCount).toBe(3);
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.some(n => n.id === originalNote.id)).toBe(false);
|
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(false);
|
|
expect(remainingNotes.some(n => n.id === renoteNote.id)).toBe(false);
|
|
});
|
|
|
|
// 古いリモートノートに新しいリプライがある時、どちらも削除されない
|
|
test('should not delete both old remote note with new reply', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const oldNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Create a reply note that is newer than the expiry period
|
|
const recentReplyNote = await createNote({
|
|
replyId: oldNote.id,
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) + 1000);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0); // Only the old note should be deleted
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.some(n => n.id === oldNote.id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === recentReplyNote.id)).toBe(true); // Recent reply note should remain
|
|
});
|
|
|
|
// 古いリモートノートに新しいリプライと古いリプライがある時、全て残る
|
|
test('should not delete old remote note with new reply and old reply', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const oldNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Create a reply note that is newer than the expiry period
|
|
const recentReplyNote = await createNote({
|
|
replyId: oldNote.id,
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) + 1000);
|
|
|
|
// Create an old reply note that should be deleted
|
|
const oldReplyNote = await createNote({
|
|
replyId: oldNote.id,
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0);
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.some(n => n.id === oldNote.id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === recentReplyNote.id)).toBe(true); // Recent reply note should remain
|
|
expect(remainingNotes.some(n => n.id === oldReplyNote.id)).toBe(true); // Old reply note should be deleted
|
|
});
|
|
|
|
// リプライがお気に入りされているとき、どちらも削除されない
|
|
test('should not delete reply note that is favorited', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Create a reply note that is newer than the expiry period
|
|
const replyNote = await createNote({
|
|
replyId: olderRemoteNote.id,
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
|
|
|
// Favorite the reply note
|
|
await noteFavoritesRepository.save({
|
|
id: idService.gen(),
|
|
userId: alice.id,
|
|
noteId: replyNote.id,
|
|
});
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0); // Only the old note should be deleted
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); // Recent reply note should remain
|
|
});
|
|
|
|
// リプライがピン留めされているとき、どちらも削除されない
|
|
test('should not delete reply note that is pinned', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Create a reply note that is newer than the expiry period
|
|
const replyNote = await createNote({
|
|
replyId: olderRemoteNote.id,
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
|
|
|
// Pin the reply note
|
|
await userNotePiningsRepository.save({
|
|
id: idService.gen(),
|
|
userId: carol.id,
|
|
noteId: replyNote.id,
|
|
});
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0); // Only the old note should be deleted
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); // Reply note should remain
|
|
});
|
|
|
|
// リプライがクリップされているとき、どちらも削除されない
|
|
test('should not delete reply note that is clipped', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create old remote note that should be deleted
|
|
const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000);
|
|
|
|
// Create a reply note that is old but clipped
|
|
const replyNote = await createNote({
|
|
replyId: olderRemoteNote.id,
|
|
clippedCount: 1, // Clipped
|
|
}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0); // Both notes should be kept because reply is clipped
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true);
|
|
expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true);
|
|
});
|
|
|
|
test('should handle mixed scenarios with multiple conditions', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create various types of notes
|
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
|
|
|
// Should be deleted: old remote note with no special conditions
|
|
const deletableNote = await createNote({}, bob, oldTime);
|
|
|
|
// Should NOT be deleted: old remote note but favorited
|
|
const favoritedNote = await createNote({}, carol, oldTime);
|
|
await noteFavoritesRepository.save({
|
|
id: idService.gen(),
|
|
userId: alice.id,
|
|
noteId: favoritedNote.id,
|
|
});
|
|
|
|
// Should NOT be deleted: old remote note but pinned
|
|
const pinnedNote = await createNote({}, bob, oldTime);
|
|
await userNotePiningsRepository.save({
|
|
id: idService.gen(),
|
|
userId: bob.id,
|
|
noteId: pinnedNote.id,
|
|
});
|
|
|
|
// Should NOT be deleted: old remote note but clipped
|
|
const clippedNote = await createNote({
|
|
clippedCount: 2,
|
|
}, carol, oldTime);
|
|
|
|
// Should NOT be deleted: old local note
|
|
const localNote = await createNote({}, alice, oldTime);
|
|
|
|
// Should NOT be deleted: new remote note
|
|
const newerRemoteNote = await createNote({}, bob);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(1); // Only deletableNote should be deleted
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.length).toBe(5);
|
|
expect(remainingNotes.some(n => n.id === deletableNote.id)).toBe(false); // Deleted
|
|
expect(remainingNotes.some(n => n.id === favoritedNote.id)).toBe(true); // Kept
|
|
expect(remainingNotes.some(n => n.id === pinnedNote.id)).toBe(true); // Kept
|
|
expect(remainingNotes.some(n => n.id === clippedNote.id)).toBe(true); // Kept
|
|
expect(remainingNotes.some(n => n.id === localNote.id)).toBe(true); // Kept
|
|
expect(remainingNotes.some(n => n.id === newerRemoteNote.id)).toBe(true); // Kept
|
|
});
|
|
|
|
// 大量のノート
|
|
test('should handle large number of notes correctly', async () => {
|
|
const AMOUNT = 130;
|
|
const job = createMockJob();
|
|
|
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
|
const noteIds = [];
|
|
for (let i = 0; i < AMOUNT; i++) {
|
|
const note = await createNote({}, bob, oldTime - i);
|
|
noteIds.push(note.id);
|
|
}
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
// Should delete all notes, but may require multiple batches
|
|
expect(result.deletedCount).toBe(AMOUNT);
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.length).toBe(0);
|
|
});
|
|
|
|
// 大量のノート + リプライ or リノート
|
|
test('should handle large number of notes with replies correctly', async () => {
|
|
const AMOUNT = 130;
|
|
const job = createMockJob();
|
|
|
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
|
const noteIds = [];
|
|
for (let i = 0; i < AMOUNT; i++) {
|
|
const note = await createNote({}, bob, oldTime - i - AMOUNT);
|
|
noteIds.push(note.id);
|
|
if (i % 2 === 0) {
|
|
// Create a reply for every second note
|
|
await createNote({ replyId: note.id }, carol, oldTime - i);
|
|
} else {
|
|
// Create a renote for every second note
|
|
await createNote({ renoteId: note.id }, bob, oldTime - i);
|
|
}
|
|
}
|
|
|
|
const result = await service.process(job as any);
|
|
// Should delete all notes, but may require multiple batches
|
|
expect(result.deletedCount).toBe(AMOUNT * 2);
|
|
expect(result.skipped).toBe(false);
|
|
});
|
|
|
|
// 大量の古いノート + 新しいリプライ or リノート
|
|
test('should handle large number of old notes with new replies correctly', async () => {
|
|
const AMOUNT = 130;
|
|
const job = createMockJob();
|
|
|
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
|
const newTime = Date.now();
|
|
const noteIds = [];
|
|
for (let i = 0; i < AMOUNT; i++) {
|
|
const note = await createNote({}, bob, oldTime - i);
|
|
noteIds.push(note.id);
|
|
if (i % 2 === 0) {
|
|
// Create a reply for every second note
|
|
await createNote({ replyId: note.id }, carol, newTime + i);
|
|
} else {
|
|
// Create a renote for every second note
|
|
await createNote({ renoteId: note.id }, bob, newTime + i);
|
|
}
|
|
}
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(0);
|
|
expect(result.skipped).toBe(false);
|
|
});
|
|
|
|
// 大量の残す対象(clippedCount: 1)と大量の削除対象
|
|
test('should handle large number of notes, mixed conditions with clippedCount', async () => {
|
|
const AMOUNT_BASE = 70;
|
|
const job = createMockJob();
|
|
|
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
|
const noteIds = [];
|
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
|
const note = await createNote({ clippedCount: 1 }, bob, oldTime - i - AMOUNT_BASE);
|
|
noteIds.push(note.id);
|
|
}
|
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
|
const note = await createNote({}, carol, oldTime - i);
|
|
noteIds.push(note.id);
|
|
}
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(AMOUNT_BASE); // Assuming half are deletable
|
|
expect(result.skipped).toBe(false);
|
|
});
|
|
|
|
// 大量の残す対象(リプライ)と大量の削除対象
|
|
test('should handle large number of notes, mixed conditions with replies', async () => {
|
|
const AMOUNT_BASE = 70;
|
|
const job = createMockJob();
|
|
const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000;
|
|
const newTime = Date.now();
|
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
|
// should remain
|
|
const note = await createNote({}, carol, oldTime - AMOUNT_BASE - i);
|
|
// should remain
|
|
await createNote({ replyId: note.id }, bob, newTime + i);
|
|
}
|
|
|
|
const noteIdsExpectedToBeDeleted = [];
|
|
for (let i = 0; i < AMOUNT_BASE; i++) {
|
|
// should be deleted
|
|
const note = await createNote({}, bob, oldTime - i);
|
|
noteIdsExpectedToBeDeleted.push(note.id);
|
|
}
|
|
|
|
const result = await service.process(job as any);
|
|
expect(result.deletedCount).toBe(AMOUNT_BASE); // Assuming all replies are deletable
|
|
expect(result.skipped).toBe(false);
|
|
|
|
const remainingNotes = await notesRepository.find();
|
|
expect(remainingNotes.length).toBe(AMOUNT_BASE * 2); // Only replies should remain
|
|
noteIdsExpectedToBeDeleted.forEach(id => {
|
|
expect(remainingNotes.some(n => n.id === id)).toBe(false); // All original notes should be deleted
|
|
});
|
|
});
|
|
|
|
test('should update cursor correctly during batch processing', async () => {
|
|
const job = createMockJob();
|
|
|
|
// Create notes with specific timing to test cursor behavior
|
|
const baseTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 10000;
|
|
|
|
const note1 = await createNote({}, bob, baseTime);
|
|
const note2 = await createNote({}, carol, baseTime - 1000);
|
|
const note3 = await createNote({}, bob, baseTime - 2000);
|
|
|
|
const result = await service.process(job as any);
|
|
|
|
expect(result.deletedCount).toBe(3);
|
|
expect(result.newest).toBe(idService.parse(note1.id).date.getTime());
|
|
expect(result.oldest).toBe(idService.parse(note3.id).date.getTime());
|
|
expect(result.skipped).toBe(false);
|
|
});
|
|
});
|
|
});
|