59 lines
1.7 KiB
Go
59 lines
1.7 KiB
Go
package controlapi
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCursorIsScopedAndTamperEvident(t *testing.T) {
|
|
codec := NewCursorCodec(bytes.Repeat([]byte{7}, 32))
|
|
position := CursorPosition{CreatedAt: time.Date(2026, 8, 7, 1, 2, 3, 4, time.UTC), DeviceID: "dev_1"}
|
|
value, err := codec.Encode("tenant-a", "site-a", "filters", position)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decoded, err := codec.Decode(value, "tenant-a", "site-a", "filters")
|
|
if err != nil || decoded.DeviceID != position.DeviceID || !decoded.CreatedAt.Equal(position.CreatedAt) {
|
|
t.Fatalf("cursor did not round trip: %+v %v", decoded, err)
|
|
}
|
|
for name, candidate := range map[string]string{
|
|
"tenant": "tenant-b", "site": "site-b", "filter": "other",
|
|
} {
|
|
tenant, site, filter := "tenant-a", "site-a", "filters"
|
|
switch name {
|
|
case "tenant":
|
|
tenant = candidate
|
|
case "site":
|
|
site = candidate
|
|
case "filter":
|
|
filter = candidate
|
|
}
|
|
if _, err := codec.Decode(value, tenant, site, filter); err == nil {
|
|
t.Fatalf("cursor was accepted across %s scope", name)
|
|
}
|
|
}
|
|
tampered := value[:len(value)-1] + strings.ToUpper(value[len(value)-1:])
|
|
if tampered == value {
|
|
tampered = value[:len(value)-1] + "A"
|
|
}
|
|
if _, err := codec.Decode(tampered, "tenant-a", "site-a", "filters"); err == nil {
|
|
t.Fatal("tampered cursor was accepted")
|
|
}
|
|
}
|
|
|
|
func TestGeneratedIdentifiersMatchContractShape(t *testing.T) {
|
|
value, err := newULID("op_", time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(value) != 29 || !strings.HasPrefix(value, "op_") {
|
|
t.Fatalf("invalid operation ID %q", value)
|
|
}
|
|
trace, err := newTraceID()
|
|
if err != nil || len(trace) != 38 {
|
|
t.Fatalf("invalid trace ID %q: %v", trace, err)
|
|
}
|
|
}
|