Sharkey/packages/backend/test-federation/test/note.test.ts

318 lines
11 KiB
TypeScript

import assert, { rejects, strictEqual } from 'node:assert';
import * as Misskey from 'misskey-js';
import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
describe('Note', () => {
let alice: LoginUser, bob: LoginUser;
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
beforeAll(async () => {
[alice, bob] = await Promise.all([
createAccount('a.test'),
createAccount('b.test'),
]);
[bobInA, aliceInB] = await Promise.all([
resolveRemoteUser('b.test', bob.id, alice),
resolveRemoteUser('a.test', alice.id, bob),
]);
});
describe('Note content', () => {
test('Consistency of Public Note', async () => {
const image = await uploadFile('a.test', alice);
const note = (await alice.client.request('notes/create', {
text: 'I am Alice!',
fileIds: [image.id],
poll: {
choices: ['neko', 'inu'],
multiple: false,
expiredAfter: 60 * 60 * 1000,
},
})).createdNote;
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id',
'emojis',
/** Consistency of files is checked at {@link file://./drive.test.ts}, so let's skip. */
'fileIds',
'files',
/** @see https://github.com/misskey-dev/misskey/issues/12409 */
'reactionAcceptance',
'userId',
'user',
'uri',
]);
strictEqual(aliceInB.id, resolvedNote.userId);
});
test('Consistency of reply', async () => {
const _replyedNote = (await alice.client.request('notes/create', {
text: 'a',
})).createdNote;
const note = (await alice.client.request('notes/create', {
text: 'b',
replyId: _replyedNote.id,
})).createdNote;
// NOTE: the repliedCount is incremented, so fetch again
const replyedNote = await alice.client.request('notes/show', { noteId: _replyedNote.id });
strictEqual(replyedNote.repliesCount, 1);
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id',
'emojis',
'reactionAcceptance',
'replyId',
'reply',
'userId',
'user',
'uri',
]);
assert(resolvedNote.replyId != null);
assert(resolvedNote.reply != null);
deepStrictEqualWithExcludedFields(replyedNote, resolvedNote.reply, [
'id',
// TODO: why clippedCount loses consistency?
'clippedCount',
'emojis',
'userId',
'user',
'uri',
// flaky because this is parallelly incremented, so let's check it below
'repliesCount',
]);
strictEqual(aliceInB.id, resolvedNote.userId);
await sleep();
const resolvedReplyedNote = await bob.client.request('notes/show', { noteId: resolvedNote.replyId });
strictEqual(resolvedReplyedNote.repliesCount, 1);
});
test('Consistency of Renote', async () => {
// NOTE: the renoteCount is not incremented, so no need to fetch again
const renotedNote = (await alice.client.request('notes/create', {
text: 'a',
})).createdNote;
const note = (await alice.client.request('notes/create', {
text: 'b',
renoteId: renotedNote.id,
})).createdNote;
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id',
'emojis',
'reactionAcceptance',
'renoteId',
'renote',
'userId',
'user',
'uri',
]);
assert(resolvedNote.renoteId != null);
assert(resolvedNote.renote != null);
deepStrictEqualWithExcludedFields(renotedNote, resolvedNote.renote, [
'id',
'emojis',
'userId',
'user',
'uri',
]);
strictEqual(aliceInB.id, resolvedNote.userId);
});
});
describe('Other props', () => {
test('localOnly', async () => {
const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
rejects(
async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
(err: any) => {
/**
* FIXME: this error is not handled
* @see https://github.com/misskey-dev/misskey/issues/12736
*/
strictEqual(err.code, 'INTERNAL_ERROR');
return true;
},
);
});
});
describe('Deletion', () => {
describe('Check Delete consistency', () => {
let carol: LoginUser;
beforeAll(async () => {
carol = await createAccount('a.test');
await carol.client.request('following/create', { userId: bobInA.id });
await sleep();
});
test('Delete is derivered to followers', async () => {
const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, carol);
await bob.client.request('notes/delete', { noteId: note.id });
await sleep();
await rejects(
async () => await carol.client.request('notes/show', { noteId: noteInA.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
describe('Deletion of remote user\'s note for moderation', () => {
let note: Misskey.entities.Note;
test('Alice post is deleted in B', async () => {
note = (await alice.client.request('notes/create', { text: 'Hello' })).createdNote;
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
const bMod = await createModerator('b.test');
await bMod.client.request('notes/delete', { noteId: noteInB.id });
await rejects(
async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
/**
* FIXME: implement soft deletion as well as user?
* @see https://github.com/misskey-dev/misskey/issues/11437
*/
test.failing('Not found even if resolve again', async () => {
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
await rejects(
async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
(err: any) => {
strictEqual(err.code, 'NO_SUCH_NOTE');
return true;
},
);
});
});
});
describe('Reaction', () => {
describe('Consistency', () => {
test('Unicode reaction', async () => {
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
const reaction = '😅';
await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction });
await sleep();
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
strictEqual(reactions.length, 1);
strictEqual(reactions[0].type, reaction);
strictEqual(reactions[0].user.id, bobInA.id);
});
test('Custom emoji reaction', async () => {
const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
const emoji = await addCustomEmoji('b.test');
await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: `:${emoji.name}:` });
await sleep();
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
strictEqual(reactions.length, 1);
strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
strictEqual(reactions[0].user.id, bobInA.id);
});
});
describe('Acceptance', () => {
test('Even if likeOnly, remote users can react with custom emoji, but it is converted to like', async () => {
const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'likeOnly' })).createdNote;
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
const emoji = await addCustomEmoji('b.test');
await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
await sleep();
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
strictEqual(reactions.length, 1);
strictEqual(reactions[0].type, '❤');
});
/**
* TODO: this may be unexpected behavior?
* @see https://github.com/misskey-dev/misskey/issues/12409
*/
test('Even if nonSensitiveOnly, remote users can react with sensitive emoji, and it is not converted', async () => {
const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'nonSensitiveOnly' })).createdNote;
const noteInB = await resolveRemoteNote('a.test', note.id, bob);
const emoji = await addCustomEmoji('b.test', { isSensitive: true });
await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
await sleep();
const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
strictEqual(reactions.length, 1);
strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
});
});
});
describe('Poll', () => {
describe('Any remote user\'s vote is delivered to the author', () => {
let carol: LoginUser;
beforeAll(async () => {
carol = await createAccount('a.test');
});
test('Bob creates poll and receives a vote from Carol', async () => {
const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
const noteInA = await resolveRemoteNote('b.test', note.id, carol);
await carol.client.request('notes/polls/vote', { noteId: noteInA.id, choice: 0 });
await sleep();
const noteAfterVote = await bob.client.request('notes/show', { noteId: note.id });
assert(noteAfterVote.poll != null);
strictEqual(noteAfterVote.poll.choices[0].votes, 1);
strictEqual(noteAfterVote.poll.choices[1].votes, 0);
});
});
describe('Local user\'s vote is delivered to the author\'s remote followers', () => {
let bobRemoteFollower: LoginUser, localVoter: LoginUser;
beforeAll(async () => {
[
bobRemoteFollower,
localVoter,
] = await Promise.all([
createAccount('a.test'),
createAccount('b.test'),
]);
await bobRemoteFollower.client.request('following/create', { userId: bobInA.id });
await sleep();
});
test('A vote in Bob\'s server is delivered to Bob\'s remote followers', async () => {
const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
// NOTE: resolve before voting
const noteInA = await resolveRemoteNote('b.test', note.id, bobRemoteFollower);
await localVoter.client.request('notes/polls/vote', { noteId: note.id, choice: 0 });
await sleep();
const noteAfterVote = await bobRemoteFollower.client.request('notes/show', { noteId: noteInA.id });
assert(noteAfterVote.poll != null);
strictEqual(noteAfterVote.poll.choices[0].votes, 1);
strictEqual(noteAfterVote.poll.choices[1].votes, 0);
});
});
});
});