Files
payments/internal/api/v3/handler_connectors_capabilities_test.go
zeekay 7d727874d5 chore: rebrand Formance Payments -> Hanzo Payments (hanzo-fi)
- go.mod module path + all self-imports: github.com/formancehq/payments -> github.com/hanzo-fi/payments
  (1590 .go files / 3152 refs; 20 modules: root, pkg/client, pkg/domain, 18 ce/plugins).
  External dep kept: formancehq/go-libs.
- plugins: each ce/plugins/* consumed a *published* pkg/domain; added local
  'replace .../pkg/domain => ../../../pkg/domain' (consistent with root) so every
  standalone plugin module builds in-monorepo. compile-plugin.sh MOD var rebranded too.
- branding: README, LICENSE (dual MIT+EE; upstream copyright retained + Hanzo added),
  goreleaser (release images -> registry.hanzo.ai/hanzo-fi, OCI labels, homepage),
  proto go_package, renovate, speakeasy gen.yaml, connector-template *.gotpl.
- CI: removed Numary/Formance workflows + composite action; added canonical hanzo.yml +
  cicd.yml (hanzoai/ci, gated HANZO_CI_ENABLED) + secretless ci.yml (go build/vet).
- kept as data/compat keys: wire capability enums ALLOW_FORMANCE_*, generated SDK type
  client.Formance, domain field FormanceRedirectURL, OTEL_SERVICE_NAME, upstream base image.
- verified: go build ./... (default + -tags ee) + all 20 nested modules green.
2026-07-05 13:08:55 -07:00

141 lines
4.7 KiB
Go

package v3
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"github.com/hanzo-fi/payments/internal/api/backend"
"github.com/hanzo-fi/payments/internal/api/services"
"github.com/hanzo-fi/payments/pkg/domain/models"
"github.com/hanzo-fi/payments/internal/storage"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/mock/gomock"
)
var _ = Describe("API v3 Connectors Capabilities", func() {
Context("list capabilities catalog", func() {
var (
w *httptest.ResponseRecorder
m *backend.MockBackend
handlerFn http.HandlerFunc
)
BeforeEach(func() {
w = httptest.NewRecorder()
ctrl := gomock.NewController(GinkgoT())
m = backend.NewMockBackend(ctrl)
handlerFn = connectorsCapabilities(m)
})
It("returns the catalog with ETag and Cache-Control headers", func(ctx SpecContext) {
m.EXPECT().ConnectorsCapabilities().Return(map[string][]models.Capability{
"stripe": {models.CAPABILITY_FETCH_ACCOUNTS, models.CAPABILITY_CREATE_TRANSFER},
})
handlerFn(w, httptest.NewRequest(http.MethodGet, "/", nil))
res := w.Result()
defer res.Body.Close()
Expect(res.StatusCode).To(Equal(http.StatusOK))
Expect(res.Header.Get("Cache-Control")).To(Equal(capabilitiesCacheControl))
etag := res.Header.Get("ETag")
Expect(etag).To(HavePrefix(`"`))
Expect(etag).To(HaveSuffix(`"`))
var body struct {
Data map[string][]models.Capability `json:"data"`
}
Expect(json.NewDecoder(res.Body).Decode(&body)).To(Succeed())
Expect(body.Data).To(HaveKeyWithValue("stripe", []models.Capability{
models.CAPABILITY_FETCH_ACCOUNTS,
models.CAPABILITY_CREATE_TRANSFER,
}))
})
It("returns 304 when If-None-Match matches the cached ETag", func(ctx SpecContext) {
// OnceValues caches per-handler so the second request hits the
// same ETag without touching the backend again.
m.EXPECT().ConnectorsCapabilities().Return(map[string][]models.Capability{
"stripe": {models.CAPABILITY_FETCH_ACCOUNTS},
}).Times(1)
first := httptest.NewRecorder()
handlerFn(first, httptest.NewRequest(http.MethodGet, "/", nil))
etag := first.Result().Header.Get("ETag")
second := httptest.NewRequest(http.MethodGet, "/", nil)
second.Header.Set("If-None-Match", etag)
handlerFn(w, second)
res := w.Result()
defer res.Body.Close()
Expect(res.StatusCode).To(Equal(http.StatusNotModified))
Expect(w.Body.Len()).To(Equal(0))
})
})
Context("get capabilities for a single connector", func() {
var (
w *httptest.ResponseRecorder
m *backend.MockBackend
handlerFn http.HandlerFunc
connID models.ConnectorID
)
BeforeEach(func() {
w = httptest.NewRecorder()
ctrl := gomock.NewController(GinkgoT())
m = backend.NewMockBackend(ctrl)
handlerFn = connectorsCapabilitiesGet(m)
connID = models.ConnectorID{Reference: uuid.New(), Provider: "psp"}
})
It("returns 400 when the connector ID is malformed", func(ctx SpecContext) {
handlerFn(w, prepareQueryRequest(http.MethodGet, "connectorID", "not-an-id"))
assertExpectedResponse(w.Result(), http.StatusBadRequest, ErrInvalidID)
})
It("returns 404 when the connector is not found", func(ctx SpecContext) {
m.EXPECT().ConnectorsCapabilitiesGet(gomock.Any(), connID).
Return(nil, services.ErrNotFound)
handlerFn(w, prepareQueryRequest(http.MethodGet, "connectorID", connID.String()))
assertExpectedResponse(w.Result(), http.StatusNotFound, "")
})
It("returns 404 when the underlying storage cannot find the row", func(ctx SpecContext) {
m.EXPECT().ConnectorsCapabilitiesGet(gomock.Any(), connID).
Return(nil, storage.ErrNotFound)
handlerFn(w, prepareQueryRequest(http.MethodGet, "connectorID", connID.String()))
assertExpectedResponse(w.Result(), http.StatusNotFound, "")
})
It("returns 500 for unexpected errors", func(ctx SpecContext) {
m.EXPECT().ConnectorsCapabilitiesGet(gomock.Any(), connID).
Return(nil, errors.New("boom"))
handlerFn(w, prepareQueryRequest(http.MethodGet, "connectorID", connID.String()))
assertExpectedResponse(w.Result(), http.StatusInternalServerError, "INTERNAL")
})
It("returns capability names on success", func(ctx SpecContext) {
caps := []models.Capability{
models.CAPABILITY_FETCH_ACCOUNTS,
models.CAPABILITY_CREATE_TRANSFER,
}
m.EXPECT().ConnectorsCapabilitiesGet(gomock.Any(), connID).Return(caps, nil)
handlerFn(w, prepareQueryRequest(http.MethodGet, "connectorID", connID.String()))
res := w.Result()
defer res.Body.Close()
Expect(res.StatusCode).To(Equal(http.StatusOK))
var body struct {
Data []models.Capability `json:"data"`
}
Expect(json.NewDecoder(res.Body).Decode(&body)).To(Succeed())
Expect(body.Data).To(Equal(caps))
})
})
})