Files
honeyDueAPI/internal/handlers/noauth_test.go
Trey t 72db9050f8 Add Stripe billing, free trials, and cross-platform subscription guards
- Stripe integration: add StripeService with checkout sessions, customer
  portal, and webhook handling for subscription lifecycle events.
- Free trials: auto-start configurable trial on first subscription check,
  with admin-controllable duration and enable/disable toggle.
- Cross-platform guard: prevent duplicate subscriptions across iOS, Android,
  and Stripe by checking existing platform before allowing purchase.
- Subscription model: add Stripe fields (customer_id, subscription_id,
  price_id), trial fields (trial_start, trial_end, trial_used), and
  SubscriptionSource/IsTrialActive helpers.
- API: add trial and source fields to status response, update OpenAPI spec.
- Clean up stale migration and audit docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:36:14 -06:00

335 lines
13 KiB
Go

package handlers
import (
"net/http"
"testing"
"github.com/treytartt/casera-api/internal/config"
"github.com/treytartt/casera-api/internal/repositories"
"github.com/treytartt/casera-api/internal/services"
"github.com/treytartt/casera-api/internal/testutil"
)
// TestTaskHandler_NoAuth_Returns401 verifies that task handler endpoints return
// 401 Unauthorized when no auth user is set in the context (e.g., auth middleware
// misconfigured or bypassed). This is a regression test for P1-1 (SEC-19).
func TestTaskHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
taskRepo := repositories.NewTaskRepository(db)
residenceRepo := repositories.NewResidenceRepository(db)
taskService := services.NewTaskService(taskRepo, residenceRepo)
handler := NewTaskHandler(taskService, nil)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/tasks/", handler.ListTasks)
e.GET("/api/tasks/:id/", handler.GetTask)
e.POST("/api/tasks/", handler.CreateTask)
e.PUT("/api/tasks/:id/", handler.UpdateTask)
e.DELETE("/api/tasks/:id/", handler.DeleteTask)
e.POST("/api/tasks/:id/cancel/", handler.CancelTask)
e.POST("/api/tasks/:id/mark-in-progress/", handler.MarkInProgress)
e.GET("/api/task-completions/", handler.ListCompletions)
e.POST("/api/task-completions/", handler.CreateCompletion)
tests := []struct {
name string
method string
path string
}{
{"ListTasks", "GET", "/api/tasks/"},
{"GetTask", "GET", "/api/tasks/1/"},
{"CreateTask", "POST", "/api/tasks/"},
{"UpdateTask", "PUT", "/api/tasks/1/"},
{"DeleteTask", "DELETE", "/api/tasks/1/"},
{"CancelTask", "POST", "/api/tasks/1/cancel/"},
{"MarkInProgress", "POST", "/api/tasks/1/mark-in-progress/"},
{"ListCompletions", "GET", "/api/task-completions/"},
{"CreateCompletion", "POST", "/api/task-completions/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestResidenceHandler_NoAuth_Returns401 verifies that residence handler endpoints
// return 401 Unauthorized when no auth user is set in the context.
func TestResidenceHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
residenceRepo := repositories.NewResidenceRepository(db)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{}
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
handler := NewResidenceHandler(residenceService, nil, nil, true)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/residences/", handler.ListResidences)
e.GET("/api/residences/my/", handler.GetMyResidences)
e.GET("/api/residences/summary/", handler.GetSummary)
e.GET("/api/residences/:id/", handler.GetResidence)
e.POST("/api/residences/", handler.CreateResidence)
e.PUT("/api/residences/:id/", handler.UpdateResidence)
e.DELETE("/api/residences/:id/", handler.DeleteResidence)
e.POST("/api/residences/:id/generate-share-code/", handler.GenerateShareCode)
e.POST("/api/residences/join-with-code/", handler.JoinWithCode)
e.GET("/api/residences/:id/users/", handler.GetResidenceUsers)
tests := []struct {
name string
method string
path string
}{
{"ListResidences", "GET", "/api/residences/"},
{"GetMyResidences", "GET", "/api/residences/my/"},
{"GetSummary", "GET", "/api/residences/summary/"},
{"GetResidence", "GET", "/api/residences/1/"},
{"CreateResidence", "POST", "/api/residences/"},
{"UpdateResidence", "PUT", "/api/residences/1/"},
{"DeleteResidence", "DELETE", "/api/residences/1/"},
{"GenerateShareCode", "POST", "/api/residences/1/generate-share-code/"},
{"JoinWithCode", "POST", "/api/residences/join-with-code/"},
{"GetResidenceUsers", "GET", "/api/residences/1/users/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestNotificationHandler_NoAuth_Returns401 verifies that notification handler
// endpoints return 401 Unauthorized when no auth user is set in the context.
func TestNotificationHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
notificationRepo := repositories.NewNotificationRepository(db)
notificationService := services.NewNotificationService(notificationRepo, nil)
handler := NewNotificationHandler(notificationService)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/notifications/", handler.ListNotifications)
e.GET("/api/notifications/unread-count/", handler.GetUnreadCount)
e.POST("/api/notifications/:id/read/", handler.MarkAsRead)
e.POST("/api/notifications/mark-all-read/", handler.MarkAllAsRead)
e.GET("/api/notifications/preferences/", handler.GetPreferences)
e.PUT("/api/notifications/preferences/", handler.UpdatePreferences)
e.POST("/api/notifications/devices/", handler.RegisterDevice)
e.GET("/api/notifications/devices/", handler.ListDevices)
tests := []struct {
name string
method string
path string
}{
{"ListNotifications", "GET", "/api/notifications/"},
{"GetUnreadCount", "GET", "/api/notifications/unread-count/"},
{"MarkAsRead", "POST", "/api/notifications/1/read/"},
{"MarkAllAsRead", "POST", "/api/notifications/mark-all-read/"},
{"GetPreferences", "GET", "/api/notifications/preferences/"},
{"UpdatePreferences", "PUT", "/api/notifications/preferences/"},
{"RegisterDevice", "POST", "/api/notifications/devices/"},
{"ListDevices", "GET", "/api/notifications/devices/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestDocumentHandler_NoAuth_Returns401 verifies that document handler endpoints
// return 401 Unauthorized when no auth user is set in the context.
func TestDocumentHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
documentRepo := repositories.NewDocumentRepository(db)
residenceRepo := repositories.NewResidenceRepository(db)
documentService := services.NewDocumentService(documentRepo, residenceRepo)
handler := NewDocumentHandler(documentService, nil)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/documents/", handler.ListDocuments)
e.GET("/api/documents/:id/", handler.GetDocument)
e.GET("/api/documents/warranties/", handler.ListWarranties)
e.POST("/api/documents/", handler.CreateDocument)
e.PUT("/api/documents/:id/", handler.UpdateDocument)
e.DELETE("/api/documents/:id/", handler.DeleteDocument)
e.POST("/api/documents/:id/activate/", handler.ActivateDocument)
e.POST("/api/documents/:id/deactivate/", handler.DeactivateDocument)
tests := []struct {
name string
method string
path string
}{
{"ListDocuments", "GET", "/api/documents/"},
{"GetDocument", "GET", "/api/documents/1/"},
{"ListWarranties", "GET", "/api/documents/warranties/"},
{"CreateDocument", "POST", "/api/documents/"},
{"UpdateDocument", "PUT", "/api/documents/1/"},
{"DeleteDocument", "DELETE", "/api/documents/1/"},
{"ActivateDocument", "POST", "/api/documents/1/activate/"},
{"DeactivateDocument", "POST", "/api/documents/1/deactivate/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestContractorHandler_NoAuth_Returns401 verifies that contractor handler endpoints
// return 401 Unauthorized when no auth user is set in the context.
func TestContractorHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
contractorRepo := repositories.NewContractorRepository(db)
residenceRepo := repositories.NewResidenceRepository(db)
contractorService := services.NewContractorService(contractorRepo, residenceRepo)
handler := NewContractorHandler(contractorService)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/contractors/", handler.ListContractors)
e.GET("/api/contractors/:id/", handler.GetContractor)
e.POST("/api/contractors/", handler.CreateContractor)
e.PUT("/api/contractors/:id/", handler.UpdateContractor)
e.DELETE("/api/contractors/:id/", handler.DeleteContractor)
e.POST("/api/contractors/:id/toggle-favorite/", handler.ToggleFavorite)
e.GET("/api/contractors/:id/tasks/", handler.GetContractorTasks)
tests := []struct {
name string
method string
path string
}{
{"ListContractors", "GET", "/api/contractors/"},
{"GetContractor", "GET", "/api/contractors/1/"},
{"CreateContractor", "POST", "/api/contractors/"},
{"UpdateContractor", "PUT", "/api/contractors/1/"},
{"DeleteContractor", "DELETE", "/api/contractors/1/"},
{"ToggleFavorite", "POST", "/api/contractors/1/toggle-favorite/"},
{"GetContractorTasks", "GET", "/api/contractors/1/tasks/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestSubscriptionHandler_NoAuth_Returns401 verifies that subscription handler
// endpoints return 401 Unauthorized when no auth user is set in the context.
func TestSubscriptionHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
subscriptionRepo := repositories.NewSubscriptionRepository(db)
residenceRepo := repositories.NewResidenceRepository(db)
taskRepo := repositories.NewTaskRepository(db)
contractorRepo := repositories.NewContractorRepository(db)
documentRepo := repositories.NewDocumentRepository(db)
subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
handler := NewSubscriptionHandler(subscriptionService, nil)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/subscription/", handler.GetSubscription)
e.GET("/api/subscription/status/", handler.GetSubscriptionStatus)
e.GET("/api/subscription/promotions/", handler.GetPromotions)
e.POST("/api/subscription/purchase/", handler.ProcessPurchase)
e.POST("/api/subscription/cancel/", handler.CancelSubscription)
e.POST("/api/subscription/restore/", handler.RestoreSubscription)
tests := []struct {
name string
method string
path string
}{
{"GetSubscription", "GET", "/api/subscription/"},
{"GetSubscriptionStatus", "GET", "/api/subscription/status/"},
{"GetPromotions", "GET", "/api/subscription/promotions/"},
{"ProcessPurchase", "POST", "/api/subscription/purchase/"},
{"CancelSubscription", "POST", "/api/subscription/cancel/"},
{"RestoreSubscription", "POST", "/api/subscription/restore/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestMediaHandler_NoAuth_Returns401 verifies that media handler endpoints return
// 401 Unauthorized when no auth user is set in the context.
func TestMediaHandler_NoAuth_Returns401(t *testing.T) {
handler := NewMediaHandler(nil, nil, nil, nil)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/media/document/:id", handler.ServeDocument)
e.GET("/api/media/document-image/:id", handler.ServeDocumentImage)
e.GET("/api/media/completion-image/:id", handler.ServeCompletionImage)
tests := []struct {
name string
method string
path string
}{
{"ServeDocument", "GET", "/api/media/document/1"},
{"ServeDocumentImage", "GET", "/api/media/document-image/1"},
{"ServeCompletionImage", "GET", "/api/media/completion-image/1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}
// TestUserHandler_NoAuth_Returns401 verifies that user handler endpoints return
// 401 Unauthorized when no auth user is set in the context.
func TestUserHandler_NoAuth_Returns401(t *testing.T) {
db := testutil.SetupTestDB(t)
userRepo := repositories.NewUserRepository(db)
userService := services.NewUserService(userRepo)
handler := NewUserHandler(userService)
e := testutil.SetupTestRouter()
// Register routes WITHOUT auth middleware
e.GET("/api/users/", handler.ListUsers)
e.GET("/api/users/:id/", handler.GetUser)
e.GET("/api/users/profiles/", handler.ListProfiles)
tests := []struct {
name string
method string
path string
}{
{"ListUsers", "GET", "/api/users/"},
{"GetUser", "GET", "/api/users/1/"},
{"ListProfiles", "GET", "/api/users/profiles/"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := testutil.MakeRequest(e, tt.method, tt.path, nil, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
}
}