package store_test import ( "context" "testing" "time" "github.com/wangjia/pangolin/server/internal/usage" ) // Per-device curve (#10②): UsageCurve(deviceUUID) must return only that device's // data, account curve (deviceUUID="") the total, and a user must never read // another user's device (JOIN enforces ownership). func TestSQLite_UsageCurve_PerDevice(t *testing.T) { ctx := context.Background() db := openSQLite(t) mustExec := func(q string, args ...any) { if _, err := db.Exec(q, args...); err != nil { t.Fatalf("seed %q: %v", q, err) } } mustExec(`INSERT INTO users (id, uuid, email, pw_hash, dp_uuid, status) VALUES (1,'u1','u1@e','h','dp1','active'),(2,'u2','u2@e','h','dp2','active')`) mustExec(`INSERT INTO devices (id, uuid, user_id, name, platform) VALUES (10,'devA',1,'A','macos'),(20,'devB',1,'B','ios'),(30,'devC',2,'C','windows')`) h := time.Now().UTC().Unix()/3600 - 2 // 2h ago, within window mustExec(`INSERT INTO usage_device_hourly (user_id, device_id, hour, bytes_up, bytes_down, minutes_used) VALUES (1,10,?,1000,2000,5),(1,20,?,300,400,2),(2,30,?,999,0,9)`, h, h, h) mustExec(`INSERT INTO usage_hourly (user_id, hour, bytes_up, bytes_down, minutes_used) VALUES (1,?,1300,2400,7)`, h) svc := usage.NewService(usage.NewStore(db), nil, nil, time.Hour) sum := func(uid int64, dev string) (up, down uint64, mins int) { pts, e := svc.UsageCurve(ctx, uid, 3, 0, dev) if e != nil { t.Fatalf("UsageCurve(dev=%q): %v", dev, e) } for _, p := range pts { up += p.BytesUp down += p.BytesDown mins += p.MinutesUsed } return } if up, down, m := sum(1, "devA"); up != 1000 || down != 2000 || m != 5 { t.Errorf("devA curve = %d/%d/%d, want 1000/2000/5", up, down, m) } if up, _, _ := sum(1, "devB"); up != 300 { t.Errorf("devB curve up = %d, want 300", up) } if up, _, _ := sum(1, ""); up != 1300 { t.Errorf("account curve up = %d, want 1300", up) } // user 1 querying user 2's device → empty (ownership via JOIN). if up, _, _ := sum(1, "devC"); up != 0 { t.Errorf("cross-user leak: devC visible to user1, up=%d", up) } }