feat: implement full landing, auth, dashboard, and browser unlock flows
This commit is contained in:
247
test/app.test.js
247
test/app.test.js
@@ -21,66 +21,161 @@ function createApp() {
|
||||
});
|
||||
}
|
||||
|
||||
function postJSON(app, path, payload, secret) {
|
||||
const rawBody = JSON.stringify(payload);
|
||||
const sig = hmacSHA256Hex(rawBody, secret);
|
||||
function call(app, { method, path, headers = {}, body = "", query = {} }) {
|
||||
return app.handleRequest({
|
||||
method: "POST",
|
||||
method,
|
||||
path,
|
||||
headers: { "x-signature": `sha256=${sig}` },
|
||||
rawBody,
|
||||
headers,
|
||||
rawBody: body,
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
test("rejects invalid X webhook signature", () => {
|
||||
function postJSONWebhook(app, path, payload, secret) {
|
||||
const rawBody = JSON.stringify(payload);
|
||||
const sig = hmacSHA256Hex(rawBody, secret);
|
||||
|
||||
return call(app, {
|
||||
method: "POST",
|
||||
path,
|
||||
headers: { "x-signature": `sha256=${sig}` },
|
||||
body: rawBody,
|
||||
});
|
||||
}
|
||||
|
||||
test("GET / renders landing page", () => {
|
||||
const app = createApp();
|
||||
const response = app.handleRequest({
|
||||
const response = call(app, { method: "GET", path: "/" });
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.body, /From X Article to audiobook in one mention/);
|
||||
});
|
||||
|
||||
test("unauthenticated /app redirects to /login with returnTo", () => {
|
||||
const app = createApp();
|
||||
const response = call(app, { method: "GET", path: "/app" });
|
||||
assert.equal(response.status, 303);
|
||||
assert.match(response.headers.location, /^\/login\?/);
|
||||
assert.match(response.headers.location, /returnTo=%2Fapp/);
|
||||
});
|
||||
|
||||
test("POST /auth/dev-login sets cookie and redirects", () => {
|
||||
const app = createApp();
|
||||
const response = call(app, {
|
||||
method: "POST",
|
||||
path: "/auth/dev-login",
|
||||
body: "userId=matiss&returnTo=%2Fapp",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 303);
|
||||
assert.equal(response.headers.location, "/app");
|
||||
assert.match(response.headers["set-cookie"], /^xartaudio_user=matiss/);
|
||||
});
|
||||
|
||||
test("authenticated dashboard topup + simulate mention flow", () => {
|
||||
const app = createApp();
|
||||
const cookieHeader = "xartaudio_user=alice";
|
||||
|
||||
const topup = call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/topup",
|
||||
headers: { cookie: cookieHeader },
|
||||
body: "amount=8",
|
||||
});
|
||||
assert.equal(topup.status, 303);
|
||||
assert.match(topup.headers.location, /Added%208%20credits/);
|
||||
|
||||
const simulate = call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/simulate-mention",
|
||||
headers: { cookie: cookieHeader },
|
||||
body: "title=Hello&body=This+is+the+article+body",
|
||||
});
|
||||
assert.equal(simulate.status, 303);
|
||||
assert.match(simulate.headers.location, /^\/audio\//);
|
||||
|
||||
const dashboard = call(app, {
|
||||
method: "GET",
|
||||
path: "/app",
|
||||
headers: { cookie: cookieHeader },
|
||||
});
|
||||
assert.equal(dashboard.status, 200);
|
||||
assert.match(dashboard.body, /Recent audiobooks/);
|
||||
assert.match(dashboard.body, /Hello/);
|
||||
});
|
||||
|
||||
test("audio flow requires auth for unlock and supports permanent unlock", () => {
|
||||
const app = createApp();
|
||||
|
||||
call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/topup",
|
||||
headers: { cookie: "xartaudio_user=owner" },
|
||||
body: "amount=5",
|
||||
});
|
||||
call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/topup",
|
||||
headers: { cookie: "xartaudio_user=viewer" },
|
||||
body: "amount=5",
|
||||
});
|
||||
|
||||
const generated = call(app, {
|
||||
method: "POST",
|
||||
path: "/app/actions/simulate-mention",
|
||||
headers: { cookie: "xartaudio_user=owner" },
|
||||
body: "title=Owned+Audio&body=Body",
|
||||
});
|
||||
|
||||
const audioPath = generated.headers.location.split("?")[0];
|
||||
const assetId = audioPath.replace("/audio/", "");
|
||||
|
||||
const beforeUnlock = call(app, {
|
||||
method: "GET",
|
||||
path: audioPath,
|
||||
headers: { cookie: "xartaudio_user=viewer" },
|
||||
});
|
||||
assert.match(beforeUnlock.body, /Unlock required: 1 credits/);
|
||||
|
||||
const unlock = call(app, {
|
||||
method: "POST",
|
||||
path: `/audio/${assetId}/unlock`,
|
||||
headers: { cookie: "xartaudio_user=viewer" },
|
||||
});
|
||||
assert.equal(unlock.status, 303);
|
||||
|
||||
const afterUnlock = call(app, {
|
||||
method: "GET",
|
||||
path: audioPath,
|
||||
headers: { cookie: "xartaudio_user=viewer" },
|
||||
});
|
||||
assert.match(afterUnlock.body, /Access granted/);
|
||||
|
||||
const wallet = call(app, {
|
||||
method: "GET",
|
||||
path: "/api/me/wallet",
|
||||
headers: { cookie: "xartaudio_user=viewer" },
|
||||
});
|
||||
const walletData = JSON.parse(wallet.body);
|
||||
assert.equal(walletData.balance, 4);
|
||||
});
|
||||
|
||||
test("X webhook invalid signature is rejected", () => {
|
||||
const app = createApp();
|
||||
const response = call(app, {
|
||||
method: "POST",
|
||||
path: "/api/webhooks/x",
|
||||
headers: { "x-signature": "sha256=deadbeef" },
|
||||
rawBody: JSON.stringify({ mentionPostId: "m1", callerUserId: "u1", parentPost: {} }),
|
||||
body: JSON.stringify({ mentionPostId: "m1", callerUserId: "u1", parentPost: {} }),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
});
|
||||
|
||||
test("serves pwa manifest route", () => {
|
||||
test("X webhook valid flow processes article", () => {
|
||||
const app = createApp();
|
||||
const response = app.handleRequest({
|
||||
method: "GET",
|
||||
path: "/manifest.webmanifest",
|
||||
headers: {},
|
||||
rawBody: "",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.headers["content-type"], /application\/manifest\+json/);
|
||||
const body = JSON.parse(response.body);
|
||||
assert.equal(body.short_name, "XArtAudio");
|
||||
});
|
||||
|
||||
test("X webhook returns not_article response and no charge", () => {
|
||||
const app = createApp();
|
||||
postJSON(app, "/api/webhooks/polar", { userId: "u1", credits: 5, eventId: "evt1" }, "polar-secret");
|
||||
|
||||
const response = postJSON(
|
||||
app,
|
||||
"/api/webhooks/x",
|
||||
{ mentionPostId: "m1", callerUserId: "u1", parentPost: { id: "p1" } },
|
||||
"x-secret",
|
||||
);
|
||||
|
||||
const body = JSON.parse(response.body);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.status, "not_article");
|
||||
assert.equal(app.engine.getWalletBalance("u1"), 5);
|
||||
});
|
||||
|
||||
test("X webhook processes article, charges caller, and exposes audio route", () => {
|
||||
const app = createApp();
|
||||
postJSON(app, "/api/webhooks/polar", { userId: "u1", credits: 5, eventId: "evt2" }, "polar-secret");
|
||||
|
||||
const response = postJSON(
|
||||
postJSONWebhook(app, "/api/webhooks/polar", { userId: "u1", credits: 4, eventId: "evt1" }, "polar-secret");
|
||||
const response = postJSONWebhook(
|
||||
app,
|
||||
"/api/webhooks/x",
|
||||
{
|
||||
@@ -88,74 +183,14 @@ test("X webhook processes article, charges caller, and exposes audio route", ()
|
||||
callerUserId: "u1",
|
||||
parentPost: {
|
||||
id: "p2",
|
||||
article: {
|
||||
id: "a2",
|
||||
title: "Article",
|
||||
body: "Hello from article",
|
||||
},
|
||||
article: { id: "a2", title: "T", body: "Hello" },
|
||||
},
|
||||
},
|
||||
"x-secret",
|
||||
);
|
||||
|
||||
const body = JSON.parse(response.body);
|
||||
assert.equal(response.status, 200);
|
||||
const body = JSON.parse(response.body);
|
||||
assert.equal(body.status, "completed");
|
||||
assert.equal(body.creditsCharged, 1);
|
||||
|
||||
const audioPageUnauthed = app.handleRequest({
|
||||
method: "GET",
|
||||
path: body.publicLink,
|
||||
headers: {},
|
||||
rawBody: "",
|
||||
});
|
||||
|
||||
assert.equal(audioPageUnauthed.status, 200);
|
||||
assert.match(audioPageUnauthed.body, /Sign in required before playback/);
|
||||
});
|
||||
|
||||
test("non-owner can unlock with same credits then access", () => {
|
||||
const app = createApp();
|
||||
postJSON(app, "/api/webhooks/polar", { userId: "u1", credits: 5, eventId: "evt3" }, "polar-secret");
|
||||
postJSON(app, "/api/webhooks/polar", { userId: "u2", credits: 5, eventId: "evt4" }, "polar-secret");
|
||||
|
||||
const makeAudio = postJSON(
|
||||
app,
|
||||
"/api/webhooks/x",
|
||||
{
|
||||
mentionPostId: "m3",
|
||||
callerUserId: "u1",
|
||||
parentPost: {
|
||||
id: "p3",
|
||||
article: {
|
||||
id: "a3",
|
||||
title: "Article",
|
||||
body: "Hello from article",
|
||||
},
|
||||
},
|
||||
},
|
||||
"x-secret",
|
||||
);
|
||||
|
||||
const { publicLink } = JSON.parse(makeAudio.body);
|
||||
const assetId = publicLink.replace("/audio/", "");
|
||||
|
||||
const unlock = app.handleRequest({
|
||||
method: "POST",
|
||||
path: `/api/audio/${assetId}/unlock`,
|
||||
headers: { "x-user-id": "u2" },
|
||||
rawBody: "",
|
||||
});
|
||||
|
||||
assert.equal(unlock.status, 200);
|
||||
assert.equal(app.engine.getWalletBalance("u2"), 4);
|
||||
|
||||
const pageAfterUnlock = app.handleRequest({
|
||||
method: "GET",
|
||||
path: publicLink,
|
||||
headers: { "x-user-id": "u2" },
|
||||
rawBody: "",
|
||||
});
|
||||
|
||||
assert.match(pageAfterUnlock.body, /Access granted/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user