From f403af588c6711d3d15c16ffad30f85931630ce4 Mon Sep 17 00:00:00 2001 From: Victorjonah-prog Date: Wed, 29 Jul 2026 22:27:38 +0100 Subject: [PATCH] test: add integration test for invalid JWT signature rejection --- tests/integration/auth-jwt-validation.test.ts | 181 ++++++++++++++---- 1 file changed, 149 insertions(+), 32 deletions(-) diff --git a/tests/integration/auth-jwt-validation.test.ts b/tests/integration/auth-jwt-validation.test.ts index be3f35a..e9b524b 100644 --- a/tests/integration/auth-jwt-validation.test.ts +++ b/tests/integration/auth-jwt-validation.test.ts @@ -1,47 +1,164 @@ -import express from "express"; +import crypto from "crypto"; +import jwt from "jsonwebtoken"; +import { Networks } from "stellar-sdk"; import request from "supertest"; -import { createErrorMiddleware } from "../../src/middleware/error.middleware"; -import { logger } from "../../src/observability/logger"; -import { createInvoiceRouter } from "../../src/routes/invoice.routes"; +import { createApp } from "../../src/app"; +import { AuthService } from "../../src/services/auth.service"; +import type { + ChallengeRepositoryContract, + UserRepositoryContract, +} from "../../src/services/auth.service"; +import { User } from "../../src/models/User.model"; +import { KYCStatus, UserType } from "../../src/types/enums"; -describe("JWT authentication validation", () => { - it("rejects invoice requests without an Authorization header", async () => { - const invoiceService = { - getInvoicesBySellerId: jest.fn(), +// ── In-memory repositories ──────────────────────────────────────────────────── + +type InMemoryUser = User; + +interface InMemoryChallenge { + id: string; + stellarAddress: string; + nonceHash: string; + message: string; + network: string; + issuedAt: Date; + expiresAt: Date; + consumedAt: Date | null; +} + +class InMemoryUserRepository implements UserRepositoryContract { + private readonly users = new Map(); + + async findById(id: string) { + return this.users.get(id) ?? null; + } + + async findByStellarAddress(stellarAddress: string) { + return ( + [...this.users.values()].find( + (user) => user.stellarAddress === stellarAddress, + ) ?? null + ); + } + + async save(user: Partial) { + const now = new Date(); + const entity: InMemoryUser = { + id: crypto.randomUUID(), + stellarAddress: user.stellarAddress ?? "", + email: user.email ?? null, + userType: user.userType ?? UserType.INVESTOR, + kycStatus: user.kycStatus ?? KYCStatus.PENDING, + createdAt: user.createdAt ?? now, + updatedAt: user.updatedAt ?? now, + deletedAt: user.deletedAt ?? null, + invoices: user.invoices ?? [], + investments: user.investments ?? [], + transactions: user.transactions ?? [], + kycVerifications: user.kycVerifications ?? [], + notifications: user.notifications ?? [], }; - const app = express(); - app.use(express.json()); - app.use( - "/api/v1/invoices", - createInvoiceRouter({ - invoiceService: invoiceService as any, - config: { - ipfs: { - maxFileSizeMB: 10, - allowedMimeTypes: ["application/pdf"], - uploadRateLimit: { - windowMs: 60_000, - maxUploads: 5, - }, - }, - kyc: { - skipVerification: true, - }, - } as any, - }), + this.users.set(entity.id, entity); + return entity; + } +} + +class InMemoryChallengeRepository implements ChallengeRepositoryContract { + readonly challenges = new Map(); + + async create(input: InMemoryChallenge) { + const challenge: InMemoryChallenge = { + id: crypto.randomUUID(), + stellarAddress: input.stellarAddress, + nonceHash: input.nonceHash, + message: input.message, + network: input.network, + issuedAt: input.issuedAt, + expiresAt: input.expiresAt, + consumedAt: null, + }; + + this.challenges.set(challenge.id, challenge); + return challenge; + } + + async findByAddressAndNonceHash(stellarAddress: string, nonceHash: string) { + return ( + [...this.challenges.values()].find( + (challenge) => + challenge.stellarAddress === stellarAddress && + challenge.nonceHash === nonceHash, + ) ?? null + ); + } + + async consume(id: string, consumedAt: Date) { + const challenge = this.challenges.get(id); + + if (!challenge || challenge.consumedAt) { + return false; + } + + challenge.consumedAt = consumedAt; + return true; + } +} + +// ── Test helpers ────────────────────────────────────────────────────────────── + +const VALID_JWT_SECRET = "valid-test-secret"; + +function createTestApp() { + const authService = new AuthService({ + userRepository: new InMemoryUserRepository(), + challengeRepository: new InMemoryChallengeRepository(), + config: { + jwt: { + secret: VALID_JWT_SECRET, + expiresIn: "15m", + }, + auth: { + challengeTtlMs: 60_000, + }, + stellar: { + network: "testnet", + networkPassphrase: Networks.TESTNET, + }, + }, + }); + + return createApp({ authService }); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe("JWT authentication validation", () => { + it("rejects GET /api/v1/auth/me when the JWT is signed with an invalid secret key", async () => { + const app = createTestApp(); + + // Forge a token signed with the wrong secret + const forgedToken = jwt.sign( + { + sub: "GFORGED_STELLAR_ADDRESS", + stellarAddress: "GFORGED_STELLAR_ADDRESS", + userId: crypto.randomUUID(), + }, + "invalid-secret-key", + { expiresIn: "15m" }, ); - app.use(createErrorMiddleware(logger)); - const response = await request(app).get("/api/v1/invoices").expect(401); + const response = await request(app) + .get("/api/v1/auth/me") + .set("Authorization", `Bearer ${forgedToken}`) + .expect(401); expect(response.body).toMatchObject({ success: false, error: { - message: "Authorization token is required.", + message: "Invalid or expired token.", }, }); - expect(invoiceService.getInvoicesBySellerId).not.toHaveBeenCalled(); }); });