fix: harden terminal-scoped analytics contracts
Keep JSON and bounded CSV exports on one canonical predicate, replace replayable offsets with encrypted tenant/filter-bound keyset cursors, and suppress merchant-grain peers under terminal scope. Validate period filters and align OpenAPI and capability specs with runtime behavior.
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
package analytics
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDetailFilterValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -59,3 +64,53 @@ func TestTerminalDisplayName(t *testing.T) {
|
||||
t.Fatalf("terminalDisplayName() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailCursorRejectsMerchantAndFilterReplay(t *testing.T) {
|
||||
t.Parallel()
|
||||
service := &Service{cursorSecret: []byte("test-cursor-secret-that-is-at-least-32-bytes")}
|
||||
period := Period{PeriodID: "jm-1405-03"}
|
||||
minimum := int64(1000)
|
||||
filter := DetailFilter{View: "ALL", TerminalID: "T318", Status: "Verified", MinAmountRial: &minimum, Limit: 25}
|
||||
createdAt := time.Date(2026, time.June, 20, 8, 30, 0, 0, time.UTC)
|
||||
|
||||
cursor, err := service.encodeDetailCursor(42, period, filter, createdAt, 99)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
position, err := service.decodeDetailCursor(42, period, filter, cursor)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeDetailCursor() error = %v", err)
|
||||
}
|
||||
if !position.CreatedAt.Equal(createdAt) || position.SessionID != 99 {
|
||||
t.Fatalf("decodeDetailCursor() = %+v", position)
|
||||
}
|
||||
|
||||
if _, err := service.decodeDetailCursor(43, period, filter, cursor); !errors.Is(err, ErrInvalidCursor) {
|
||||
t.Fatalf("merchant replay error = %v, want ErrInvalidCursor", err)
|
||||
}
|
||||
changed := filter
|
||||
changed.Status = "Paid"
|
||||
if _, err := service.decodeDetailCursor(42, period, changed, cursor); !errors.Is(err, ErrInvalidCursor) {
|
||||
t.Fatalf("filter replay error = %v, want ErrInvalidCursor", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailCursorRejectsTampering(t *testing.T) {
|
||||
t.Parallel()
|
||||
service := &Service{cursorSecret: []byte("test-cursor-secret-that-is-at-least-32-bytes")}
|
||||
period := Period{PeriodID: "jm-1405-03"}
|
||||
filter := DetailFilter{View: "ALL", Limit: 25}
|
||||
cursor, err := service.encodeDetailCursor(42, period, filter, time.Now().UTC(), 99)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealed, err := base64.RawURLEncoding.DecodeString(cursor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealed[len(sealed)-1] ^= 1
|
||||
tampered := base64.RawURLEncoding.EncodeToString(sealed)
|
||||
if _, err := service.decodeDetailCursor(42, period, filter, tampered); !errors.Is(err, ErrInvalidCursor) {
|
||||
t.Fatalf("tampered cursor error = %v, want ErrInvalidCursor", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user