ProofedMesh/services/verification-service/internal/httpapi/server_test.go

142 lines
4.6 KiB
Go
Raw Normal View History

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)
}
}