internal/store/sessions_test.go

v1.40.0
gitbay/internal/store/sessions_test.go history · blame · raw

159 lines · 4959 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}
120
121// A session's sign-in time is its creation; using the session renews
122// its idle expiry and leaves the sign-in time alone (#297).
123func TestWebSessionUserSignedInAt(t *testing.T) {
124	s, uid := sessionFixture(t)
125	_, hash, err := NewToken()
126	if err != nil {
127		t.Fatal(err)
128	}
129	if err := s.CreateWebSession(hash, uid, 7*24*time.Hour); err != nil {
130		t.Fatal(err)
131	}
132	u, err := s.WebSessionUser(hash)
133	if err != nil {
134		t.Fatal(err)
135	}
136	if age := time.Since(u.SignedInAt); age < 0 || age > time.Minute {
137		t.Fatalf("fresh session signed in %v ago", age)
138	}
139
140	signedIn := time.Now().Add(-2 * time.Hour)
141	if _, err := s.DB.Exec("UPDATE web_sessions SET created_at = ?, last_used_at = ? WHERE token_hash = ?",
142		fmtTime(signedIn), fmtTime(signedIn), hash); err != nil {
143		t.Fatal(err)
144	}
145	u, err = s.WebSessionUser(hash)
146	if err != nil {
147		t.Fatal(err)
148	}
149	var last string
150	if err := s.DB.QueryRow("SELECT last_used_at FROM web_sessions WHERE token_hash = ?", hash).Scan(&last); err != nil {
151		t.Fatal(err)
152	}
153	if last == fmtTime(signedIn) {
154		t.Fatal("using the session did not renew it")
155	}
156	if want := signedIn.UTC().Truncate(time.Millisecond); !u.SignedInAt.Equal(want) {
157		t.Fatalf("SignedInAt = %v, want %v", u.SignedInAt, want)
158	}
159}