diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92759da..e3f54a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,8 @@ jobs: - name: Run linter run: npm run lint + - name: Run unit tests + run: npm run test:unit + - name: Build project - run: npm run build \ No newline at end of file + run: npm run build diff --git a/lib/utils/stellar-address.test.ts b/lib/utils/stellar-address.test.ts new file mode 100644 index 0000000..31d2aec --- /dev/null +++ b/lib/utils/stellar-address.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import * as StellarSdk from "@stellar/stellar-sdk"; +import { isValidStellarAddress, getStellarAddressError } from "./stellar-address"; + +// A real valid Stellar public key (from a random keypair) +const VALID_ADDRESS = StellarSdk.Keypair.random().publicKey(); + +describe("isValidStellarAddress", () => { + it("accepts a real Stellar public key", () => { + expect(isValidStellarAddress(VALID_ADDRESS)).toBe(true); + }); + + it("rejects an address with wrong length", () => { + expect(isValidStellarAddress("GABC123")).toBe(false); + }); + + it("rejects an address that does not start with G", () => { + // Replace leading 'G' with 'A' + const bad = "A" + VALID_ADDRESS.slice(1); + expect(isValidStellarAddress(bad)).toBe(false); + }); + + it("rejects an address with a bad checksum", () => { + // Flip the last character + const last = VALID_ADDRESS[VALID_ADDRESS.length - 1]; + const flipped = last === "A" ? "B" : "A"; + expect(isValidStellarAddress(VALID_ADDRESS.slice(0, -1) + flipped)).toBe(false); + }); + + it("rejects empty string", () => { + expect(isValidStellarAddress("")).toBe(false); + }); + + it("rejects non-string values", () => { + expect(isValidStellarAddress(null)).toBe(false); + expect(isValidStellarAddress(undefined)).toBe(false); + expect(isValidStellarAddress(123)).toBe(false); + expect(isValidStellarAddress({})).toBe(false); + }); + + it("rejects whitespace-only string", () => { + expect(isValidStellarAddress(" ")).toBe(false); + }); + + it("rejects an address with invalid base32 characters", () => { + expect(isValidStellarAddress("G" + "0".repeat(55))).toBe(false); + }); +}); + +describe("getStellarAddressError", () => { + it("returns null for a valid address", () => { + expect(getStellarAddressError(VALID_ADDRESS)).toBeNull(); + }); + + it("returns required message for empty string", () => { + expect(getStellarAddressError("")).toBe("Stellar address is required"); + }); + + it("returns required message for non-string", () => { + expect(getStellarAddressError(null)).toBe("Stellar address is required"); + expect(getStellarAddressError(undefined)).toBe("Stellar address is required"); + }); + + it("returns invalid message for a malformed address", () => { + expect(getStellarAddressError("GABC123")).toBe("Invalid Stellar address"); + }); + + it("returns invalid message for wrong-length address starting with G", () => { + expect(getStellarAddressError("G" + "A".repeat(54))).toBe("Invalid Stellar address"); + }); +}); diff --git a/lib/utils/stellar-address.ts b/lib/utils/stellar-address.ts new file mode 100644 index 0000000..7bbc283 --- /dev/null +++ b/lib/utils/stellar-address.ts @@ -0,0 +1,31 @@ +/** + * Stellar address validation utility using the Stellar SDK. + * + * Uses StrKey.isValidEd25519PublicKey which decodes the Base32-encoded + * address and validates the checksum — far more robust than a simple + * length + prefix check. + */ +import { StrKey } from "@stellar/stellar-sdk"; + +/** + * Returns true if `address` is a valid Stellar Ed25519 public key (G…). + */ +export function isValidStellarAddress(address: unknown): boolean { + if (typeof address !== "string" || address.trim() === "") return false; + try { + return StrKey.isValidEd25519PublicKey(address); + } catch { + return false; + } +} + +/** + * Returns a descriptive error message when `address` is invalid, + * or null when it is valid. + */ +export function getStellarAddressError(address: unknown): string | null { + if (typeof address !== "string" || address.trim() === "") { + return "Stellar address is required"; + } + return isValidStellarAddress(address) ? null : "Invalid Stellar address"; +} diff --git a/package-lock.json b/package-lock.json index 48ce039..40c72d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4098,9 +4098,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4115,9 +4112,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4132,9 +4126,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4149,9 +4140,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4166,9 +4154,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4183,9 +4168,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4200,9 +4182,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4217,9 +4196,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4234,9 +4210,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4251,9 +4224,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4268,9 +4238,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4285,9 +4252,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4302,9 +4266,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 34ee712..09b29be 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "test:sensitive-actions": "node scripts/test-sensitive-actions-api.mjs", "test:vote-remove": "node scripts/test-vote-remove-api.mjs", "test:ephemeral": "node scripts/test-ephemeral-cleanup.mjs", - "test:unit": "vitest", "start": "next start", "cleanup:worker": "node scripts/ephemeral-cleanup-worker.js", "cleanup:trigger": "node scripts/trigger-cleanup.mjs" diff --git a/vitest.config.ts b/vitest.config.ts index 7df5736..8cfbec8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ environment: "happy-dom", include: [ "lib/blockchain/group-verification.test.ts", + "lib/utils/stellar-address.test.ts", "components/GroupVerificationBadge.test.tsx", ], globals: false,