Skip to content

Commit fb17178

Browse files
committed
More tests
1 parent 11fa7e1 commit fb17178

File tree

3 files changed

+206
-2
lines changed

3 files changed

+206
-2
lines changed

src/message-impl.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
// BotKit by Fedify: A framework for creating ActivityPub bots
2+
// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
116
import { MemoryKvStore } from "@fedify/fedify/federation";
217
import {
318
Hashtag,

src/message-impl.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
import type { LanguageTag } from "@phensley/language-tag";
3838
import { unescape } from "@std/html/entities";
3939
import { generate as uuidv7 } from "@std/uuid/unstable-v7";
40-
import { FilterXSS } from "xss";
40+
import { FilterXSS, getDefaultWhiteList } from "xss";
4141
import type {
4242
Message,
4343
MessageClass,
@@ -272,7 +272,13 @@ export class MessageImpl<T extends MessageClass, TContextData>
272272
}
273273
}
274274

275-
const htmlXss = new FilterXSS();
275+
const allowList = getDefaultWhiteList();
276+
const htmlXss = new FilterXSS({
277+
allowList: {
278+
...allowList,
279+
a: [...allowList.a ?? [], "class", "translate"],
280+
},
281+
});
276282
const textXss = new FilterXSS({
277283
allowList: {},
278284
stripIgnoreTag: true,

src/session-impl.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// BotKit by Fedify: A framework for creating ActivityPub bots
2+
// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
import { type Context, MemoryKvStore } from "@fedify/fedify/federation";
17+
import {
18+
type Activity,
19+
Create,
20+
Note,
21+
Person,
22+
PUBLIC_COLLECTION,
23+
type Recipient,
24+
} from "@fedify/fedify/vocab";
25+
import { assertEquals } from "@std/assert/equals";
26+
import { assertInstanceOf } from "@std/assert/instance-of";
27+
import { BotImpl } from "./bot-impl.ts";
28+
import { SessionImpl } from "./session-impl.ts";
29+
import { mention, text } from "./text.ts";
30+
31+
Deno.test("SessionImpl.publish()", async (t) => {
32+
const kv = new MemoryKvStore();
33+
const bot = new BotImpl<void>({ kv, username: "bot" });
34+
const ctx = createMockContext(bot, "https://example.com");
35+
const session = new SessionImpl(bot, ctx);
36+
37+
await t.step("public", async () => {
38+
const publicMsg = await session.publish(text`Hello, world!`);
39+
assertEquals(ctx.sentActivities.length, 1);
40+
const { recipients, activity } = ctx.sentActivities[0];
41+
assertEquals(recipients, "followers");
42+
assertInstanceOf(activity, Create);
43+
assertEquals(activity.actorId, ctx.getActorUri(bot.identifier));
44+
assertEquals(activity.toIds, [PUBLIC_COLLECTION]);
45+
assertEquals(activity.ccIds, [ctx.getFollowersUri(bot.identifier)]);
46+
const object = await activity.getObject(ctx);
47+
assertInstanceOf(object, Note);
48+
assertEquals(object.attributionId, ctx.getActorUri(bot.identifier));
49+
assertEquals(object.toIds, [PUBLIC_COLLECTION]);
50+
assertEquals(object.ccIds, [ctx.getFollowersUri(bot.identifier)]);
51+
assertEquals(object.content, "<p>Hello, world!</p>");
52+
assertEquals(object.tagIds, []);
53+
assertEquals(publicMsg.id, object.id);
54+
assertEquals(publicMsg.text, "Hello, world!");
55+
assertEquals(publicMsg.html, "<p>Hello, world!</p>");
56+
assertEquals(publicMsg.visibility, "public");
57+
assertEquals(publicMsg.mentions, []);
58+
});
59+
60+
ctx.sentActivities = [];
61+
62+
await t.step("unlisted", async () => {
63+
const unlistedMsg = await session.publish(text`Hello!`, {
64+
visibility: "unlisted",
65+
});
66+
assertEquals(ctx.sentActivities.length, 1);
67+
const { recipients, activity } = ctx.sentActivities[0];
68+
assertEquals(recipients, "followers");
69+
assertInstanceOf(activity, Create);
70+
assertEquals(activity.actorId, ctx.getActorUri(bot.identifier));
71+
assertEquals(activity.toIds, [ctx.getFollowersUri(bot.identifier)]);
72+
assertEquals(activity.ccIds, [PUBLIC_COLLECTION]);
73+
const object = await activity.getObject(ctx);
74+
assertInstanceOf(object, Note);
75+
assertEquals(object.attributionId, ctx.getActorUri(bot.identifier));
76+
assertEquals(object.toIds, [ctx.getFollowersUri(bot.identifier)]);
77+
assertEquals(object.ccIds, [PUBLIC_COLLECTION]);
78+
assertEquals(object.content, "<p>Hello!</p>");
79+
assertEquals(object.tagIds, []);
80+
assertEquals(unlistedMsg.id, object.id);
81+
assertEquals(unlistedMsg.text, "Hello!");
82+
assertEquals(unlistedMsg.html, "<p>Hello!</p>");
83+
assertEquals(unlistedMsg.visibility, "unlisted");
84+
assertEquals(unlistedMsg.mentions, []);
85+
});
86+
87+
ctx.sentActivities = [];
88+
89+
await t.step("followers", async () => {
90+
const followersMsg = await session.publish(text`Hi!`, {
91+
visibility: "followers",
92+
});
93+
assertEquals(ctx.sentActivities.length, 1);
94+
const { recipients, activity } = ctx.sentActivities[0];
95+
assertEquals(recipients, "followers");
96+
assertInstanceOf(activity, Create);
97+
assertEquals(activity.actorId, ctx.getActorUri(bot.identifier));
98+
assertEquals(activity.toIds, [ctx.getFollowersUri(bot.identifier)]);
99+
assertEquals(activity.ccIds, []);
100+
const object = await activity.getObject(ctx);
101+
assertInstanceOf(object, Note);
102+
assertEquals(object.attributionId, ctx.getActorUri(bot.identifier));
103+
assertEquals(object.toIds, [ctx.getFollowersUri(bot.identifier)]);
104+
assertEquals(object.ccIds, []);
105+
assertEquals(object.content, "<p>Hi!</p>");
106+
assertEquals(object.tagIds, []);
107+
assertEquals(followersMsg.id, object.id);
108+
assertEquals(followersMsg.text, "Hi!");
109+
assertEquals(followersMsg.html, "<p>Hi!</p>");
110+
assertEquals(followersMsg.visibility, "followers");
111+
assertEquals(followersMsg.mentions, []);
112+
});
113+
114+
ctx.sentActivities = [];
115+
116+
await t.step("direct", async () => {
117+
const mentioned = new Person({
118+
id: new URL("https://example.com/ap/actor/john"),
119+
preferredUsername: "john",
120+
});
121+
const directMsg = await session.publish(
122+
text`Hey ${mention(mentioned)}!`,
123+
{ visibility: "direct" },
124+
);
125+
assertEquals(ctx.sentActivities.length, 1);
126+
const { recipients, activity } = ctx.sentActivities[0];
127+
assertEquals(recipients, [mentioned]);
128+
assertInstanceOf(activity, Create);
129+
assertEquals(activity.actorId, ctx.getActorUri(bot.identifier));
130+
assertEquals(activity.toIds, [mentioned.id]);
131+
assertEquals(activity.ccIds, []);
132+
const object = await activity.getObject(ctx);
133+
assertInstanceOf(object, Note);
134+
assertEquals(object.attributionId, ctx.getActorUri(bot.identifier));
135+
assertEquals(object.toIds, [mentioned.id]);
136+
assertEquals(object.ccIds, []);
137+
assertEquals(
138+
object.content,
139+
'<p>Hey <a href="https://example.com/ap/actor/john" translate="no" ' +
140+
'class="h-card u-url mention" target="_blank">@<span>john@example.com' +
141+
"</span></a>!</p>",
142+
);
143+
const tags = await Array.fromAsync(object.getTags());
144+
assertEquals(tags.length, 1);
145+
assertEquals(directMsg.id, object.id);
146+
assertEquals(directMsg.text, "Hey @john@example.com!");
147+
assertEquals(directMsg.html, object.content);
148+
assertEquals(directMsg.visibility, "direct");
149+
// assertEquals(directMsg.mentions, [mentioned]); // FIXME
150+
});
151+
});
152+
153+
interface SentActivity {
154+
recipients: "followers" | Recipient[];
155+
activity: Activity;
156+
}
157+
158+
interface MockContext extends Context<void> {
159+
sentActivities: SentActivity[];
160+
}
161+
162+
function createMockContext(
163+
bot: BotImpl<void>,
164+
origin: URL | string,
165+
): MockContext {
166+
const ctx = bot.federation.createContext(
167+
new URL(origin),
168+
undefined,
169+
) as MockContext;
170+
ctx.sentActivities = [];
171+
ctx.sendActivity = (_, recipients, activity) => {
172+
ctx.sentActivities.push({
173+
recipients: recipients === "followers"
174+
? "followers"
175+
: Array.isArray(recipients)
176+
? recipients
177+
: [recipients],
178+
activity,
179+
});
180+
return Promise.resolve();
181+
};
182+
return ctx;
183+
}

0 commit comments

Comments
 (0)