internal/store/sessions_test.go
119 lines · 3726 bytes
1package store
2
3import (
4 "database/sql"
5 "testing"
6 "time"
7)
8
9func TestCountLoginTokensSince(t *testing.T) {
10 s := open(t)
11 if err := s.MigrateUp(); err != nil {
12 t.Fatal(err)
13 }
14 uid, err := s.CreateUser("cmc", true)
15 if err != nil {
16 t.Fatal(err)
17 }
18 for i := 0; i < 3; i++ {
19 _, hash, err := NewToken()
20 if err != nil {
21 t.Fatal(err)
22 }
23 if err := s.CreateLoginToken(uid, hash, time.Minute); err != nil {
24 t.Fatal(err)
25 }
26 }
27
28 n, err := s.CountLoginTokensSince(uid, time.Now().Add(-time.Hour))
29 if err != nil || n != 3 {
30 t.Fatalf("count in the last hour = %d, %v; want 3", n, err)
31 }
32
33 // A window that opens in the future sees none of them, which is what
34 // makes the hourly bound a window rather than a lifetime total.
35 if n, err := s.CountLoginTokensSince(uid, time.Now().Add(time.Hour)); err != nil || n != 0 {
36 t.Fatalf("count in a future window = %d, %v; want 0", n, err)
37 }
38
39 // One account's requests must not spend another account's budget.
40 other, err := s.CreateUser("kim", false)
41 if err != nil {
42 t.Fatal(err)
43 }
44 if n, err := s.CountLoginTokensSince(other, time.Now().Add(-time.Hour)); err != nil || n != 0 {
45 t.Fatalf("other account count = %d, %v; want 0", n, err)
46 }
47}
48
49func sessionFixture(t *testing.T) (*Store, int64) {
50 t.Helper()
51 s := open(t)
52 if err := s.MigrateUp(); err != nil {
53 t.Fatal(err)
54 }
55 uid, err := s.CreateUser("cmc", false)
56 if err != nil {
57 t.Fatal(err)
58 }
59 return s, uid
60}
61
62func sessionTimes(t *testing.T, s *Store, hash string) (expires, absolute time.Time) {
63 t.Helper()
64 var e, a string
65 if err := s.DB.QueryRow("SELECT expires_at, absolute_expires_at FROM web_sessions WHERE token_hash = ?", hash).Scan(&e, &a); err != nil {
66 t.Fatal(err)
67 }
68 return *parseTime(sql.NullString{String: e, Valid: true}), *parseTime(sql.NullString{String: a, Valid: true})
69}
70
71func TestWebSessionIdleExpiry(t *testing.T) {
72 s, uid := sessionFixture(t)
73 if err := s.CreateWebSession("h", uid, 7*24*time.Hour); err != nil {
74 t.Fatal(err)
75 }
76 exp, abs := sessionTimes(t, s, "h")
77 if d := time.Until(exp); d < WebSessionIdle-time.Minute || d > WebSessionIdle {
78 t.Fatalf("a new session expires in %s, want %s", d, WebSessionIdle)
79 }
80 if d := time.Until(abs); d < 7*24*time.Hour-time.Minute {
81 t.Fatalf("absolute cap in %s", d)
82 }
83 // Idle past the window: gone.
84 old := fmtTime(time.Now().Add(-time.Second))
85 s.DB.Exec("UPDATE web_sessions SET expires_at = ? WHERE token_hash = 'h'", old)
86 if _, err := s.WebSessionUser("h"); err != ErrNotFound {
87 t.Fatalf("idle session: %v", err)
88 }
89}
90
91func TestWebSessionRenewsUpToTheCap(t *testing.T) {
92 s, uid := sessionFixture(t)
93 if err := s.CreateWebSession("h", uid, 7*24*time.Hour); err != nil {
94 t.Fatal(err)
95 }
96 // Last used two minutes ago, one minute left: a request renews it.
97 s.DB.Exec("UPDATE web_sessions SET last_used_at = ?, expires_at = ? WHERE token_hash = 'h'",
98 fmtTime(time.Now().Add(-2*time.Minute)), fmtTime(time.Now().Add(time.Minute)))
99 if _, err := s.WebSessionUser("h"); err != nil {
100 t.Fatal(err)
101 }
102 if exp, _ := sessionTimes(t, s, "h"); time.Until(exp) < WebSessionIdle-time.Minute {
103 t.Fatalf("not renewed: expires in %s", time.Until(exp))
104 }
105 // Near the cap, renewal stops at it.
106 capAt := time.Now().Add(time.Hour)
107 s.DB.Exec("UPDATE web_sessions SET last_used_at = ?, absolute_expires_at = ? WHERE token_hash = 'h'",
108 fmtTime(time.Now().Add(-2*time.Minute)), fmtTime(capAt))
109 if _, err := s.WebSessionUser("h"); err != nil {
110 t.Fatal(err)
111 }
112 if exp, _ := sessionTimes(t, s, "h"); exp.After(capAt) {
113 t.Fatalf("renewed past the cap: %s > %s", exp, capAt)
114 }
115 list, err := s.ListWebSessions(uid)
116 if err != nil || len(list) != 1 || list[0].LastUsedAt == "" {
117 t.Fatalf("list: %+v %v", list, err)
118 }
119}