Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test-Suite for endpoints #26

Merged
merged 13 commits into from
Aug 13, 2024
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
coverage
dist
.env
358 changes: 299 additions & 59 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"@types/node": "^20.11.0",
"@types/pg": "^8.10.9",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.11",
"nodemon": "^3.0.2",
Expand Down
22 changes: 22 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import express, { Response } from "express";
import bodyparser from "body-parser";
import cors from "cors";
import compression from "compression";
import { requireHTTPS } from "./middleware/https";
import routes from "./routes";
import path from "path";

const app = express();

app.use(bodyparser.json());
app.use(compression());
app.use(cors());
app.use(requireHTTPS);

app.use(routes);
app.use("/", express.static(path.join(__dirname, "../npubcash-website/dist")));
app.get("*", (_, res: Response) => {
res.sendFile(path.join(__dirname, "../npubcash-website/dist/index.html"));
});

export default app;
13 changes: 13 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SimplePool, getPublicKey } from "nostr-tools";
import { CashuMint, CashuWallet } from "@cashu/cashu-ts";
import { LightningHandler } from "./utils/lightning";
import { BlinkProvider } from "./utils/blink";

export const wallet = new CashuWallet(new CashuMint(process.env.MINTURL!));
export const lnProvider = new LightningHandler(new BlinkProvider());
export const nostrPool = new SimplePool();

export let ZAP_PUBKEY: string;
if (process.env.ZAP_SECRET_KEY) {
ZAP_PUBKEY = getPublicKey(Buffer.from(process.env.ZAP_SECRET_KEY, "hex"));
}
135 changes: 135 additions & 0 deletions src/controller/__tests__/infoController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import supertest from "supertest";
import { beforeEach, describe, expect, test, vi } from "vitest";
import app from "../../app";
import { User } from "../../models";

const pubkey =
"ca9881c70e72981b356353453f4bbfd8153d209acd9b7b5b4200e80c7dec8c7a";
const npub = "npub1e2vgr3cww2vpkdtr2dzn7jalmq2n6gy6ekdhkk6zqr5qcl0v33aqa87qqk";

const mockAuthMiddleware = vi.hoisted(() =>
vi.fn((req, res, next) => {
req.authData = {
authorized: true,
data: { pubkey, npub },
};
next();
}),
);

vi.mock("../../middleware/auth.ts", () => ({
isAuthMiddleware: (path, method) => {
return mockAuthMiddleware;
},
}));

vi.mock("../../models/user.ts");

describe("PUT username", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should return 400 if username is missing", async () => {
vi.stubEnv("NODE_ENV", "development");
const res = await supertest(app)
.put("/api/v1/info/username")
.set("authorization", "validHeader");

expect(res.status).toBe(400);
expect(res.body).toEqual({ error: true, message: "Missing parameters" });
});
test("should return 400 if username starts with npub", async () => {
vi.stubEnv("NODE_ENV", "development");
const res = await supertest(app)
.put("/api/v1/info/username")
.send({ username: "npub1234" })
.set("authorization", "validHeader");

expect(res.status).toBe(400);
expect(res.body).toEqual({ error: true, message: "Invalid username" });
});
test("should return 400 is username is already taken", async () => {
vi.stubEnv("NODE_ENV", "development");
vi.mocked(User.checkIfUsernameExists).mockResolvedValueOnce(true);
const res = await supertest(app)
.put("/api/v1/info/username")
.send({ username: "testUser" })
.set("authorization", "validHeader");

expect(res.status).toBe(400);
expect(res.body).toEqual({
error: true,
message: "This username is already taken",
});
});

test("should return 400 is username is already set", async () => {
vi.stubEnv("NODE_ENV", "development");
vi.mocked(User.getUserByPubkey, { partial: true }).mockResolvedValueOnce({
pubkey: pubkey,
name: "username",
});
const res = await supertest(app)
.put("/api/v1/info/username")
.send({ username: "testUser" })
.set("authorization", "validHeader");

expect(res.status).toBe(400);
expect(res.body).toEqual({
error: true,
message: "Username already set",
});
});
});

describe("GET /info ", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("should return default info, when user is not set", async () => {
vi.stubEnv("MINTURL", "url");
const res = await supertest(app)
.get("/api/v1/info")
.set("authorization", "validHeader");
expect(mockAuthMiddleware).toHaveBeenCalled();
expect(res.status).toBe(200);
expect(res.body).toEqual({ username: null, npub, mintUrl: "url" });
});
});

describe("PUT /info/mint", () => {
test("should return 400 if URL is missing", async () => {
const res = await supertest(app)
.put("/api/v1/info/mint")
.set("authorization", "validHeader");
expect(res.status).toBe(400);
// expect(res.body).toEqual({ error: true, message: "Missing parameters" });
});
test("should return 400 if URL is invalid", async () => {
const res = await supertest(app)
.put("/api/v1/info/mint")
.set("authorization", "validHeader")
.send({ mintUrl: "invalid url" });
expect(res.status).toBe(400);
// expect(res.body).toEqual({ error: true, message: "Invalid URL" });
});

test("should return 500 if db failed", async () => {
vi.mocked(User.upsertMintByPubkey).mockRejectedValueOnce("error");
const res = await supertest(app)
.put("/api/v1/info/mint")
.set("authorization", "validHeader")
.send({ mintUrl: "https://validurl.com" });
expect(res.status).toBe(500);
// expect(res.body).toEqual({ error: true, message: "Invalid URL" });
});
test("should return 204 if successfull", async () => {
vi.mocked(User.upsertMintByPubkey).mockResolvedValueOnce();
const res = await supertest(app)
.put("/api/v1/info/mint")
.set("authorization", "validHeader")
.send({ mintUrl: "https://validurl.com" });
expect(res.status).toBe(204);
// expect(res.body).toEqual({ error: true, message: "Invalid URL" });
});
});
166 changes: 166 additions & 0 deletions src/controller/__tests__/lnurlController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import request from "supertest";
import { decodeAndValidateZapRequest } from "../../utils/nostr";
import app from "../../app";
import { Transaction, User } from "../../models";
import { createLnurlResponse } from "../../utils/lnurl";
import { lnProvider, wallet } from "../../config";

vi.mock("../../models/user.ts");
vi.mock("../../models/transaction.ts");

vi.mock("../../utils/nostr", () => ({
decodeAndValidateZapRequest: vi.fn(),
}));

vi.mock("../../utils/lnurl", () => ({
createLnurlResponse: vi.fn(),
}));

vi.mock("../utils/lnurl", async () => {
return {
createLnurlResponse: vi.fn(),
};
});

vi.mock("crypto", () => ({
createHash: () => ({
update: () => ({
digest: vi.fn().mockReturnValue("mockedHash"),
}),
}),
}));

vi.mock("../utils/lightning", () => ({
parseInvoice: vi.fn(),
}));

vi.mock("nostr-tools", () => ({
SimplePool: vi.fn(),
}));

vi.mock("../../config.ts", () => ({
wallet: {
requestMint: vi.fn(),
},
lnProvider: {
createInvoice: vi.fn(),
},
}));

describe("lnurlController", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
process.env.NODE_ENV = "development";
});

it("should return 401 for invalid npub", async () => {
const res = await request(app).get("/.well-known/lnurlp/npubIsInvalid");

expect(res.status).toBe(401);
expect(res.body).toEqual({});
});

it("should return 404 if user not found", async () => {
vi.mocked(User.getUserByName).mockResolvedValue(undefined);

const res = await request(app).get("/.well-known/lnurlp/nonexistentUser");

expect(res.status).toBe(404);
expect(res.body).toEqual({});
});

it("should return lnurl response if no amount provided", async () => {
vi.mocked(User.getUserByName, { partial: true }).mockResolvedValue({
name: "testUser",
mint_url: "https://mint.minibits.cash/Bitcoin",
pubkey: "testPubkey...",
});
vi.mocked(createLnurlResponse).mockReturnValue({
callback: "https://npub.cash/.well-known/lnurlp/testUser",
minSendable: 1000,
maxSendable: 100000,
metadata: "",
tag: "pay",
});

const res = await request(app).get("/.well-known/lnurlp/testUser");

expect(res.status).toBe(200);
expect(res.body).toEqual({
callback: "https://npub.cash/.well-known/lnurlp/testUser",
minSendable: 1000,
maxSendable: 100000,
metadata: "",
tag: "pay",
});
});

it("should return error for invalid amount", async () => {
vi.stubEnv("LNURL_MIN_AMOUNT", "10");
vi.stubEnv("LNURL_MAX_AMOUNT", "1000");
const res = await request(app).get("/.well-known/lnurlp/testUser?amount=5");

expect(res.status).toBe(500);
});

it("should return error for invalid zap request", async () => {
vi.mocked(decodeAndValidateZapRequest).mockImplementation(() => {
throw new Error("Invalid zap request");
});

const res = await request(app).get(
"/.well-known/lnurlp/testUser?amount=100&nostr=invalidZapRequest",
);

expect(res.status).toBe(400);
expect(res.body).toEqual({ error: true, message: "Invalid zap request" });
});

it("should return invoice for valid request without nostr", async () => {
vi.mocked(User.getUserByName, { partial: true }).mockResolvedValue({
name: "testUser",
mint_url: "https://mint.minibits.cash/Bitcoin",
pubkey: "testPubkey...",
});
vi.mocked(wallet.requestMint).mockResolvedValue({
pr: "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
hash: "456",
});
const lnProviderMock = vi
.mocked(lnProvider.createInvoice, { partial: true })
.mockResolvedValue({
paymentRequest: "invoice",
paymentHash: "hash",
});
vi.mocked(Transaction.createTransaction, {
partial: true,
}).mockResolvedValue({
mint_pr: "123",
mint_hash: "456",
server_pr: "invoice",
server_hash: "hash",
user: "testUser",
zap_request: undefined,
amount: 21,
fulfilled: false,
});

vi.stubEnv("LNURL_MIN_AMOUNT", "10");
vi.stubEnv("LNURL_MAX_AMOUNT", "1000000");

const res = await request(app).get(
"/.well-known/lnurlp/testUser?amount=21000",
);

expect(lnProviderMock).toHaveBeenCalledWith(
1500,
"Cashu Address",
undefined,
);

expect(res.status).toBe(200);
expect(res.body).toEqual({ pr: "invoice", routes: [] });
});
});
2 changes: 1 addition & 1 deletion src/controller/infoController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextFunction, Request, Response } from "express";
import { User } from "../models";
import { sign, verify } from "jsonwebtoken";
import { lnProvider } from "..";
import { lnProvider } from "../config";
import { PaymentJWTPayload } from "../types";
import { usernameRegex } from "../constants/regex";

Expand Down
3 changes: 2 additions & 1 deletion src/controller/lnurlController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express";
import { Event, nip19 } from "nostr-tools";

import { parseInvoice } from ".././utils/lightning";
import { lnProvider, wallet } from "..";
import { lnProvider, wallet } from "../config";
import { Transaction, User } from "../models";
import { createLnurlResponse } from "../utils/lnurl";
import { decodeAndValidateZapRequest } from "../utils/nostr";
Expand Down Expand Up @@ -74,6 +74,7 @@ export async function lnurlController(

const { amount: mintAmount } = parseInvoice(mintPr);

//TODO:)Parse invoice for expiry and pass it to blink
try {
invoiceRes = await lnProvider.createInvoice(
mintAmount / 1000,
Expand Down
2 changes: 1 addition & 1 deletion src/controller/paidController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Request, Response } from "express";
import { lnProvider, nostrPool, wallet } from "..";
import { lnProvider, nostrPool, wallet } from "../config";
import { Claim, Transaction } from "../models";
import { createZapReceipt, extractZapRequestData } from "../utils/nostr";

Expand Down
Loading