Files
story/internal/webevents/service_integration_test.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

117 lines
4.7 KiB
Go

package webevents
import (
"context"
"errors"
"os"
"testing"
"time"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestServiceCursorIsolationCleanupAndPresenceExpiry(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
service := NewService(pool)
owner := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
outsider := identity.Principal{Issuer: owner.Issuer, Subject: "00000000-0000-0000-0000-000000000103"}
outsideBaseline, err := service.StartingCursor(ctx, outsider)
if err != nil {
t.Fatal(err)
}
resourceIDs := []string{uuid.NewString(), uuid.NewString(), uuid.NewString()}
testStarted := time.Now().UTC().Add(-time.Second)
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM device_presence_state WHERE tenant_id='10000000-0000-0000-0000-000000000001' AND device_id='10000000-0000-0000-0000-000000000501'`)
_, _ = pool.Exec(ctx, `DELETE FROM web_event WHERE resource_public_id=ANY($1::uuid[]) OR (resource_public_id='10000000-0000-0000-0000-000000000511' AND occurred_at>=$2)`, resourceIDs, testStarted)
}()
ids := make([]int64, 3)
for index, resourceID := range resourceIDs {
expiresAt := time.Now().UTC().Add(10 * time.Minute)
if index == 2 {
expiresAt = time.Now().UTC().Add(-time.Minute)
}
if err := pool.QueryRow(ctx, `
INSERT INTO web_event(tenant_id,household_id,subject_id,event_type,resource_type,resource_public_id,status,expires_at)
VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000201','10000000-0000-0000-0000-000000000401',
'conversation.completed','conversation',$1::uuid,$2,$3) RETURNING id`, resourceID, "TEST_"+string(rune('A'+index)), expiresAt).Scan(&ids[index]); err != nil {
t.Fatal(err)
}
}
cursor, err := service.StartingCursor(ctx, owner)
if err != nil || cursor != ids[1] {
t.Fatalf("owner starting cursor=%d want=%d err=%v", cursor, ids[1], err)
}
outsideCursor, err := service.StartingCursor(ctx, outsider)
if err != nil || outsideCursor != outsideBaseline {
t.Fatalf("outsider starting cursor=%d err=%v", outsideCursor, err)
}
if err := service.ValidateCursor(ctx, owner, 0); err != nil {
t.Fatalf("zero cursor: %v", err)
}
if err := service.ValidateCursor(ctx, owner, ids[0]); err != nil {
t.Fatalf("valid cursor: %v", err)
}
if err := service.ValidateCursor(ctx, owner, ids[2]); !errors.Is(err, ErrCursorUnavailable) {
t.Fatalf("expired cursor error=%v", err)
}
if err := service.ValidateCursor(ctx, outsider, ids[0]); !errors.Is(err, ErrCursorUnavailable) {
t.Fatalf("cross-user cursor error=%v", err)
}
items, err := service.ListAfter(ctx, owner, ids[0], 1)
if err != nil || len(items) != 1 || items[0].ID != ids[1] || items[0].HouseholdID != "10000000-0000-0000-0000-000000000211" || items[0].SubjectID != "10000000-0000-0000-0000-000000000411" {
t.Fatalf("events=%+v err=%v", items, err)
}
allItems, err := service.ListAfter(ctx, owner, 0, 0)
if err != nil {
t.Fatal(err)
}
found := 0
for _, item := range allItems {
if item.ID == ids[0] || item.ID == ids[1] {
found++
}
}
if found != 2 {
t.Fatalf("active synthetic events found=%d", found)
}
outsideItems, err := service.ListAfter(ctx, outsider, outsideBaseline, 100)
if err != nil || len(outsideItems) != 0 {
t.Fatalf("outsider events=%+v err=%v", outsideItems, err)
}
deleted, err := service.DeleteExpired(ctx, 1)
if err != nil || deleted != 1 {
t.Fatalf("deleted expired events=%d err=%v", deleted, err)
}
if err := service.ValidateCursor(ctx, owner, ids[2]); !errors.Is(err, ErrCursorUnavailable) {
t.Fatalf("deleted cursor error=%v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO device_presence_state(tenant_id,device_id,household_id,subject_id,online,last_seen_at,changed_at)
VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000501','10000000-0000-0000-0000-000000000201',
'10000000-0000-0000-0000-000000000401',true,now()-interval '10 minutes',now()-interval '10 minutes')`); err != nil {
t.Fatal(err)
}
expired, err := service.ExpireDevicePresence(ctx, time.Now().UTC().Add(-5*time.Minute), 0)
if err != nil || expired != 1 {
t.Fatalf("expired device presence=%d err=%v", expired, err)
}
var online bool
if err := pool.QueryRow(ctx, `SELECT online FROM device_presence_state WHERE tenant_id='10000000-0000-0000-0000-000000000001' AND device_id='10000000-0000-0000-0000-000000000501'`).Scan(&online); err != nil || online {
t.Fatalf("device presence online=%v err=%v", online, err)
}
}