You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

86 lines
3.5 KiB

package directstream
import (
"bytes"
"errors"
"testing"
"time"
)
func TestTicketIsBoundAndConsumedOnce(t *testing.T) {
now := time.Date(2026, 7, 22, 10, 0, 0, 0, time.UTC)
store := NewTicketStore(TicketStoreOptions{
Clock: func() time.Time { return now },
Entropy: bytes.NewReader(bytes.Repeat([]byte{0x3a}, TicketEntropyBytes)),
TTL: 30 * time.Second,
MaxPending: 4,
})
claims := TicketClaims{SessionID: "direct_CH01_000001", Channel: "CH01", Actor: "operator", Access: AccessViewer}
issued, err := store.Issue(claims)
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
if issued.Value == "" {
t.Fatal("Issue() returned an empty ticket")
}
if got, want := issued.ExpiresAt, now.Add(30*time.Second); !got.Equal(want) {
t.Fatalf("ExpiresAt = %v, want %v", got, want)
}
if _, err := store.Consume(issued.Value, TicketBinding{SessionID: claims.SessionID, Channel: "CH02"}); !errors.Is(err, ErrInvalidTicket) {
t.Fatalf("Consume() wrong binding error = %v, want ErrInvalidTicket", err)
}
consumed, err := store.Consume(issued.Value, TicketBinding{SessionID: claims.SessionID, Channel: claims.Channel})
if err != nil {
t.Fatalf("Consume() error = %v", err)
}
if consumed != claims {
t.Fatalf("Consume() claims = %#v, want %#v", consumed, claims)
}
if _, err := store.Consume(issued.Value, TicketBinding{SessionID: claims.SessionID, Channel: claims.Channel}); !errors.Is(err, ErrInvalidTicket) {
t.Fatalf("Consume() replay error = %v, want ErrInvalidTicket", err)
}
}
func TestTicketExpiresAndCapacityIsBounded(t *testing.T) {
now := time.Date(2026, 7, 22, 10, 0, 0, 0, time.UTC)
entropy := bytes.NewReader(append(bytes.Repeat([]byte{0x11}, TicketEntropyBytes), bytes.Repeat([]byte{0x22}, TicketEntropyBytes)...))
store := NewTicketStore(TicketStoreOptions{
Clock: func() time.Time { return now },
Entropy: entropy,
TTL: 30 * time.Second,
MaxPending: 1,
})
firstClaims := TicketClaims{SessionID: "direct_CH01_000001", Channel: "CH01", Actor: "operator", Access: AccessWriter}
first, err := store.Issue(firstClaims)
if err != nil {
t.Fatalf("Issue(first) error = %v", err)
}
if _, err := store.Issue(TicketClaims{SessionID: "direct_CH02_000001", Channel: "CH02", Actor: "operator", Access: AccessViewer}); !errors.Is(err, ErrTicketCapacity) {
t.Fatalf("Issue(over capacity) error = %v, want ErrTicketCapacity", err)
}
now = now.Add(30 * time.Second)
if _, err := store.Consume(first.Value, TicketBinding{SessionID: firstClaims.SessionID, Channel: firstClaims.Channel}); !errors.Is(err, ErrInvalidTicket) {
t.Fatalf("Consume(expired) error = %v, want ErrInvalidTicket", err)
}
if _, err := store.Issue(TicketClaims{SessionID: "direct_CH02_000001", Channel: "CH02", Actor: "operator", Access: AccessViewer}); err != nil {
t.Fatalf("Issue(after expiry pruning) error = %v", err)
}
}
func TestTicketRejectsInvalidClaimsAndEntropyFailure(t *testing.T) {
store := NewTicketStore(TicketStoreOptions{
Clock: func() time.Time { return time.Unix(0, 0).UTC() },
Entropy: bytes.NewReader(nil),
TTL: 30 * time.Second,
MaxPending: 1,
})
if _, err := store.Issue(TicketClaims{SessionID: "session", Channel: "CH49", Actor: "operator", Access: AccessViewer}); !errors.Is(err, ErrInvalidTicketClaims) {
t.Fatalf("Issue(invalid claims) error = %v, want ErrInvalidTicketClaims", err)
}
if _, err := store.Issue(TicketClaims{SessionID: "session", Channel: "CH01", Actor: "operator", Access: AccessViewer}); err == nil {
t.Fatal("Issue(entropy failure) error = nil")
}
}