Assignment 4: tests, interface refactor, e2e suite
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
const plain = "secret123"
|
||||
|
||||
t.Run("hash never equals plaintext", func(t *testing.T) {
|
||||
hash, err := HashPassword(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword returned error: %v", err)
|
||||
}
|
||||
if hash == "" {
|
||||
t.Fatal("hash must not be empty")
|
||||
}
|
||||
if hash == plain {
|
||||
t.Fatal("hash must not equal the plaintext password")
|
||||
}
|
||||
if strings.Contains(hash, plain) {
|
||||
t.Fatal("hash must not contain the plaintext password")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("same password hashed twice gives different hashes", func(t *testing.T) {
|
||||
h1, err := HashPassword(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("first HashPassword returned error: %v", err)
|
||||
}
|
||||
h2, err := HashPassword(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("second HashPassword returned error: %v", err)
|
||||
}
|
||||
if h1 == h2 {
|
||||
t.Fatal("two hashes of the same password must differ (salting)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hash verifies against original password", func(t *testing.T) {
|
||||
hash, err := HashPassword(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword returned error: %v", err)
|
||||
}
|
||||
if !CheckPassword(plain, hash) {
|
||||
t.Fatal("CheckPassword must accept the original password")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("password longer than 72 bytes returns an error", func(t *testing.T) {
|
||||
hash, err := HashPassword(strings.Repeat("a", 73))
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a password longer than 72 bytes")
|
||||
}
|
||||
if hash != "" {
|
||||
t.Fatalf("expected empty hash on error, got %q", hash)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckPassword(t *testing.T) {
|
||||
const plain = "secret123"
|
||||
|
||||
hash, err := HashPassword(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword returned error: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
hash string
|
||||
want bool
|
||||
}{
|
||||
{name: "correct password", password: plain, hash: hash, want: true},
|
||||
{name: "wrong password", password: "wrong-password", hash: hash, want: false},
|
||||
{name: "empty password", password: "", hash: hash, want: false},
|
||||
{name: "garbage hash string", password: plain, hash: "this-is-not-a-bcrypt-hash", want: false},
|
||||
{name: "empty hash string", password: plain, hash: "", want: false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("CheckPassword panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
got := CheckPassword(tc.password, tc.hash)
|
||||
if got != tc.want {
|
||||
t.Fatalf("CheckPassword(%q, hash) = %v, want %v", tc.password, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,16 @@ package auth
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
)
|
||||
|
||||
func GenerateToken() (string, error) {
|
||||
return generateToken(rand.Reader)
|
||||
}
|
||||
|
||||
func generateToken(r io.Reader) (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestGenerateToken(t *testing.T) {
|
||||
const n = 1000
|
||||
|
||||
tokens := make([]string, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
tok, err := GenerateToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken returned error on call %d: %v", i, err)
|
||||
}
|
||||
tokens = append(tokens, tok)
|
||||
}
|
||||
|
||||
t.Run("all tokens are non-empty", func(t *testing.T) {
|
||||
for i, tok := range tokens {
|
||||
if tok == "" {
|
||||
t.Fatalf("token %d is empty", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all tokens are distinct", func(t *testing.T) {
|
||||
seen := make(map[string]struct{}, len(tokens))
|
||||
for i, tok := range tokens {
|
||||
if _, dup := seen[tok]; dup {
|
||||
t.Fatalf("token %d is a duplicate: %q", i, tok)
|
||||
}
|
||||
seen[tok] = struct{}{}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no token contains whitespace", func(t *testing.T) {
|
||||
for i, tok := range tokens {
|
||||
if strings.IndexFunc(tok, unicode.IsSpace) >= 0 {
|
||||
t.Fatalf("token %d contains whitespace: %q", i, tok)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("every token is at least 32 characters", func(t *testing.T) {
|
||||
for i, tok := range tokens {
|
||||
if len(tok) < 32 {
|
||||
t.Fatalf("token %d has length %d, want >= 32", i, len(tok))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type failingReader struct{}
|
||||
|
||||
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("read failed") }
|
||||
|
||||
func TestGenerateTokenReadError(t *testing.T) {
|
||||
tok, err := generateToken(failingReader{})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when the random source fails")
|
||||
}
|
||||
if tok != "" {
|
||||
t.Fatalf("expected empty token on error, got %q", tok)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user