hippofish/packages/backend/test/streaming.ts
2023-10-22 17:33:40 +09:00

766 lines
20 KiB
TypeScript

process.env.NODE_ENV = "test";
import * as assert from "assert";
import * as childProcess from "child_process";
import { Following } from "../src/models/entities/following.js";
import {
connectStream,
signup,
api,
post,
startServer,
shutdownServer,
initTestDb,
waitFire,
} from "./utils.js";
describe("Streaming", () => {
let p: childProcess.ChildProcess;
let Followings: any;
const follow = async (follower: any, followee: any) => {
await Followings.save({
id: "a",
createdAt: new Date(),
followerId: follower.id,
followeeId: followee.id,
followerHost: follower.host,
followerInbox: null,
followerSharedInbox: null,
followeeHost: followee.host,
followeeInbox: null,
followeeSharedInbox: null,
});
};
describe("Streaming", () => {
// Local users
let ayano: any;
let kyoko: any;
let chitose: any;
// Remote users
let akari: any;
let chinatsu: any;
let kyokoNote: any;
let list: any;
before(async () => {
p = await startServer();
const connection = await initTestDb(true);
Followings = connection.getRepository(Following);
ayano = await signup({ username: "ayano" });
kyoko = await signup({ username: "kyoko" });
chitose = await signup({ username: "chitose" });
akari = await signup({ username: "akari", host: "example.com" });
chinatsu = await signup({ username: "chinatsu", host: "example.com" });
kyokoNote = await post(kyoko, { text: "foo" });
// Follow: ayano => kyoko
await api("following/create", { userId: kyoko.id }, ayano);
// Follow: ayano => akari
await follow(ayano, akari);
// List: chitose => ayano, kyoko
list = await api(
"users/lists/create",
{
name: "my list",
},
chitose,
).then((x) => x.body);
await api(
"users/lists/push",
{
listId: list.id,
userId: ayano.id,
},
chitose,
);
await api(
"users/lists/push",
{
listId: list.id,
userId: kyoko.id,
},
chitose,
);
});
after(async () => {
await shutdownServer(p);
});
describe("Events", () => {
it("mention event", async () => {
const fired = await waitFire(
kyoko,
"main", // kyoko:main
() => post(ayano, { text: "foo @kyoko bar" }), // ayano mention => kyoko
(msg) => msg.type === "mention" && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, true);
});
it("renote event", async () => {
const fired = await waitFire(
kyoko,
"main", // kyoko:main
() => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
(msg) => msg.type === "renote" && msg.body.renoteId === kyokoNote.id, // wait renote
);
assert.strictEqual(fired, true);
});
});
describe("Home Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:Home
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしているユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:home
() => api("notes/create", { text: "foo" }, kyoko), // kyoko posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしていないユーザーの投稿は流れない", async () => {
const fired = await waitFire(
kyoko,
"homeTimeline", // kyoko:home
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, false);
});
it("フォローしているユーザーのダイレクト投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:home
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
), // kyoko dm => ayano
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"homeTimeline", // ayano:home
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [chitose.id],
},
kyoko,
), // kyoko dm => chitose
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
}); // Home
describe("Local Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("リモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, false);
});
it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, akari), // akari posts
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
);
assert.strictEqual(fired, false);
});
it("ホーム指定の投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
), // kyoko DM => ayano
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"localTimeline", // ayano:Local
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
chitose,
),
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, false);
});
});
describe("Recommended Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("リモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, false);
});
it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo" }, akari), // akari posts
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
);
assert.strictEqual(fired, false);
});
it("ホーム指定の投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
), // kyoko DM => ayano
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"recommendedTimeline", // ayano:Local
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
chitose,
),
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, false);
});
});
describe("Hybrid Timeline", () => {
it("自分の投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
(msg) => msg.type === "note" && msg.body.text === "foo",
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("フォローしているリモートユーザーの投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, akari), // akari posts
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
);
assert.strictEqual(fired, true);
});
it("フォローしていないリモートユーザーの投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, false);
});
it("フォローしているユーザーのダイレクト投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [ayano.id],
},
kyoko,
),
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしているユーザーのホーム投稿が流れる", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko),
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
it("フォローしていないローカルユーザーのホーム投稿は流れない", async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() =>
api("notes/create", { text: "foo", visibility: "home" }, chitose),
(msg) => msg.type === "note" && msg.body.userId === chitose.id,
);
assert.strictEqual(fired, false);
});
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", () =>
async () => {
const fired = await waitFire(
ayano,
"hybridTimeline", // ayano:Hybrid
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
chitose,
),
(msg) => msg.type === "note" && msg.body.userId === chitose.id,
);
assert.strictEqual(fired, false);
});
});
describe("Global Timeline", () => {
it("フォローしていないローカルユーザーの投稿が流れる", () => async () => {
const fired = await waitFire(
ayano,
"globalTimeline", // ayano:Global
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
);
assert.strictEqual(fired, true);
});
it("フォローしていないリモートユーザーの投稿が流れる", () => async () => {
const fired = await waitFire(
ayano,
"globalTimeline", // ayano:Global
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
);
assert.strictEqual(fired, true);
});
it("ホーム投稿は流れない", () => async () => {
const fired = await waitFire(
ayano,
"globalTimeline", // ayano:Global
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko posts
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, false);
});
});
describe("UserList Timeline", () => {
it("リストに入れているユーザーの投稿が流れる", () => async () => {
const fired = await waitFire(
chitose,
"userList",
() => api("notes/create", { text: "foo" }, ayano),
(msg) => msg.type === "note" && msg.body.userId === ayano.id,
{ listId: list.id },
);
assert.strictEqual(fired, true);
});
it("リストに入れていないユーザーの投稿は流れない", () => async () => {
const fired = await waitFire(
chitose,
"userList",
() => api("notes/create", { text: "foo" }, chinatsu),
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id,
{ listId: list.id },
);
assert.strictEqual(fired, false);
});
// #4471
it("リストに入れているユーザーのダイレクト投稿が流れる", () =>
async () => {
const fired = await waitFire(
chitose,
"userList",
() =>
api(
"notes/create",
{
text: "foo",
visibility: "specified",
visibleUserIds: [chitose.id],
},
ayano,
),
(msg) => msg.type === "note" && msg.body.userId === ayano.id,
{ listId: list.id },
);
assert.strictEqual(fired, true);
});
// #4335
it("リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない", () =>
async () => {
const fired = await waitFire(
chitose,
"userList",
() =>
api(
"notes/create",
{ text: "foo", visibility: "followers" },
kyoko,
),
(msg) => msg.type === "note" && msg.body.userId === kyoko.id,
{ listId: list.id },
);
assert.strictEqual(fired, false);
});
});
describe("Hashtag Timeline", () => {
it("指定したハッシュタグの投稿が流れる", () =>
new Promise<void>(async (done) => {
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
assert.deepStrictEqual(body.text, "#foo");
ws.close();
done();
}
},
{
q: [["foo"]],
},
);
post(chitose, {
text: "#foo",
});
}));
it("指定したハッシュタグの投稿が流れる (AND)", () =>
new Promise<void>(async (done) => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
if (body.text === "#foo") fooCount++;
if (body.text === "#bar") barCount++;
if (body.text === "#foo #bar") fooBarCount++;
}
},
{
q: [["foo", "bar"]],
},
);
post(chitose, {
text: "#foo",
});
post(chitose, {
text: "#bar",
});
post(chitose, {
text: "#foo #bar",
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
ws.close();
done();
}, 3000);
}));
it("指定したハッシュタグの投稿が流れる (OR)", () =>
new Promise<void>(async (done) => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
if (body.text === "#foo") fooCount++;
if (body.text === "#bar") barCount++;
if (body.text === "#foo #bar") fooBarCount++;
if (body.text === "#piyo") piyoCount++;
}
},
{
q: [["foo"], ["bar"]],
},
);
post(chitose, {
text: "#foo",
});
post(chitose, {
text: "#bar",
});
post(chitose, {
text: "#foo #bar",
});
post(chitose, {
text: "#piyo",
});
setTimeout(() => {
assert.strictEqual(fooCount, 1);
assert.strictEqual(barCount, 1);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 0);
ws.close();
done();
}, 3000);
}));
it("指定したハッシュタグの投稿が流れる (AND + OR)", () =>
new Promise<void>(async (done) => {
let fooCount = 0;
let barCount = 0;
let fooBarCount = 0;
let piyoCount = 0;
let waaaCount = 0;
const ws = await connectStream(
chitose,
"hashtag",
({ type, body }) => {
if (type == "note") {
if (body.text === "#foo") fooCount++;
if (body.text === "#bar") barCount++;
if (body.text === "#foo #bar") fooBarCount++;
if (body.text === "#piyo") piyoCount++;
if (body.text === "#waaa") waaaCount++;
}
},
{
q: [["foo", "bar"], ["piyo"]],
},
);
post(chitose, {
text: "#foo",
});
post(chitose, {
text: "#bar",
});
post(chitose, {
text: "#foo #bar",
});
post(chitose, {
text: "#piyo",
});
post(chitose, {
text: "#waaa",
});
setTimeout(() => {
assert.strictEqual(fooCount, 0);
assert.strictEqual(barCount, 0);
assert.strictEqual(fooBarCount, 1);
assert.strictEqual(piyoCount, 1);
assert.strictEqual(waaaCount, 0);
ws.close();
done();
}, 3000);
}));
});
});
});