Improve README SEO and increase test coverage to 50%
Optimized README for better discoverability with keywords: api, habits, self-hosted. Consolidated content to reduce redundancy while maintaining clarity. Added comprehensive test coverage across multiple layers: - Infrastructure: bcrypt hashing, JWT tokens, configuration validation - Application commands: user deletion, password reset, token revocation, email verification - Application queries: login, token refresh - Domain entities: refresh tokens, password reset tokens Coverage increased from 42.5% to 50.8% with meaningful business logic tests. Fixed integration test handler initialization with correct parameters.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewPasswordResetToken(t *testing.T) {
|
||||
userID := "user-123"
|
||||
token := "reset-token-abc"
|
||||
expiresAt := time.Now().Add(1 * time.Hour)
|
||||
|
||||
prt := NewPasswordResetToken(userID, token, expiresAt)
|
||||
|
||||
if prt == nil {
|
||||
t.Fatal("NewPasswordResetToken() returned nil")
|
||||
}
|
||||
|
||||
if prt.ID == "" {
|
||||
t.Error("ID is empty")
|
||||
}
|
||||
|
||||
if prt.UserID != userID {
|
||||
t.Errorf("UserID = %v, want %v", prt.UserID, userID)
|
||||
}
|
||||
|
||||
if prt.Token != token {
|
||||
t.Errorf("Token = %v, want %v", prt.Token, token)
|
||||
}
|
||||
|
||||
if !prt.ExpiresAt.Equal(expiresAt) {
|
||||
t.Errorf("ExpiresAt = %v, want %v", prt.ExpiresAt, expiresAt)
|
||||
}
|
||||
|
||||
if prt.UsedAt != nil {
|
||||
t.Error("UsedAt should be nil for new token")
|
||||
}
|
||||
|
||||
if prt.CreatedAt.IsZero() {
|
||||
t.Error("CreatedAt is zero")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetToken_IsExpired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expiresAt time.Time
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "future expiry",
|
||||
expiresAt: time.Now().Add(1 * time.Hour),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "past expiry",
|
||||
expiresAt: time.Now().Add(-1 * time.Hour),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "expires in 1 second",
|
||||
expiresAt: time.Now().Add(1 * time.Second),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prt := NewPasswordResetToken("user-123", "token", tt.expiresAt)
|
||||
got := prt.IsExpired()
|
||||
if got != tt.want {
|
||||
t.Errorf("IsExpired() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetToken_IsUsed(t *testing.T) {
|
||||
prt := NewPasswordResetToken("user-123", "token", time.Now().Add(1*time.Hour))
|
||||
|
||||
if prt.IsUsed() {
|
||||
t.Error("IsUsed() should return false for new token")
|
||||
}
|
||||
|
||||
prt.MarkAsUsed()
|
||||
|
||||
if !prt.IsUsed() {
|
||||
t.Error("IsUsed() should return true after MarkAsUsed()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetToken_MarkAsUsed(t *testing.T) {
|
||||
prt := NewPasswordResetToken("user-123", "token", time.Now().Add(1*time.Hour))
|
||||
|
||||
if prt.UsedAt != nil {
|
||||
t.Error("UsedAt should be nil before MarkAsUsed()")
|
||||
}
|
||||
|
||||
prt.MarkAsUsed()
|
||||
|
||||
if prt.UsedAt == nil {
|
||||
t.Fatal("UsedAt should not be nil after MarkAsUsed()")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
diff := now.Sub(*prt.UsedAt)
|
||||
if diff > time.Second || diff < 0 {
|
||||
t.Errorf("UsedAt difference too large: %v", diff)
|
||||
}
|
||||
|
||||
firstUsedAt := prt.UsedAt
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
prt.MarkAsUsed()
|
||||
|
||||
if prt.UsedAt == firstUsedAt {
|
||||
t.Error("MarkAsUsed() should update UsedAt on subsequent calls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetToken_MultipleTokens(t *testing.T) {
|
||||
token1 := NewPasswordResetToken("user-1", "token-1", time.Now().Add(1*time.Hour))
|
||||
token2 := NewPasswordResetToken("user-2", "token-2", time.Now().Add(1*time.Hour))
|
||||
|
||||
if token1.ID == token2.ID {
|
||||
t.Error("NewPasswordResetToken() generated identical IDs")
|
||||
}
|
||||
|
||||
if token1.UserID == token2.UserID {
|
||||
t.Error("UserIDs should be different")
|
||||
}
|
||||
|
||||
if token1.Token == token2.Token {
|
||||
t.Error("Tokens should be different")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewRefreshToken(t *testing.T) {
|
||||
userID := "user-123"
|
||||
token := "refresh-token-abc"
|
||||
expiresAt := time.Now().Add(7 * 24 * time.Hour)
|
||||
|
||||
rt := NewRefreshToken(userID, token, expiresAt)
|
||||
|
||||
if rt == nil {
|
||||
t.Fatal("NewRefreshToken() returned nil")
|
||||
}
|
||||
|
||||
if rt.UserID != userID {
|
||||
t.Errorf("UserID = %v, want %v", rt.UserID, userID)
|
||||
}
|
||||
|
||||
if rt.Token != token {
|
||||
t.Errorf("Token = %v, want %v", rt.Token, token)
|
||||
}
|
||||
|
||||
if !rt.ExpiresAt.Equal(expiresAt) {
|
||||
t.Errorf("ExpiresAt = %v, want %v", rt.ExpiresAt, expiresAt)
|
||||
}
|
||||
|
||||
if rt.RevokedAt != nil {
|
||||
t.Error("RevokedAt should be nil for new token")
|
||||
}
|
||||
|
||||
if rt.CreatedAt.IsZero() {
|
||||
t.Error("CreatedAt is zero")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_IsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expiresAt time.Time
|
||||
revoked bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid token",
|
||||
expiresAt: time.Now().Add(24 * time.Hour),
|
||||
revoked: false,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "expired token",
|
||||
expiresAt: time.Now().Add(-1 * time.Hour),
|
||||
revoked: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "revoked token",
|
||||
expiresAt: time.Now().Add(24 * time.Hour),
|
||||
revoked: true,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "expired and revoked",
|
||||
expiresAt: time.Now().Add(-1 * time.Hour),
|
||||
revoked: true,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rt := NewRefreshToken("user-123", "token", tt.expiresAt)
|
||||
if tt.revoked {
|
||||
rt.Revoke()
|
||||
}
|
||||
|
||||
got := rt.IsValid()
|
||||
if got != tt.want {
|
||||
t.Errorf("IsValid() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_Revoke(t *testing.T) {
|
||||
rt := NewRefreshToken("user-123", "token", time.Now().Add(24*time.Hour))
|
||||
|
||||
if rt.RevokedAt != nil {
|
||||
t.Error("RevokedAt should be nil before Revoke()")
|
||||
}
|
||||
|
||||
if !rt.IsValid() {
|
||||
t.Error("Token should be valid before Revoke()")
|
||||
}
|
||||
|
||||
rt.Revoke()
|
||||
|
||||
if rt.RevokedAt == nil {
|
||||
t.Fatal("RevokedAt should not be nil after Revoke()")
|
||||
}
|
||||
|
||||
if rt.IsValid() {
|
||||
t.Error("Token should not be valid after Revoke()")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
diff := now.Sub(*rt.RevokedAt)
|
||||
if diff > time.Second || diff < 0 {
|
||||
t.Errorf("RevokedAt difference too large: %v", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_RevokeMultipleTimes(t *testing.T) {
|
||||
rt := NewRefreshToken("user-123", "token", time.Now().Add(24*time.Hour))
|
||||
|
||||
rt.Revoke()
|
||||
firstRevokedAt := rt.RevokedAt
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
rt.Revoke()
|
||||
|
||||
if rt.RevokedAt == firstRevokedAt {
|
||||
t.Error("Revoke() should update RevokedAt on subsequent calls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_ExpirationCheck(t *testing.T) {
|
||||
expiresIn := 100 * time.Millisecond
|
||||
rt := NewRefreshToken("user-123", "token", time.Now().Add(expiresIn))
|
||||
|
||||
if !rt.IsValid() {
|
||||
t.Error("Token should be valid initially")
|
||||
}
|
||||
|
||||
time.Sleep(expiresIn + 10*time.Millisecond)
|
||||
|
||||
if rt.IsValid() {
|
||||
t.Error("Token should be invalid after expiration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken_MultipleTokens(t *testing.T) {
|
||||
token1 := NewRefreshToken("user-1", "token-1", time.Now().Add(24*time.Hour))
|
||||
token2 := NewRefreshToken("user-2", "token-2", time.Now().Add(24*time.Hour))
|
||||
|
||||
if token1.UserID == token2.UserID {
|
||||
t.Error("UserIDs should be different")
|
||||
}
|
||||
|
||||
if token1.Token == token2.Token {
|
||||
t.Error("Tokens should be different")
|
||||
}
|
||||
|
||||
if token1.CreatedAt.Equal(token2.CreatedAt) {
|
||||
t.Log("Warning: CreatedAt timestamps are identical (possible race condition in test)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user