Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11,801 changes: 4,361 additions & 7,440 deletions backend/package-lock.json

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions backend/src/__tests__/updatesApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import fs from "fs";
import http from "http";
import path from "path";
import express from "express";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";

const TEST_DB_PATH = path.join("/tmp", `stellar-goal-vault-updates-api-${process.pid}.db`);

process.env.DB_PATH = TEST_DB_PATH;
process.env.CONTRACT_ID = "";

type IndexModule = typeof import("../index");
type CampaignStoreModule = typeof import("../services/campaignStore");
type DbModule = typeof import("../services/db");

let app: express.Express;
let createCampaign: CampaignStoreModule["createCampaign"];
let initCampaignStore: CampaignStoreModule["initCampaignStore"];
let getDb: DbModule["getDb"];
let server: http.Server;
let baseUrl: string;

const CREATOR = `G${"A".repeat(55)}`;
const NON_CREATOR = `G${"B".repeat(55)}`;

beforeAll(async () => {
fs.rmSync(TEST_DB_PATH, { force: true });
({ app } = await import("../index"));
({ createCampaign, initCampaignStore } = await import("../services/campaignStore"));
({ getDb } = await import("../services/db"));
initCampaignStore();

await new Promise<void>((resolve) => {
server = http.createServer(app).listen(0, "127.0.0.1", () => {
const address = server.address() as { port: number };
baseUrl = `http://127.0.0.1:${address.port}`;
resolve();
});
});
});

afterAll(async () => {
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});

beforeEach(() => {
const db = getDb();
db.prepare(`DELETE FROM campaign_updates`).run();
db.prepare(`DELETE FROM campaign_events`).run();
db.prepare(`DELETE FROM pledges`).run();
db.prepare(`DELETE FROM campaigns`).run();
});

async function apiRequest(
method: "GET" | "POST",
endpoint: string,
body?: any,
): Promise<{ status: number; body: any }> {
const options: RequestInit = {
method,
headers: { "Content-Type": "application/json" },
};
if (body) {
options.body = JSON.stringify(body);
}

const response = await fetch(`${baseUrl}${endpoint}`, options);
const data = await response.json();
return { status: response.status, body: data };
}

describe("Campaign Updates API Endpoints", () => {
it("POST /api/campaigns/:id/updates returns 201 when posted by creator", async () => {
const campaign = createCampaign({
creator: CREATOR,
title: "API Update Test",
description: "Testing campaign updates HTTP API endpoint.",
assetCode: "USDC",
targetAmount: 500,
deadline: Math.floor(Date.now() / 1000) + 86400,
});

const res = await apiRequest("POST", `/api/campaigns/${campaign.id}/updates`, {
creator_address: CREATOR,
content: "Hello backers! Here is an update.",
});

expect(res.status).toBe(201);
expect(res.body.data.creatorAddress).toBe(CREATOR);
expect(res.body.data.content).toBe("Hello backers! Here is an update.");
});

it("POST /api/campaigns/:id/updates returns 403 when posted by non-creator", async () => {
const campaign = createCampaign({
creator: CREATOR,
title: "API Update Auth Test",
description: "Testing authorization check on updates endpoint.",
assetCode: "USDC",
targetAmount: 500,
deadline: Math.floor(Date.now() / 1000) + 86400,
});

const res = await apiRequest("POST", `/api/campaigns/${campaign.id}/updates`, {
creator_address: NON_CREATOR,
content: "Fake update from non-creator.",
});

expect(res.status).toBe(403);
expect(res.body.error.code).toBe("FORBIDDEN");
});

it("POST /api/campaigns/:id/updates returns 400 when content exceeds 2000 characters", async () => {
const campaign = createCampaign({
creator: CREATOR,
title: "API Update Validation Test",
description: "Testing 2000 char max length validation.",
assetCode: "USDC",
targetAmount: 500,
deadline: Math.floor(Date.now() / 1000) + 86400,
});

const longContent = "a".repeat(2001);

const res = await apiRequest("POST", `/api/campaigns/${campaign.id}/updates`, {
creator_address: CREATOR,
content: longContent,
});

expect(res.status).toBe(400);
expect(res.body.error.code).toBe("VALIDATION_ERROR");
});

it("GET /api/campaigns/:id/updates returns 200 with list of updates", async () => {
const campaign = createCampaign({
creator: CREATOR,
title: "API Get Updates Test",
description: "Testing GET updates endpoint.",
assetCode: "USDC",
targetAmount: 500,
deadline: Math.floor(Date.now() / 1000) + 86400,
});

await apiRequest("POST", `/api/campaigns/${campaign.id}/updates`, {
creator: CREATOR,
content: "First post",
});

await apiRequest("POST", `/api/campaigns/${campaign.id}/updates`, {
creatorAddress: CREATOR,
content: "Second post",
});

const res = await apiRequest("GET", `/api/campaigns/${campaign.id}/updates`);

expect(res.status).toBe(200);
expect(res.body.data).toHaveLength(2);
expect(res.body.data[0].content).toBe("Second post");
expect(res.body.data[1].content).toBe("First post");
});
});
23 changes: 10 additions & 13 deletions backend/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,22 @@ type CampaignStoreModule = typeof import('./services/campaignStore');
type DbModule = typeof import('./services/db');
type ValidationModule = typeof import('./validation/schemas');

let listCampaigns: CampaignStoreModule['listCampaigns'];
let parseCampaignListFilters: IndexModule['parseCampaignListFilters'];
let parseCampaignListQuery: ValidationModule['parseCampaignListQuery'];
let createCampaign: CampaignStoreModule['createCampaign'];
let addPledge: CampaignStoreModule['addPledge'];
let calculateProgress: CampaignStoreModule['calculateProgress'];
let initCampaignStore: CampaignStoreModule['initCampaignStore'];
let getDb: DbModule['getDb'];
let parseCampaignListFilters: IndexModule["parseCampaignListFilters"];
let listCampaigns: CampaignStoreModule["listCampaigns"];
let initCampaignStore: CampaignStoreModule["initCampaignStore"];
let createCampaign: CampaignStoreModule["createCampaign"];
let addPledge: CampaignStoreModule["addPledge"];
let calculateProgress: CampaignStoreModule["calculateProgress"];
let getDb: DbModule["getDb"];

const CREATOR = `G${'A'.repeat(55)}`;
const CONTRIBUTOR = `G${'B'.repeat(55)}`;

beforeAll(async () => {
fs.rmSync(TEST_DB_PATH, { force: true });
({ parseCampaignListFilters } = await import('./index'));
({ getDb } = await import('./services/db'));
({ parseCampaignListQuery } = await import('./validation/schemas'));
({ initCampaignStore, listCampaigns, createCampaign, addPledge, calculateProgress } =
await import('./services/campaignStore'));
({ parseCampaignListFilters } = await import("./index"));
({ listCampaigns, createCampaign, addPledge, calculateProgress, initCampaignStore } = await import("./services/campaignStore"));
({ getDb } = await import("./services/db"));
initCampaignStore();
}, 20000);

Expand Down
33 changes: 29 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import {
CampaignStatus,
claimCampaign,
createCampaign,
createComment,
deleteComment,
createCampaignUpdate,
getCampaign,
getCampaignUpdates,
getCampaignWithProgress,
getContributorSummary,
getGlobalStats,
Expand Down Expand Up @@ -63,7 +63,7 @@ import {
claimCampaignPayloadSchema,
commentIdSchema,
createCampaignPayloadSchema,
createCommentPayloadSchema,
createCampaignUpdatePayloadSchema,
createPledgePayloadSchema,
deleteCommentPayloadSchema,
parseCommentListPaginationQuery,
Expand Down Expand Up @@ -804,7 +804,32 @@ app.get('/api/campaigns/:id/timeline', (req: Request, res: Response) => {
res.json({ data: result.data, pagination: { nextCursor: result.nextCursor, hasMore: result.hasMore } });
});

app.get('/api/open-issues', async (_req: Request, res: Response) => {
app.post("/api/campaigns/:id/updates", (req: Request, res: Response) => {
const parsedId = parseCampaignId(req.params.id);
if (!parsedId.ok) {
sendValidationError(parsedId.issues);
}

const parsedBody = createCampaignUpdatePayloadSchema.safeParse(req.body);
if (!parsedBody.success) {
sendValidationError(parsedBody.error.issues);
}

const update = createCampaignUpdate(parsedId.value, parsedBody.data);
res.status(201).json({ data: update });
});

app.get("/api/campaigns/:id/updates", (req: Request, res: Response) => {
const parsedId = parseCampaignId(req.params.id);
if (!parsedId.ok) {
sendValidationError(parsedId.issues);
}

const updates = getCampaignUpdates(parsedId.value);
res.json({ data: updates });
});

app.get("/api/open-issues", async (_req: Request, res: Response) => {
const data = await fetchOpenIssues();
res.json({ data });
});
Expand Down
Loading
Loading