From 898023c79491a68593e8ef12cb4a19466c8e509e Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Mon, 20 Apr 2026 22:11:12 +0200 Subject: [PATCH] feat: Open Payments aggregator mock for acctest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tiny in-memory stand-in for the Open Payments PSD2/BerlinGroup aggregator, extracted from acctest where it lived inline (which couldn't docker-build on the self-hosted CI runner — /.docker/buildx was read-only). A separate repo: - lets the mock build + publish its image via the standard shiny CI flow, pulled like any other service - keeps acctest focused on test infra, not service source - gives the mock its own versioned release lifecycle **Endpoints** (see README for the full list) - OAuth: POST /token → canned bearer token. - AIS: aspsps catalog, consent lifecycle, /authorize self-redirect SCA stub, payment + card accounts + transactions. - PIS: sepa-credit-transfers initiate + status (RCVD → PDNG → ACSC; creditor name 'REJECT ME' → RJCT), signing baskets (RCVD → ACCP → ACSC; basket ACSC advances all linked payments). - Admin: /admin/* endpoints let acctest force deterministic transitions, seed transactions, reset state, override consent expiry. **CI** Standard shiny ci.yaml: check (go build + vet), vulnerabilities (govulncheck), build (buildtools publishes oci.unbound.se/shiny/ openpayments-mock:${COMMIT}). No deploy job — image is consumed by acctest only. **k8s/deploy.yaml** Deployment + Service on port 8080 with /healthz readiness/liveness. acctest's infra manifest will reference the published tag. --- .gitea/workflows/ci.yaml | 69 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 4 +++ .pre-commit-config.yaml | 33 +++++++++++++++++++ CLAUDE.md | 28 ++++++++++++++++ Dockerfile | 25 +++++++++++++++ README.md | 48 ++++++++++++++++++++++++++++ go.mod | 5 +++ go.sum | 2 ++ k8s/deploy.yaml | 61 +++++++++++++++++++++++++++++++++++ 9 files changed, 275 insertions(+) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 k8s/deploy.yaml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..a4b501a --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,69 @@ +name: openpayments-mock + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + if: gitea.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: 'stable' + - name: Build + run: go build ./... + - name: Vet + run: go vet ./... + + vulnerabilities: + if: gitea.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: 'stable' + - name: Check vulnerabilities + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... + + build: + runs-on: ubuntu-latest + env: + BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }} + GITEA_REPOSITORY: ${{ gitea.repository }} + steps: + - uses: actions/checkout@v6 + - uses: buildtool/setup-buildtools-action@v1 + - name: Build and push + run: unset GITEA_TOKEN && build && push + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: deployment-artifacts + path: release/ + retention-days: 30 + - name: Trigger acceptance tests + if: gitea.event_name == 'pull_request' + env: + GITEA_TOKEN: ${{ secrets.ACCTEST_TOKEN }} + GITEA_URL: ${{ gitea.server_url }} + run: | + curl --fail-with-body -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "ref": "main", + "inputs": { + "trigger_repo": "openpayments-mock", + "trigger_commit": "${{ gitea.sha }}", + "trigger_run": "${{ gitea.run_id }}" + } + }' \ + "${GITEA_URL}/api/v1/repos/shiny/acctest/actions/workflows/ci.yaml/dispatches" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104f3c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +openpayments-mock +service +coverage.txt +coverage.out diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..40f01ba --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + args: + - --allow-multiple-documents + - id: check-added-large-files +- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.24.0 + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: ['@commitlint/config-conventional'] +- repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-mod-tidy + - id: go-imports + args: + - -local + - gitea.unbound.se/shiny/openpayments-mock +- repo: https://github.com/lietu/go-pre-commit + rev: v1.0.0 + hooks: + - id: gofumpt + - id: golangci-lint-full +- repo: https://github.com/gitleaks/gitleaks + rev: v8.28.0 + hooks: + - id: gitleaks diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e4ea31f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,28 @@ +## openpayments-mock + +Tiny Go HTTP server that mimics the Open Payments (PSD2/BerlinGroup) aggregator for Shiny's acceptance tests. Consumed by `acctest`'s `banking-suite` via the image published from this repo's CI. + +## Shared Documentation + +@../docs/claude/architecture.md +@../docs/claude/go-services.md +@../docs/claude/conventions.md +@../docs/claude/cicd.md + +## Service-Specific Information + +### Purpose + +Stand-in for the real Open Payments aggregator. Not safe for anything but tests — state is entirely in-memory, there are no authentication checks, and admin endpoints let the acctest suite force deterministic transitions (payment status, basket status, transaction seeding, consent expiry). + +### Port + +8080 (matching production aggregator convention; acctest reaches it via cluster DNS `openpayments-mock:8080`). + +### Not deployed to staging/prod + +CI builds + publishes the image but does not run `deploy` — the mock is test-only infrastructure. acctest's `k8s/infra/base/openpayments-mock.yaml` references the published image. + +### Endpoints + +See `README.md` for the full endpoint list. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e01c15 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM amd64/golang:1.26.2@sha256:3e677b9776e5fcb030321772b4fe13c58b22b8abe772c647be8f746159d1a2dc as modules +WORKDIR /build +ENV GOPRIVATE=gitea.unbound.se/shiny,gitea.unbound.se/unboundsoftware +ADD go.* /build +RUN go mod download + +FROM modules as build +ARG CI_COMMIT +WORKDIR /build +ENV CGO_ENABLED=0 +ADD . /build +RUN GOOS=linux GOARCH=amd64 go build \ + -tags prod \ + -a -installsuffix cgo \ + -mod=readonly \ + -o /release/service \ + -ldflags "-w -s -X main.buildVersion=${CI_COMMIT}" \ + ./cmd/service/service.go + +FROM scratch +ENV TZ Europe/Stockholm +COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /release/service / +CMD ["/service"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9767efe --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# openpayments-mock + +Tiny in-memory stand-in for the Open Payments PSD2/BerlinGroup aggregator, +used by the `banking-suite` acceptance tests. Accepts any bearer token +and returns canned responses. + +Not safe for anything but tests — state is entirely in-memory and there +are no authentication checks. + +## Endpoints + +### OAuth +- `POST /token` — returns static `{access_token: "acctest", ...}` + +### AIS (BerlinGroup shape) +- `GET /psd2/aspspinformation/v1/aspsps` — catalog +- `POST /psd2/consent/v1/consents` +- `GET /psd2/consent/v1/consents/{id}/status` +- `DELETE /psd2/consent/v1/consents/{id}` +- `GET /authorize` — mock SCA stub that immediately redirects back to the + `TPP-Redirect-URI` with `?code=mock-auth-code&state=...` +- `GET /psd2/accountinformation/v1/accounts` +- `GET /psd2/accountinformation/v1/accounts/{id}/balances` +- `GET /psd2/accountinformation/v1/accounts/{id}/transactions` +- `GET /psd2/cardaccountinformation/v1/card-accounts` + +### PIS +- `POST /psd2/paymentinitiation/v1/payments/sepa-credit-transfers` +- `GET /psd2/paymentinitiation/v1/payments/sepa-credit-transfers/{id}/status` + — transitions RCVD → PDNG → ACSC deterministically on successive polls. + Creditor name `"REJECT ME"` transitions to RJCT. +- `POST /psd2/v1/signing-baskets` +- `GET /psd2/v1/signing-baskets/{id}/status` — RCVD → ACCP → ACSC; + reaching ACSC advances all linked payments to ACSC. + +### Admin (acctest only, not part of PSD2) +- `POST /admin/reset` +- `GET /admin/consents` +- `POST /admin/transactions` — body `{ accountId, bookingStatus, entries: [...] }` +- `POST /admin/payments/{id}/status` — body `{ status }` +- `POST /admin/baskets/{id}/status` — body `{ status }` +- `POST /admin/consents/{id}/expires-at` — body `{ expiresAt }` + +## Build + +```bash +docker build -t openpayments-mock:acctest . +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db2cdae --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitea.unbound.se/shiny/openpayments-mock + +go 1.24 + +require github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7790d7c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/k8s/deploy.yaml b/k8s/deploy.yaml new file mode 100644 index 0000000..8932274 --- /dev/null +++ b/k8s/deploy.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openpayments-mock + labels: + app.kubernetes.io/name: openpayments-mock + annotations: + kubernetes.io/change-cause: "${TIMESTAMP} Deployed commit id: ${COMMIT}" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: openpayments-mock + template: + metadata: + labels: + app.kubernetes.io/name: openpayments-mock + app.kubernetes.io/instance: shiny + spec: + containers: + - name: openpayments-mock + image: oci.unbound.se/shiny/openpayments-mock:${COMMIT} + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: PUBLIC_BASE_URL + value: "https://openpayments-mock" + resources: + requests: + cpu: "10m" + memory: "20Mi" + limits: + memory: "64Mi" + readinessProbe: + httpGet: + path: /healthz + port: 8080 + periodSeconds: 2 + failureThreshold: 10 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + periodSeconds: 10 + failureThreshold: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: openpayments-mock + labels: + app.kubernetes.io/name: openpayments-mock +spec: + selector: + app.kubernetes.io/name: openpayments-mock + ports: + - name: http + port: 8080 + targetPort: 8080