Skip to content
Merged
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
d10c1da
Extract shared Postgres test helpers
dahlia Apr 15, 2026
4cebc21
Add Postgres tests for following state
dahlia Apr 15, 2026
7bd58bb
Add Postgres tests for reaction lifecycle
dahlia Apr 15, 2026
6bd6d9f
Add GraphQL notification ordering test
dahlia Apr 15, 2026
d427530
Add Postgres tests for share lifecycle
dahlia Apr 15, 2026
5028202
Fix deletePost counter updates
dahlia Apr 15, 2026
6ec4a84
Add GraphQL tests for post mutations
dahlia Apr 15, 2026
2ae35d1
Add Postgres tests for timeline fanout
dahlia Apr 15, 2026
335ca61
Add Postgres tests for timeline queries
dahlia Apr 15, 2026
8274e5f
Add GraphQL tests for timeline queries
dahlia Apr 15, 2026
d602ff3
Add GraphQL tests for actor mutations
dahlia Apr 15, 2026
957c94a
Add stateful auth test helpers
dahlia Apr 15, 2026
f0c99a6
Add GraphQL tests for signup flow
dahlia Apr 15, 2026
e48d01b
Add GraphQL tests for login flow
dahlia Apr 15, 2026
5c5098e
Add GraphQL tests for passkey flows
dahlia Apr 15, 2026
538afb1
Fix local block relationship cleanup
dahlia Apr 15, 2026
a3ba9a5
Add GraphQL tests for invitation links
dahlia Apr 15, 2026
0a2a7f9
Fix invite validation and cleanup
dahlia Apr 15, 2026
d83baab
Fix notification merging in transactions
dahlia Apr 15, 2026
5828807
Add signup model tests
dahlia Apr 15, 2026
59fb7da
Add signin model tests
dahlia Apr 15, 2026
bff8587
Add passkey model tests
dahlia Apr 15, 2026
a130b26
Add session model tests
dahlia Apr 15, 2026
b50d782
Add actor model tests
dahlia Apr 15, 2026
355814e
Add notification query tests
dahlia Apr 15, 2026
8ec5aa9
Add account model DB tests
dahlia Apr 15, 2026
d8c7230
Add GraphQL account query tests
dahlia Apr 15, 2026
7781ac5
Add GraphQL search tests
dahlia Apr 15, 2026
d8d4be8
Add note source model tests
dahlia Apr 15, 2026
5068e76
Add note lifecycle tests
dahlia Apr 15, 2026
7602536
Add poll model vote tests
dahlia Apr 15, 2026
36386ea
Add GraphQL poll tests
dahlia Apr 15, 2026
ca2db17
Add article draft model tests
dahlia Apr 15, 2026
fd106cb
Add article source model tests
dahlia Apr 15, 2026
cb31166
Add article lifecycle model tests
dahlia Apr 15, 2026
e448bf8
Add GraphQL lookup helper tests
dahlia Apr 15, 2026
42ac7f8
Add GraphQL account mutation tests
dahlia Apr 15, 2026
3cc16e8
Add GraphQL misc tests
dahlia Apr 15, 2026
5bbef36
Add GraphQL document tests
dahlia Apr 15, 2026
ad87108
Add GraphQL webfinger tests
dahlia Apr 15, 2026
b8379b8
Extend poll model tests
dahlia Apr 15, 2026
d3be91e
Add more GraphQL post tests
dahlia Apr 15, 2026
220f7b6
Add remote post helper tests
dahlia Apr 15, 2026
b177023
Add post sync helper tests
dahlia Apr 15, 2026
1d265cc
Add actor model persistence tests
dahlia Apr 15, 2026
5eb0d64
Add more GraphQL actor tests
dahlia Apr 15, 2026
02a4ca3
Add account helper model tests
dahlia Apr 15, 2026
164dc15
Add APNS model tests
dahlia Apr 15, 2026
8e95377
Add GraphQL APNS tests
dahlia Apr 15, 2026
ba4da8f
Add article background tests
dahlia Apr 15, 2026
d2bf92c
Add GraphQL searchPost tests
dahlia Apr 15, 2026
f00055e
Add post medium model tests
dahlia Apr 15, 2026
67490d6
Add markup model tests
dahlia Apr 15, 2026
6f2d0a4
Add more GraphQL passkey tests
dahlia Apr 15, 2026
fb4f028
Add login error path tests
dahlia Apr 15, 2026
bf5ca79
Add signup failure-path tests
dahlia Apr 15, 2026
42ba1de
Add remote relationship model tests
dahlia Apr 15, 2026
6c7c5fe
Set up PostgreSQL in CI
dahlia Apr 15, 2026
061e29f
Fix blocking cleanup and invite refunds
dahlia Apr 15, 2026
86cc16a
Stabilize tests flagged in review
dahlia Apr 15, 2026
383ce2e
Fix duplicate follow notification handling
dahlia Apr 15, 2026
f295ab7
Clean up shared test helpers
dahlia Apr 15, 2026
c0afadb
Polish tests from review feedback
dahlia Apr 15, 2026
7f96178
Make poll tests time-independent
dahlia Apr 15, 2026
3bb2e3c
Preserve notification ordering on merges
dahlia Apr 15, 2026
446279c
Fix notification merge timestamp casting
dahlia Apr 15, 2026
950aabb
Avoid no-op notification merge side effects
dahlia Apr 16, 2026
f31c6cb
Isolate parallel test fixtures
dahlia Apr 16, 2026
4b9ae89
Keep notification merges properly scoped
dahlia Apr 16, 2026
c93173f
Clean up test fixtures and helpers
dahlia Apr 16, 2026
b256f8e
Clean up remote follows on block
dahlia Apr 16, 2026
78e56a0
Stub writable test disk methods
dahlia Apr 16, 2026
1d3eb11
Simplify invite validation guards
dahlia Apr 16, 2026
626090d
Retry notification merges after insert races
dahlia Apr 16, 2026
c03fbfe
Skip empty original-post lookups
dahlia Apr 16, 2026
65c932f
Avoid notification insert race aborts
dahlia Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.ci
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
DATABASE_URL=postgresql://postgres:password@localhost:5432/hackerspub
MODE=development
ORIGIN=http://localhost/
DATABASE_URL=postgresql://localhost/hackerspub
KV_URL=file:///tmp/kv.db
SECRET_KEY=SecretKeyForUnitTests
INSTANCE_ACTOR_KEY='{"kty":"RSA","alg":"RS256","n":"2fG1bUonD7dhunje36058uoNxW_CKREX1pd-63mRoYo-4XbLCyLqTbqy1ar-gdYNqd8OUhqjYXLvjRtS36N3DBb8zhIHmNvMFCMCVaX2hBp_kZhwBPch9Nk2g84rukVSOzjARaN6BhotlvJJ8RfHbTQHJnTIrgRZ5h9nifBfwjfCkS0S3plJfXzHo4XjlF5oTHRfvjM1sOGvsC4G9jZXgLP4MVgOZi3ry3NoYIhtnczk3lt7NWMtr37NK9jxXItXgo9juy-R88C8NPgq6wtuWr_fbMoERyq6wNG05MofnDAgk73PDid47fNkIueybgbFo8dJo68IcgMF8TiDeASp7sOl8TTpxlwOUtbtIGDB8FQDblVw16K7VAOD-ZEng0L_bmeDKf8VZ3WtH95QeBe_2JVPNecuut3NkHy4quWt6g4AujG08pDHWQUwT8ivyqsbBjxRAlqf1_8mcn_1EHnjuC_kuNYZ-lN9IuPk40hChbQpU8kLPTe0nx0JOSvTLumJT0ElDG0WJTOMJnB85B6Q68IiqTXMsKK3dWJWlstZ2Q6CHhAnxd1OyedS3Uq0fcynaePvD2AaA18fuxnTpM4XeFrLxuZ9DjuNLbIdkZMGQxB-o76eefHR1BTuz_sulFAL6Tapvn-P92A2XuGgL6x104MlLTpyOg9z3isFQOjsKTc","e":"AQAB","d":"W7APJnpCsp7wc1V0Vd93eJU00_HvWf2B7NpxH7lWJgKV755dT0MSFT9NCwzT42Vc_DEFwk7Imy-STefH1qPbam-lTUBSh-E4GuSbj9KxQeQv9N2Pitc5JtvWzl6HEOcm8Bkw8lsS88xBj7ZAmzfowy1XMLuCzYqxE1n04hgJARkaNp8iyBjuzOcYydeC5aFN-ZQfz1GV7eMUWWcTsLMt3sxXxtKHxD7fawkTSUE79F4FjvInhE9EIgeTYzXyXH-Wgiu2xkH-AfD8fMBGtflqJK2B3i-i2xnD1Wij1HigEcJBgkYg_JBolJqHbxPQge9BaGRnu3_gaq6Q6mlfDbfQkQZflNn0fCcp49D6YIcGD_1swObr8XDX7rgjSS5WmzMRQbNZsL6qCMohi0h65zcZk9ZUfnwX7vLKWjKTUfpF2uZqUOn_q1DsLMBDCu4ayFQPxA4C2GDlnR4stVFaWRtjNTNmiYWf7nO3H8PgoJHYpQt-vdn0i946INGiceJK5hhFkyiidh1ZJHgfPumOJeEDxBLlmOwaWHsqmplh5yq5RSCN1qpOqzGOJOop_RrHrZ5h9rRR1A4IQOFUE-8ivo8Mg4e6eLNDbLSSsQvRfgv47rbH5cP8LOzY4q9J3Kupl5a7df3Ar7FyyOAR3-QyzXajWLyiy0CJw4k1HtasCK6yBsE","p":"55S8olIxe8x8tu7DK0TpnDi-3bAR3jcetuZX4oSaTE_xu8kpl_TqALuecmswYHQA3IZ2i9rR0uIJHyeQuvHp0lTNhs-fwxIlnJTLXLZmNcwIVe5z2CMaL80WEBFIEG_6J4KNtt13Mg8h56Ij5d7Ry0NpVYEQCDege291LPN7x_cKf8jbdsRRFVckv9nZz8kQ6Prhn3kALV-PAxCum_sl07iz-YNZn0gRnYhP8pWddahdtUQeo_mCH__eHmIJAmpJsxufLgPXH9v0btuXhLz4P5pRO533wpmzE0t7U9pisPnMdi9NIssHyM0p6fEhPd87YBq8VMjkgYhXukYY0ah5YQ","q":"8Ozcf5BUMJnU6Ov5kQ2Ap6W1g_Ig_JOWonAkDHlHo0ElEcHRmkC5SeA7PCsbaEqGVq8Oqi6x90UaODedy-qSzjKx5i0FLKmDjb_4B4fLVFrUST6cQQu7bPcvWRRP2NyAN1AZrt0Y_uSbhIHFVOouHoMqVfQj0aTDK-sp1lPmPn9g7wch8y3Lc9iqcIgn43wy67XJtP5AYdplJrs4JuXYrZtbsfXsRdA2PO6mcazetIBVMETkuKIGF7gEGTI_8JpM23Y_ykM7XFwEhx4ecJtKw0-1e57w-8pt-nlfIbRNQDkJwKX-w6J5z6J6VgFAWp4jvSG15qoC5gU51xHkm_Exlw","dp":"V_jor8EJiz3jIpsRCR7kn7PuzchVAVVvFYvrVuGIu_Sin_OLGW6wdhbP2idd-UYYDa4G2poFm1bCoFMnZ9z-NiiA6vV0e2YpY5IirtWbflRD1mD_INw01nPSLchi19ux69BshUscPKgC-Tte57P9fnndSd39eSGolTuCB9F29D-kfWaP-E4bfz_bdgYL-CMpiVfE3g_ZQWNLsJ5ltltxwzwnImIDab628mEV_dFYP5n1_yYhfakLBZzthB05zvERjjiv_4r17eRgtrw0kvg2VfMJaNxZglNg87N45iHP2-sJANx3MQBvtJg7k-NF_XsP0zJU2OB37b0dlmWKiBq4AQ","dq":"BDSeRLvIPHDy_n3gBWAu9r41xO_dE4uf_YXnmzAix_7DHuQ7PG4Uze1UG2DFQbTLU4gKwX2_LYnPQ1v1LTITDfZklJgElxr-aOMI-Vite_N58S1enOQPiX9nHC72ldqDgnOrfxns7cYf0NhTEYBk_bNccdOpLGer4IBiYpNkWYLvtjxxo0URYBxQHVbDG313hhXOR5KInSyqx1pNSKCKf71OhHS-gxl3WOjxjtptqMj0s7sAIxjw8kkMCUSPSSvoW4xc4LL7vkj86z7jWSPc0jv59wZ3Pm3yLYUg2_3Bu3VDblF3eQLFDZLQf9_Vt7868Ho-KqCdHFbLA64UR4SWUQ","qi":"u1riaXXLRX6mnhMPbnxOBZDUbjqDv5Sq-4WawFWDQuiZvEn_Vp52tgVV7Ra9-kEM4CHTLdo2MZfLizjAXyDMrSWuDw8G_cVy8G75mg7c41s-TUSC-7ioqzbVvOkMDuZ_KoNqDLiDos3GT8XFXxVQcscU37Qo31L4usE2Y3mmlZfpm_GqGGh4rwkiWxjXrr5VtypehFZFRf_mbY8dLBJW-wF2f1uisUp8xh_s9cG2PPnzVO8Gi0V4Od2fK3OTky4SCp-mRqvbll94DCrFyS07P9_35K2k64wgKNVeg57n-1ByNPRQS5NjKbtMVUHX0Qnk2YawD3gQXb2IFhX4S8Gvzg","key_ops":["sign"],"ext":true}'
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ on:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: hackerspub
ports:
- 5432:5432
Comment thread
dahlia marked this conversation as resolved.
options: >-
--health-cmd "pg_isready -U postgres -d hackerspub"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
Expand All @@ -28,6 +42,8 @@ jobs:
- name: Build web-next
run: pnpm build
working-directory: web-next
- name: Run database migrations
run: npx @dotenvx/dotenvx run -f .env.ci -- deno task migrate
- name: Run tests
if: always()
run: npx @dotenvx/dotenvx run -f .env.ci -- deno task test
Expand Down
285 changes: 285 additions & 0 deletions graphql/account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import assert from "node:assert/strict";
import test from "node:test";
Comment thread
dahlia marked this conversation as resolved.
Comment thread
dahlia marked this conversation as resolved.
import { encodeGlobalID } from "@pothos/plugin-relay";
import * as vocab from "@fedify/vocab";
import { execute, parse } from "graphql";
import { updateAccountData } from "@hackerspub/models/account";
import { schema } from "./mod.ts";
import {
createFedCtx,
insertAccountWithActor,
makeGuestContext,
makeUserContext,
toPlainJson,
withRollback,
} from "../test/postgres.ts";

const viewerQuery = parse(`
query Viewer {
viewer {
username
name
handle
}
}
`);

const accountByUsernameQuery = parse(`
query AccountByUsername($username: String!) {
accountByUsername(username: $username) {
username
name
handle
}
}
`);

const invitationTreeQuery = parse(`
query InvitationTree {
invitationTree {
id
username
name
avatarUrl
inviterId
hidden
}
}
`);

const updateAccountMutation = parse(`
mutation UpdateAccount($input: UpdateAccountInput!) {
updateAccount(input: $input) {
account {
username
bio
locales
preferAiSummary
defaultNoteVisibility
defaultShareVisibility
}
}
}
`);

test("viewer returns the signed-in account and null for guests", async () => {
await withRollback(async (tx) => {
const account = await insertAccountWithActor(tx, {
username: "viewerquery",
name: "Viewer Query",
email: "viewerquery@example.com",
});

const signedInResult = await execute({
schema,
document: viewerQuery,
contextValue: makeUserContext(tx, account.account),
onError: "NO_PROPAGATE",
});

assert.equal(signedInResult.errors, undefined);
assert.deepEqual(
toPlainJson(signedInResult.data),
{
viewer: {
username: "viewerquery",
name: "Viewer Query",
handle: "@viewerquery@localhost",
},
},
);

const guestResult = await execute({
schema,
document: viewerQuery,
contextValue: makeGuestContext(tx),
onError: "NO_PROPAGATE",
});

assert.equal(guestResult.errors, undefined);
assert.deepEqual(toPlainJson(guestResult.data), { viewer: null });
});
});

test("accountByUsername returns a local account by username", async () => {
await withRollback(async (tx) => {
const account = await insertAccountWithActor(tx, {
username: "lookupgraphql",
name: "Lookup GraphQL",
email: "lookupgraphql@example.com",
});

const result = await execute({
schema,
document: accountByUsernameQuery,
variableValues: { username: account.account.username },
contextValue: makeGuestContext(tx),
onError: "NO_PROPAGATE",
});

assert.equal(result.errors, undefined);
assert.deepEqual(toPlainJson(result.data), {
accountByUsername: {
username: "lookupgraphql",
name: "Lookup GraphQL",
handle: "@lookupgraphql@localhost",
},
});
});
});

test("invitationTree redacts hidden accounts", async () => {
await withRollback(async (tx) => {
const visible = await insertAccountWithActor(tx, {
username: "visibletree",
name: "Visible Tree",
email: "visibletree@example.com",
});
const hidden = await insertAccountWithActor(tx, {
username: "hiddentree",
name: "Hidden Tree",
email: "hiddentree@example.com",
});

const updated = await updateAccountData(tx, {
id: hidden.account.id,
hideFromInvitationTree: true,
});
assert.ok(updated != null);

const result = await execute({
schema,
document: invitationTreeQuery,
contextValue: makeGuestContext(tx),
onError: "NO_PROPAGATE",
});

assert.equal(result.errors, undefined);

const nodes = (result.data as {
invitationTree: Array<{
id: string;
username: string | null;
name: string | null;
avatarUrl: string;
inviterId: string | null;
hidden: boolean;
}>;
}).invitationTree;
const visibleNode = nodes.find((node) => node.id === visible.account.id);
const hiddenNode = nodes.find((node) => node.id === hidden.account.id);

assert.ok(visibleNode != null);
assert.ok(hiddenNode != null);
assert.equal(visibleNode.hidden, false);
assert.equal(visibleNode.username, "visibletree");
assert.equal(visibleNode.name, "Visible Tree");

assert.equal(hiddenNode.hidden, true);
assert.equal(hiddenNode.username, null);
assert.equal(hiddenNode.name, null);
assert.equal(
hiddenNode.avatarUrl,
"https://gravatar.com/avatar/?d=mp&s=128",
);
});
});

test("updateAccount updates profile preferences for the signed-in account", async () => {
await withRollback(async (tx) => {
const account = await insertAccountWithActor(tx, {
username: "updateaccountgraphql",
name: "Update Account GraphQL",
email: "updateaccountgraphql@example.com",
});

const fedCtx = createFedCtx(tx);
fedCtx.getActor = (identifier: string) =>
Promise.resolve(
new vocab.Person({
id: fedCtx.getActorUri(identifier),
}),
);

const result = await execute({
schema,
document: updateAccountMutation,
variableValues: {
input: {
id: encodeGlobalID("Account", account.account.id),
bio: "Updated profile bio",
locales: ["ko-KR", "en-US"],
preferAiSummary: true,
hideFromInvitationTree: true,
hideForeignLanguages: true,
defaultNoteVisibility: "FOLLOWERS",
defaultShareVisibility: "UNLISTED",
},
},
contextValue: makeUserContext(tx, account.account, { fedCtx }),
onError: "NO_PROPAGATE",
});

assert.equal(result.errors, undefined);
assert.deepEqual(toPlainJson(result.data), {
updateAccount: {
account: {
username: "updateaccountgraphql",
bio: "Updated profile bio",
locales: ["ko-KR", "en-US"],
preferAiSummary: true,
defaultNoteVisibility: "FOLLOWERS",
defaultShareVisibility: "UNLISTED",
},
},
});

const stored = await tx.query.accountTable.findFirst({
where: { id: account.account.id },
});
assert.ok(stored != null);
assert.equal(stored.hideFromInvitationTree, true);
assert.equal(stored.hideForeignLanguages, true);
assert.deepEqual(stored.locales, ["ko-KR", "en-US"]);
assert.equal(stored.preferAiSummary, true);
assert.equal(stored.noteVisibility, "followers");
assert.equal(stored.shareVisibility, "unlisted");
});
});

test("updateAccount rejects a second username change", async () => {
await withRollback(async (tx) => {
const account = await insertAccountWithActor(tx, {
username: "renameonce",
name: "Rename Once",
email: "renameonce@example.com",
});

const renamed = await updateAccountData(tx, {
id: account.account.id,
username: "renamedonce",
});
assert.ok(renamed != null);
assert.ok(renamed.usernameChanged != null);

const result = await execute({
schema,
document: updateAccountMutation,
variableValues: {
input: {
id: encodeGlobalID("Account", account.account.id),
username: "renamedtwice",
},
},
contextValue: makeUserContext(tx, { ...account.account, ...renamed }),
onError: "NO_PROPAGATE",
});

assert.deepEqual(toPlainJson(result.data), { updateAccount: null });
assert.equal(result.errors?.length, 1);
assert.equal(
result.errors?.[0].message,
"Username cannot be changed after it has been changed.",
);
});
});
Loading
Loading