Skip to content

Commit 9ce4e31

Browse files
committed
Add test for actor GraphQL API using AI
Prompts: Actor 를 위한 GraphQL 빌더를 추가했습니다. @packages/graphql/src/actor.test.ts 에 이를 위한 테스트를 생성해주세요. 파일 추가 직후 `mise run fmt` 로 라이선스를 추가하세요. 최소한 다음과 같은 테스트가 필요합니다. - 액터 생성 - 로컬 액터 조회 - 리모트 액터 조회 Assisted-by: Codex:gpt-5-6-sol
1 parent 5ca653b commit 9ce4e31

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

‎packages/graphql/src/actor.test.ts‎

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
// DrFed: A web-based platform for developing and debugging ActivityPub apps
2+
// Copyright (C) 2026 DrFed team
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 published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (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+
17+
// oxlint-disable max-lines
18+
19+
import assert from "node:assert/strict";
20+
21+
import { type Database, schema } from "@drfed/models";
22+
import { describe, it } from "@logtape/testing-node/autoload";
23+
24+
import { withTestHarness } from "./harness.test.ts";
25+
26+
const accepted = new Date("2026-08-04T00:00:00.000Z");
27+
const created = new Date("2026-08-04T00:00:00.000Z");
28+
const expires = new Date("2030-08-04T00:00:00.000Z");
29+
const ok = 200;
30+
31+
const accountId = "00000000-0000-4000-8000-000000000001";
32+
const localInstanceId = "00000000-0000-4000-8000-000000000101";
33+
const remoteInstanceId = "00000000-0000-4000-8000-000000000102";
34+
const localActorId = "00000000-0000-4000-8000-000000000201";
35+
const remoteActorId = "00000000-0000-4000-8000-000000000202";
36+
const sessionId = "00000000-0000-4000-8000-000000000301";
37+
const accessToken = "test-access-token";
38+
39+
const genActorsMutation = `
40+
mutation GenActors($instance: ID!, $size: Int!) {
41+
genActors(instance: $instance, size: $size) {
42+
resultType: __typename
43+
... on CreateActorsSuccess {
44+
actors {
45+
username
46+
}
47+
}
48+
... on CreateActorsError {
49+
type
50+
message
51+
}
52+
}
53+
}
54+
`;
55+
56+
const actorQuery = `
57+
query Actor($id: ID!) {
58+
node(id: $id) {
59+
... on Actor {
60+
id
61+
iri
62+
handle
63+
type
64+
username
65+
instance {
66+
uuid
67+
location
68+
host
69+
}
70+
inboxUrl
71+
outboxUrl
72+
avatarUrl
73+
followersUrl
74+
followeesUrl
75+
headerUrl
76+
profileUrl
77+
featuredUrl
78+
}
79+
}
80+
}
81+
`;
82+
83+
describe("Mutation.genActors", () => {
84+
it("creates local actors", async () => {
85+
await withTestHarness(async ({ db, post }) => {
86+
const auth = await seedAuthenticatedLocalInstance(db);
87+
88+
const response = await post(
89+
{
90+
query: genActorsMutation,
91+
variables: {
92+
instance: globalId("Instance", localInstanceId),
93+
size: 2,
94+
},
95+
},
96+
auth,
97+
);
98+
99+
assert.equal(response.status, ok);
100+
const body = await response.json();
101+
assert.equal(body.errors, undefined);
102+
assert.equal(body.data.genActors.resultType, "CreateActorsSuccess");
103+
assert.equal(body.data.genActors.actors.length, 2);
104+
assert.equal(
105+
body.data.genActors.actors.every(
106+
({ username }: { username: unknown }) => typeof username === "string",
107+
),
108+
true,
109+
);
110+
111+
const actors = await db.select().from(schema.actors);
112+
assert.equal(actors.length, 2);
113+
assert.equal(
114+
actors.every(
115+
(actor) =>
116+
actor.instanceId === localInstanceId &&
117+
actor.location === "Local" &&
118+
actor.type === "Person",
119+
),
120+
true,
121+
);
122+
123+
const localActors = await db.select().from(schema.localActors);
124+
assert.equal(localActors.length, 2);
125+
assert.deepEqual(
126+
new Set(localActors.map(({ id }) => id)),
127+
new Set(actors.map(({ id }) => id)),
128+
);
129+
});
130+
});
131+
});
132+
133+
describe("Actor", () => {
134+
it("returns a local actor", async () => {
135+
await withTestHarness(async ({ db, post }) => {
136+
await seedLocalActor(db);
137+
138+
const response = await post({
139+
query: actorQuery,
140+
variables: { id: globalId("Actor", localActorId) },
141+
});
142+
143+
assert.equal(response.status, ok);
144+
assert.deepEqual(await response.json(), {
145+
data: {
146+
node: {
147+
id: globalId("Actor", localActorId),
148+
iri: `https://test-instance.drfed.org/users/${localActorId}`,
149+
handle: "@alice@test-instance.drfed.org",
150+
type: "Person",
151+
username: "alice",
152+
instance: {
153+
uuid: localInstanceId,
154+
location: "Local",
155+
host: "test-instance.drfed.org",
156+
},
157+
inboxUrl: `https://test-instance.drfed.org/users/${localActorId}/inbox`,
158+
outboxUrl: `https://test-instance.drfed.org/users/${localActorId}/outbox`,
159+
avatarUrl: `https://test-instance.drfed.org/users/${localActorId}/avatar/avatar.png`,
160+
followersUrl: `https://test-instance.drfed.org/users/${localActorId}/followers`,
161+
followeesUrl: `https://test-instance.drfed.org/users/${localActorId}/followees`,
162+
headerUrl: `https://test-instance.drfed.org/users/${localActorId}/header/header.png`,
163+
profileUrl: "https://test-instance.drfed.org/@alice",
164+
featuredUrl: `https://test-instance.drfed.org/users/${localActorId}/featured`,
165+
},
166+
},
167+
});
168+
});
169+
});
170+
171+
it("returns a remote actor", async () => {
172+
await withTestHarness(async ({ db, post }) => {
173+
await seedRemoteActor(db);
174+
175+
const response = await post({
176+
query: actorQuery,
177+
variables: { id: globalId("Actor", remoteActorId) },
178+
});
179+
180+
assert.equal(response.status, ok);
181+
assert.deepEqual(await response.json(), {
182+
data: {
183+
node: {
184+
id: globalId("Actor", remoteActorId),
185+
iri: "https://remote.example.com/users/bob",
186+
handle: "@bob@remote.example.com",
187+
type: "Service",
188+
username: "bob",
189+
instance: {
190+
uuid: remoteInstanceId,
191+
location: "Remote",
192+
host: "remote.example.com",
193+
},
194+
inboxUrl: "https://remote.example.com/users/bob/inbox",
195+
outboxUrl: "https://remote.example.com/users/bob/outbox",
196+
avatarUrl: "https://remote.example.com/users/bob/avatar.png",
197+
followersUrl: "https://remote.example.com/users/bob/followers",
198+
followeesUrl: "https://remote.example.com/users/bob/followees",
199+
headerUrl: "https://remote.example.com/users/bob/header.png",
200+
profileUrl: "https://remote.example.com/@bob",
201+
featuredUrl: "https://remote.example.com/users/bob/featured",
202+
},
203+
},
204+
});
205+
});
206+
});
207+
});
208+
209+
function globalId(type: "Actor" | "Instance", id: string): string {
210+
return Buffer.from(`${type}:${id}`).toString("base64");
211+
}
212+
213+
async function seedAuthenticatedLocalInstance(
214+
db: Database,
215+
): Promise<RequestInit> {
216+
await db.insert(schema.accounts).values({
217+
id: accountId,
218+
email: "owner@example.com",
219+
name: "Owner",
220+
created,
221+
});
222+
await db.insert(schema.sessions).values({
223+
id: sessionId,
224+
accountId,
225+
tokenHash: await hashSecret(accessToken),
226+
});
227+
await seedLocalInstance(db);
228+
await db.insert(schema.instanceMembers).values({
229+
accountId,
230+
instanceId: localInstanceId,
231+
admin: true,
232+
accepted,
233+
created,
234+
});
235+
return { headers: { authorization: `Bearer ${accessToken}` } };
236+
}
237+
238+
async function seedLocalActor(db: Database): Promise<void> {
239+
await seedLocalInstance(db);
240+
await db.insert(schema.actors).values({
241+
id: localActorId,
242+
instanceId: localInstanceId,
243+
location: "Local",
244+
type: "Person",
245+
username: "alice",
246+
created,
247+
});
248+
await db.insert(schema.localActors).values({
249+
id: localActorId,
250+
avatar: "avatar.png",
251+
header: "header.png",
252+
});
253+
}
254+
255+
async function seedLocalInstance(db: Database): Promise<void> {
256+
await db.insert(schema.instances).values({
257+
id: localInstanceId,
258+
location: "Local",
259+
created,
260+
});
261+
await db.insert(schema.localInstances).values({
262+
id: localInstanceId,
263+
slug: "test-instance",
264+
expires,
265+
});
266+
}
267+
268+
async function seedRemoteActor(db: Database): Promise<void> {
269+
await db.insert(schema.instances).values({
270+
id: remoteInstanceId,
271+
location: "Remote",
272+
created,
273+
});
274+
await db.insert(schema.remoteInstances).values({
275+
id: remoteInstanceId,
276+
host: "remote.example.com",
277+
});
278+
await db.insert(schema.actors).values({
279+
id: remoteActorId,
280+
instanceId: remoteInstanceId,
281+
location: "Remote",
282+
type: "Service",
283+
username: "bob",
284+
created,
285+
});
286+
await db.insert(schema.remoteActors).values({
287+
id: remoteActorId,
288+
iriUrl: "https://remote.example.com/users/bob",
289+
inboxUrl: "https://remote.example.com/users/bob/inbox",
290+
outboxUrl: "https://remote.example.com/users/bob/outbox",
291+
avatarUrl: "https://remote.example.com/users/bob/avatar.png",
292+
followersUrl: "https://remote.example.com/users/bob/followers",
293+
followeesUrl: "https://remote.example.com/users/bob/followees",
294+
headerUrl: "https://remote.example.com/users/bob/header.png",
295+
profileUrl: "https://remote.example.com/@bob",
296+
featuredUrl: "https://remote.example.com/users/bob/featured",
297+
});
298+
}
299+
300+
async function hashSecret(raw: string): Promise<string> {
301+
return new Uint8Array(
302+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw)),
303+
).toHex();
304+
}

‎packages/graphql/src/actor.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ builder.mutationFields((t) => ({
326326
schema.instances,
327327
eq(schema.instanceMembers.instanceId, schema.instances.id),
328328
)
329+
.innerJoin(
330+
schema.localInstances,
331+
eq(schema.instances.id, schema.localInstances.id),
332+
)
329333
.where(
330334
and(
331335
eq(schema.instanceMembers.accountId, account.id),

0 commit comments

Comments
 (0)