package httpapi import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "basicstack.de/proofedmesh/services/transparency-log/pkg/backend" ) func fixtureEnvelope() backend.Envelope { return backend.Envelope{ PayloadType: "application/vnd.in-toto+json", Payload: "aGVsbG8", Signatures: []backend.EnvelopeSignature{{KeyID: "k", Sig: "c2ln"}}, } } func newServer(t *testing.T) *httptest.Server { t.Helper() s := &Server{Backend: backend.NewMemory()} return httptest.NewServer(s.Handler()) } func TestSubmit_HappyPath_201(t *testing.T) { srv := newServer(t) defer srv.Close() env := fixtureEnvelope() ch, err := backend.EnvelopeContentHash(env) if err != nil { t.Fatalf("hash: %v", err) } body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: ch}) resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body))) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { raw, _ := io.ReadAll(resp.Body) t.Fatalf("want 201, got %d: %s", resp.StatusCode, raw) } var rct backend.Receipt if err := json.NewDecoder(resp.Body).Decode(&rct); err != nil { t.Fatalf("decode: %v", err) } if rct.ContentHash != ch { t.Errorf("content_hash echoed wrong: %q vs %q", rct.ContentHash, ch) } if rct.LogIndex != 0 { t.Errorf("log_index: %d", rct.LogIndex) } if rct.InclusionProof.TreeSize != 1 { t.Errorf("tree_size: %d", rct.InclusionProof.TreeSize) } } func TestSubmit_Idempotent_200_SameIndex(t *testing.T) { srv := newServer(t) defer srv.Close() env := fixtureEnvelope() ch, _ := backend.EnvelopeContentHash(env) body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: ch}) // First call -> 201 resp1, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body))) if err != nil { t.Fatalf("post1: %v", err) } resp1.Body.Close() if resp1.StatusCode != http.StatusCreated { t.Fatalf("first status: %d", resp1.StatusCode) } // Second call -> 200 resp2, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body))) if err != nil { t.Fatalf("post2: %v", err) } defer resp2.Body.Close() if resp2.StatusCode != http.StatusOK { raw, _ := io.ReadAll(resp2.Body) t.Fatalf("idempotent repeat status: want 200, got %d, body=%s", resp2.StatusCode, raw) } var rct2 backend.Receipt if err := json.NewDecoder(resp2.Body).Decode(&rct2); err != nil { t.Fatalf("decode: %v", err) } if rct2.LogIndex != 0 { t.Errorf("idempotent log_index: want 0, got %d", rct2.LogIndex) } } func TestSubmit_ContentHashMismatch_409(t *testing.T) { srv := newServer(t) defer srv.Close() env := fixtureEnvelope() bogus := "sha256:" + strings.Repeat("0", 64) body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: bogus}) resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body))) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusConflict { raw, _ := io.ReadAll(resp.Body) t.Fatalf("want 409, got %d: %s", resp.StatusCode, raw) } } func TestSubmit_RejectsMissingContentHash(t *testing.T) { srv := newServer(t) defer srv.Close() body := `{"envelope":{"payloadType":"application/vnd.in-toto+json","payload":"aGVsbG8","signatures":[]}}` resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("want 400, got %d", resp.StatusCode) } } func TestSubmit_UnknownRoute_404(t *testing.T) { srv := newServer(t) defer srv.Close() resp, err := http.Post(srv.URL+"/api/v1/log/other", "application/json", strings.NewReader("{}")) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("want 404, got %d", resp.StatusCode) } } func TestSubmit_TrillianStub_Returns503(t *testing.T) { srv := httptest.NewServer((&Server{Backend: backend.Trillian{}}).Handler()) defer srv.Close() env := fixtureEnvelope() ch, _ := backend.EnvelopeContentHash(env) body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: ch}) resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body))) if err != nil { t.Fatalf("post: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { raw, _ := io.ReadAll(resp.Body) t.Fatalf("trillian stub want 503, got %d: %s", resp.StatusCode, raw) } }