106 lines
3.9 KiB
Go
106 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|