package main import ( "bytes" "context" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "encoding/base64" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) func TestWriteJSONFileCreatesParentDirectory(t *testing.T) { path := filepath.Join(t.TempDir(), "workflow", "publication-result.json") if err := writeJSONFile(path, map[string]bool{"published": true}); err != nil { t.Fatal(err) } contents, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(contents) != "{\"published\":true}\n" { t.Fatalf("unexpected contents %q", contents) } } func TestObjectURL(t *testing.T) { result, err := objectURL("https://objectstorage.example.invalid", "namespace", "bucket", "admission/v1/current.json.gz") if err != nil { t.Fatal(err) } if result != "https://objectstorage.example.invalid/n/namespace/b/bucket/o/admission%2Fv1%2Fcurrent.json.gz" { t.Fatalf("unexpected object URL %q", result) } } func TestPublicationPrefixes(t *testing.T) { prefixes, err := publicationPrefixes("admission/v1", []string{"web-allow/v1"}) if err != nil { t.Fatal(err) } if strings.Join(prefixes, ",") != "admission/v1,web-allow/v1" { t.Fatalf("unexpected prefixes %q", prefixes) } version, current := publicationObjects(prefixes[1], strings.Repeat("a", 64)) if version != "web-allow/v1/versions/"+strings.Repeat("a", 64)+".json.gz" { t.Fatalf("unexpected version object %q", version) } if current != "web-allow/v1/current.json.gz" { t.Fatalf("unexpected current object %q", current) } } func TestPublicationPrefixesRejectInvalidOrDuplicateValues(t *testing.T) { for name, values := range map[string][]string{ "empty": {""}, "traversal": {"../web-allow/v1"}, "nested": {"mail/smtp-deny/v1"}, "duplicate": {"web-allow/v1", "web-allow/v1"}, } { t.Run(name, func(t *testing.T) { if _, err := publicationPrefixes(values[0], values[1:]); err == nil { t.Fatal("expected prefix validation failure") } }) } } func TestPublishBundleUsesIdenticalContentForBothPrefixes(t *testing.T) { type uploadRequest struct { body string immutable bool escapedPath string } requests := make([]uploadRequest, 0, 4) objects := map[string][]byte{} server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodGet { body, exists := objects[request.URL.EscapedPath()] if !exists { response.WriteHeader(http.StatusNotFound) return } _, _ = response.Write(body) return } body, err := io.ReadAll(request.Body) if err != nil { t.Error(err) } objects[request.URL.EscapedPath()] = body requests = append(requests, uploadRequest{ body: string(body), immutable: request.Header.Get("If-None-Match") == "*", escapedPath: request.URL.EscapedPath(), }) response.WriteHeader(http.StatusOK) })) defer server.Close() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } body := []byte("validated bundle") digest, publications, err := publishBundle( context.Background(), server.Client(), server.URL, "namespace", "bucket", []string{"admission/v1", "web-allow/v1"}, body, credentials{tenancy: "tenancy", user: "user", fingerprint: "fingerprint", privateKey: key}, true, ) if err != nil { t.Fatal(err) } if len(publications) != 2 || len(requests) != 4 { t.Fatalf("publications=%d requests=%d", len(publications), len(requests)) } expectedPaths := []string{ "/n/namespace/b/bucket/o/admission%2Fv1%2Fversions%2F" + digest + ".json.gz", "/n/namespace/b/bucket/o/admission%2Fv1%2Fcurrent.json.gz", "/n/namespace/b/bucket/o/web-allow%2Fv1%2Fversions%2F" + digest + ".json.gz", "/n/namespace/b/bucket/o/web-allow%2Fv1%2Fcurrent.json.gz", } for index, request := range requests { if request.body != string(body) { t.Errorf("request %d changed bundle content", index) } if request.escapedPath != expectedPaths[index] { t.Errorf("request %d path %q", index, request.escapedPath) } if request.immutable != (index%2 == 0) { t.Errorf("request %d immutable=%t", index, request.immutable) } } } func TestPublishBundleVerifiesExistingImmutableObject(t *testing.T) { body := []byte("validated bundle") digest := sha256.Sum256(body) versionPath := "/n/namespace/b/bucket/o/smtp-deny%2Fv1%2Fversions%2F" + fmt.Sprintf("%x", digest) + ".json.gz" server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodPut { response.WriteHeader(http.StatusPreconditionFailed) return } if request.URL.EscapedPath() != versionPath { t.Errorf("unexpected path %q", request.URL.EscapedPath()) } _, _ = response.Write(body) })) defer server.Close() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } _, publications, err := publishBundle( context.Background(), server.Client(), server.URL, "namespace", "bucket", []string{"smtp-deny/v1"}, body, credentials{tenancy: "tenancy", user: "user", fingerprint: "fingerprint", privateKey: key}, false, ) if err != nil { t.Fatal(err) } if len(publications) != 1 || publications[0].VersionCreated { t.Fatalf("unexpected publications %#v", publications) } } func TestPublishBundleRejectsMismatchedExistingImmutableObject(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodPut { response.WriteHeader(http.StatusPreconditionFailed) return } _, _ = response.Write([]byte("different bundle")) })) defer server.Close() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } _, _, err = publishBundle( context.Background(), server.Client(), server.URL, "namespace", "bucket", []string{"smtp-deny/v1"}, []byte("validated bundle"), credentials{tenancy: "tenancy", user: "user", fingerprint: "fingerprint", privateKey: key}, false, ) if err == nil || !strings.Contains(err.Error(), "SHA-256 mismatch") { t.Fatalf("unexpected error %v", err) } } func TestPublishBundleRejectsMismatchedCurrentObject(t *testing.T) { objects := map[string][]byte{} server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { path := request.URL.EscapedPath() if request.Method == http.MethodPut { body, err := io.ReadAll(request.Body) if err != nil { t.Error(err) } objects[path] = body response.WriteHeader(http.StatusOK) return } if strings.HasSuffix(path, "current.json.gz") { _, _ = response.Write([]byte("different bundle")) return } _, _ = response.Write(objects[path]) })) defer server.Close() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } _, _, err = publishBundle( context.Background(), server.Client(), server.URL, "namespace", "bucket", []string{"smtp-deny/v1"}, []byte("validated bundle"), credentials{tenancy: "tenancy", user: "user", fingerprint: "fingerprint", privateKey: key}, true, ) if err == nil || !strings.Contains(err.Error(), "current.json.gz") || !strings.Contains(err.Error(), "SHA-256 mismatch") { t.Fatalf("unexpected error %v", err) } } func TestReadSMTPDenySummary(t *testing.T) { path := filepath.Join(t.TempDir(), "summary.json") contents := `{"generated_at":"2026-07-26T00:00:00Z","expires_at":"2026-07-27T00:00:00Z","denied_interval_count":2,"denied_address_count":17,"removed_interval_count":1}` if err := os.WriteFile(path, []byte(contents), 0600); err != nil { t.Fatal(err) } summary, err := readSMTPDenySummary(path) if err != nil { t.Fatal(err) } if summary.DeniedIntervalCount != 2 || summary.DeniedAddressCount != 17 || summary.RemovedIntervalCount != 1 { t.Fatalf("unexpected summary %#v", summary) } } func TestReadSMTPDenySummaryRejectsContentEvidence(t *testing.T) { path := filepath.Join(t.TempDir(), "summary.json") contents := `{"generated_at":"2026-07-26T00:00:00Z","expires_at":"2026-07-27T00:00:00Z","denied_interval_count":1,"denied_address_count":1,"removed_interval_count":0,"recipient":"user@example.test"}` if err := os.WriteFile(path, []byte(contents), 0600); err != nil { t.Fatal(err) } if _, err := readSMTPDenySummary(path); err == nil { t.Fatal("expected content-bearing summary rejection") } } func TestSignRequest(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } body := []byte("bundle") request, err := http.NewRequest(http.MethodPut, "https://objectstorage.example.invalid/n/ns/b/b/o/current.json.gz", bytes.NewReader(body)) if err != nil { t.Fatal(err) } identity := credentials{tenancy: "tenancy", user: "user", fingerprint: "fingerprint", privateKey: key} if err := signRequest(request, body, identity, time.Unix(0, 0)); err != nil { t.Fatal(err) } authorization := request.Header.Get("Authorization") marker := `signature="` start := strings.Index(authorization, marker) if start < 0 { t.Fatal("missing signature") } encoded := strings.TrimSuffix(authorization[start+len(marker):], `"`) signature, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatal(err) } signingString := strings.Join([]string{ "(request-target): put /n/ns/b/b/o/current.json.gz", "host: objectstorage.example.invalid", "date: " + request.Header.Get("Date"), "x-content-sha256: " + request.Header.Get("X-Content-SHA256"), "content-type: application/gzip", "content-length: 6", }, "\n") digest := sha256.Sum256([]byte(signingString)) if err := rsa.VerifyPKCS1v15(&key.PublicKey, 0, digest[:], signature); err == nil { t.Fatal("signature unexpectedly verified without SHA-256") } if err := rsa.VerifyPKCS1v15(&key.PublicKey, crypto.SHA256, digest[:], signature); err != nil { t.Fatal(err) } } func TestSignDownloadRequest(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } request, err := http.NewRequest(http.MethodGet, "https://objectstorage.example.invalid/n/ns/b/b/o/admission%2Fv1%2Fversions%2Fbundle.json.gz", nil) if err != nil { t.Fatal(err) } identity := credentials{tenancy: "tenancy", user: "user", fingerprint: "fingerprint", privateKey: key} if err := signDownloadRequest(request, identity, time.Unix(0, 0)); err != nil { t.Fatal(err) } authorization := request.Header.Get("Authorization") marker := `signature="` start := strings.Index(authorization, marker) if start < 0 { t.Fatal("missing signature") } encoded := strings.TrimSuffix(authorization[start+len(marker):], `"`) signature, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatal(err) } signingString := strings.Join([]string{ "(request-target): get /n/ns/b/b/o/admission%2Fv1%2Fversions%2Fbundle.json.gz", "host: objectstorage.example.invalid", "date: " + request.Header.Get("Date"), }, "\n") digest := sha256.Sum256([]byte(signingString)) if err := rsa.VerifyPKCS1v15(&key.PublicKey, crypto.SHA256, digest[:], signature); err != nil { t.Fatal(err) } }