Add PDF reports, file uploads, admin auth, and comprehensive tests
Features: - PDF service for generating task reports with ReportLab-style formatting - Storage service for file uploads (local and S3-compatible) - Admin authentication middleware with JWT support - Admin user model and repository Infrastructure: - Updated Docker configuration for admin panel builds - Email service enhancements for task notifications - Updated router with admin and file upload routes - Environment configuration updates Tests: - Unit tests for handlers (auth, residence, task) - Unit tests for models (user, residence, task) - Unit tests for repositories (user, residence, task) - Unit tests for services (residence, task) - Integration test setup - Test utilities for mocking database and services Database: - Admin user seed data - Updated test data seeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
63
internal/models/admin.go
Normal file
63
internal/models/admin.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// AdminRole represents the role of an admin user
|
||||
type AdminRole string
|
||||
|
||||
const (
|
||||
AdminRoleAdmin AdminRole = "admin"
|
||||
AdminRoleSuperAdmin AdminRole = "super_admin"
|
||||
)
|
||||
|
||||
// AdminUser represents an administrator for the admin panel
|
||||
type AdminUser struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Email string `gorm:"uniqueIndex;size:254;not null" json:"email"`
|
||||
Password string `gorm:"size:128;not null" json:"-"`
|
||||
FirstName string `gorm:"size:100" json:"first_name"`
|
||||
LastName string `gorm:"size:100" json:"last_name"`
|
||||
Role AdminRole `gorm:"size:20;default:'admin'" json:"role"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for AdminUser
|
||||
func (AdminUser) TableName() string {
|
||||
return "admin_users"
|
||||
}
|
||||
|
||||
// SetPassword hashes and sets the password
|
||||
func (a *AdminUser) SetPassword(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.Password = string(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword verifies the password against the stored hash
|
||||
func (a *AdminUser) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// FullName returns the admin's full name
|
||||
func (a *AdminUser) FullName() string {
|
||||
if a.FirstName == "" && a.LastName == "" {
|
||||
return a.Email
|
||||
}
|
||||
return a.FirstName + " " + a.LastName
|
||||
}
|
||||
|
||||
// IsSuperAdmin checks if the admin has super admin privileges
|
||||
func (a *AdminUser) IsSuperAdmin() bool {
|
||||
return a.Role == AdminRoleSuperAdmin
|
||||
}
|
||||
113
internal/models/residence_test.go
Normal file
113
internal/models/residence_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResidence_TableName(t *testing.T) {
|
||||
r := Residence{}
|
||||
assert.Equal(t, "residence_residence", r.TableName())
|
||||
}
|
||||
|
||||
func TestResidenceType_TableName(t *testing.T) {
|
||||
rt := ResidenceType{}
|
||||
assert.Equal(t, "residence_residencetype", rt.TableName())
|
||||
}
|
||||
|
||||
func TestResidenceShareCode_TableName(t *testing.T) {
|
||||
sc := ResidenceShareCode{}
|
||||
assert.Equal(t, "residence_residencesharecode", sc.TableName())
|
||||
}
|
||||
|
||||
func TestResidence_JSONSerialization(t *testing.T) {
|
||||
bedrooms := 3
|
||||
bathrooms := decimal.NewFromFloat(2.5)
|
||||
sqft := 2000
|
||||
yearBuilt := 2020
|
||||
|
||||
residence := Residence{
|
||||
Name: "Test House",
|
||||
StreetAddress: "123 Main St",
|
||||
City: "Austin",
|
||||
StateProvince: "TX",
|
||||
PostalCode: "78701",
|
||||
Country: "USA",
|
||||
Bedrooms: &bedrooms,
|
||||
Bathrooms: &bathrooms,
|
||||
SquareFootage: &sqft,
|
||||
YearBuilt: &yearBuilt,
|
||||
IsActive: true,
|
||||
IsPrimary: true,
|
||||
}
|
||||
residence.ID = 1
|
||||
|
||||
data, err := json.Marshal(residence)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check JSON field names
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, "Test House", result["name"])
|
||||
assert.Equal(t, "123 Main St", result["street_address"])
|
||||
assert.Equal(t, "Austin", result["city"])
|
||||
assert.Equal(t, "TX", result["state_province"])
|
||||
assert.Equal(t, "78701", result["postal_code"])
|
||||
assert.Equal(t, "USA", result["country"])
|
||||
assert.Equal(t, float64(3), result["bedrooms"])
|
||||
assert.Equal(t, "2.5", result["bathrooms"]) // Decimal serializes as string
|
||||
assert.Equal(t, float64(2000), result["square_footage"])
|
||||
assert.Equal(t, float64(2020), result["year_built"])
|
||||
assert.Equal(t, true, result["is_active"])
|
||||
assert.Equal(t, true, result["is_primary"])
|
||||
}
|
||||
|
||||
func TestResidenceType_JSONSerialization(t *testing.T) {
|
||||
rt := ResidenceType{
|
||||
Name: "House",
|
||||
}
|
||||
rt.ID = 1
|
||||
|
||||
data, err := json.Marshal(rt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, "House", result["name"])
|
||||
}
|
||||
|
||||
func TestResidence_NilOptionalFields(t *testing.T) {
|
||||
residence := Residence{
|
||||
Name: "Test House",
|
||||
StreetAddress: "123 Main St",
|
||||
City: "Austin",
|
||||
StateProvince: "TX",
|
||||
PostalCode: "78701",
|
||||
Country: "USA",
|
||||
IsActive: true,
|
||||
IsPrimary: false,
|
||||
// All optional fields are nil
|
||||
}
|
||||
|
||||
data, err := json.Marshal(residence)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Nil pointer fields should serialize as null
|
||||
assert.Nil(t, result["bedrooms"])
|
||||
assert.Nil(t, result["bathrooms"])
|
||||
assert.Nil(t, result["square_footage"])
|
||||
assert.Nil(t, result["year_built"])
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func (SubscriptionSettings) TableName() string {
|
||||
type UserSubscription struct {
|
||||
BaseModel
|
||||
UserID uint `gorm:"column:user_id;uniqueIndex;not null" json:"user_id"`
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
Tier SubscriptionTier `gorm:"column:tier;size:10;default:'free'" json:"tier"`
|
||||
|
||||
// In-App Purchase data
|
||||
|
||||
275
internal/models/task_test.go
Normal file
275
internal/models/task_test.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTask_TableName(t *testing.T) {
|
||||
task := Task{}
|
||||
assert.Equal(t, "task_task", task.TableName())
|
||||
}
|
||||
|
||||
func TestTaskCategory_TableName(t *testing.T) {
|
||||
cat := TaskCategory{}
|
||||
assert.Equal(t, "task_taskcategory", cat.TableName())
|
||||
}
|
||||
|
||||
func TestTaskPriority_TableName(t *testing.T) {
|
||||
p := TaskPriority{}
|
||||
assert.Equal(t, "task_taskpriority", p.TableName())
|
||||
}
|
||||
|
||||
func TestTaskStatus_TableName(t *testing.T) {
|
||||
s := TaskStatus{}
|
||||
assert.Equal(t, "task_taskstatus", s.TableName())
|
||||
}
|
||||
|
||||
func TestTaskFrequency_TableName(t *testing.T) {
|
||||
f := TaskFrequency{}
|
||||
assert.Equal(t, "task_taskfrequency", f.TableName())
|
||||
}
|
||||
|
||||
func TestTaskCompletion_TableName(t *testing.T) {
|
||||
c := TaskCompletion{}
|
||||
assert.Equal(t, "task_taskcompletion", c.TableName())
|
||||
}
|
||||
|
||||
func TestContractor_TableName(t *testing.T) {
|
||||
c := Contractor{}
|
||||
assert.Equal(t, "task_contractor", c.TableName())
|
||||
}
|
||||
|
||||
func TestContractorSpecialty_TableName(t *testing.T) {
|
||||
s := ContractorSpecialty{}
|
||||
assert.Equal(t, "task_contractorspecialty", s.TableName())
|
||||
}
|
||||
|
||||
func TestDocument_TableName(t *testing.T) {
|
||||
d := Document{}
|
||||
assert.Equal(t, "task_document", d.TableName())
|
||||
}
|
||||
|
||||
func TestTask_JSONSerialization(t *testing.T) {
|
||||
dueDate := time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC)
|
||||
cost := decimal.NewFromFloat(150.50)
|
||||
|
||||
task := Task{
|
||||
ResidenceID: 1,
|
||||
CreatedByID: 1,
|
||||
Title: "Fix leaky faucet",
|
||||
Description: "Kitchen faucet is dripping",
|
||||
DueDate: &dueDate,
|
||||
EstimatedCost: &cost,
|
||||
IsCancelled: false,
|
||||
IsArchived: false,
|
||||
}
|
||||
task.ID = 1
|
||||
|
||||
data, err := json.Marshal(task)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, float64(1), result["residence_id"])
|
||||
assert.Equal(t, float64(1), result["created_by_id"])
|
||||
assert.Equal(t, "Fix leaky faucet", result["title"])
|
||||
assert.Equal(t, "Kitchen faucet is dripping", result["description"])
|
||||
assert.Equal(t, "150.5", result["estimated_cost"]) // Decimal serializes as string
|
||||
assert.Equal(t, false, result["is_cancelled"])
|
||||
assert.Equal(t, false, result["is_archived"])
|
||||
}
|
||||
|
||||
func TestTaskCategory_JSONSerialization(t *testing.T) {
|
||||
cat := TaskCategory{
|
||||
Name: "Plumbing",
|
||||
Description: "Plumbing related tasks",
|
||||
Icon: "wrench",
|
||||
Color: "#3498db",
|
||||
DisplayOrder: 1,
|
||||
}
|
||||
cat.ID = 1
|
||||
|
||||
data, err := json.Marshal(cat)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, "Plumbing", result["name"])
|
||||
assert.Equal(t, "Plumbing related tasks", result["description"])
|
||||
assert.Equal(t, "wrench", result["icon"])
|
||||
assert.Equal(t, "#3498db", result["color"])
|
||||
assert.Equal(t, float64(1), result["display_order"])
|
||||
}
|
||||
|
||||
func TestTaskPriority_JSONSerialization(t *testing.T) {
|
||||
priority := TaskPriority{
|
||||
Name: "High",
|
||||
Level: 3,
|
||||
Color: "#e74c3c",
|
||||
DisplayOrder: 3,
|
||||
}
|
||||
priority.ID = 3
|
||||
|
||||
data, err := json.Marshal(priority)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(3), result["id"])
|
||||
assert.Equal(t, "High", result["name"])
|
||||
assert.Equal(t, float64(3), result["level"])
|
||||
assert.Equal(t, "#e74c3c", result["color"])
|
||||
}
|
||||
|
||||
func TestTaskStatus_JSONSerialization(t *testing.T) {
|
||||
status := TaskStatus{
|
||||
Name: "In Progress",
|
||||
Description: "Task is being worked on",
|
||||
Color: "#3498db",
|
||||
DisplayOrder: 2,
|
||||
}
|
||||
status.ID = 2
|
||||
|
||||
data, err := json.Marshal(status)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(2), result["id"])
|
||||
assert.Equal(t, "In Progress", result["name"])
|
||||
assert.Equal(t, "Task is being worked on", result["description"])
|
||||
assert.Equal(t, "#3498db", result["color"])
|
||||
}
|
||||
|
||||
func TestTaskFrequency_JSONSerialization(t *testing.T) {
|
||||
days := 7
|
||||
freq := TaskFrequency{
|
||||
Name: "Weekly",
|
||||
Days: &days,
|
||||
DisplayOrder: 3,
|
||||
}
|
||||
freq.ID = 3
|
||||
|
||||
data, err := json.Marshal(freq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(3), result["id"])
|
||||
assert.Equal(t, "Weekly", result["name"])
|
||||
assert.Equal(t, float64(7), result["days"])
|
||||
}
|
||||
|
||||
func TestTaskCompletion_JSONSerialization(t *testing.T) {
|
||||
completedAt := time.Date(2024, 6, 15, 14, 30, 0, 0, time.UTC)
|
||||
cost := decimal.NewFromFloat(125.00)
|
||||
|
||||
completion := TaskCompletion{
|
||||
TaskID: 1,
|
||||
CompletedByID: 2,
|
||||
CompletedAt: completedAt,
|
||||
Notes: "Fixed the leak",
|
||||
ActualCost: &cost,
|
||||
}
|
||||
completion.ID = 1
|
||||
|
||||
data, err := json.Marshal(completion)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, float64(1), result["task_id"])
|
||||
assert.Equal(t, float64(2), result["completed_by_id"])
|
||||
assert.Equal(t, "Fixed the leak", result["notes"])
|
||||
assert.Equal(t, "125", result["actual_cost"]) // Decimal serializes as string
|
||||
}
|
||||
|
||||
func TestContractor_JSONSerialization(t *testing.T) {
|
||||
contractor := Contractor{
|
||||
ResidenceID: 1,
|
||||
CreatedByID: 1,
|
||||
Name: "Mike's Plumbing",
|
||||
Company: "Mike's Plumbing Co.",
|
||||
Phone: "+1-555-1234",
|
||||
Email: "mike@plumbing.com",
|
||||
Website: "https://mikesplumbing.com",
|
||||
Notes: "Great service",
|
||||
IsFavorite: true,
|
||||
IsActive: true,
|
||||
}
|
||||
contractor.ID = 1
|
||||
|
||||
data, err := json.Marshal(contractor)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, float64(1), result["residence_id"])
|
||||
assert.Equal(t, "Mike's Plumbing", result["name"])
|
||||
assert.Equal(t, "Mike's Plumbing Co.", result["company"])
|
||||
assert.Equal(t, "+1-555-1234", result["phone"])
|
||||
assert.Equal(t, "mike@plumbing.com", result["email"])
|
||||
assert.Equal(t, "https://mikesplumbing.com", result["website"])
|
||||
assert.Equal(t, true, result["is_favorite"])
|
||||
assert.Equal(t, true, result["is_active"])
|
||||
}
|
||||
|
||||
func TestDocument_JSONSerialization(t *testing.T) {
|
||||
purchaseDate := time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
expiryDate := time.Date(2028, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
price := decimal.NewFromFloat(5000.00)
|
||||
|
||||
doc := Document{
|
||||
ResidenceID: 1,
|
||||
CreatedByID: 1,
|
||||
Title: "HVAC Warranty",
|
||||
Description: "Warranty for central air",
|
||||
DocumentType: "warranty",
|
||||
FileURL: "/uploads/hvac.pdf",
|
||||
FileName: "hvac.pdf",
|
||||
PurchaseDate: &purchaseDate,
|
||||
ExpiryDate: &expiryDate,
|
||||
PurchasePrice: &price,
|
||||
Vendor: "Cool Air HVAC",
|
||||
SerialNumber: "HVAC-123",
|
||||
}
|
||||
doc.ID = 1
|
||||
|
||||
data, err := json.Marshal(doc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(data, &result)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, float64(1), result["id"])
|
||||
assert.Equal(t, "HVAC Warranty", result["title"])
|
||||
assert.Equal(t, "warranty", result["document_type"])
|
||||
assert.Equal(t, "/uploads/hvac.pdf", result["file_url"])
|
||||
assert.Equal(t, "Cool Air HVAC", result["vendor"])
|
||||
assert.Equal(t, "HVAC-123", result["serial_number"])
|
||||
assert.Equal(t, "5000", result["purchase_price"]) // Decimal serializes as string
|
||||
}
|
||||
217
internal/models/user_test.go
Normal file
217
internal/models/user_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser_SetPassword(t *testing.T) {
|
||||
user := &User{}
|
||||
|
||||
err := user.SetPassword("testpassword123")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, user.Password)
|
||||
assert.NotEqual(t, "testpassword123", user.Password) // Should be hashed
|
||||
}
|
||||
|
||||
func TestUser_CheckPassword(t *testing.T) {
|
||||
user := &User{}
|
||||
err := user.SetPassword("correctpassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
expected bool
|
||||
}{
|
||||
{"correct password", "correctpassword", true},
|
||||
{"wrong password", "wrongpassword", false},
|
||||
{"empty password", "", false},
|
||||
{"similar password", "correctpassword1", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := user.CheckPassword(tt.password)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_GetFullName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
user User
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "first and last name",
|
||||
user: User{FirstName: "John", LastName: "Doe", Username: "johndoe"},
|
||||
expected: "John Doe",
|
||||
},
|
||||
{
|
||||
name: "first name only",
|
||||
user: User{FirstName: "John", LastName: "", Username: "johndoe"},
|
||||
expected: "John",
|
||||
},
|
||||
{
|
||||
name: "username fallback",
|
||||
user: User{FirstName: "", LastName: "", Username: "johndoe"},
|
||||
expected: "johndoe",
|
||||
},
|
||||
{
|
||||
name: "last name only returns username",
|
||||
user: User{FirstName: "", LastName: "Doe", Username: "johndoe"},
|
||||
expected: "johndoe",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.user.GetFullName()
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_TableName(t *testing.T) {
|
||||
user := User{}
|
||||
assert.Equal(t, "auth_user", user.TableName())
|
||||
}
|
||||
|
||||
func TestAuthToken_TableName(t *testing.T) {
|
||||
token := AuthToken{}
|
||||
assert.Equal(t, "user_authtoken", token.TableName())
|
||||
}
|
||||
|
||||
func TestUserProfile_TableName(t *testing.T) {
|
||||
profile := UserProfile{}
|
||||
assert.Equal(t, "user_userprofile", profile.TableName())
|
||||
}
|
||||
|
||||
func TestConfirmationCode_IsValid(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
future := now.Add(1 * time.Hour)
|
||||
past := now.Add(-1 * time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
code ConfirmationCode
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid code",
|
||||
code: ConfirmationCode{IsUsed: false, ExpiresAt: future},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "used code",
|
||||
code: ConfirmationCode{IsUsed: true, ExpiresAt: future},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expired code",
|
||||
code: ConfirmationCode{IsUsed: false, ExpiresAt: past},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "used and expired",
|
||||
code: ConfirmationCode{IsUsed: true, ExpiresAt: past},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.code.IsValid()
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetCode_IsValid(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
future := now.Add(1 * time.Hour)
|
||||
past := now.Add(-1 * time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
code PasswordResetCode
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid code",
|
||||
code: PasswordResetCode{Used: false, ExpiresAt: future, Attempts: 0, MaxAttempts: 5},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "used code",
|
||||
code: PasswordResetCode{Used: true, ExpiresAt: future, Attempts: 0, MaxAttempts: 5},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expired code",
|
||||
code: PasswordResetCode{Used: false, ExpiresAt: past, Attempts: 0, MaxAttempts: 5},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "max attempts reached",
|
||||
code: PasswordResetCode{Used: false, ExpiresAt: future, Attempts: 5, MaxAttempts: 5},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "attempts under max",
|
||||
code: PasswordResetCode{Used: false, ExpiresAt: future, Attempts: 4, MaxAttempts: 5},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.code.IsValid()
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetCode_SetAndCheckCode(t *testing.T) {
|
||||
code := &PasswordResetCode{}
|
||||
|
||||
err := code.SetCode("123456")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, code.CodeHash)
|
||||
|
||||
// Check correct code
|
||||
assert.True(t, code.CheckCode("123456"))
|
||||
|
||||
// Check wrong code
|
||||
assert.False(t, code.CheckCode("654321"))
|
||||
assert.False(t, code.CheckCode(""))
|
||||
}
|
||||
|
||||
func TestGenerateConfirmationCode(t *testing.T) {
|
||||
code := GenerateConfirmationCode()
|
||||
assert.Len(t, code, 6)
|
||||
|
||||
// Generate multiple codes and ensure they're different
|
||||
codes := make(map[string]bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
c := GenerateConfirmationCode()
|
||||
assert.Len(t, c, 6)
|
||||
codes[c] = true
|
||||
}
|
||||
// Most codes should be unique (very unlikely to have collisions)
|
||||
assert.Greater(t, len(codes), 5)
|
||||
}
|
||||
|
||||
func TestGenerateResetToken(t *testing.T) {
|
||||
token := GenerateResetToken()
|
||||
assert.Len(t, token, 64) // 32 bytes = 64 hex chars
|
||||
|
||||
// Ensure uniqueness
|
||||
token2 := GenerateResetToken()
|
||||
assert.NotEqual(t, token, token2)
|
||||
}
|
||||
Reference in New Issue
Block a user