142 lines
4.6 KiB
Go
142 lines
4.6 KiB
Go
|
|
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)
|
||
|
|
}
|
||
|
|
}
|