accounts/store_test.go (3564 bytes)
1 package main 2 3 import ( 4 "errors" 5 "regexp" 6 "testing" 7 "time" 8 9 "pgregory.net/rapid" 10 ) 11 12 var tokenForm = regexp.MustCompile(`^[A-Z2-7]{26}$`) 13 14 // TestTokensProperty checks that tokens are URL-safe, are all different, 15 // and that different tokens have different hashes. 16 func TestTokensProperty(t *testing.T) { 17 seen := map[string]bool{} 18 rapid.Check(t, func(t *rapid.T) { 19 tok := newToken() 20 if !tokenForm.MatchString(tok) { 21 t.Fatalf("token %q is not 26 base32 characters", tok) 22 } 23 if seen[tok] { 24 t.Fatalf("token %q came two times", tok) 25 } 26 seen[tok] = true 27 other := rapid.String().Draw(t, "other") 28 if other != tok && hashToken(other) == hashToken(tok) { 29 t.Fatal("two tokens have one hash") 30 } 31 }) 32 } 33 34 // TestLoginTokenProperty checks that a sign-in token works one time for 35 // its address, and that no other string works. 36 func TestLoginTokenProperty(t *testing.T) { 37 rapid.Check(t, func(rt *rapid.T) { 38 e := newTestEnv(t, nil) 39 email := rapid.StringMatching(`[a-z0-9]{1,10}@[a-z]{1,10}\.[a-z]{2,4}`).Draw(rt, "email") 40 tok, err := createLoginToken(e.app.db, email, e.clock.now()) 41 if err != nil || tok == "" { 42 rt.Fatalf("createLoginToken: %q, %v", tok, err) 43 } 44 guess := rapid.String().Filter(func(s string) bool { return s != tok }).Draw(rt, "guess") 45 if _, err := consumeLoginToken(e.app.db, guess, e.clock.now()); !errors.Is(err, errNotFound) { 46 rt.Fatalf("a wrong token gave %v", err) 47 } 48 e.advance(time.Duration(rapid.IntRange(0, int(loginTTL/time.Second)-1).Draw(rt, "wait")) * time.Second) 49 u, err := consumeLoginToken(e.app.db, tok, e.clock.now()) 50 if err != nil || u.Email != email { 51 rt.Fatalf("consume: %+v, %v", u, err) 52 } 53 if _, err := consumeLoginToken(e.app.db, tok, e.clock.now()); !errors.Is(err, errNotFound) { 54 rt.Fatalf("second use gave %v", err) 55 } 56 }) 57 } 58 59 func TestLoginTokenExpires(t *testing.T) { 60 e := newTestEnv(t, nil) 61 tok, _ := createLoginToken(e.app.db, "a@example.com", e.clock.now()) 62 e.advance(loginTTL) 63 if _, err := consumeLoginToken(e.app.db, tok, e.clock.now()); !errors.Is(err, errNotFound) { 64 t.Fatalf("expired token gave %v", err) 65 } 66 } 67 68 func TestLoginTokenSpacing(t *testing.T) { 69 e := newTestEnv(t, nil) 70 if tok, _ := createLoginToken(e.app.db, "a@example.com", e.clock.now()); tok == "" { 71 t.Fatal("first token is empty") 72 } 73 e.advance(loginSpacing - time.Second) 74 if tok, _ := createLoginToken(e.app.db, "a@example.com", e.clock.now()); tok != "" { 75 t.Fatal("second token came too soon") 76 } 77 if tok, _ := createLoginToken(e.app.db, "b@example.com", e.clock.now()); tok == "" { 78 t.Fatal("other address got no token") 79 } 80 e.advance(time.Second) 81 if tok, _ := createLoginToken(e.app.db, "a@example.com", e.clock.now()); tok == "" { 82 t.Fatal("no token after the spacing") 83 } 84 } 85 86 func TestSignInTwiceKeepsOneUser(t *testing.T) { 87 e := newTestEnv(t, nil) 88 a := e.signUp(t, "a@example.com") 89 e.advance(loginSpacing) 90 b := e.signUp(t, "a@example.com") 91 if a.ID != b.ID || a.FeedToken != b.FeedToken || a.CreatedAt != b.CreatedAt { 92 t.Fatalf("second sign-in changed the user: %+v, %+v", a, b) 93 } 94 } 95 96 func TestSessionExpires(t *testing.T) { 97 e := newTestEnv(t, nil) 98 u := e.signUp(t, "a@example.com") 99 s, err := createSession(e.app.db, u.ID, e.clock.now()) 100 if err != nil { 101 t.Fatal(err) 102 } 103 if got, err := sessionUser(e.app.db, s, e.clock.now()); err != nil || got.ID != u.ID { 104 t.Fatalf("sessionUser: %+v, %v", got, err) 105 } 106 e.advance(sessionTTL) 107 if _, err := sessionUser(e.app.db, s, e.clock.now()); !errors.Is(err, errNotFound) { 108 t.Fatalf("expired session gave %v", err) 109 } 110 }