From aac9ad052460bdaf80efb0bb6c11284c45cd3c89 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 16:03:34 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add unit tests for changes --- tests/unit/data.test.ts | 186 ++++++++++ tests/unit/env.test.ts | 583 ++++++++++++++++++++++++++++++ tests/unit/error.model.test.ts | 497 +++++++++++++++++++++++++ tests/unit/stellar.config.test.ts | 237 ++++++++++++ 4 files changed, 1503 insertions(+) create mode 100644 tests/unit/data.test.ts create mode 100644 tests/unit/env.test.ts create mode 100644 tests/unit/error.model.test.ts create mode 100644 tests/unit/stellar.config.test.ts diff --git a/tests/unit/data.test.ts b/tests/unit/data.test.ts new file mode 100644 index 0000000..4c7ed0d --- /dev/null +++ b/tests/unit/data.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect } from "vitest"; +import { STATUS_CODES, ENDPOINTS } from "../../api/utils/data.js"; + +describe("STATUS_CODES", () => { + // ========================================================================= + // 2xx Success codes + // ========================================================================= + describe("2xx Success codes", () => { + it("OK should be 200", () => { + expect(STATUS_CODES.OK).toBe(200); + }); + + it("CREATED should be 201", () => { + expect(STATUS_CODES.CREATED).toBe(201); + }); + + it("ACCEPTED should be 202", () => { + expect(STATUS_CODES.ACCEPTED).toBe(202); + }); + + it("NO_CONTENT should be 204", () => { + expect(STATUS_CODES.NO_CONTENT).toBe(204); + }); + }); + + // ========================================================================= + // 4xx Client error codes + // ========================================================================= + describe("4xx Client error codes", () => { + it("BAD_REQUEST should be 400", () => { + expect(STATUS_CODES.BAD_REQUEST).toBe(400); + }); + + it("UNAUTHORIZED should be 401", () => { + expect(STATUS_CODES.UNAUTHORIZED).toBe(401); + }); + + it("FORBIDDEN should be 403", () => { + expect(STATUS_CODES.FORBIDDEN).toBe(403); + }); + + it("NOT_FOUND should be 404", () => { + expect(STATUS_CODES.NOT_FOUND).toBe(404); + }); + + it("REQUEST_TIMEOUT should be 408", () => { + expect(STATUS_CODES.REQUEST_TIMEOUT).toBe(408); + }); + + it("CONFLICT should be 409", () => { + expect(STATUS_CODES.CONFLICT).toBe(409); + }); + + it("UNPROCESSABLE_ENTITY should be 422", () => { + expect(STATUS_CODES.UNPROCESSABLE_ENTITY).toBe(422); + }); + + it("TOO_MANY_REQUESTS should be 429", () => { + expect(STATUS_CODES.TOO_MANY_REQUESTS).toBe(429); + }); + }); + + // ========================================================================= + // 5xx Server error codes + // ========================================================================= + describe("5xx Server error codes", () => { + it("INTERNAL_SERVER_ERROR should be 500", () => { + expect(STATUS_CODES.INTERNAL_SERVER_ERROR).toBe(500); + }); + }); + + // ========================================================================= + // Verify old renamed codes no longer exist + // ========================================================================= + describe("Renamed constants (old names should not exist)", () => { + it("should not have SUCCESS code (renamed to OK)", () => { + expect((STATUS_CODES as any).SUCCESS).toBeUndefined(); + }); + + it("should not have SERVER_ERROR code (renamed to INTERNAL_SERVER_ERROR)", () => { + expect((STATUS_CODES as any).SERVER_ERROR).toBeUndefined(); + }); + + it("should not have UNAUTHENTICATED code (renamed to UNAUTHORIZED)", () => { + expect((STATUS_CODES as any).UNAUTHENTICATED).toBeUndefined(); + }); + + it("should not have RATE_LIMIT code (renamed to TOO_MANY_REQUESTS)", () => { + expect((STATUS_CODES as any).RATE_LIMIT).toBeUndefined(); + }); + + it("should not have PARTIAL_SUCCESS code (removed)", () => { + expect((STATUS_CODES as any).PARTIAL_SUCCESS).toBeUndefined(); + }); + + it("should not have BAD_PAYLOAD code (renamed to BAD_REQUEST)", () => { + expect((STATUS_CODES as any).BAD_PAYLOAD).toBeUndefined(); + }); + + it("should not have UNKNOWN code (renamed to INTERNAL_SERVER_ERROR)", () => { + expect((STATUS_CODES as any).UNKNOWN).toBeUndefined(); + }); + + it("should not have TIMEOUT code (renamed to REQUEST_TIMEOUT)", () => { + expect((STATUS_CODES as any).TIMEOUT).toBeUndefined(); + }); + }); + + // ========================================================================= + // Semantic correctness + // ========================================================================= + describe("Semantic correctness", () => { + it("UNAUTHORIZED (401) should differ from FORBIDDEN (403)", () => { + expect(STATUS_CODES.UNAUTHORIZED).not.toBe(STATUS_CODES.FORBIDDEN); + expect(STATUS_CODES.UNAUTHORIZED).toBeLessThan(STATUS_CODES.FORBIDDEN); + }); + + it("OK (200) should be less than BAD_REQUEST (400)", () => { + expect(STATUS_CODES.OK).toBeLessThan(STATUS_CODES.BAD_REQUEST); + }); + + it("BAD_REQUEST (400) should be less than INTERNAL_SERVER_ERROR (500)", () => { + expect(STATUS_CODES.BAD_REQUEST).toBeLessThan(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("all codes should be positive integers", () => { + for (const [key, value] of Object.entries(STATUS_CODES)) { + expect(value, `${key} should be a positive integer`).toBeGreaterThan(0); + expect(Number.isInteger(value), `${key} should be an integer`).toBe(true); + } + }); + + it("all codes should be in valid HTTP range (100-599)", () => { + for (const [key, value] of Object.entries(STATUS_CODES)) { + expect(value, `${key} should be >= 100`).toBeGreaterThanOrEqual(100); + expect(value, `${key} should be <= 599`).toBeLessThanOrEqual(599); + } + }); + }); +}); + +describe("ENDPOINTS", () => { + describe("Structure validation", () => { + it("should have USER endpoints", () => { + expect(ENDPOINTS.USER.PREFIX).toBe("/users"); + expect(ENDPOINTS.USER.CREATE).toBe("/"); + expect(ENDPOINTS.USER.GET).toBe("/"); + expect(ENDPOINTS.USER.UPDATE_ADDRESS_BOOK).toBe("/address-book"); + expect(ENDPOINTS.USER.SUMSUB_TOKEN).toBe("/sumsub-token"); + }); + + it("should have INSTALLATION endpoints", () => { + expect(ENDPOINTS.INSTALLATION.PREFIX).toBe("/installations"); + expect(ENDPOINTS.INSTALLATION.GET_ALL).toBe("/"); + expect(ENDPOINTS.INSTALLATION.CREATE).toBe("/"); + }); + + it("should have TASK endpoints", () => { + expect(ENDPOINTS.TASK.PREFIX).toBe("/tasks"); + expect(ENDPOINTS.TASK.GET_ALL).toBe("/"); + expect(ENDPOINTS.TASK.CREATE).toBe("/"); + }); + + it("should have WALLET endpoints", () => { + expect(ENDPOINTS.WALLET.PREFIX).toBe("/wallet"); + expect(ENDPOINTS.WALLET.GET_ACCOUNT).toBe("/account"); + expect(ENDPOINTS.WALLET.WITHDRAW).toBe("/withdraw"); + }); + + it("should have WEBHOOK endpoints", () => { + expect(ENDPOINTS.WEBHOOK.PREFIX).toBe("/webhook"); + expect(ENDPOINTS.WEBHOOK.GITHUB).toBe("/github"); + expect(ENDPOINTS.WEBHOOK.SUMSUB).toBe("/sumsub"); + }); + + it("should have INTERNAL endpoints", () => { + expect(ENDPOINTS.INTERNAL.PREFIX).toBe("/internal"); + expect(ENDPOINTS.INTERNAL.BOUNTY_PAYOUT).toBe("/bounty-payout"); + }); + + it("should have AGENT endpoints", () => { + expect(ENDPOINTS.AGENT.PREFIX).toBe("/agent"); + expect(ENDPOINTS.AGENT.REVIEW).toBe("/review"); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/env.test.ts b/tests/unit/env.test.ts new file mode 100644 index 0000000..508702b --- /dev/null +++ b/tests/unit/env.test.ts @@ -0,0 +1,583 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; + +// Must mock error.model before importing Env, since Env imports it +vi.mock("../../api/models/error.model.js", async (importOriginal) => { + const actual = await importOriginal(); + return actual; +}); + +import { Env } from "../../api/utils/env.js"; +import { STATUS_CODES } from "../../api/utils/data.js"; + +describe("Env class", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all potentially relevant env vars before each test + const keysToDelete = [ + "NODE_ENV", "CORS_ORIGINS", "DATABASE_URL", "FIREBASE_PROJECT_ID", + "FIREBASE_PRIVATE_KEY", "FIREBASE_CLIENT_EMAIL", "GITHUB_ACCESS_TOKEN", + "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY", "GITHUB_WEBHOOK_SECRET", + "STELLAR_NETWORK", "STELLAR_HORIZON_URL", "STELLAR_RPC_URL", + "STELLAR_MASTER_PUBLIC_KEY", "STELLAR_MASTER_SECRET_KEY", + "TASK_ESCROW_CONTRACT_ID", "USDC_CONTRACT_ID", "USDC_ASSET_ID", + "MAX_FEE", "X402_PAYEE_ADDRESS", "X402_FACILITATOR_URL", "X402_API_KEY", + "GCP_PROJECT_ID", "GCP_LOCATION_ID", "GCP_KEY_RING_ID", "GCP_KEY_ID", + "CLOUD_TASKS_PR_ANALYSIS_QUEUE", "CLOUD_TASKS_MANUAL_PR_ANALYSIS_QUEUE", + "CLOUD_TASKS_REPO_INDEXING_QUEUE", "CLOUD_TASKS_INCREMENTAL_INDEXING_QUEUE", + "CLOUD_TASKS_BOUNTY_PAYOUT_QUEUE", "CLOUD_TASKS_CLEAR_INSTALLATION_QUEUE", + "CLOUD_TASKS_CLEAR_REPO_QUEUE", "CLOUD_RUN_SERVICE_URL", + "CLOUD_RUN_PRIVATE_SERVICE_URL", "CLOUD_TASKS_SERVICE_ACCOUNT_EMAIL", + "DEFAULT_SUBSCRIPTION_PACKAGE_ID", "SUMSUB_APP_TOKEN", "SUMSUB_SECRET_KEY", + "SUMSUB_WEBHOOK_SECRET", "SUMSUB_LEVEL_NAME", "SUMSUB_BASE_URL", + "LOG_LEVEL", "STATSIG_API_KEY", "PORT", "CONTRIBUTOR_APP_URL" + ]; + keysToDelete.forEach(key => delete process.env[key]); + }); + + afterEach(() => { + // Restore original environment + Object.assign(process.env, originalEnv); + }); + + // ========================================================================= + // getOrThrowError (private, tested indirectly) + // ========================================================================= + + describe("Error throwing behavior", () => { + it("should throw an ErrorClass with correct status when required env var is missing", () => { + expect(() => Env.nodeEnv(true)).toThrow(); + try { + Env.nodeEnv(true); + } catch (err: any) { + expect(err.code).toBe("SERVER_MISCONFIGURATION"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + expect(err.message).toContain("NODE_ENV"); + } + }); + + it("should not throw when throwError=false and env var is missing", () => { + expect(() => Env.nodeEnv(false)).not.toThrow(); + expect(Env.nodeEnv()).toBeUndefined(); + }); + + it("should return the value when env var is present and throwError=true", () => { + process.env.NODE_ENV = "production"; + expect(Env.nodeEnv(true)).toBe("production"); + }); + }); + + // ========================================================================= + // nodeEnv + // ========================================================================= + + describe("nodeEnv", () => { + it("should return undefined when NODE_ENV is not set", () => { + expect(Env.nodeEnv()).toBeUndefined(); + }); + + it("should return NODE_ENV value when set", () => { + process.env.NODE_ENV = "development"; + expect(Env.nodeEnv()).toBe("development"); + }); + + it("should return 'production' when NODE_ENV is production", () => { + process.env.NODE_ENV = "production"; + expect(Env.nodeEnv()).toBe("production"); + }); + + it("should return 'test' when NODE_ENV is test", () => { + process.env.NODE_ENV = "test"; + expect(Env.nodeEnv()).toBe("test"); + }); + + it("should throw when throwError=true and NODE_ENV is not set", () => { + expect(() => Env.nodeEnv(true)).toThrow(); + }); + + it("should not throw when throwError=false and NODE_ENV is not set", () => { + expect(() => Env.nodeEnv(false)).not.toThrow(); + }); + }); + + // ========================================================================= + // corsOrigins + // ========================================================================= + + describe("corsOrigins", () => { + it("should return empty array when CORS_ORIGINS is not set", () => { + expect(Env.corsOrigins()).toEqual([]); + }); + + it("should parse a single origin", () => { + process.env.CORS_ORIGINS = "http://localhost:3000"; + expect(Env.corsOrigins()).toEqual(["http://localhost:3000"]); + }); + + it("should split multiple origins by comma", () => { + process.env.CORS_ORIGINS = "http://localhost:3000,http://localhost:4000,http://localhost:3001"; + expect(Env.corsOrigins()).toEqual([ + "http://localhost:3000", + "http://localhost:4000", + "http://localhost:3001" + ]); + }); + + it("should throw when throwError=true and CORS_ORIGINS is not set", () => { + expect(() => Env.corsOrigins(true)).toThrow(); + }); + + it("should split and return array when throwError=true and CORS_ORIGINS is set", () => { + process.env.CORS_ORIGINS = "http://localhost:3000,http://localhost:4000"; + const result = Env.corsOrigins(true); + expect(result).toEqual(["http://localhost:3000", "http://localhost:4000"]); + }); + }); + + // ========================================================================= + // port + // ========================================================================= + + describe("port", () => { + it("should return NaN when PORT is not set", () => { + const result = Env.port(); + // Number(undefined) === NaN + expect(isNaN(result)).toBe(true); + }); + + it("should return port as a number when PORT is set", () => { + process.env.PORT = "5000"; + expect(Env.port()).toBe(5000); + }); + + it("should return port as number type (not string)", () => { + process.env.PORT = "8080"; + const result = Env.port(); + expect(typeof result).toBe("number"); + expect(result).toBe(8080); + }); + + it("should throw when throwError=true and PORT is not set", () => { + expect(() => Env.port(true)).toThrow(); + }); + }); + + // ========================================================================= + // databaseUrl + // ========================================================================= + + describe("databaseUrl", () => { + it("should return undefined when DATABASE_URL is not set", () => { + expect(Env.databaseUrl()).toBeUndefined(); + }); + + it("should return the database URL when set", () => { + process.env.DATABASE_URL = "postgresql://user:pass@localhost/db"; + expect(Env.databaseUrl()).toBe("postgresql://user:pass@localhost/db"); + }); + + it("should throw when throwError=true and DATABASE_URL is not set", () => { + expect(() => Env.databaseUrl(true)).toThrow(); + }); + }); + + // ========================================================================= + // Firebase env vars + // ========================================================================= + + describe("Firebase environment variables", () => { + it("firebaseProjectId should return undefined when not set", () => { + expect(Env.firebaseProjectId()).toBeUndefined(); + }); + + it("firebaseProjectId should return value when set", () => { + process.env.FIREBASE_PROJECT_ID = "my-project"; + expect(Env.firebaseProjectId()).toBe("my-project"); + }); + + it("firebaseProjectId should throw when throwError=true and not set", () => { + expect(() => Env.firebaseProjectId(true)).toThrow(); + }); + + it("firebasePrivateKey should return undefined when not set", () => { + expect(Env.firebasePrivateKey()).toBeUndefined(); + }); + + it("firebasePrivateKey should return value when set", () => { + process.env.FIREBASE_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\\ntest\\n-----END RSA PRIVATE KEY-----"; + expect(Env.firebasePrivateKey()).toBe("-----BEGIN RSA PRIVATE KEY-----\\ntest\\n-----END RSA PRIVATE KEY-----"); + }); + + it("firebaseClientEmail should return undefined when not set", () => { + expect(Env.firebaseClientEmail()).toBeUndefined(); + }); + + it("firebaseClientEmail should return value when set", () => { + process.env.FIREBASE_CLIENT_EMAIL = "service@project.iam.gserviceaccount.com"; + expect(Env.firebaseClientEmail()).toBe("service@project.iam.gserviceaccount.com"); + }); + }); + + // ========================================================================= + // GitHub env vars + // ========================================================================= + + describe("GitHub environment variables", () => { + it("githubAccessToken should return undefined when not set", () => { + expect(Env.githubAccessToken()).toBeUndefined(); + }); + + it("githubAccessToken should return value when set", () => { + process.env.GITHUB_ACCESS_TOKEN = "ghp_token123"; + expect(Env.githubAccessToken()).toBe("ghp_token123"); + }); + + it("githubAccessToken should throw when throwError=true and not set", () => { + expect(() => Env.githubAccessToken(true)).toThrow(); + }); + + it("githubAppId should return undefined when not set", () => { + expect(Env.githubAppId()).toBeUndefined(); + }); + + it("githubAppId should return value when set", () => { + process.env.GITHUB_APP_ID = "12345"; + expect(Env.githubAppId()).toBe("12345"); + }); + + it("githubWebhookSecret should return undefined when not set", () => { + expect(Env.githubWebhookSecret()).toBeUndefined(); + }); + + it("githubWebhookSecret should return value when set", () => { + process.env.GITHUB_WEBHOOK_SECRET = "my-webhook-secret"; + expect(Env.githubWebhookSecret()).toBe("my-webhook-secret"); + }); + + it("githubWebhookSecret should throw when throwError=true and not set", () => { + expect(() => Env.githubWebhookSecret(true)).toThrow(); + }); + }); + + // ========================================================================= + // Stellar env vars + // ========================================================================= + + describe("Stellar environment variables", () => { + it("stellarNetwork should return undefined when not set", () => { + expect(Env.stellarNetwork()).toBeUndefined(); + }); + + it("stellarNetwork should return 'public' for mainnet", () => { + process.env.STELLAR_NETWORK = "public"; + expect(Env.stellarNetwork()).toBe("public"); + }); + + it("stellarNetwork should return 'testnet' for testnet", () => { + process.env.STELLAR_NETWORK = "testnet"; + expect(Env.stellarNetwork()).toBe("testnet"); + }); + + it("stellarMasterSecretKey should return undefined when not set", () => { + expect(Env.stellarMasterSecretKey()).toBeUndefined(); + }); + + it("stellarMasterSecretKey should return value when set", () => { + process.env.STELLAR_MASTER_SECRET_KEY = "SXXXXXXXXXXXXXXXX"; + expect(Env.stellarMasterSecretKey()).toBe("SXXXXXXXXXXXXXXXX"); + }); + + it("stellarMasterSecretKey should throw when throwError=true and not set", () => { + expect(() => Env.stellarMasterSecretKey(true)).toThrow(); + }); + + it("usdcAssetId should return undefined when not set", () => { + expect(Env.usdcAssetId()).toBeUndefined(); + }); + + it("usdcAssetId should return value when set", () => { + process.env.USDC_ASSET_ID = "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"; + expect(Env.usdcAssetId()).toBe("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"); + }); + + it("usdcAssetId should throw when throwError=true and not set", () => { + expect(() => Env.usdcAssetId(true)).toThrow(); + }); + }); + + // ========================================================================= + // Sumsub env vars + // ========================================================================= + + describe("Sumsub environment variables", () => { + it("sumsubAppToken should return undefined when not set", () => { + expect(Env.sumsubAppToken()).toBeUndefined(); + }); + + it("sumsubAppToken should return value when set", () => { + process.env.SUMSUB_APP_TOKEN = "sumsub-token-xyz"; + expect(Env.sumsubAppToken()).toBe("sumsub-token-xyz"); + }); + + it("sumsubAppToken should throw when throwError=true and not set", () => { + expect(() => Env.sumsubAppToken(true)).toThrow(); + }); + + it("sumsubSecretKey should return undefined when not set", () => { + expect(Env.sumsubSecretKey()).toBeUndefined(); + }); + + it("sumsubSecretKey should throw when throwError=true and not set", () => { + expect(() => Env.sumsubSecretKey(true)).toThrow(); + }); + + it("sumsubWebhookSecret should return undefined when not set", () => { + expect(Env.sumsubWebhookSecret()).toBeUndefined(); + }); + + it("sumsubWebhookSecret should return value when set", () => { + process.env.SUMSUB_WEBHOOK_SECRET = "webhook-secret-abc"; + expect(Env.sumsubWebhookSecret()).toBe("webhook-secret-abc"); + }); + + it("sumsubLevelName should return undefined when not set", () => { + expect(Env.sumsubLevelName()).toBeUndefined(); + }); + + it("sumsubBaseUrl should return undefined when not set", () => { + expect(Env.sumsubBaseUrl()).toBeUndefined(); + }); + + it("sumsubBaseUrl should return value when set", () => { + process.env.SUMSUB_BASE_URL = "https://api.sumsub.com"; + expect(Env.sumsubBaseUrl()).toBe("https://api.sumsub.com"); + }); + }); + + // ========================================================================= + // GCP env vars + // ========================================================================= + + describe("GCP environment variables", () => { + it("gcpProjectId should return undefined when not set", () => { + expect(Env.gcpProjectId()).toBeUndefined(); + }); + + it("gcpProjectId should return value when set", () => { + process.env.GCP_PROJECT_ID = "my-gcp-project"; + expect(Env.gcpProjectId()).toBe("my-gcp-project"); + }); + + it("gcpProjectId should throw when throwError=true and not set", () => { + expect(() => Env.gcpProjectId(true)).toThrow(); + }); + + it("gcpLocationId should return undefined when not set", () => { + expect(Env.gcpLocationId()).toBeUndefined(); + }); + + it("cloudRunServiceUrl should return undefined when not set", () => { + expect(Env.cloudRunServiceUrl()).toBeUndefined(); + }); + + it("cloudRunServiceUrl should return value when set", () => { + process.env.CLOUD_RUN_SERVICE_URL = "https://my-service.run.app"; + expect(Env.cloudRunServiceUrl()).toBe("https://my-service.run.app"); + }); + + it("cloudRunServiceUrl should throw when throwError=true and not set", () => { + expect(() => Env.cloudRunServiceUrl(true)).toThrow(); + }); + + it("cloudTasksServiceAccountEmail should return undefined when not set", () => { + expect(Env.cloudTasksServiceAccountEmail()).toBeUndefined(); + }); + + it("cloudTasksServiceAccountEmail should return value when set", () => { + process.env.CLOUD_TASKS_SERVICE_ACCOUNT_EMAIL = "tasks@project.iam.gserviceaccount.com"; + expect(Env.cloudTasksServiceAccountEmail()).toBe("tasks@project.iam.gserviceaccount.com"); + }); + }); + + // ========================================================================= + // Cloud Tasks queue env vars + // ========================================================================= + + describe("Cloud Tasks queue environment variables", () => { + it("cloudTasksPrAnalysisQueue should return undefined when not set", () => { + expect(Env.cloudTasksPrAnalysisQueue()).toBeUndefined(); + }); + + it("cloudTasksPrAnalysisQueue should return value when set", () => { + process.env.CLOUD_TASKS_PR_ANALYSIS_QUEUE = "pr-analysis-queue"; + expect(Env.cloudTasksPrAnalysisQueue()).toBe("pr-analysis-queue"); + }); + + it("cloudTasksBountyPayoutQueue should return undefined when not set", () => { + expect(Env.cloudTasksBountyPayoutQueue()).toBeUndefined(); + }); + + it("cloudTasksBountyPayoutQueue should throw when throwError=true and not set", () => { + expect(() => Env.cloudTasksBountyPayoutQueue(true)).toThrow(); + }); + + it("cloudTasksClearInstallationQueue should return undefined when not set", () => { + expect(Env.cloudTasksClearInstallationQueue()).toBeUndefined(); + }); + + it("cloudTasksClearRepoQueue should return undefined when not set", () => { + expect(Env.cloudTasksClearRepoQueue()).toBeUndefined(); + }); + }); + + // ========================================================================= + // Other env vars + // ========================================================================= + + describe("Other environment variables", () => { + it("logLevel should return undefined when not set", () => { + expect(Env.logLevel()).toBeUndefined(); + }); + + it("logLevel should return value when set", () => { + process.env.LOG_LEVEL = "debug"; + expect(Env.logLevel()).toBe("debug"); + }); + + it("statsigApiKey should return undefined when not set", () => { + expect(Env.statsigApiKey()).toBeUndefined(); + }); + + it("contributorAppUrl should return undefined when not set", () => { + expect(Env.contributorAppUrl()).toBeUndefined(); + }); + + it("contributorAppUrl should return value when set", () => { + process.env.CONTRIBUTOR_APP_URL = "http://localhost:4000"; + expect(Env.contributorAppUrl()).toBe("http://localhost:4000"); + }); + + it("defaultSubscriptionPackageId should return undefined when not set", () => { + expect(Env.defaultSubscriptionPackageId()).toBeUndefined(); + }); + + it("defaultSubscriptionPackageId should return value when set", () => { + process.env.DEFAULT_SUBSCRIPTION_PACKAGE_ID = "pkg-id-123"; + expect(Env.defaultSubscriptionPackageId()).toBe("pkg-id-123"); + }); + + it("defaultSubscriptionPackageId should throw when throwError=true and not set", () => { + expect(() => Env.defaultSubscriptionPackageId(true)).toThrow(); + }); + + it("x402PayeeAddress should return undefined when not set", () => { + expect(Env.x402PayeeAddress()).toBeUndefined(); + }); + + it("x402FacilitatorUrl should return undefined when not set", () => { + expect(Env.x402FacilitatorUrl()).toBeUndefined(); + }); + + it("x402ApiKey should return undefined when not set", () => { + expect(Env.x402ApiKey()).toBeUndefined(); + }); + }); + + // ========================================================================= + // Error structure validation + // ========================================================================= + + describe("Error structure when required var is missing", () => { + it("should throw with name 'ErrorClass'", () => { + try { + Env.databaseUrl(true); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.name).toBe("ErrorClass"); + } + }); + + it("should include the missing variable name in the error message", () => { + try { + Env.stellarMasterSecretKey(true); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.message).toContain("STELLAR_MASTER_SECRET_KEY"); + } + }); + + it("should use INTERNAL_SERVER_ERROR status", () => { + try { + Env.githubWebhookSecret(true); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + } + }); + + it("should use SERVER_MISCONFIGURATION code", () => { + try { + Env.usdcAssetId(true); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.code).toBe("SERVER_MISCONFIGURATION"); + } + }); + + it("should have null details", () => { + try { + Env.sumsubAppToken(true); + expect.fail("Should have thrown"); + } catch (err: any) { + expect(err.details).toBeNull(); + } + }); + }); + + // ========================================================================= + // Edge cases: empty string behavior + // ========================================================================= + + describe("Edge cases", () => { + it("should treat empty string as falsy and throw when throwError=true", () => { + process.env.NODE_ENV = ""; + expect(() => Env.nodeEnv(true)).toThrow(); + }); + + it("should return empty string as undefined-like when not required", () => { + process.env.NODE_ENV = ""; + // process.env.NODE_ENV = "" means getOrThrowError would throw + // but without throwError, we just return process.env.NODE_ENV which is "" + // The falsy check matters for getOrThrowError + const result = Env.nodeEnv(false); + // When throwError=false, returns process.env.NODE_ENV directly + expect(result).toBe(""); + }); + + it("corsOrigins should return empty array when CORS_ORIGINS is not set", () => { + delete process.env.CORS_ORIGINS; + expect(Env.corsOrigins()).toEqual([]); + }); + + it("port should handle non-numeric PORT value", () => { + process.env.PORT = "not-a-number"; + const result = Env.port(); + expect(isNaN(result)).toBe(true); + }); + + it("all methods should default throwError to false", () => { + // Verify no env vars set, methods don't throw by default + expect(() => Env.nodeEnv()).not.toThrow(); + expect(() => Env.corsOrigins()).not.toThrow(); + expect(() => Env.databaseUrl()).not.toThrow(); + expect(() => Env.firebaseProjectId()).not.toThrow(); + expect(() => Env.githubAccessToken()).not.toThrow(); + expect(() => Env.stellarNetwork()).not.toThrow(); + expect(() => Env.stellarMasterSecretKey()).not.toThrow(); + expect(() => Env.usdcAssetId()).not.toThrow(); + expect(() => Env.sumsubAppToken()).not.toThrow(); + expect(() => Env.gcpProjectId()).not.toThrow(); + expect(() => Env.port()).not.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/error.model.test.ts b/tests/unit/error.model.test.ts new file mode 100644 index 0000000..741ac5c --- /dev/null +++ b/tests/unit/error.model.test.ts @@ -0,0 +1,497 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + ErrorClass, + AuthorizationError, + ValidationError, + NotFoundError, + KmsServiceError, + StellarServiceError, + EscrowContractError, + GeminiServiceError, + GitHubAPIError, + VoyageAPIError, + GitHubWebhookError, + SumsubWebhookError, + CloudTasksError, + TimeoutError, + AIReviewError, + ErrorUtils +} from "../../api/models/error.model.js"; +import { STATUS_CODES } from "../../api/utils/data.js"; + +/** + * Tests for api/models/error.model.ts + * + * Focus: status code updates from the PR — + * - AuthorizationError now uses FORBIDDEN (403) instead of 401 + * - ValidationError now uses BAD_REQUEST (400) instead of 500 + * - Default ErrorClass status is INTERNAL_SERVER_ERROR (500) + * - TimeoutError uses REQUEST_TIMEOUT (408) + * - ErrorUtils.sanitizeError uses Env.nodeEnv() !== "production" + */ + +describe("Error Models", () => { + describe("ErrorClass (base)", () => { + it("should default to INTERNAL_SERVER_ERROR status", () => { + const err = new ErrorClass("SOME_CODE", null, "some message"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + expect(err.status).toBe(500); + }); + + it("should accept custom status code", () => { + const err = new ErrorClass("CUSTOM", null, "msg", STATUS_CODES.BAD_REQUEST); + expect(err.status).toBe(STATUS_CODES.BAD_REQUEST); + }); + + it("should set name to 'ErrorClass'", () => { + const err = new ErrorClass("CODE", null, "msg"); + expect(err.name).toBe("ErrorClass"); + }); + + it("should store code, message, details, and status", () => { + const details = { extra: "info" }; + const err = new ErrorClass("MY_CODE", details, "My message", STATUS_CODES.NOT_FOUND); + expect(err.code).toBe("MY_CODE"); + expect(err.message).toBe("My message"); + expect(err.details).toBe(details); + expect(err.status).toBe(STATUS_CODES.NOT_FOUND); + }); + }); + + // ========================================================================= + // AuthorizationError — changed from UNAUTHORIZED (401) to FORBIDDEN (403) + // ========================================================================= + describe("AuthorizationError", () => { + it("should use FORBIDDEN (403) status", () => { + const err = new AuthorizationError("Not allowed"); + expect(err.status).toBe(STATUS_CODES.FORBIDDEN); + expect(err.status).toBe(403); + }); + + it("should NOT use UNAUTHORIZED (401) status", () => { + const err = new AuthorizationError("Not allowed"); + expect(err.status).not.toBe(STATUS_CODES.UNAUTHORIZED); + expect(err.status).not.toBe(401); + }); + + it("should use code 'UNAUTHORIZED'", () => { + const err = new AuthorizationError("Not allowed"); + expect(err.code).toBe("UNAUTHORIZED"); + }); + + it("should accept a details argument", () => { + const details = { resource: "task" }; + const err = new AuthorizationError("Not allowed", details); + expect(err.details).toBe(details); + }); + + it("should default details to null", () => { + const err = new AuthorizationError("Not allowed"); + expect(err.details).toBeNull(); + }); + + it("should be an instance of ErrorClass", () => { + const err = new AuthorizationError("Not allowed"); + expect(err).toBeInstanceOf(ErrorClass); + }); + }); + + // ========================================================================= + // ValidationError — changed from SERVER_ERROR (500) to BAD_REQUEST (400) + // ========================================================================= + describe("ValidationError", () => { + it("should use BAD_REQUEST (400) status", () => { + const err = new ValidationError("Invalid input"); + expect(err.status).toBe(STATUS_CODES.BAD_REQUEST); + expect(err.status).toBe(400); + }); + + it("should NOT use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new ValidationError("Invalid input"); + expect(err.status).not.toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'VALIDATION_ERROR'", () => { + const err = new ValidationError("Invalid input"); + expect(err.code).toBe("VALIDATION_ERROR"); + }); + + it("should accept details", () => { + const details = ["field required"]; + const err = new ValidationError("Invalid", details); + expect(err.details).toEqual(details); + }); + + it("should be an instance of ErrorClass", () => { + const err = new ValidationError("Invalid"); + expect(err).toBeInstanceOf(ErrorClass); + }); + }); + + // ========================================================================= + // NotFoundError + // ========================================================================= + describe("NotFoundError", () => { + it("should use NOT_FOUND (404) status", () => { + const err = new NotFoundError("Resource not found"); + expect(err.status).toBe(STATUS_CODES.NOT_FOUND); + expect(err.status).toBe(404); + }); + + it("should use code 'NOT_FOUND'", () => { + const err = new NotFoundError("Resource not found"); + expect(err.code).toBe("NOT_FOUND"); + }); + }); + + // ========================================================================= + // KmsServiceError + // ========================================================================= + describe("KmsServiceError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new KmsServiceError("KMS failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'KMS_SERVICE_ERROR'", () => { + const err = new KmsServiceError("KMS failed"); + expect(err.code).toBe("KMS_SERVICE_ERROR"); + }); + }); + + // ========================================================================= + // StellarServiceError + // ========================================================================= + describe("StellarServiceError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new StellarServiceError("Stellar failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'STELLAR_SERVICE_ERROR'", () => { + const err = new StellarServiceError("Stellar failed"); + expect(err.code).toBe("STELLAR_SERVICE_ERROR"); + }); + }); + + // ========================================================================= + // EscrowContractError + // ========================================================================= + describe("EscrowContractError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new EscrowContractError("Escrow failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'ESCROW_CONTRACT_ERROR'", () => { + const err = new EscrowContractError("Escrow failed"); + expect(err.code).toBe("ESCROW_CONTRACT_ERROR"); + }); + }); + + // ========================================================================= + // GeminiServiceError + // ========================================================================= + describe("GeminiServiceError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new GeminiServiceError("Gemini failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'GEMINI_SERVICE_ERROR'", () => { + const err = new GeminiServiceError("Gemini failed"); + expect(err.code).toBe("GEMINI_SERVICE_ERROR"); + }); + }); + + // ========================================================================= + // GitHubAPIError + // ========================================================================= + describe("GitHubAPIError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new GitHubAPIError("GitHub API failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use default code 'GITHUB_API_ERROR'", () => { + const err = new GitHubAPIError("GitHub API failed"); + expect(err.code).toBe("GITHUB_API_ERROR"); + }); + + it("should accept custom code", () => { + const err = new GitHubAPIError("Failed", null, 404, undefined, "REPO_NOT_FOUND"); + expect(err.code).toBe("REPO_NOT_FOUND"); + }); + + it("should store optional statusCode and rateLimitRemaining", () => { + const err = new GitHubAPIError("Rate limited", null, 429, 0); + expect(err.statusCode).toBe(429); + expect(err.rateLimitRemaining).toBe(0); + }); + }); + + // ========================================================================= + // VoyageAPIError + // ========================================================================= + describe("VoyageAPIError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new VoyageAPIError("Voyage failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'VOYAGE_API_ERROR'", () => { + const err = new VoyageAPIError("Voyage failed"); + expect(err.code).toBe("VOYAGE_API_ERROR"); + }); + }); + + // ========================================================================= + // GitHubWebhookError + // ========================================================================= + describe("GitHubWebhookError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new GitHubWebhookError("Webhook validation failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'GITHUB_WEBHOOK_ERROR'", () => { + const err = new GitHubWebhookError("Webhook validation failed"); + expect(err.code).toBe("GITHUB_WEBHOOK_ERROR"); + }); + + it("should not be retryable", () => { + const err = new GitHubWebhookError("Webhook validation failed"); + expect(err.retryable).toBe(false); + }); + }); + + // ========================================================================= + // SumsubWebhookError + // ========================================================================= + describe("SumsubWebhookError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new SumsubWebhookError("Sumsub webhook failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'SUMSUB_WEBHOOK_ERROR'", () => { + const err = new SumsubWebhookError("Sumsub webhook failed"); + expect(err.code).toBe("SUMSUB_WEBHOOK_ERROR"); + }); + + it("should not be retryable", () => { + const err = new SumsubWebhookError("Sumsub webhook failed"); + expect(err.retryable).toBe(false); + }); + }); + + // ========================================================================= + // CloudTasksError + // ========================================================================= + describe("CloudTasksError", () => { + it("should use INTERNAL_SERVER_ERROR (500) status", () => { + const err = new CloudTasksError("Cloud Tasks failed"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should use code 'CLOUD_TASKS_ERROR'", () => { + const err = new CloudTasksError("Cloud Tasks failed"); + expect(err.code).toBe("CLOUD_TASKS_ERROR"); + }); + }); + + // ========================================================================= + // TimeoutError — changed from TIMEOUT to REQUEST_TIMEOUT (408) + // ========================================================================= + describe("TimeoutError", () => { + it("should use REQUEST_TIMEOUT (408) status", () => { + const err = new TimeoutError("database-query", 5000); + expect(err.status).toBe(STATUS_CODES.REQUEST_TIMEOUT); + expect(err.status).toBe(408); + }); + + it("should be retryable", () => { + const err = new TimeoutError("database-query", 5000); + expect(err.retryable).toBe(true); + }); + + it("should include operation and timeout in message", () => { + const err = new TimeoutError("pr-analysis", 30000); + expect(err.message).toContain("pr-analysis"); + expect(err.message).toContain("30000ms"); + }); + + it("should store operation and timeoutMs", () => { + const err = new TimeoutError("some-op", 10000); + expect(err.operation).toBe("some-op"); + expect(err.timeoutMs).toBe(10000); + }); + + it("should be an instance of AIReviewError and ErrorClass", () => { + const err = new TimeoutError("op", 1000); + expect(err).toBeInstanceOf(AIReviewError); + expect(err).toBeInstanceOf(ErrorClass); + }); + }); + + // ========================================================================= + // AIReviewError + // ========================================================================= + describe("AIReviewError", () => { + it("should default to INTERNAL_SERVER_ERROR status", () => { + const err = new AIReviewError("CODE", null, "message"); + expect(err.status).toBe(STATUS_CODES.INTERNAL_SERVER_ERROR); + }); + + it("should store retryable flag", () => { + const retryableErr = new AIReviewError("CODE", null, "message", true); + const nonRetryableErr = new AIReviewError("CODE", null, "message", false); + expect(retryableErr.retryable).toBe(true); + expect(nonRetryableErr.retryable).toBe(false); + }); + + it("toJSON should include retryable field", () => { + const err = new AIReviewError("MY_CODE", { detail: "x" }, "msg", true, 500); + const json = err.toJSON(); + expect(json).toEqual({ + code: "MY_CODE", + details: { detail: "x" }, + message: "msg", + status: 500, + retryable: true + }); + }); + }); + + // ========================================================================= + // ErrorUtils.sanitizeError + // ========================================================================= + describe("ErrorUtils.sanitizeError", () => { + const originalEnv = process.env.NODE_ENV; + + afterEach(() => { + process.env.NODE_ENV = originalEnv; + }); + + it("should return full error object in development environment", () => { + process.env.NODE_ENV = "development"; + const err = new ErrorClass("CODE", { data: "sensitive" }, "msg", 400); + const sanitized = ErrorUtils.sanitizeError(err); + expect(sanitized).toMatchObject({ + code: "CODE", + message: "msg", + status: 400, + details: { data: "sensitive" } + }); + }); + + it("should return full error object in test environment", () => { + process.env.NODE_ENV = "test"; + const err = new ErrorClass("CODE", { data: "sensitive" }, "msg", 400); + const sanitized = ErrorUtils.sanitizeError(err); + expect(sanitized).toMatchObject({ + code: "CODE", + message: "msg", + status: 400, + details: { data: "sensitive" } + }); + }); + + it("should strip sensitive details in production environment", () => { + process.env.NODE_ENV = "production"; + const err = new ErrorClass("CODE", { data: "sensitive" }, "msg", 400); + const sanitized = ErrorUtils.sanitizeError(err); + expect(sanitized).toEqual({ + code: "CODE", + message: "msg", + status: 400 + }); + // Should NOT include details + expect((sanitized as any).details).toBeUndefined(); + }); + + it("should return full error in non-production (staging) environment", () => { + process.env.NODE_ENV = "staging"; + const err = new ErrorClass("CODE", { secret: "value" }, "msg", 500); + const sanitized = ErrorUtils.sanitizeError(err); + // Env.nodeEnv() !== "production" → should return full error + expect((sanitized as any).details).toBeDefined(); + }); + }); + + // ========================================================================= + // ErrorUtils.extractAxiosErrorData + // ========================================================================= + describe("ErrorUtils.extractAxiosErrorData", () => { + it("should return details as-is when not an Axios error", () => { + const plain = { message: "plain error" }; + expect(ErrorUtils.extractAxiosErrorData(plain)).toBe(plain); + }); + + it("should return null as-is", () => { + expect(ErrorUtils.extractAxiosErrorData(null)).toBeNull(); + }); + + it("should extract axios error data when isAxiosError is true", () => { + const axiosError = { + isAxiosError: true, + code: "ECONNREFUSED", + message: "Connection refused", + response: { + status: 503, + data: { error: "Service unavailable" } + } + }; + const result = ErrorUtils.extractAxiosErrorData(axiosError) as any; + expect(result.code).toBe("ECONNREFUSED"); + expect(result.status).toBe(503); + expect(result.message).toBe("Connection refused"); + expect(result.data).toEqual({ error: "Service unavailable" }); + }); + + it("should handle wrapped axios error ({ error: AxiosError })", () => { + const wrapped = { + error: { + isAxiosError: true, + code: "ETIMEDOUT", + message: "Timeout", + response: { status: 408, data: "timeout" } + } + }; + const result = ErrorUtils.extractAxiosErrorData(wrapped) as any; + expect(result.code).toBe("ETIMEDOUT"); + expect(result.status).toBe(408); + }); + + it("should return undefined as-is", () => { + expect(ErrorUtils.extractAxiosErrorData(undefined)).toBeUndefined(); + }); + }); + + // ========================================================================= + // ErrorUtils.isRetryable + // ========================================================================= + describe("ErrorUtils.isRetryable", () => { + it("should use AIReviewError.retryable flag", () => { + const retryable = new GeminiServiceError("msg", null, true); + const nonRetryable = new GeminiServiceError("msg", null, false); + expect(ErrorUtils.isRetryable(retryable as any)).toBe(true); + expect(ErrorUtils.isRetryable(nonRetryable as any)).toBe(false); + }); + + it("should return true for timeout-related generic errors", () => { + const err = new Error("connection timeout"); + expect(ErrorUtils.isRetryable(err)).toBe(true); + }); + + it("should return true for network-related generic errors", () => { + const err = new Error("network error occurred"); + expect(ErrorUtils.isRetryable(err)).toBe(true); + }); + + it("should return false for unrecognized errors", () => { + const err = new Error("something else went wrong"); + expect(ErrorUtils.isRetryable(err)).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/stellar.config.test.ts b/tests/unit/stellar.config.test.ts new file mode 100644 index 0000000..19afc81 --- /dev/null +++ b/tests/unit/stellar.config.test.ts @@ -0,0 +1,237 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; + +/** + * Tests for api/config/stellar.config.ts + * + * The stellar config exports top-level constants that are computed once at module + * load time. To test both mainnet and testnet branches we mock the Env utility + * and use dynamic imports so we can re-import the module with different env vars. + */ + +// We need to mock Env before importing stellar config +vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn(), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } +})); + +// Also mock the error model (imported transitively via env.ts) +vi.mock("../../api/models/error.model.js", async (importOriginal) => { + const actual = await importOriginal(); + return actual; +}); + +import { Env } from "../../api/utils/env.js"; + +describe("Stellar config", () => { + describe("isMainnet", () => { + it("should be true when STELLAR_NETWORK is 'public'", async () => { + vi.mocked(Env.stellarNetwork).mockReturnValue("public"); + + // Dynamically import and reset module to recompute constants + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("public"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.isMainnet).toBe(true); + }); + + it("should be false when STELLAR_NETWORK is 'testnet'", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.isMainnet).toBe(false); + }); + + it("should be false when STELLAR_NETWORK is not set (empty string)", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue(""), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.isMainnet).toBe(false); + }); + + it("should be false when STELLAR_NETWORK is undefined", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue(undefined), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.isMainnet).toBe(false); + }); + }); + + describe("horizonUrl", () => { + it("should be mainnet horizon URL when isMainnet is true", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("public"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.horizonUrl).toBe("https://horizon.stellar.org"); + }); + + it("should be testnet horizon URL when isMainnet is false", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.horizonUrl).toBe("https://horizon-testnet.stellar.org"); + }); + }); + + describe("networkPassphrase", () => { + it("should use PUBLIC network passphrase when isMainnet is true", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("public"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const { Networks } = await import("@stellar/stellar-sdk"); + const config = await import("../../api/config/stellar.config.js"); + expect(config.networkPassphrase).toBe(Networks.PUBLIC); + }); + + it("should use TESTNET network passphrase when isMainnet is false", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const { Networks } = await import("@stellar/stellar-sdk"); + const config = await import("../../api/config/stellar.config.js"); + expect(config.networkPassphrase).toBe(Networks.TESTNET); + }); + + it("public passphrase should differ from testnet passphrase", () => { + const { Networks } = require("@stellar/stellar-sdk"); + expect(Networks.PUBLIC).not.toBe(Networks.TESTNET); + }); + }); + + describe("anchorHomeDomain", () => { + it("should be 'anchor.stellar.org' on mainnet", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("public"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.anchorHomeDomain).toBe("anchor.stellar.org"); + }); + + it("should be 'testanchor.stellar.org' on testnet", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.anchorHomeDomain).toBe("testanchor.stellar.org"); + }); + }); + + describe("xlmAsset", () => { + it("should be the native XLM asset", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const { Asset } = await import("@stellar/stellar-sdk"); + const config = await import("../../api/config/stellar.config.js"); + expect(config.xlmAsset.isNative()).toBe(true); + expect(config.xlmAsset).toEqual(Asset.native()); + }); + + it("should have XLM code", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.xlmAsset.getCode()).toBe("XLM"); + }); + }); + + describe("usdcAsset", () => { + it("should be a non-native asset with code USDC", async () => { + const mockIssuer = "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"; + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue(mockIssuer) + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.usdcAsset.isNative()).toBe(false); + expect(config.usdcAsset.getCode()).toBe("USDC"); + }); + + it("should use the USDC_ASSET_ID as issuer", async () => { + const mockIssuer = "GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP"; + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue(mockIssuer) + } + })); + const config = await import("../../api/config/stellar.config.js"); + expect(config.usdcAsset.getIssuer()).toBe(mockIssuer); + }); + }); + + describe("stellarServer", () => { + it("should be a Horizon.Server instance", async () => { + vi.resetModules(); + vi.mock("../../api/utils/env.js", () => ({ + Env: { + stellarNetwork: vi.fn().mockReturnValue("testnet"), + usdcAssetId: vi.fn().mockReturnValue("GDQOE23CFSUMSVQK4Y5JHPPYK73VYCNHZHA7ENKCV37P6SUEO6XQBKPP") + } + })); + const { Horizon } = await import("@stellar/stellar-sdk"); + const config = await import("../../api/config/stellar.config.js"); + expect(config.stellarServer).toBeInstanceOf(Horizon.Server); + }); + }); +});