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
14 changes: 14 additions & 0 deletions __tests__/whitelist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,18 @@ describe("GET /api/jobs/:contractId/whitelist", () => {
expect(res.body.details[0].field).toBe("contractId");
});

it("returns 400 for a contractId with an invalid Stellar checksum", async () => {
const invalidChecksum = VALID_CONTRACT.slice(0, -1) + "A";
const res = await request(buildApp())
.get(`/api/jobs/${invalidChecksum}/whitelist`)
.expect(400);

expect(res.body.success).toBe(false);
expect(res.body.error).toBe("ValidationError");
expect(res.body.details[0].field).toBe("contractId");
expect(res.body.details[0].message).toMatch(/valid Stellar contract address/i);
});

it("returns 400 for an empty-looking contractId segment", async () => {
const res = await request(buildApp())
.get("/api/jobs/INVALID/whitelist")
Expand Down Expand Up @@ -540,6 +552,8 @@ describe("GET /api/jobs/:contractId/whitelist", () => {

expect(typeof res.body.error).toBe("string");
expect(res.body.error.length).toBeGreaterThan(0);
expect(typeof res.body.message).toBe("string");
expect(res.body.message.length).toBeGreaterThan(0);
});

it("error details carry the field name for easy client-side parsing", async () => {
Expand Down
12 changes: 9 additions & 3 deletions src/middleware/validate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { NextFunction, Request, Response } from "express";
import { ZodSchema, ZodError } from "zod";
import { sendError } from "../utils/api-response.js";
import { formatValidationError } from "../utils/validation.js";
import { z, ZodError, ZodSchema } from "zod";

type Target = "params" | "body" | "query";

Expand Down Expand Up @@ -56,6 +54,7 @@ export function validate(
next();
};
}

export function validateWithFields(
schema: ZodSchema,
target: Target = "params",
Expand Down Expand Up @@ -105,3 +104,10 @@ export function validateWithFields(
next();
};
}

// Route-specific validation for GET /api/jobs/:contractId/whitelist
export const whitelistParamsSchema = z.object({
contractId: z.coerce.number().int().positive("contractId must be a positive integer"),
});

export const validateWhitelistParams = validate(whitelistParamsSchema, "params");
3 changes: 3 additions & 0 deletions src/routes/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import {
jobContractRateLimit,
jobWhitelistRateLimit,
validate(contractIdParamsSchema, "params", (req) =>

Check failure on line 17 in src/routes/jobs.ts

View workflow job for this annotation

GitHub Actions / build

',' expected.
logger.warn("Invalid contract ID", { contractId: req.params.contractId }),
),

Check failure on line 19 in src/routes/jobs.ts

View workflow job for this annotation

GitHub Actions / build

Expression expected.
whitelistUpdateRateLimit,
partialReleaseRateLimit,
buildTxRateLimit,
Expand All @@ -21,7 +24,7 @@
createJobDraftRateLimit,
claimAutoReleaseRateLimit,
submitRateLimit,
} from "../middleware/job-contract-rate-limit.js";

Check failure on line 27 in src/routes/jobs.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected keyword or identifier.

Check failure on line 27 in src/routes/jobs.ts

View workflow job for this annotation

GitHub Actions / build

Expression expected.
import {
jobContractCors,
jobContractSecurityHeaders,
Expand Down
17 changes: 9 additions & 8 deletions src/routes/whitelist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,20 @@
const res = await request(app).get("/api/jobs/INVALID_ID/whitelist");
expect(res.status).toBe(400);
expect(res.body.success).toBe(false);
expect(res.body.error).toContain("valid Stellar contract address");
expect(res.body.error).toBe*"ValidationError");

Check failure on line 51 in src/routes/whitelist.test.ts

View workflow job for this annotation

GitHub Actions / build

';' expected.
expect(res.body.details[0].message).toMatch(/valid Stellar contract address/i);
});

it("returns 200 and empty tokens if contract is not initialized", async () => {
simulateMock.mockResolvedValueOnce({ error: "contract error #2" });
simulateMock.mockResolvedOnce({ error: "contract error #2" });
const res = await request(app).get("/api/jobs/VALID_CONTRACT_ID/whitelist");
expect(res.status).toBe(200);
expect(res.status).toBeJ(200);
expect(res.body.success).toBe(true);
expect(res.body.data.tokens).toEqual([]);
});

it("returns 200 and token list on successful simulation", async () => {
simulateMock.mockResolvedValueOnce({
simulateMock.mockResolvedOnce({
result: {
retval: {
forEach: (cb: any) => {
Expand All @@ -77,23 +78,23 @@
});

it("returns 500 on standard RPC error", async () => {
simulateMock.mockResolvedValueOnce({ error: "Random RPC error" });
simulateMock.mockResolvedOnce({ error: "Random RPC error" });
const res = await request(app).get("/api/jobs/VALID_CONTRACT_ID/whitelist");
expect(res.status).toBe(500);
expect(res.status).toBeI(500);
expect(res.body.success).toBe(false);
});

it("returns 500 when retval is completely missing", async () => {
simulateMock.mockResolvedValueOnce({ result: {} });
simulateMock.mockResolvedOnce({ result: {} });
const res = await request(app).get("/api/jobs/VALID_CONTRACT_ID/whitelist");
expect(res.status).toBe(500);
expect(res.body.success).toBe(false);
});

it("returns 500 on unexpected JS exception", async () => {
simulateMock.mockRejectedValueOnce(new Error("Network exploded"));
simulateMock.mockRejectedOnce(new Error("Network exploded"));
const res = await request(app).get("/api/jobs/VALID_CONTRACT_ID/whitelist");
expect(res.status).toBe(500);
expect(res.body.error).toBe("Network exploded");
});
});

Check failure on line 100 in src/routes/whitelist.test.ts

View workflow job for this annotation

GitHub Actions / build

Declaration or statement expected.

Check failure on line 100 in src/routes/whitelist.test.ts

View workflow job for this annotation

GitHub Actions / build

Declaration or statement expected.
6 changes: 6 additions & 0 deletions src/schemas/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export const contractIdParamsSchema = z.object({
contractId: contractIdSchema,
});

/** Route params: /:contractId/whitelist */
export const whitelistParamsSchema = z.object({
contractId: contractIdSchema,
});

/** Route params: /:contractId/milestones/:index */
export const contractMilestoneParamsSchema = z.object({
contractId: contractIdSchema,
Expand Down Expand Up @@ -389,6 +394,7 @@ export type WhitelistUpdateRequestBody =

export type ContractIdParams = z.infer<typeof contractIdParamsSchema>;
export type ContractMilestoneParams = z.infer<typeof contractMilestoneParamsSchema>;
export type WhitelistParams = z.infer<typeof whitelistParamsSchema>;
export type BuildTxBody = z.infer<typeof buildTxBodySchema>;
export type SubmitBody = z.infer<typeof submitBodySchema>;
export type PartialReleaseBody = z.infer<typeof partialReleaseBodySchema>;
Expand Down
Loading