Files
OwnCord/Server/auth/password_test.go
T
jevb b7dd6eabe9 feat: implement Phase 2 auth & security with TDD
- auth/session: 256-bit crypto-random tokens, SHA-256 hashing for storage
- auth/password: bcrypt cost 12, strength validation (8-72 chars)
- auth/ratelimit: sliding-window RateLimiter with lockout, thread-safe
- db/models: User, Session, Invite, Role types
- db/auth_queries: full user/session/invite CRUD with in-memory test coverage
- api/middleware: AuthMiddleware (Bearer token), RequirePermission (bitfield),
  RateLimitMiddleware (X-Real-IP, Retry-After header)
- api/auth_handler: POST register/login, POST logout, GET me
  - Generic errors — username existence never revealed
  - Rate limits: 3/min register, 5/min login, lockout after 10 failures
- api/invite_handler: create/list/revoke behind MANAGE_INVITES permission
- bluemonday sanitization on all user-supplied string fields

Test coverage: auth 90.9%, db 84.4%, api 80.9%
2026-03-14 20:52:11 +01:00

107 lines
2.8 KiB
Go

package auth_test
import (
"strings"
"testing"
"github.com/owncord/server/auth"
)
func TestHashPassword_DiffersFromPlaintext(t *testing.T) {
hash, err := auth.HashPassword("mypassword")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
if hash == "mypassword" {
t.Error("HashPassword() hash equals plaintext")
}
}
func TestHashPassword_BcryptPrefix(t *testing.T) {
hash, err := auth.HashPassword("mypassword")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
if !strings.HasPrefix(hash, "$2") {
t.Errorf("HashPassword() = %q, want bcrypt prefix $2*", hash)
}
}
func TestCheckPassword_CorrectPassword(t *testing.T) {
hash, err := auth.HashPassword("correctpassword")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
if !auth.CheckPassword(hash, "correctpassword") {
t.Error("CheckPassword() returned false for correct password")
}
}
func TestCheckPassword_WrongPassword(t *testing.T) {
hash, err := auth.HashPassword("correctpassword")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
if auth.CheckPassword(hash, "wrongpassword") {
t.Error("CheckPassword() returned true for wrong password")
}
}
func TestCheckPassword_EmptyPassword(t *testing.T) {
hash, err := auth.HashPassword("somepassword")
if err != nil {
t.Fatalf("HashPassword() error = %v", err)
}
if auth.CheckPassword(hash, "") {
t.Error("CheckPassword() returned true for empty password")
}
}
func TestCheckPassword_EmptyHash(t *testing.T) {
if auth.CheckPassword("", "somepassword") {
t.Error("CheckPassword() returned true with empty hash")
}
}
func TestValidatePasswordStrength_Valid(t *testing.T) {
cases := []string{
"12345678", // exactly 8 chars
"abcdefghij", // 10 chars
strings.Repeat("a", 72), // exactly 72 chars (bcrypt max)
}
for _, pw := range cases {
if err := auth.ValidatePasswordStrength(pw); err != nil {
t.Errorf("ValidatePasswordStrength(%q) error = %v, want nil", pw, err)
}
}
}
func TestValidatePasswordStrength_TooShort(t *testing.T) {
cases := []string{
"", // empty
"1234567", // 7 chars
"abc", // 3 chars
}
for _, pw := range cases {
if err := auth.ValidatePasswordStrength(pw); err == nil {
t.Errorf("ValidatePasswordStrength(%q) error = nil, want error", pw)
}
}
}
func TestValidatePasswordStrength_TooLong(t *testing.T) {
pw := strings.Repeat("a", 73) // 73 chars — over bcrypt 72 byte limit
if err := auth.ValidatePasswordStrength(pw); err == nil {
t.Errorf("ValidatePasswordStrength(%q) error = nil, want error for >72 chars", pw)
}
}
func TestHashPassword_TwoCallsDifferentHashes(t *testing.T) {
// bcrypt includes a random salt
h1, _ := auth.HashPassword("password")
h2, _ := auth.HashPassword("password")
if h1 == h2 {
t.Error("HashPassword() produced identical hashes for the same password (salt missing?)")
}
}