feat: deliver Sense audits to Bell (T-016)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user