Skip to content

Fix issues: 82, 83, 84 - #118

Merged
believetimothy merged 2 commits into
soroworks:mainfrom
abercrombie04:fix-issues-82-83-84
Oct 25, 2026
Merged

believetimothy merged 2 commits into
soroworks:mainfrom
abercrombie04:fix-issues-82-83-84

Conversation

@abercrombie04

Copy link
Copy Markdown
Contributor

This PR resolves the following assigned issues:

Changes Made

#82 — test(deploy): cover DefaultClientFactory wiring

  • Added internal/deploy/client_factory_test.go (new file, owned by this issue) with three test groups:
    • TestDefaultClientFactoryWiresTheNetworkPassphrase — builds a client from a config.Network for testnet and mainnet (using the same passphrase literals as the existing deploy fixtures) and asserts NetworkPassphrase() returns exactly the passphrase configured for that network. This pins the rpc_url → URL and passphrase → NetworkPassphrase field wiring, so a mix-up between the two fields (which would sign transactions for the wrong network) fails immediately.
    • TestDefaultClientFactoryKeepsNetworksApart — builds clients for two networks back to back, the sequence a server serves when consecutive requests deploy to testnet and then mainnet, and asserts neither client picked up the other's passphrase.
    • TestDefaultClientFactoryRejectsInvalidRPCURLs — table of five malformed endpoints (not a url, ://missing-scheme, ftp://example.org, empty string, https:// with no host), each asserting construction returns an error naming the offending value and no client.
  • Added a small, additive URL check at the top of DefaultClientFactory (internal/deploy/service.go): parse with net/url, require scheme http/https and a non-empty host, otherwise return invalid RPC URL %q: .... Rationale: stellar.NewRPCClient only rejects an empty string, so any other malformed endpoint survived construction and failed later as an opaque error on the first request; checking in the factory names the bad value while the config file is still open. Construction makes no network call, so the tests need no server and no httptest.
  • Test file uses package deploy_test and only the public surface (deploy.DefaultClientFactory, config.Network), matching how other packages' external tests are written.

#83 — refactor(config): register constructor argument types in a table

  • internal/config/args.go: replaced the single switch in Arg.ToScVal with one ordered registry, argTypeRegistry []argTypeEntry{name, encode}, populated in init():
    • Each type's encoder is now a named function (encodeBool, encodeU32, …, encodeVec, encodeMap, encodeXDR) registered in exactly one place.
    • SupportedArgTypes is derived from the registry (built in the same init), never a hand-maintained literal — the drift the old design allowed (a type encoded but missing from the message, or listed but never encoded) is now impossible. The current order (bool, void, u32, i32, u64, i64, u128, i128, string, symbol, bytes, address, vec, map, xdr) and therefore every "supported: ..." error message are byte-identical to before.
    • ToScVal normalises the type name with the same strings.ToLower(strings.TrimSpace(...)), keeps the exact missing type (...) and unsupported type %q (... use type: xdr ...) messages, and looks the name up in the registry.
    • vec and map recurse through Arg.ToScVal rather than calling encoders directly, so nested values take the same registry lookup and the same vec[%d] / map[%d].key / map[%d].value error wrapping as top-level ones.
    • The init() pattern is used because the pieces reference each other (registry holds the encoders, vec/map recurse through ToScVal, ToScVal reads the registry and SupportedArgTypes); written as initialization expressions that is a cycle the compiler rejects, and init runs once after all package variables are set with deterministic order.
    • The Arg doc comment now describes the registry workflow instead of "extend the switch".
  • internal/config/args_test.go: existing table-driven cases were moved to package-level vars verbatim (zero expectation changes — every pre-existing test passes unchanged, as the issue requires), plus:
    • TestEveryRegisteredArgTypeHasATableCase — asserts every type in argTypeRegistry has at least one case in the table test, so a new type cannot be registered without a test.
    • TestArgToScValContainerTypes — extra nested vec/map coverage exercising recursion through the registry.

#84 — feat(api): configurable CORS for browser dashboards

  • internal/config/env.go:
    • New EnvAPICORSOrigins = "SOROFORGE_API_CORS_ORIGINS" and Env.APICORSOrigins (trimmed in LoadEnv; included in Redacted since origins are public).
    • ParseCORSOrigins(raw) — splits the comma-separated value, trims, skips empty entries (a trailing comma is harmless), validates each origin, and errors on any invalid entry so a typo cannot silently disable or widen the policy. Empty input yields no origins, which is how CORS stays opt-in: unset ⇒ exactly today's behaviour of no CORS headers.
    • ValidateCORSOrigin(origin) (exported so api.NewRouter can hold Options to the same rules) — rejects * anywhere with an explicit explanation: SOROFORGE_API_CORS_ORIGINS must not contain "*": list the exact origins the dashboard is served from. It also requires an exact scheme://host[:port]: no path, query, fragment, userinfo, or non-http(s) scheme. Rationale documented in code: the API can deploy contracts with the configured signing key, and browsers compare origins exactly, so accepting a path/trailing slash would configure a list that never matches with CORS silently off.
  • internal/api/api.go:
    • Options gains CORSOrigins []string; NewRouter re-validates each origin (library callers get the same refusal at startup as the env var) and returns an error rather than a server.
    • When origins are configured, the chi router is wrapped with github.com/go-chi/cors (new direct dependency, v1.2.1) configured narrowly: AllowedOrigins = the exact list, AllowedMethods = GET, POST (only what routes() declares), AllowedHeaders = Authorization, Content-Type (the bearer token and JSON bodies), MaxAge = 10 minutes, and no AllowCredentials (the API authenticates with a bearer header, not cookies).
    • The middleware wraps the router rather than joining its stack: preflight OPTIONS requests deliberately carry no Authorization header, so the CORS handler answers them before the auth-wrapped routes are ever reached, while actual requests still pass through auth.authorize in full.
  • cmd/soroforge/commands.go: the serve command parses SOROFORGE_API_CORS_ORIGINS via config.ParseCORSOrigins and passes the result to api.Options, so a bad value (including *) fails at startup.
  • README.md: documented the variable in the environment-variables table plus a "Browser dashboards (CORS)" section with an example, the opt-in rationale, the * refusal, and the preflight-without-token / actual-requests-still-authenticated behaviour.
  • go.mod/go.sum: added github.com/go-chi/cors v1.2.1.

Tests

  • New/updated coverage: allowed origin echoes Access-Control-Allow-Origin; a disallowed origin gets no CORS header; preflight OPTIONS to a write route with no token returns 2xx with CORS headers (and the handler is never invoked); a tokenless actual request with an allowed origin still returns 401; empty config sends no CORS headers at all; * and malformed origins are refused by both ParseCORSOrigins and api.NewRouter.
  • Verification (repo root): make check — gofmt clean, go vet ./... clean, go test ./... all 8 packages OK (cmd/soroforge, internal/api, internal/catalog, internal/config, internal/deploy, internal/readiness, internal/stellar, internal/store), re-run with -count=1. make lint reports golangci-lint not installed and exits 0.

@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@abercrombie04 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

github-actions Bot pushed a commit that referenced this pull request Oct 19, 2026
…125)

This PR resolves the following assigned issues:
- closes #79
- closes #80
- closes #81

## Changes Made

**#79 — docker: run soroforge serve and its migrations with Compose** (`docker-compose.yml`, `README.md`)
- Adds two profiled services under the `app` profile, extending the existing Postgres service (untouched, healthcheck comment intact):
  - `migrate`: one-shot `soroforge migrate up`, starts after `postgres` is healthy, exits 0 on success.
  - `serve`: `soroforge serve`, waits on `migrate` via `depends_on: condition: service_completed_successfully` (the SoroVault pattern the issue names).
- Secrets come from the environment only (`SOROFORGE_API_TOKEN` is required at the compose layer via `${...:?...}` so a missing token fails fast; the keystore is mounted read-only). Nothing secret is stored in the file.
- Host port publish stays `127.0.0.1:8080:8080`, matching the binary's own loopback default (`config.DefaultAPIAddr`).
- Plain `docker compose up -d` remains Postgres-only because both new services are profiled, so the current workflow is unchanged.

**#80 — feat(cli): require typed confirmation before submitting to mainnet** (`cmd/soroforge/confirm.go`, `cmd/soroforge/commands.go`, `internal/config/config.go`, `soroforge.yaml`, tests)
- `Network.Protected` (`protected: true` in `soroforge.yaml`); `mainnet` ships protected.
- `deploy` and `upgrade` print what will happen (action, target, network, passphrase) and require the exact network name typed on stdin before anything is submitted.
- Fails closed: `--yes` skips for CI (and says so on stderr); **without a terminal on stdin and without `--yes` the command refuses rather than hanging**; a read error or empty input is a refusal, not consent; `--dry-run` never prompts. Prompts go to stderr so `--json` on stdout stays parseable.
- Tests (`cmd/soroforge/confirm_test.go`) cover prompt accepted, prompt rejected, `--yes`, non-TTY refusal, dry-run, and read-error refusal; config tests cover `protected` parsing.
- Position on the HTTP API, as the issue asks: protected networks are **not** refused through the API in this PR — `/v1/deploy` and `/v1/upgrade` keep proceeding behind a write-scoped token, and the README says so. Making the API refuse protected networks (or require an explicit allow flag per token) is a product decision this PR deliberately does not pre-empt.

**#81 — feat(store): record failed deploy and upgrade attempts** (`internal/store/*`, migrations, `internal/deploy/*`, CLI, tests)
- New `Attempt` type (alias, network, action, stage `simulate|submit|confirm`, error text, tx hash when one was sent, time) and two `Store` interface methods, `RecordAttempt` / `Attempts`, implemented by **all three shipped backends** (MemoryStore, SQLite, Postgres).
- Paired `000006_deployment_attempts` up/down migrations for both the `postgres/` and `sqlite/` dialect directories, following the repo's embed + golang-migrate convention; the reversibility tests' version gate moved 5 → 6, and the existing SQLite/Postgres dialect-parity test covers the new table.
- `internal/deploy`: failures out of `runTransaction` now carry their submission-cycle stage in a transparent wrapper (`stagedError`; message text unchanged, `errors.Is/As` pass through `Unwrap`), and `Deploy`/`Upgrade` record every non-dry-run failure via a deferred guard so no error path can be missed. Recording failures can never mask the operator's original error.
- Failures never enter `deployments`: `status`, drift detection, and `history` read confirmed-only rows (asserted in both the conformance suite and the deploy tests).
- `soroforge history <alias> --failures` shows them (table and `--json`); README documents the flag and the semantics.
- Key-material guarantee: `SanitizeKeyMaterial` redacts secret-seed-shaped text on every write path; tests assert a stored attempt error contains the redaction marker and never the seed, both at store level and end-to-end through a failing deploy.

## Verification (run on this branch head `22cd756`)

- Baseline at `72d11e6`: `gofmt -l` empty, `go vet ./...` clean, `go build ./...` clean, `go test ./...` all 9 packages ok.
- After: `gofmt -l` empty; `go vet ./...` clean; `go build ./...` clean; `go test ./... -count=1 -v`: **742 pass lines (tests incl. subtests), 0 fail**.
- `go test -race ./internal/store/ ./internal/deploy/ ./cmd/soroforge/ ./internal/config/`: all ok.
- `make check` (the testing command all three issues name): exit 0.
- `make lint`: **not meaningfully run** — `golangci-lint` is not installed on this machine and the repo's lint target self-skips when it is absent.
- `make test-integration` / the Postgres half of the store suite: **not run** — it needs `TEST_DATABASE_URL` against a live Postgres and this machine has no container runtime (docker/podman absent) and installing one was out of scope. The Postgres code path and the new `000006` pair are therefore only exercised via the conformance suite's Memory/SQLite runs and compile-level checks, same as CI-gated today.
- #79 compose: validated statically and semantically, not by booting containers (no daemon here): YAML parses (`yaml.safe_load`); `migrate`/`serve` are profile-gated; `serve.depends_on.migrate = service_completed_successfully`; env var names cross-checked against `.env.example` and `internal/config/env.go`; `command: ["migrate","up"]` / `["serve"]` verified against the actual cobra subcommands; port default matches `config.DefaultAPIAddr`. `docker compose up --profile app` itself is the one criterion that needs a daemon and is stated as unverified.

No new Go dependencies were added (stdlib only). `deployment_attempts` was added to the integration-test truncate list so the shared-DB suite stays clean.

## Overlap note

Open PRs #118/#117/#110 touch some of the same files (`README.md`, `cmd/soroforge/commands.go`, `internal/deploy/service.go`, `internal/deploy/deploy.go`, `internal/config/*`) in different regions; none of them references #79/#80/#81. Merge conflicts there are theirs-to-resolve order-wise; nothing here edits their issue areas.
@believetimothy believetimothy added do-not-merge A maintainer has stopped this PR from being merged automatically and removed do-not-merge A maintainer has stopped this PR from being merged automatically labels Oct 25, 2026
soroworks#120 landed the same changes first and later PRs built on them, so the
conflicting files follow main.
@believetimothy
believetimothy merged commit 2f86c10 into soroworks:main Oct 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants