package audit import ( "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "time" ) type recordingRepository struct { candidates []Candidate results []Result err error } func (r *recordingRepository) ProcessAuditBatch(_ context.Context, _, _ string, _ [sha256.Size]byte, values []Candidate) ([]Result, error) { r.candidates = values return r.results, r.err } func validBody(t *testing.T) []byte { t.Helper() value := map[string]any{"events": []any{map[string]any{ "schema_version": 1, "event": map[string]any{ "event_id": "audit_00000000000000000000000000000001", "event_type": "device.created", "tenant_id": "tenant", "site_id": "site", "device_id": "camera-1", "actor": map[string]any{"type": "system", "id": "sense"}, "reason": nil, "trace_id": nil, "aggregate_generation": 1, "projection_versions": map[string]any{"quota_source_version": 1, "area_policy_source_version": 1}, "data": map[string]any{"kind": "device_created", "area_id": "area", "modality": "video", "capabilities": []any{"video_capture"}, "desired_state": "enabled"}, "occurred_at": "2026-08-11T00:00:00Z", }, }}} raw, err := json.Marshal(value) if err != nil { t.Fatal(err) } return raw } func signedRequest(t *testing.T, body, secret []byte, timestamp time.Time, nonce string) *http.Request { t.Helper() request := httptest.NewRequest(http.MethodPost, RelayPath, bytes.NewReader(body)) stamp := strconv.FormatInt(timestamp.Unix(), 10) request.Header.Set(HeaderKeyID, "sense-a") request.Header.Set(HeaderTimestamp, stamp) request.Header.Set(HeaderNonce, nonce) request.Header.Set(HeaderSignature, base64.RawURLEncoding.EncodeToString(signature(secret, canonicalString(http.MethodPost, RelayPath, stamp, nonce, body)))) return request } func TestHandlerAuthenticatesAndReturnsPerItemResults(t *testing.T) { secret := bytes.Repeat([]byte{3}, 32) repository := &recordingRepository{results: []Result{{EventID: "audit_00000000000000000000000000000001", Status: "accepted"}}} handler, err := NewHandler(repository, map[string][]byte{"sense-a": secret}) if err != nil { t.Fatal(err) } now := time.Date(2026, 8, 11, 0, 1, 0, 0, time.UTC) handler.now = func() time.Time { return now } response := httptest.NewRecorder() handler.ServeHTTP(response, signedRequest(t, validBody(t), secret, now, "AAAAAAAAAAAAAAAAAAAAAA")) if response.Code != http.StatusOK || len(repository.candidates) != 1 || repository.candidates[0].ErrorCode != "" { t.Fatalf("valid batch rejected: status=%d candidates=%+v", response.Code, repository.candidates) } } func TestHandlerRejectsStaleOrTamperedRequests(t *testing.T) { secret := bytes.Repeat([]byte{4}, 32) repository := &recordingRepository{} handler, _ := NewHandler(repository, map[string][]byte{"sense-a": secret}) now := time.Date(2026, 8, 11, 0, 10, 0, 0, time.UTC) handler.now = func() time.Time { return now } for _, request := range []*http.Request{ signedRequest(t, validBody(t), secret, now.Add(-301*time.Second), "BBBBBBBBBBBBBBBBBBBBBB"), signedRequest(t, append(validBody(t), ' '), bytes.Repeat([]byte{5}, 32), now, "CCCCCCCCCCCCCCCCCCCCCC"), } { response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusUnauthorized { t.Fatalf("unsafe request returned %d", response.Code) } } } func TestDecodeCandidatesRejectsSensitiveItemWithoutRejectingBatch(t *testing.T) { body := validBody(t) var value map[string]any _ = json.Unmarshal(body, &value) events := value["events"].([]any) event := events[0].(map[string]any)["event"].(map[string]any) event["data"].(map[string]any)["password"] = "must-not-persist" body, _ = json.Marshal(value) candidates, err := decodeCandidates(body) if err != nil || len(candidates) != 1 || candidates[0].ErrorCode != "payload_invalid" { t.Fatalf("unexpected per-item validation: %+v %v", candidates, err) } }