package httpapi import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "basicstack.de/proofedmesh/services/verification-service/internal/chain" "basicstack.de/proofedmesh/services/verification-service/internal/tlog" ) type stubVerifier struct { ret chain.Verdict err error } func (s *stubVerifier) Verify(_ context.Context, _ chain.Request) (chain.Verdict, error) { return s.ret, s.err } // Rebuild a Server with the stub by replacing the *chain.Verifier's // dependencies. The handler only needs to call Verifier.Verify, so we // shim through a tiny indirection: build a Verifier whose TLog.Fetcher // is a stub that returns ErrTLogUnavailable to force the 502 path, or // wire the stubVerifier via a type-asserting handler clone. // // Simpler: pass a verifier whose dependencies make the Verify call land // on the branch we want. Since Verifier is a concrete struct, we expose // a tiny shim type here to avoid pulling the whole chain package into // the test as a hard dependency. type fakeFetcher struct{ err error } func (f fakeFetcher) Fetch(_ context.Context, _ string) (tlog.Entry, error) { return tlog.Entry{}, f.err } func newServerWithFetcher(f fakeFetcher) *Server { return &Server{ Verifier: &chain.Verifier{ TLog: f, // Keyring + Store intentionally nil: the request hits // Fetch first and bails out before touching them. Approval: chain.ApprovalConfig{ ExpectedKeyID: "k1", TransformKeyID: "k2", }, }, } } func TestHandleVerifyChain_BadRequestOnMalformedJSON(t *testing.T) { s := &Server{Verifier: &chain.Verifier{}} mux := http.NewServeMux() s.Mount(mux) ts := httptest.NewServer(mux) defer ts.Close() resp, err := http.Post(ts.URL+"/api/v1/verify/chain", "application/json", strings.NewReader("not-json")) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("status: want 400, got %d", resp.StatusCode) } } func TestHandleVerifyChain_SinkTargetAliases(t *testing.T) { // Even when the request names `target_sink` instead of `sink_target`, // the request must reach the verifier. We prove it by observing the // verifier produce a verdict reasons list that references the sink // field — the simplest signal is to force a TLog-not-found verdict // (not an error) by setting ErrNotFound. s := newServerWithFetcher(fakeFetcher{err: tlog.ErrNotFound}) mux := http.NewServeMux() s.Mount(mux) ts := httptest.NewServer(mux) defer ts.Close() for _, body := range []string{ `{"decision_id":"d","approval_ref":"sha256:` + strings.Repeat("a", 64) + `","transform_ref":"sha256:` + strings.Repeat("b", 64) + `","payload_hash":"sha256:` + strings.Repeat("c", 64) + `","sink_target":"sink:dummy-sink"}`, `{"decision_id":"d","approval_ref":"sha256:` + strings.Repeat("a", 64) + `","transform_ref":"sha256:` + strings.Repeat("b", 64) + `","payload_hash":"sha256:` + strings.Repeat("c", 64) + `","target_sink":"sink:dummy-sink"}`, } { resp, err := http.Post(ts.URL+"/api/v1/verify/chain", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("post: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("status: want 200, got %d", resp.StatusCode) } var out chain.Verdict if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatalf("decode: %v", err) } resp.Body.Close() if out.Valid { t.Fatalf("verdict unexpectedly valid: %+v", out) } if len(out.Reasons) == 0 || !strings.HasPrefix(out.Reasons[0], chain.ReasonApprovalFetch) { t.Fatalf("reasons: %v", out.Reasons) } } } func TestHandleVerifyChain_BadGatewayOnTLogUnavailable(t *testing.T) { s := newServerWithFetcher(fakeFetcher{err: tlog.ErrTLogUnavailable}) mux := http.NewServeMux() s.Mount(mux) ts := httptest.NewServer(mux) defer ts.Close() body := `{"decision_id":"d","approval_ref":"sha256:` + strings.Repeat("a", 64) + `","transform_ref":"sha256:` + strings.Repeat("b", 64) + `","payload_hash":"sha256:` + strings.Repeat("c", 64) + `","sink_target":"sink:dummy-sink"}` resp, err := http.Post(ts.URL+"/api/v1/verify/chain", "application/json", bytes.NewReader([]byte(body))) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadGateway { t.Fatalf("status: want 502, got %d", resp.StatusCode) } } func TestHandleHealth(t *testing.T) { s := &Server{} mux := http.NewServeMux() s.Mount(mux) ts := httptest.NewServer(mux) defer ts.Close() resp, err := http.Get(ts.URL + "/healthz") if err != nil { t.Fatalf("get: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status: want 200, got %d", resp.StatusCode) } }