diff --git a/billing/model.go b/billing/model.go index c74554c..10f6bf0 100644 --- a/billing/model.go +++ b/billing/model.go @@ -3,6 +3,7 @@ package billing import ( "encoding/json" "fmt" + "log/slog" "os" "strconv" "time" @@ -13,6 +14,7 @@ type Billing struct { VariantIDIndividual int VariantIDPro int VariantIDPremium int + Logger *slog.Logger } type CheckoutPayload struct { Data CheckoutData `json:"data"` @@ -100,7 +102,7 @@ type BillingRepo interface { MarkWebhookProcessed(id string, event string) error } -func NewBilling() (*Billing, error) { +func NewBilling(logger *slog.Logger) (*Billing, error) { individualID, err := strconv.Atoi(os.Getenv("LEMON_VARIANT_ID_INDIVIDUAL")) if err != nil { return nil, fmt.Errorf("invalid INDIVIDUAL ID: %w", err) @@ -118,5 +120,6 @@ func NewBilling() (*Billing, error) { VariantIDIndividual: individualID, VariantIDPro: proID, VariantIDPremium: premiumID, + Logger: logger, }, nil } diff --git a/billing/repository_mock.go b/billing/repository_mock.go index 9caba66..9ec7131 100644 --- a/billing/repository_mock.go +++ b/billing/repository_mock.go @@ -12,7 +12,7 @@ func NewMockRepo() *MockRepo { func (m *MockRepo) LogCreditTransaction(tx CreditTransaction) error { if m.FailLogCreditTransaction { - return errors.New("Mocked LogCreditTransaction failure") + return errors.New("mocked LogCreditTransaction failure") } return nil } diff --git a/billing/service.go b/billing/service.go index dcd0607..7f32790 100644 --- a/billing/service.go +++ b/billing/service.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "os" "strconv" @@ -45,13 +44,13 @@ func (b *Billing) RequestCheckoutSession(userEmail string, variantID int) (strin body, err := json.Marshal(payload) if err != nil { - log.Printf("json.Marshal failed: %v", err) + b.Logger.Error("json.Marshal failed", "error", err) return "", err } req, err := http.NewRequest("POST", "https://api.lemonsqueezy.com/v1/checkouts", bytes.NewBuffer(body)) if err != nil { - log.Printf("http.NewRequest failed: %v", err) + b.Logger.Error("http.NewRequest failed", "error", err) return "", err } @@ -64,20 +63,20 @@ func (b *Billing) RequestCheckoutSession(userEmail string, variantID int) (strin } res, err := client.Do(req) if err != nil { - log.Printf("client.Do failed: %v", err) + b.Logger.Error("client.Do failed", "error", err) return "", err } defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode >= 300 { bodyBytes, _ := io.ReadAll(res.Body) - log.Printf("LemonSqueezy returned error: %s", string(bodyBytes)) - return "", fmt.Errorf("LemonSqueezy API error: %s", res.Status) + b.Logger.Error("LemonSqueezy returned error", "error", string(bodyBytes)) + return "", fmt.Errorf("lemonSqueezy API error: %s", res.Status) } var result CheckoutResponse if err := json.NewDecoder(res.Body).Decode(&result); err != nil { - log.Printf("NewDecoder failed: %v", err) + b.Logger.Error("NewDecoder failed", "error", err) return "", err } @@ -190,7 +189,7 @@ func (b *Billing) VerifyBillingSignature(signature string, body []byte, secret s func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, email string, variantID int) error { user, err := userRepo.GetUserByEmail(email) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -213,12 +212,12 @@ func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, creditType = "subscription" reason = "Premium subscription monthly credit grant" default: - log.Printf("ERROR: unknown variantID: %d", variantID) + b.Logger.Error("ERROR: unknown variantID", "variantID", variantID) return fmt.Errorf("unknown variant ID: %d", variantID) } if err := userRepo.AddCredits(user.ID, credits, creditType); err != nil { - log.Printf("repo.AddCredits failed: %v", err) + b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -229,7 +228,7 @@ func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, Reason: reason, } if err := billingRepo.LogCreditTransaction(tx); err != nil { - log.Printf("Warning: credit granted but failed to log transaction: %v", err) + b.Logger.Error("Warning: credit granted but failed to log transaction", "error", err) return err } @@ -239,7 +238,7 @@ func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, orderAttrs OrderAttributes) error { user, err := userRepo.GetUserByEmail(orderAttrs.UserEmail) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -264,12 +263,12 @@ func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, creditType = "subscription" reason = "Premium subscription monthly credit refund" default: - log.Printf("ERROR: unknown variantID: %d", variantID) + b.Logger.Error("unknown variantID", "variantID", variantID) return fmt.Errorf("unknown variant ID: %d", variantID) } if err := userRepo.AddCredits(user.ID, -credits, creditType); err != nil { - log.Printf("repo.DeductCredits failed: %v", err) + b.Logger.Error("repo.DeductCredits failed", "error", err) return err } @@ -280,7 +279,7 @@ func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, Reason: reason, } if err := billingRepo.LogCreditTransaction(tx); err != nil { - log.Printf("Warning: refund deduction succeeded but failed to log transaction: %v", err) + b.Logger.Warn("Refund deduction succeeded but failed to log transaction", "error", err) return err } @@ -290,7 +289,7 @@ func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs SubscriptionAttributes, subscriptionID string) error { user, err := userRepo.GetUserByEmail(subCreatedAttrs.UserEmail) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -301,7 +300,7 @@ func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs Sub case b.VariantIDPremium: tier = "premium" default: - log.Printf("ERROR: unknown variantID: %d", subCreatedAttrs.VariantID) + b.Logger.Error("unknown variantID", "variantID", subCreatedAttrs.VariantID) return fmt.Errorf("unknown variant ID: %d", subCreatedAttrs.VariantID) } @@ -314,7 +313,7 @@ func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs Sub subCreatedAttrs.EndsAt, ) if err != nil { - log.Printf("CreateSubscriptionData failed: %v", err) + b.Logger.Error("CreateSubscriptionData failed", "error", err) return err } @@ -324,7 +323,7 @@ func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs Sub func (b *Billing) CancelSubscription(userRepo user.UserRepo, email string) error { user, err := userRepo.GetUserByEmail(email) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -333,7 +332,7 @@ func (b *Billing) CancelSubscription(userRepo user.UserRepo, email string) error "cancelled", ) if err != nil { - log.Printf("CancelSubscriptionData failed: %v", err) + b.Logger.Error("CancelSubscriptionData failed", "error", err) return err } @@ -343,7 +342,7 @@ func (b *Billing) CancelSubscription(userRepo user.UserRepo, email string) error func (b *Billing) ResumeSubscription(userRepo user.UserRepo, email string) error { user, err := userRepo.GetUserByEmail(email) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -352,7 +351,7 @@ func (b *Billing) ResumeSubscription(userRepo user.UserRepo, email string) error "active", ) if err != nil { - log.Printf("CancelSubscriptionData failed: %v", err) + b.Logger.Error("CancelSubscriptionData failed", "error", err) return err } @@ -362,7 +361,7 @@ func (b *Billing) ResumeSubscription(userRepo user.UserRepo, email string) error func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo BillingRepo, email string) error { user, err := userRepo.GetUserByEmail(email) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -371,14 +370,14 @@ func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo Billing "expired", ) if err != nil { - log.Printf("CancelSubscriptionData failed: %v", err) + b.Logger.Error("CancelSubscriptionData failed", "error", err) return err } if user.SubscriptionCredits > 0 { err = userRepo.AddCredits(user.ID, -user.SubscriptionCredits, "subscription") if err != nil { - log.Printf("repo.AddCredits failed: %v", err) + b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -389,7 +388,7 @@ func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo Billing Reason: "Zeroed out credits on subscription expiration", } if err := billingRepo.LogCreditTransaction(tx); err != nil { - log.Printf("Warning: zero-out succeeded but failed to log transaction: %v", err) + b.Logger.Warn("Zero-out succeeded but failed to log transaction", "error", err) } } @@ -399,7 +398,7 @@ func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo Billing func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingRepo, subRenewAttrs SubscriptionRenewAttributes) error { user, err := userRepo.GetUserByEmail(subRenewAttrs.UserEmail) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -419,12 +418,12 @@ func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingR credits = 20 reason = "Premium subscription monthly credit" default: - log.Printf("ERROR: unknown user.SubscriptionTier: %s", user.SubscriptionTier) + b.Logger.Error("unknown user.SubscriptionTier", "subscriptionTier", user.SubscriptionTier) return fmt.Errorf("unknown user.SubscriptionTier: %s", user.SubscriptionTier) } if err := userRepo.AddCredits(user.ID, credits, "subscription"); err != nil { - log.Printf("repo.AddCredits failed: %v", err) + b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -435,7 +434,7 @@ func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingR Reason: reason, } if err := billingRepo.LogCreditTransaction(tx); err != nil { - log.Printf("Warning: credit granted but failed to log transaction: %v", err) + b.Logger.Warn("credit granted but failed to log transaction", "error", err) return err } @@ -445,7 +444,7 @@ func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingR func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo BillingRepo, subChangedAttrs SubscriptionAttributes) error { user, err := userRepo.GetUserByEmail(subChangedAttrs.UserEmail) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -468,12 +467,12 @@ func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo Billing credits = 10 reason = "Pro upgraded to Premium subscription monthly credit" default: - log.Printf("ERROR: unknown user.SubscriptionTier: %s", user.SubscriptionTier) + b.Logger.Error("unknown user.SubscriptionTier", "subscriptionTier", user.SubscriptionTier) return fmt.Errorf("unknown user.SubscriptionTier: %s", user.SubscriptionTier) } if err := userRepo.AddCredits(user.ID, credits, "subscription"); err != nil { - log.Printf("repo.AddCredits failed: %v", err) + b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -484,7 +483,7 @@ func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo Billing Reason: reason, } if err := billingRepo.LogCreditTransaction(tx); err != nil { - log.Printf("Warning: credit granted but failed to log transaction: %v", err) + b.Logger.Warn("credit granted but failed to log transaction", "error", err) return err } @@ -494,7 +493,7 @@ func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo Billing func (b *Billing) UpdateSubscription(userRepo user.UserRepo, subUpdatedAttrs SubscriptionAttributes, subscriptionID string) error { user, err := userRepo.GetUserByEmail(subUpdatedAttrs.UserEmail) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } @@ -505,7 +504,7 @@ func (b *Billing) UpdateSubscription(userRepo user.UserRepo, subUpdatedAttrs Sub case b.VariantIDPremium: tier = "premium" default: - log.Printf("ERROR: unknown variantID: %d", subUpdatedAttrs.VariantID) + b.Logger.Error("unknown variantID", "variantID", subUpdatedAttrs.VariantID) return fmt.Errorf("unknown variant ID: %d", subUpdatedAttrs.VariantID) } @@ -518,7 +517,7 @@ func (b *Billing) UpdateSubscription(userRepo user.UserRepo, subUpdatedAttrs Sub subUpdatedAttrs.EndsAt, ) if err != nil { - log.Printf("UpdateSubscriptionData failed: %v", err) + b.Logger.Error("UpdateSubscriptionData failed", "error", err) return err } diff --git a/billing/service_test.go b/billing/service_test.go index 5056966..07b13b9 100644 --- a/billing/service_test.go +++ b/billing/service_test.go @@ -5,6 +5,8 @@ import ( "crypto/sha256" "fmt" "log" + "log/slog" + "os" "strings" "testing" @@ -12,6 +14,20 @@ import ( "github.com/michaelboegner/interviewer/user" ) +func NewTestBilling() *billing.Billing { + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + logger := slog.New(handler) + + return &billing.Billing{ + VariantIDIndividual: 1, + VariantIDPro: 2, + VariantIDPremium: 3, + Logger: logger, + } +} + func TestApplyCredits(t *testing.T) { tests := []struct { name string @@ -84,11 +100,7 @@ func TestApplyCredits(t *testing.T) { userRepo.FailAddCredits = tc.failCredit billingRepo.FailLogCreditTransaction = tc.failLog - b := billing.Billing{ - VariantIDIndividual: 1, - VariantIDPro: 2, - VariantIDPremium: 3, - } + b := NewTestBilling() err := b.ApplyCredits(userRepo, billingRepo, "test@example.com", tc.variantID) if tc.expectErr && err == nil { @@ -165,11 +177,7 @@ func TestDeductCredits(t *testing.T) { userRepo.FailAddCredits = tc.failCredit billingRepo.FailLogCreditTransaction = tc.failLog - b := billing.Billing{ - VariantIDIndividual: 1, - VariantIDPro: 2, - VariantIDPremium: 3, - } + b := NewTestBilling() attrs := billing.OrderAttributes{ UserEmail: "test@example.com", @@ -192,7 +200,7 @@ func TestDeductCredits(t *testing.T) { } func TestVerifyBillingSignature(t *testing.T) { - b := billing.Billing{} + b := NewTestBilling() body := []byte(`{"key":"value"}`) secret := "testsecret" mac := hmacSha256(body, secret) diff --git a/chatgpt/model.go b/chatgpt/model.go index c143ab2..9a1c81b 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -2,6 +2,7 @@ package chatgpt import ( "fmt" + "log/slog" "os" "strings" ) @@ -24,6 +25,7 @@ type ChatGPTResponse struct { type OpenAIClient struct { APIKey string + Logger *slog.Logger } type OpenAIError struct { @@ -43,9 +45,10 @@ func (e *OpenAIError) Error() string { return fmt.Sprintf("OpenAI error %d: %s", e.StatusCode, e.Message) } -func NewOpenAI() *OpenAIClient { +func NewOpenAI(logger *slog.Logger) *OpenAIClient { return &OpenAIClient{ APIKey: os.Getenv("OPENAI_API_KEY"), + Logger: logger, } } diff --git a/chatgpt/service.go b/chatgpt/service.go index e627646..03c6d19 100644 --- a/chatgpt/service.go +++ b/chatgpt/service.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "log" "net/http" "strings" ) @@ -33,7 +32,7 @@ func (c *OpenAIClient) GetChatGPTResponse(prompt string) (*ChatGPTResponse, erro req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBody)) if err != nil { - log.Printf("NewRequestWithContext failed: %v", err) + c.Logger.Error("NewRequestWithContext failed", "error", err) return nil, err } @@ -64,7 +63,7 @@ func (c *OpenAIClient) GetChatGPTResponse(prompt string) (*ChatGPTResponse, erro var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { - log.Printf("Unmarshal result err: %v", err) + c.Logger.Error("Unmarshal result err", "error", err) return nil, err } @@ -78,7 +77,7 @@ func (c *OpenAIClient) GetChatGPTResponse(prompt string) (*ChatGPTResponse, erro var chatGPTResponse ChatGPTResponse if err := json.Unmarshal([]byte(chatGPTResponseRaw), &chatGPTResponse); err != nil { - log.Printf("Unmarshal chatGPTResponse err: %v", err) + c.Logger.Error("Unmarshal chatGPTResponse err", "error", err) return nil, err } @@ -100,7 +99,7 @@ func (c *OpenAIClient) GetChatGPTResponseConversation(conversationHistory []map[ req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBody)) if err != nil { - log.Printf("NewRequestWithContext failed: %v", err) + c.Logger.Error("NewRequestWithContext failed", "error", err) return nil, err } @@ -131,7 +130,7 @@ func (c *OpenAIClient) GetChatGPTResponseConversation(conversationHistory []map[ var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { - log.Printf("Unmarshal result err: %v", err) + c.Logger.Error("Unmarshal result err", "error", err) return nil, err } @@ -145,7 +144,7 @@ func (c *OpenAIClient) GetChatGPTResponseConversation(conversationHistory []map[ var chatGPTResponse ChatGPTResponse if err := json.Unmarshal([]byte(chatGPTResponseRaw), &chatGPTResponse); err != nil { - log.Printf("Unmarshal chatGPTResponse err: %v", err) + c.Logger.Error("Unmarshal chatGPTResponse err", "error", err) return nil, err } @@ -173,7 +172,7 @@ func (c *OpenAIClient) GetChatGPT35Response(prompt string) (*ChatGPTResponse, er req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBody)) if err != nil { - log.Printf("NewRequestWithContext failed: %v", err) + c.Logger.Error("NewRequestWithContext failed", "error", err) return nil, err } @@ -204,7 +203,7 @@ func (c *OpenAIClient) GetChatGPT35Response(prompt string) (*ChatGPTResponse, er var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { - log.Printf("Unmarshal result err: %v", err) + c.Logger.Error("Unmarshal result err", "error", err) return nil, err } @@ -218,7 +217,7 @@ func (c *OpenAIClient) GetChatGPT35Response(prompt string) (*ChatGPTResponse, er var chatGPTResponse ChatGPTResponse if err := json.Unmarshal([]byte(chatGPTResponseRaw), &chatGPTResponse); err != nil { - log.Printf("Unmarshal chatGPTResponse err: %v", err) + c.Logger.Error("Unmarshal chatGPTResponse err", "error", err) return nil, err } diff --git a/cmd/interviewer/main.go b/cmd/interviewer/main.go index 2627568..3a26c07 100644 --- a/cmd/interviewer/main.go +++ b/cmd/interviewer/main.go @@ -30,5 +30,5 @@ func main() { } logger.Info("starting server...") - srv.StartServer() + srv.StartServer(logger) } diff --git a/handlers/handlers.go b/handlers/handlers.go index e5ba68b..8c34f36 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "io" - "log" "net/http" "net/url" "os" @@ -55,15 +54,16 @@ func (h *Handler) RequestVerificationHandler(w http.ResponseWriter, r *http.Requ verificationJWT, err := user.VerificationToken(req.Email, req.Username, req.Password) if err != nil { - log.Printf("GenerateEmailVerificationToken failed: %v", err) + h.Logger.Error("GenerateEmailVerificationToken failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to create token") return } verifyURL := os.Getenv("FRONTEND_URL") + "verify-email?token=" + verificationJWT + go func(email, url string) { if err := h.Mailer.SendVerificationEmail(email, url); err != nil { - log.Printf("SendVerificationEmail failed: %v", err) + h.Logger.Error("SendVerificationEmail failed", "error", err) } }(req.Email, verifyURL) @@ -84,7 +84,7 @@ func (h *Handler) CheckEmailHandler(w http.ResponseWriter, r *http.Request) { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - log.Printf("Decoding check-email body failed: %v", err) + h.Logger.Error("Decoding check-email body failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid request body") return } @@ -99,7 +99,7 @@ func (h *Handler) CheckEmailHandler(w http.ResponseWriter, r *http.Request) { RespondWithJSON(w, http.StatusOK, map[string]bool{"exists": false}) return } - log.Printf("CheckEmailHandler internal error: %v", err) + h.Logger.Error("CheckEmailHandler internal error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -118,28 +118,28 @@ func (h *Handler) CreateUsersHandler(w http.ResponseWriter, r *http.Request) { } err := json.NewDecoder(r.Body).Decode(&req) if err != nil { - log.Printf("Decoding params failed: %v", err) + h.Logger.Error("Decoding params failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal Server Error") return } userCreated, err := user.CreateUser(h.UserRepo, req.Token) if err != nil { - log.Printf("CreateUser error: %v", err) + h.Logger.Error("CreateUser error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } jwt, err := token.CreateJWT(strconv.Itoa(userCreated.ID), 0) if err != nil { - log.Printf("token.CreateJWT failed: %v", err) + h.Logger.Error("token.CreateJWT failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } go func(email string) { if err := h.Mailer.SendWelcome(email); err != nil { - log.Printf("SendWelcome failed: %v", err) + h.Logger.Error("SendWelcome failed", "error", err) } }(userCreated.Email) @@ -164,22 +164,22 @@ func (h *Handler) GetUsersHandler(w http.ResponseWriter, r *http.Request) { return } - userIDParam, err := GetPathID(r, "/api/users/") + userIDParam, err := GetPathID(r, "/api/users/", h.Logger) if err != nil { - log.Printf("GetPathID error: %v\n", err) + h.Logger.Error("GetPathID error", "error", err) RespondWithError(w, http.StatusBadRequest, "UserID required") return } if userID != userIDParam { - log.Printf("UserID mismatch: %v vs. %v", userID, userIDParam) + h.Logger.Error("UserID mismatch", "got", userIDParam, "want", userID) RespondWithError(w, http.StatusUnauthorized, "Invalid ID") return } userReturned, err := user.GetUser(h.UserRepo, userID) if err != nil { - log.Printf("GetUsers error: %v", err) + h.Logger.Error("GetUsers error", "error", err) return } @@ -204,50 +204,50 @@ func (h *Handler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { return } - userIDParam, err := GetPathID(r, "/api/users/delete/") + userIDParam, err := GetPathID(r, "/api/users/delete/", h.Logger) if err != nil { - log.Printf("GetPathID error: %v\n", err) + h.Logger.Error("GetPathID error", "error", err) RespondWithError(w, http.StatusBadRequest, "UserID required") return } if userID != userIDParam { - log.Printf("UserID mismatch: %v vs. %v", userID, userIDParam) + h.Logger.Error("UserID mismatch", "got", userIDParam, "want", userID) RespondWithError(w, http.StatusUnauthorized, "Invalid ID") return } userReturned, err := user.GetUser(h.UserRepo, userID) if err != nil { - log.Printf("GetUser error: %v", err) + h.Logger.Error("GetUser error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to find user") return } err = h.Billing.CancelSubscription(h.UserRepo, userReturned.Email) if err != nil { - log.Printf("h.Billing.CancelSubscription failed: %v", err) + h.Logger.Error("h.Billing.CancelSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } err = user.MarkUserDeleted(h.UserRepo, userID) if err != nil { - log.Printf("DeleteUser failed: %v", err) + h.Logger.Error("DeleteUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to delete user") return } err = token.DeleteRefreshToken(h.TokenRepo, userID) if err != nil { - log.Printf("DeleteRefreshTokensForUser failed: %v", err) + h.Logger.Error("DeleteRefreshTokensForUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } err = h.Mailer.SendDeletionConfirmation(userReturned.Email) if err != nil { - log.Printf("h.Mailer.SendDeletionConfirmation failed: %v", err) + h.Logger.Error("h.Mailer.SendDeletionConfirmation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -264,32 +264,32 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { params := &middleware.AcceptedVals{} err := json.NewDecoder(r.Body).Decode(params) if err != nil { - log.Printf("Decoding params failed: %v", err) + h.Logger.Error("Decoding params failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal Server Error") return } if params.Email == "" || params.Password == "" { - log.Printf("Invalid username or password.") - RespondWithError(w, http.StatusBadRequest, "Invalid username or password.") + h.Logger.Error("Invalid username or password.") + RespondWithError(w, http.StatusBadRequest, "Authentication failed.") return } jwToken, username, userID, err := user.LoginUser(h.UserRepo, params.Email, params.Password) if err != nil { - log.Printf("LoginUser error: %v", err) + h.Logger.Error("LoginUser error", "error", err) if errors.Is(err, user.ErrAccountDeleted) { RespondWithError(w, http.StatusUnauthorized, user.ErrAccountDeleted.Error()) return } - RespondWithError(w, http.StatusUnauthorized, "Invalid username or password.") + RespondWithError(w, http.StatusUnauthorized, "Authentication failed.") return } refreshToken, err := token.CreateRefreshToken(h.TokenRepo, userID) if err != nil { - log.Printf("RefreshToken error: %v", err) + h.Logger.Error("RefreshToken error", "error", err) RespondWithError(w, http.StatusUnauthorized, "") return } @@ -351,14 +351,14 @@ func (h *Handler) GithubLoginHandler(w http.ResponseWriter, r *http.Request) { client = &http.Client{} req, err = http.NewRequest("GET", "https://api.github.com/user", nil) if err != nil { - log.Printf("http.NewRequest failed: %v", err) + h.Logger.Error("http.NewRequest failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } req.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken) githubResp, err := client.Do(req) if err != nil { - log.Printf("GET api.github.com/user failed: %v", err) + h.Logger.Error("GET api.github.com/user failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -373,14 +373,14 @@ func (h *Handler) GithubLoginHandler(w http.ResponseWriter, r *http.Request) { if githubUser.Email == "" { req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) if err != nil { - log.Printf("http.NewRequest failed: %v", err) + h.Logger.Error("http.NewRequest failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } req.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken) emailResp, err := client.Do(req) if err != nil { - log.Printf("GET api.github.com/user/emails failed: %v", err) + h.Logger.Error("GET api.github.com/user/emails failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -402,7 +402,7 @@ func (h *Handler) GithubLoginHandler(w http.ResponseWriter, r *http.Request) { } if githubUser.Email == "" { - log.Printf("GitHub login failed: no verified email found for user %s", githubUser.Login) + h.Logger.Error("GitHub login failed: no verified email found for user", "userEmail", githubUser.Login) RespondWithError(w, http.StatusUnauthorized, "We couldn’t retrieve a valid email address from GitHub. Please check your GitHub email settings and try again.") return } @@ -415,13 +415,13 @@ func (h *Handler) GithubLoginHandler(w http.ResponseWriter, r *http.Request) { jwt, err := token.CreateJWT(strconv.Itoa(user.ID), 0) if err != nil { - log.Printf("token.CreateJWT failed: %v", err) + h.Logger.Error("token.CreateJWT failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } refreshToken, err := token.CreateRefreshToken(h.TokenRepo, user.ID) if err != nil { - log.Printf("token.CreateRefreshToken failed: %v", err) + h.Logger.Error("token.CreateRefreshToken failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -450,48 +450,53 @@ func (h *Handler) RefreshTokensHandler(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(params) if err != nil { - log.Printf("Decoding params failed: %v", err) + h.Logger.Error("Decoding params failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal Server Error") return } if params.UserID == 0 { - log.Printf("Invalid userID") - RespondWithError(w, http.StatusBadRequest, "Invalid username or password") + h.Logger.Error("Invalid userID") + RespondWithError(w, http.StatusBadRequest, "Authentication failed") return } storedToken, err := token.GetStoredRefreshToken(h.TokenRepo, params.UserID) if err != nil { - log.Printf("GetStoredRefreshToken error: %v", err) + h.Logger.Error("GetStoredRefreshToken error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid user_id") return } ok := token.VerifyRefreshToken(storedToken, providedToken) if !ok { - log.Printf("VerifyRefreshToken error") + h.Logger.Error("VerifyRefreshToken error") RespondWithError(w, http.StatusUnauthorized, "Refresh token is invalid") return } refreshToken, err := token.CreateRefreshToken(h.TokenRepo, params.UserID) if err != nil { - log.Printf("CreateRefreshToken error: %v", err) + h.Logger.Error("CreateRefreshToken error", "error", err) RespondWithError(w, http.StatusUnauthorized, "") return } user, err := h.UserRepo.GetUser(params.UserID) - if err != nil || user.AccountStatus == "deleted" { - log.Printf("Refresh attempt for deleted account ID %d", params.UserID) + if err != nil { + h.Logger.Error("h.UserRepo.GetUser error", "error", err) + RespondWithError(w, http.StatusUnauthorized, "Account deactivated") + return + } + if user.AccountStatus == "deleted" { + h.Logger.Error("Refresh attempt for deleted account ID", "userID", params.UserID) RespondWithError(w, http.StatusUnauthorized, "Account deactivated") return } jwToken, err := token.CreateJWT(strconv.Itoa(params.UserID), 0) if err != nil { - log.Printf("JWT creation failed: %v", err) + h.Logger.Error("JWT creation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "") return } @@ -519,14 +524,14 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { params := &middleware.AcceptedVals{} err := json.NewDecoder(r.Body).Decode(params) if err != nil { - log.Printf("Decoding params failed: %v", err) + h.Logger.Error("Decoding params failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal Server Error") return } userReturned, err := user.GetUser(h.UserRepo, userID) if err != nil { - log.Printf("GetUser error: %v", err) + h.Logger.Error("GetUser error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to find user") return } @@ -544,7 +549,7 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { - log.Printf("OpenAI error: %v", openaiErr) + h.Logger.Error("OpenAI error", "error", openaiErr) RespondWithError(w, openaiErr.StatusCode, openaiErr.Message) return } @@ -552,21 +557,21 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { RespondWithError(w, http.StatusPaymentRequired, "You do not have enough credits to start a new interview or your subscription has expired.") return } - log.Printf("Interview failed to start: %v", err) + h.Logger.Error("Interview failed to start", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to start interview.") return } conversationID, err := conversation.CreateEmptyConversation(h.ConversationRepo, interviewStarted.Id, interviewStarted.Subtopic) if err != nil { - log.Printf("conversation.CreateEmptyConversation failed: %v", err) + h.Logger.Error("conversation.CreateEmptyConversation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } err = interview.LinkConversation(h.InterviewRepo, interviewStarted.Id, conversationID) if err != nil { - log.Printf("interview.LinkConversation failed: %v", err) + h.Logger.Error("interview.LinkConversation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -592,21 +597,21 @@ func (h *Handler) GetInterviewHandler(w http.ResponseWriter, r *http.Request) { return } - interviewID, err := GetPathID(r, "/api/interviews/") + interviewID, err := GetPathID(r, "/api/interviews/", h.Logger) if err != nil { - log.Printf("GetPathID failed: %v", err) + h.Logger.Error("GetPathID failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid interview ID") return } interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) if err != nil { - log.Printf("GetInterview failed: %v", err) + h.Logger.Error("GetInterview failed", "error", err) RespondWithError(w, http.StatusNotFound, "Interview not found") return } if interviewReturned.UserId != userID { - log.Printf("User ID mismatch on interview fetch") + h.Logger.Error("User ID mismatch on interview fetch", "got", userID, "want", interviewReturned.UserId) RespondWithError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -630,7 +635,7 @@ func (h *Handler) UpdateInterviewStatusHandler(w http.ResponseWriter, r *http.Re return } - interviewID, err := GetPathID(r, "/api/interviews/") + interviewID, err := GetPathID(r, "/api/interviews/", h.Logger) if err != nil { RespondWithError(w, http.StatusBadRequest, "Invalid interview ID") return @@ -646,21 +651,21 @@ func (h *Handler) UpdateInterviewStatusHandler(w http.ResponseWriter, r *http.Re interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) if err != nil { - log.Printf("GetInterview error: %v\n", err) + h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") return } err = ValidateInterviewStatusTransition(interviewReturned.Status, payload.Status) if err != nil { - log.Printf("ValidateInterviewStatusTransition failed: %v", err) + h.Logger.Error("ValidateInterviewStatusTransition failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid status transition") return } err = h.InterviewRepo.UpdateStatus(interviewID, userID, payload.Status) if err != nil { - log.Printf("UpdateInterviewStatus failed: %v", err) + h.Logger.Error("UpdateInterviewStatus failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not update status") return } @@ -683,27 +688,27 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ params := &middleware.AcceptedVals{} err := json.NewDecoder(r.Body).Decode(params) if err != nil { - log.Printf("Decoding params failed: %v", err) + h.Logger.Error("Decoding params failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal Server Error") return } - interviewID, err := GetPathID(r, "/api/conversations/create/") + interviewID, err := GetPathID(r, "/api/conversations/create/", h.Logger) if err != nil { - log.Printf("PathID error: %v\n", err) + h.Logger.Error("PathID error", "error", err) RespondWithError(w, http.StatusBadRequest, "Missing ID") return } interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) if err != nil { - log.Printf("GetInterview error: %v\n", err) + h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") return } if interviewReturned.UserId != userID { - log.Printf("interview.userid != token user_id") + h.Logger.Error("userID does not exist", "got", userID, "want", interviewReturned.UserId) RespondWithError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -714,7 +719,7 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ conversationReturned, err := conversation.GetConversation(h.ConversationRepo, interviewID) if err != nil { - log.Printf("conversation.GetConversation failed: %v", err) + h.Logger.Error("conversation.GetConversation failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") return } @@ -732,11 +737,11 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { - log.Printf("OpenAI error: %v", openaiErr) + h.Logger.Error("OpenAI error", "error", openaiErr) RespondWithError(w, openaiErr.StatusCode, openaiErr.Message) return } - log.Printf("CreateConversation error: %v", err) + h.Logger.Error("CreateConversation error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid interview_id") return } @@ -762,32 +767,32 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ params := &middleware.AcceptedVals{} err := json.NewDecoder(r.Body).Decode(params) if err != nil { - log.Printf("Decoding params failed: %v", err) + h.Logger.Error("Decoding params failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal Server Error") return } if params.Message == "" { - log.Printf("messageUserResponse is nil") + h.Logger.Error("messageUserResponse is nil") RespondWithError(w, http.StatusBadRequest, "Missing message") return } - interviewID, err := GetPathID(r, "/api/conversations/append/") + interviewID, err := GetPathID(r, "/api/conversations/append/", h.Logger) if err != nil { - log.Printf("PathID error: %v\n", err) + h.Logger.Error("PathID error", "error", err) RespondWithError(w, http.StatusBadRequest, "Missing ID") return } interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) if err != nil { - log.Printf("GetInterview error: %v\n", err) + h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") return } if interviewReturned.UserId != userID { - log.Printf("interview.userid != token user_id") + h.Logger.Error("incorrect userID", "got", userID, "want", interviewReturned.UserId) RespondWithError(w, http.StatusUnauthorized, "Unauthorized") return } @@ -798,7 +803,7 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ conversationReturned, err := conversation.GetConversation(h.ConversationRepo, interviewID) if err != nil { - log.Printf("GetConversation error: %v", err) + h.Logger.Error("GetConversation error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID.") return } @@ -815,11 +820,11 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { - log.Printf("OpenAI error: %v", openaiErr) + h.Logger.Error("OpenAI error", "error", openaiErr) RespondWithError(w, openaiErr.StatusCode, openaiErr.Message) return } - log.Printf("AppendConversation error: %v", err) + h.Logger.Error("AppendConversation error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID.") return } @@ -842,28 +847,28 @@ func (h *Handler) GetConversationHandler(w http.ResponseWriter, r *http.Request) return } - interviewID, err := GetPathID(r, "/api/conversations/") + interviewID, err := GetPathID(r, "/api/conversations/", h.Logger) if err != nil { - log.Printf("PathID error: %v\n", err) + h.Logger.Error("PathID error", "error", err) RespondWithError(w, http.StatusBadRequest, "Missing ID") return } interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) if err != nil { - log.Printf("GetInterview error: %v\n", err) + h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") return } if interviewReturned.UserId != userID { - log.Printf("interview.userid != token user_id") + h.Logger.Error("incorrect userID", "got", userID, "want", interviewReturned.UserId) RespondWithError(w, http.StatusUnauthorized, "Unauthorized") return } conversationReturned, err := conversation.GetConversation(h.ConversationRepo, interviewID) if err != nil { - log.Printf("GetConversation error: %v", err) + h.Logger.Error("GetConversation error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID.") return } @@ -883,14 +888,14 @@ func (h *Handler) RequestResetHandler(w http.ResponseWriter, r *http.Request) { var params PasswordResetRequest err := json.NewDecoder(r.Body).Decode(¶ms) if err != nil { - log.Printf("Decoding request failed: %v", err) + h.Logger.Error("Decoding request failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid request body") return } resetJWT, err := user.RequestPasswordReset(h.UserRepo, params.Email) if err != nil { - log.Printf("Error generating reset token for email %s: %v", params.Email, err) + h.Logger.Error("Error generating reset token for email", "error", err) w.WriteHeader(http.StatusOK) return } @@ -901,7 +906,7 @@ func (h *Handler) RequestResetHandler(w http.ResponseWriter, r *http.Request) { go func(email, resetURL string) { err := h.Mailer.SendPasswordReset(email, resetURL) if err != nil { - log.Printf("SendPasswordReset error: %v", err) + h.Logger.Error("SendPasswordReset error", "error", err) return } }(params.Email, resetURL) @@ -918,13 +923,13 @@ func (h *Handler) ResetPasswordHandler(w http.ResponseWriter, r *http.Request) { var params PasswordResetPayload if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - log.Printf("Decoding payload failed: %v", err) + h.Logger.Error("Decoding payload failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid request body") return } err := user.ResetPassword(h.UserRepo, params.NewPassword, params.Token) if err != nil { - log.Printf("ResetPasswordHandler failed: %v", err) + h.Logger.Error("ResetPasswordHandler failed", "error", err) RespondWithError(w, http.StatusUnauthorized, "Invalid or expired token") return } @@ -941,14 +946,14 @@ func (h *Handler) CreateCheckoutSessionHandler(w http.ResponseWriter, r *http.Re userID, ok := r.Context().Value(middleware.ContextKeyTokenParams).(int) if !ok { - log.Printf("r.Context().Value() failed") + h.Logger.Error("r.Context().Value() error") RespondWithError(w, http.StatusUnauthorized, "Unauthorized") return } var params CheckoutRequest if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil || params.Tier == "" { - log.Printf("jsonNewDecoder failed: %v", err) + h.Logger.Error("jsonNewDecoder failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Missing or invalid tier") return } @@ -974,14 +979,14 @@ func (h *Handler) CreateCheckoutSessionHandler(w http.ResponseWriter, r *http.Re priceIDInt, err := strconv.Atoi(priceID) if err != nil { - log.Printf("strconv.Atoi() failed: %v", err) + h.Logger.Error("strconv.Atoi() failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } url, err := h.Billing.RequestCheckoutSession(user.Email, priceIDInt) if err != nil { - log.Printf("billing.CreateCheckoutSession failed: %v", err) + h.Logger.Error("billing.CreateCheckoutSession failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not start checkout") return } @@ -1003,14 +1008,14 @@ func (h *Handler) CancelSubscriptionHandler(w http.ResponseWriter, r *http.Reque userReturned, err := user.GetUser(h.UserRepo, userID) if err != nil { - log.Printf("GetUser failed: %v", err) + h.Logger.Error("GetUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not retrieve user") return } err = h.Billing.RequestDeleteSubscription(userReturned.SubscriptionID) if err != nil { - log.Printf("DeleteSubscription failed: %v", err) + h.Logger.Error("DeleteSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not cancel subscription") return } @@ -1032,14 +1037,14 @@ func (h *Handler) ResumeSubscriptionHandler(w http.ResponseWriter, r *http.Reque userReturned, err := user.GetUser(h.UserRepo, userID) if err != nil { - log.Printf("GetUser failed: %v", err) + h.Logger.Error("GetUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not retrieve user") return } err = h.Billing.RequestResumeSubscription(userReturned.SubscriptionID) if err != nil { - log.Printf("DeleteSubscription failed: %v", err) + h.Logger.Error("DeleteSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not cancel subscription") return } @@ -1061,7 +1066,7 @@ func (h *Handler) ChangePlanHandler(w http.ResponseWriter, r *http.Request) { var params CheckoutRequest if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil || params.Tier == "" { - log.Printf("jsonNewDecoder failed: %v", err) + h.Logger.Error("jsonNewDecoder failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Missing or invalid tier") return } @@ -1085,13 +1090,13 @@ func (h *Handler) ChangePlanHandler(w http.ResponseWriter, r *http.Request) { priceIDInt, err := strconv.Atoi(priceID) if err != nil { - log.Printf("strconv.Atoi() failed: %v", err) + h.Logger.Error("strconv.Atoi() failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } if err := h.Billing.RequestUpdateSubscriptionVariant(user.SubscriptionID, priceIDInt); err != nil { - log.Printf("UpdateLemonSubscriptionVariant failed: %v", err) + h.Logger.Error("UpdateLemonSubscriptionVariant failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update subscription") return } @@ -1107,7 +1112,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) body, err := io.ReadAll(r.Body) if err != nil { - log.Printf("io.ReadAll failed: %v", err) + h.Logger.Error("io.ReadAll failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Bad Request") return } @@ -1115,7 +1120,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) signature := r.Header.Get("X-Signature") if !h.Billing.VerifyBillingSignature(signature, body, os.Getenv("LEMON_WEBHOOK_SECRET")) { - log.Printf("Invalid billing event signature") + h.Logger.Error("Invalid billing event signature") RespondWithError(w, http.StatusUnauthorized, "Invalid signature") return } @@ -1123,7 +1128,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) var webhookPayload billing.BillingWebhookPayload err = json.Unmarshal(body, &webhookPayload) if err != nil { - log.Printf("json.Unmarshal failed: %v", err) + h.Logger.Error("json.Unmarshal failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid JSON") return } @@ -1131,12 +1136,12 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) webhookID := webhookPayload.Meta.WebhookID exists, err := h.BillingRepo.HasWebhookBeenProcessed(webhookID) if err != nil { - log.Printf("h.BillingRepo.HasWebhookBeenProcessed failed: %v", err) + h.Logger.Error("h.BillingRepo.HasWebhookBeenProcessed failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Error checking webhook") return } if exists { - log.Printf("Webhook %s already processed", webhookID) + h.Logger.Info("Webhook already processed", "webhookID", webhookID) w.WriteHeader(http.StatusOK) return } @@ -1147,170 +1152,170 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) eventType := webhookPayload.Meta.EventName - log.Printf("Received webhook: eventType=%q, webhookID=%s, subscriptionID=%s", eventType, webhookID, subscriptionID) + h.Logger.Info("Received webhook", "eventType", eventType, "webhookID", webhookID, "subscriptionID", subscriptionID) switch eventType { case "order_created": var orderAttrs billing.OrderAttributes if err := json.Unmarshal(webhookPayload.Data.Attributes, &orderAttrs); err != nil { - log.Printf("Unmarshal order_created failed: %v", err) + h.Logger.Error("Unmarshal order_created failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid order_created payload") return } err = h.Billing.ApplyCredits(h.UserRepo, h.BillingRepo, orderAttrs.UserEmail, orderAttrs.FirstOrderItem.VariantID) if err != nil { - log.Printf("h.Billing.ApplyCredits failed: %v", err) + h.Logger.Error("h.Billing.ApplyCredits failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_created": var SubCreatedAttrs billing.SubscriptionAttributes if err := json.Unmarshal(webhookPayload.Data.Attributes, &SubCreatedAttrs); err != nil { - log.Printf("Unmarshal subscription_created failed: %v", err) + h.Logger.Error("Unmarshal subscription_created failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_created payload") return } exists, err := h.UserRepo.HasActiveOrCancelledSubscription(SubCreatedAttrs.UserEmail) if err != nil { - log.Printf("Subscription duplicate check failed: %v", err) + h.Logger.Error("Subscription duplicate check failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Subscription check failed") return } if exists { - log.Printf("Duplicate subscription attempt blocked for %s", SubCreatedAttrs.UserEmail) + h.Logger.Info("Duplicate subscription attempt blocked", "userEmail", SubCreatedAttrs.UserEmail) return } err = h.Billing.CreateSubscription(h.UserRepo, SubCreatedAttrs, subscriptionID) if err != nil { - log.Printf("h.Billing.CreateSubscription failed: %v", err) + h.Logger.Error("h.Billing.CreateSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_cancelled": if err := json.Unmarshal(webhookPayload.Data.Attributes, &emailAttribute); err != nil { - log.Printf("Unmarshal subscription_cancelled failed: %v", err) + h.Logger.Error("Unmarshal subscription_cancelled failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_cancelled payload") return } err = h.Billing.CancelSubscription(h.UserRepo, emailAttribute.UserEmail) if err != nil { - log.Printf("h.Billing.CancelSubscription failed: %v", err) + h.Logger.Error("h.Billing.CancelSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_resumed": if err := json.Unmarshal(webhookPayload.Data.Attributes, &emailAttribute); err != nil { - log.Printf("Unmarshal subscription_resumed failed: %v", err) + h.Logger.Error("Unmarshal subscription_resumed failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_resumed payload") return } err = h.Billing.ResumeSubscription(h.UserRepo, emailAttribute.UserEmail) if err != nil { - log.Printf("h.Billing.ResumeSubscription failed: %v", err) + h.Logger.Error("h.Billing.ResumeSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_expired": if err := json.Unmarshal(webhookPayload.Data.Attributes, &emailAttribute); err != nil { - log.Printf("Unmarshal subscription_expired failed: %v", err) + h.Logger.Error("Unmarshal subscription_expired failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_expired payload") return } err = h.Billing.ExpireSubscription(h.UserRepo, h.BillingRepo, emailAttribute.UserEmail) if err != nil { - log.Printf("h.Billing.ExpireSubscription failed: %v", err) + h.Logger.Error("h.Billing.ExpireSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_payment_success": var SubRenewAttrs billing.SubscriptionRenewAttributes if err := json.Unmarshal(webhookPayload.Data.Attributes, &SubRenewAttrs); err != nil { - log.Printf("Unmarshal subscription_payment_success failed: %v", err) + h.Logger.Error("Unmarshal subscription_payment_success failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_payment_success payload") return } if SubRenewAttrs.BillingReason == "initial" { - log.Println("Skipping credits on initial charge (already granted via order_created)") + h.Logger.Info("Skipping credits on initial charge (already granted via order_created)") return } err = h.Billing.RenewSubscription(h.UserRepo, h.BillingRepo, SubRenewAttrs) if err != nil { - log.Printf("h.Billing.RenewSubscription failed: %v", err) + h.Logger.Error("h.Billing.RenewSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_plan_changed": var SubChangedAttrs billing.SubscriptionAttributes if err := json.Unmarshal(webhookPayload.Data.Attributes, &SubChangedAttrs); err != nil { - log.Printf("Unmarshal subscription_plan_changed failed: %v", err) + h.Logger.Error("Unmarshal subscription_plan_changed failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_plan_changed payload") return } err = h.Billing.ChangeSubscription(h.UserRepo, h.BillingRepo, SubChangedAttrs) if err != nil { - log.Printf("h.Billing.ChangeSubscription failed: %v", err) + h.Logger.Error("h.Billing.ChangeSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_updated": var SubChangedAttrs billing.SubscriptionAttributes if err := json.Unmarshal(webhookPayload.Data.Attributes, &SubChangedAttrs); err != nil { - log.Printf("Unmarshal subscription_updated failed: %v", err) + h.Logger.Error("Unmarshal subscription_updated failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid subscription_updated payload") return } err = h.Billing.UpdateSubscription(h.UserRepo, SubChangedAttrs, subscriptionID) if err != nil { - log.Printf("h.Billing.UpdateSubscription failed: %v", err) + h.Logger.Error("h.Billing.UpdateSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "order_refunded": var orderAttrs billing.OrderAttributes if err := json.Unmarshal(webhookPayload.Data.Attributes, &orderAttrs); err != nil { - log.Printf("Unmarshal order_created failed: %v", err) + h.Logger.Error("Unmarshal order_created failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid order_created payload") return } err = h.Billing.DeductCredits(h.UserRepo, h.BillingRepo, orderAttrs) if err != nil { - log.Printf("h.Billing.DeductCredits failed: %v", err) + h.Logger.Error("h.Billing.DeductCredits failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } case "subscription_payment_failed", "subscription_payment_recovered": if err := json.Unmarshal(webhookPayload.Data.Attributes, &emailAttribute); err != nil { - log.Printf("Unmarshal %s failed: %v", eventType, err) + h.Logger.Error("Unmarshal failed", "eventType", eventType, "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid payment status payload") return } - log.Printf("Payment event: %s for user %s", eventType, emailAttribute.UserEmail) + h.Logger.Info("Payment event", "eventType", eventType, "user", emailAttribute.UserEmail) default: - log.Printf("Unhandled event type: %s", eventType) + h.Logger.Info("Unhandled event type", "eventType", eventType) RespondWithError(w, http.StatusNotImplemented, "Unhandled event type") return } if err != nil { - log.Printf("eventType switch func failed: %v", err) + h.Logger.Error("eventType switch func failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } err = h.BillingRepo.MarkWebhookProcessed(webhookID, eventType) if err != nil { - log.Printf("MarkWebhookProcessed failed: %v", err) + h.Logger.Error("MarkWebhookProcessed failed", "error", err) w.WriteHeader(http.StatusOK) return } @@ -1336,7 +1341,7 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { RespondWithError(w, http.StatusUnauthorized, "User not found") return } - log.Printf("dashboard.GetDashboardData failed: %v", err) + h.Logger.Error("dashboard.GetDashboardData failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not load dashboard") return } @@ -1362,11 +1367,11 @@ func (h *Handler) JDInputHandler(w http.ResponseWriter, r *http.Request) { if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { - log.Printf("OpenAI error: %v", openaiErr) + h.Logger.Error("OpenAI error", "error", openaiErr) RespondWithError(w, openaiErr.StatusCode, openaiErr.Message) return } - log.Printf("chatgpt.ExtractJDInput failed: %v", err) + h.Logger.Error("chatgpt.ExtractJDInput failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to process job description") return } @@ -1375,11 +1380,11 @@ func (h *Handler) JDInputHandler(w http.ResponseWriter, r *http.Request) { if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { - log.Printf("OpenAI error: %v", openaiErr) + h.Logger.Error("OpenAI error", "error", openaiErr) RespondWithError(w, openaiErr.StatusCode, openaiErr.Message) return } - log.Printf("chatgpt.ExtractJDSummary failed: %v", err) + h.Logger.Error("chatgpt.ExtractJDSummary failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to process job description") return diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 28acbd4..7e1d311 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "io" - "log" + "log/slog" "net/http" "os" "strings" @@ -19,7 +19,6 @@ import ( "github.com/michaelboegner/interviewer/internal/mocks" "github.com/michaelboegner/interviewer/internal/testutil" "github.com/michaelboegner/interviewer/interview" - "github.com/michaelboegner/interviewer/middleware" "github.com/michaelboegner/interviewer/token" "github.com/michaelboegner/interviewer/user" ) @@ -34,9 +33,7 @@ type TestCase struct { reqBody string headerKey string headerValue string - params middleware.AcceptedVals expectedStatus int - expectError bool respBody handlers.ReturnVals respBodyFunc func() handlers.ReturnVals Interview *interview.Interview @@ -53,33 +50,41 @@ var ( mockAI *mocks.MockOpenAIClient ) -func TestMain(m *testing.M) { - log.SetFlags(log.LstdFlags | log.Llongfile) +var logger *slog.Logger - log.Println("Loading environment variables...") - err := godotenv.Load("../.env.test") - if err != nil { - log.Fatalf("Error loading .env.test file: %v", err) +func TestMain(m *testing.M) { + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + logger = slog.New(handler) + + logger.Info("Loading environment variables...") + if err := godotenv.Load("../.env.test"); err != nil { + logger.Error("failed to load .env.test", "error", err) + os.Exit(1) } - log.Println("Initializing test server...") - Handler, err = testutil.InitTestServer() + logger.Info("Initializing test server...") + var err error + Handler, err = testutil.InitTestServer(logger) if err != nil { - log.Fatalf("Test server initialization failed: %v", err) + logger.Error("test server initialization failed", "error", err) + os.Exit(1) } if testutil.TestServerURL == "" { - log.Fatal("TestMain: TestServerURL is empty! The server did not start properly.") + logger.Error("TestServerURL is empty! The server did not start properly") + os.Exit(1) } - log.Printf("TestMain: Test server started successfully at: %s", testutil.TestServerURL) + logger.Info("Test server started", "url", testutil.TestServerURL) mockAI = Handler.OpenAI.(*mocks.MockOpenAIClient) conversationBuilder = testutil.NewConversationBuilder() code := m.Run() - log.Println("Stopping test server...") + logger.Info("Stopping test server...") testutil.StopTestServer() os.Exit(code) @@ -147,14 +152,10 @@ func Test_RequestVerificationHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, strings.NewReader(tc.reqBody)) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -226,13 +227,9 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - verificationJWT, err := user.VerificationToken(tc.email, tc.username, tc.password) if err != nil { - log.Printf("GenerateEmailVerificationToken failed: %v", err) + t.Fatalf("GenerateEmailVerificationToken failed: %v", err) } reqBodyUser := strings.NewReader(fmt.Sprintf(`{ "token": "%s" @@ -241,7 +238,7 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, reqBodyUser) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -258,6 +255,14 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { expected := tc.respBody got := *respUnmarshalled + // Assert JWT is non-empty + if got.JWToken == "" { + t.Fatalf("[%s] expected JWT, got empty string", tc.name) + } + + // Ignore JWToken when diffing + got.JWToken = "" + if diff := cmp.Diff(expected, got, cmpopts.EquateApproxTime(time.Second)); diff != "" { t.Errorf("Mismatch (-expected +got):\n%s", diff) } @@ -283,7 +288,7 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { func Test_GetUsersHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, userID := testutil.CreateTestUserAndJWT() + jwtoken, userID := testutil.CreateTestUserAndJWT(logger) tests := []TestCase{ { @@ -328,14 +333,10 @@ func Test_GetUsersHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, strings.NewReader(tc.reqBody)) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -377,7 +378,7 @@ func Test_GetUsersHandler_Integration(t *testing.T) { func Test_LoginHandler_Integration(t *testing.T) { cleanDBOrFail(t) - _, _ = testutil.CreateTestUserAndJWT() + _, _ = testutil.CreateTestUserAndJWT(logger) tests := []TestCase{ { @@ -442,14 +443,10 @@ func Test_LoginHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, strings.NewReader(tc.reqBody)) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -502,7 +499,7 @@ func Test_LoginHandler_Integration(t *testing.T) { func Test_RefreshTokensHandler_Integration(t *testing.T) { cleanDBOrFail(t) - _, userID := testutil.CreateTestUserAndJWT() + _, userID := testutil.CreateTestUserAndJWT(logger) refreshToken, err := token.GetStoredRefreshToken(Handler.TokenRepo, userID) if err != nil { t.Fatalf("TC GetStoredRefreshToken failed: %v", err) @@ -577,14 +574,10 @@ func Test_RefreshTokensHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, strings.NewReader(tc.reqBody)) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -637,8 +630,8 @@ func Test_RefreshTokensHandler_Integration(t *testing.T) { func Test_InterviewsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, userID := testutil.CreateTestUserAndJWT() - expiredJWT := testutil.CreateTestExpiredJWT(userID, -1) + jwtoken, userID := testutil.CreateTestUserAndJWT(logger) + expiredJWT := testutil.CreateTestExpiredJWT(userID, -1, logger) tests := []TestCase{ { @@ -726,10 +719,6 @@ func Test_InterviewsHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - if tc.setup != nil { tc.setup() } @@ -737,7 +726,7 @@ func Test_InterviewsHandler_Integration(t *testing.T) { // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, strings.NewReader(tc.reqBody)) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -779,9 +768,9 @@ func Test_InterviewsHandler_Integration(t *testing.T) { func Test_CreateConversationsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, _ := testutil.CreateTestUserAndJWT() + jwtoken, _ := testutil.CreateTestUserAndJWT(logger) mockAI.Scenario = mocks.ScenarioInterview - interviewID := testutil.CreateTestInterview(jwtoken) + interviewID := testutil.CreateTestInterview(jwtoken, logger) conversationsURL := testutil.TestServerURL + fmt.Sprintf("/api/conversations/create/%d", interviewID) tests := []TestCase{ @@ -835,10 +824,6 @@ func Test_CreateConversationsHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - if tc.setup != nil { tc.setup() } @@ -846,7 +831,7 @@ func Test_CreateConversationsHandler_Integration(t *testing.T) { // Act resp, respCode, err := testRequests(t, tc.headerKey, tc.headerValue, tc.method, tc.url, strings.NewReader(tc.reqBody)) if err != nil { - log.Fatalf("TestRequest for interview creation failed: %v", err) + t.Fatalf("TestRequest failed: %v", err) } respUnmarshalled := &handlers.ReturnVals{} @@ -893,13 +878,13 @@ func Test_CreateConversationsHandler_Integration(t *testing.T) { func Test_AppendConversationsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, _ := testutil.CreateTestUserAndJWT() + jwtoken, _ := testutil.CreateTestUserAndJWT(logger) mockAI.Scenario = mocks.ScenarioInterview - interviewID := testutil.CreateTestInterview(jwtoken) + interviewID := testutil.CreateTestInterview(jwtoken, logger) mockAI.Scenario = mocks.ScenarioCreated - conversationID := testutil.CreateTestConversation(jwtoken, interviewID) + conversationID := testutil.CreateTestConversation(jwtoken, interviewID, logger) urlTest := testutil.TestServerURL + fmt.Sprintf("/api/conversations/append/%d", interviewID) tests := []TestCase{ @@ -973,10 +958,6 @@ func Test_AppendConversationsHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - if tc.name == "AppendConversation_IsFinished" { reqBodyPre := fmt.Sprintf(`{ "conversation_id" : %d, @@ -1032,13 +1013,6 @@ func Test_AppendConversationsHandler_Integration(t *testing.T) { } } -func showLogsIfFail(t *testing.T, name string, buf strings.Builder) { - log.SetOutput(os.Stderr) - if t.Failed() { - fmt.Printf("---- logs for test: %s ----\n%s\n", name, buf.String()) - } -} - func cleanDBOrFail(t *testing.T) { if err := testutil.TruncateAllTables(Handler.DB); err != nil { t.Fatalf("Failed to clean DB: %v", err) diff --git a/handlers/helpers.go b/handlers/helpers.go index 052145b..6adce44 100644 --- a/handlers/helpers.go +++ b/handlers/helpers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "log/slog" "net/http" "strconv" "strings" @@ -31,19 +32,19 @@ func ValidateInterviewStatusTransition(currentStatus, nextStatus string) error { return fmt.Errorf("invalid state transition: %s → %s", currentStatus, nextStatus) } -func GetPathID(r *http.Request, prefix string) (int, error) { +func GetPathID(r *http.Request, prefix string, logger *slog.Logger) (int, error) { path := strings.TrimPrefix(r.URL.Path, prefix) path = strings.Trim(path, "/") if path == "" { - log.Printf("getPathID returned empty string") - err := errors.New("Missing or invalid url param") + logger.Error("getPathID returned empty string") + err := errors.New("missing or invalid url param") return 0, err } id, err := strconv.Atoi(path) if err != nil { - log.Printf("getPathID failed: %v", err) + logger.Error("getPathID failed", "error", err) return 0, err } diff --git a/handlers/model.go b/handlers/model.go index f28abac..be887d3 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -2,6 +2,7 @@ package handlers import ( "database/sql" + "log/slog" "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/chatgpt" @@ -61,6 +62,7 @@ type Handler struct { Mailer mailer.MailerClient OpenAI chatgpt.AIClient DB *sql.DB + Logger *slog.Logger } func NewHandler( @@ -72,7 +74,8 @@ func NewHandler( billing *billing.Billing, mailer mailer.MailerClient, openAI chatgpt.AIClient, - db *sql.DB) *Handler { + db *sql.DB, + logger *slog.Logger) *Handler { return &Handler{ InterviewRepo: interviewRepo, UserRepo: userRepo, @@ -83,5 +86,6 @@ func NewHandler( Mailer: mailer, OpenAI: openAI, DB: db, + Logger: logger, } } diff --git a/internal/mocks/openai_mock.go b/internal/mocks/openai_mock.go index e16832f..93bd72f 100644 --- a/internal/mocks/openai_mock.go +++ b/internal/mocks/openai_mock.go @@ -77,6 +77,12 @@ type MockOpenAIClient struct { Scenario string } +func NewMockOpenAIClient() *MockOpenAIClient { + mockOpenAIClient := &MockOpenAIClient{} + + return mockOpenAIClient +} + func (m *MockOpenAIClient) GetChatGPTResponse(prompt string) (*chatgpt.ChatGPTResponse, error) { return responseFixtures[ScenarioInterview], nil } diff --git a/internal/server/server.go b/internal/server/server.go index d3e9ca8..ec7f53c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -23,7 +23,6 @@ type Server struct { } func NewServer(logger *slog.Logger) (*Server, error) { - logger.Info("logger is functioning in server") mux := http.NewServeMux() db, err := database.StartDB() @@ -36,15 +35,15 @@ func NewServer(logger *slog.Logger) (*Server, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAI := chatgpt.NewOpenAI() - mailer := mailer.NewMailer() - billing, err := billing.NewBilling() + openAI := chatgpt.NewOpenAI(logger) + mailer := mailer.NewMailer(logger) + billing, err := billing.NewBilling(logger) if err != nil { - log.Printf("billing.NewBilling failed: %v", err) + logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db) + handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) @@ -155,7 +154,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { return &Server{mux: mux}, nil } -func (s *Server) StartServer() { - log.Printf("Serving files from %s on port: %s\n", ".", "8080") +func (s *Server) StartServer(logger *slog.Logger) { + logger.Info("Serving files", "directory", ".", "port", "8080") log.Fatal(http.ListenAndServe(":8080", middleware.EnableCors(s.mux))) } diff --git a/internal/testutil/helpers.go b/internal/testutil/helpers.go index 652f5f4..50587d6 100644 --- a/internal/testutil/helpers.go +++ b/internal/testutil/helpers.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "io" - "log" + "log/slog" "net/http" "os" "strconv" @@ -18,7 +18,7 @@ import ( "github.com/michaelboegner/interviewer/user" ) -func CreateTestUserAndJWT() (string, int) { +func CreateTestUserAndJWT(logger *slog.Logger) (string, int) { var ( jwt string userID int @@ -30,16 +30,16 @@ func CreateTestUserAndJWT() (string, int) { verificationJWT, err := user.VerificationToken(email, username, password) if err != nil { - log.Printf("GenerateEmailVerificationToken failed: %v", err) + logger.Error("GenerateEmailVerificationToken failed", "error", err) } reqBodyUser := strings.NewReader(fmt.Sprintf(`{ "token": "%s" }`, verificationJWT)) - _, err = testRequests("", "", "POST", TestServerURL+"/api/users", reqBodyUser) + _, err = testRequests("", "", "POST", TestServerURL+"/api/users", reqBodyUser, logger) if err != nil { - log.Printf("CreateTestUserAndJWT user creation failed: %v", err) + logger.Error("CreateTestUserAndJWT user creation failed", "error", err) } //test jwt retrieve @@ -50,9 +50,9 @@ func CreateTestUserAndJWT() (string, int) { } `) - resp, err := testRequests("", "", "POST", TestServerURL+"/api/auth/login", reqBodyLogin) + resp, err := testRequests("", "", "POST", TestServerURL+"/api/auth/login", reqBodyLogin, logger) if err != nil { - log.Printf("CreateTestUserAndJWT JWT creation failed: %v", err) + logger.Error("CreateTestUserAndJWT JWT creation failed", "error", err) } returnVals := &handlers.ReturnVals{} @@ -63,18 +63,18 @@ func CreateTestUserAndJWT() (string, int) { //test userID extract userID, err = token.ExtractUserIDFromToken(jwt) if err != nil { - log.Printf("CreateTestUserandJWT userID extraction failed: %v", err) + logger.Error("CreateTestUserandJWT userID extraction failed", "error", err) } return jwt, userID } -func CreateTestInterview(jwt string) int { +func CreateTestInterview(jwt string, logger *slog.Logger) int { reqBodyInterview := strings.NewReader(`{}`) - resp, err := testRequests("Authorization", "Bearer "+jwt, "POST", TestServerURL+"/api/interviews", reqBodyInterview) + resp, err := testRequests("Authorization", "Bearer "+jwt, "POST", TestServerURL+"/api/interviews", reqBodyInterview, logger) if err != nil { - log.Printf("CreateTestUserAndJWT JWT creation failed: %v", err) + logger.Error("CreateTestUserAndJWT JWT creation failed", "error", err) return 0 } @@ -84,16 +84,16 @@ func CreateTestInterview(jwt string) int { return returnVals.InterviewID } -func CreateTestConversation(jwt string, interviewID int) int { +func CreateTestConversation(jwt string, interviewID int, logger *slog.Logger) int { reqBodyConversation := strings.NewReader(`{ "conversation_id" : 1, "message" : "T1Q1A1" }`) reqURL := TestServerURL + fmt.Sprintf("/api/conversations/create/%d", interviewID) - resp, err := testRequests("Authorization", "Bearer "+jwt, "POST", reqURL, reqBodyConversation) + resp, err := testRequests("Authorization", "Bearer "+jwt, "POST", reqURL, reqBodyConversation, logger) if err != nil { - log.Printf("CreateTestUserAndJWT JWT creation failed: %v", err) + logger.Error("CreateTestUserAndJWT JWT creation failed", "error", err) return 0 } @@ -103,7 +103,7 @@ func CreateTestConversation(jwt string, interviewID int) int { return returnVals.Conversation.ID } -func CreateTestExpiredJWT(id, expires int) string { +func CreateTestExpiredJWT(id, expires int, logger *slog.Logger) string { var token *jwt.Token jwtSecret := os.Getenv("JWT_SECRET") key := []byte(jwtSecret) @@ -124,7 +124,7 @@ func CreateTestExpiredJWT(id, expires int) string { s, err := token.SignedString(key) if err != nil { - log.Printf("SignedString failed: %s", err) + logger.Error("SignedString failed", "error", err) return "" } @@ -138,12 +138,12 @@ func TruncateAllTables(db *sql.DB) error { return err } -func testRequests(headerKey, headerValue, method, url string, reqBody *strings.Reader) ([]byte, error) { +func testRequests(headerKey, headerValue, method, url string, reqBody *strings.Reader, logger *slog.Logger) ([]byte, error) { client := &http.Client{} req, err := http.NewRequest(method, url, reqBody) if err != nil { - log.Printf("CreateTestUserAndJWT user creation failed: %v", err) + logger.Error("CreateTestUserAndJWT user creation failed", "error", err) return nil, err } req.Header.Set("Content-Type", "application/json") @@ -153,14 +153,14 @@ func testRequests(headerKey, headerValue, method, url string, reqBody *strings.R resp, err := client.Do(req) if err != nil { - log.Printf("Request failed: %v", err) + logger.Error("Request failed", "error", err) return nil, err } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - log.Printf("Reading response failed: %v", err) + logger.Error("Reading response failed", "error", err) return nil, err } diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 8d547e2..081bf42 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -1,7 +1,7 @@ package testutil import ( - "log" + "log/slog" "net/http" "net/http/httptest" @@ -22,30 +22,30 @@ var ( TestServerURL string ) -func InitTestServer() (*handlers.Handler, error) { - log.Println("Initializing test database connection...") +func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { + logger.Info("Initializing test database connection...") db, err := database.StartDB() if err != nil { - log.Fatalf("Failed to connect to test database: %v", err) + logger.Error("Failed to connect to test database", "error", err) } - log.Println("Database connected successfully.") + logger.Info("Database connected successfully.") interviewRepo := interview.NewRepository(db) userRepo := user.NewRepository(db) tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAI := &mocks.MockOpenAIClient{} + openAI := mocks.NewMockOpenAIClient() mailer := mocks.NewMockMailer() - billing, err := billing.NewBilling() + billing, err := billing.NewBilling(logger) if err != nil { - log.Printf("billing.NewBilling failed: %v", err) + logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db) + handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) @@ -154,7 +154,7 @@ func InitTestServer() (*handlers.Handler, error) { ), ) - log.Println("Starting in-memory test server...") + logger.Info("Starting in-memory test server...") TestServer = httptest.NewServer(TestMux) TestServerURL = TestServer.URL diff --git a/mailer/model.go b/mailer/model.go index 0294e70..36170c3 100644 --- a/mailer/model.go +++ b/mailer/model.go @@ -1,10 +1,14 @@ package mailer -import "os" +import ( + "log/slog" + "os" +) type Mailer struct { APIKey string BaseURL string + Logger *slog.Logger } const signature = ` @@ -19,10 +23,11 @@ const signature = `

` -func NewMailer() *Mailer { +func NewMailer(logger *slog.Logger) *Mailer { return &Mailer{ APIKey: os.Getenv("RESEND_API_KEY"), BaseURL: "https://api.resend.com", + Logger: logger, } } diff --git a/mailer/service.go b/mailer/service.go index 01327bb..70c3b6e 100644 --- a/mailer/service.go +++ b/mailer/service.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "log" "net/http" ) @@ -23,13 +22,13 @@ func (m *Mailer) SendPasswordReset(email, resetURL string) error { body, err := json.Marshal(payload) if err != nil { - log.Printf("Marshal failed: %v", err) + m.Logger.Error("Marshal failed", "error", err) return err } req, err := http.NewRequest("POST", fmt.Sprintf("%s/emails", m.BaseURL), bytes.NewBuffer(body)) if err != nil { - log.Printf("Mailer NewRequest failed: %v", err) + m.Logger.Error("Mailer NewRequest failed", "error", err) return err } req.Header.Set("Authorization", "Bearer "+m.APIKey) @@ -37,13 +36,13 @@ func (m *Mailer) SendPasswordReset(email, resetURL string) error { resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Mailer Client Do failed: %v", err) + m.Logger.Error("Mailer Client Do failed", "error", err) return err } defer resp.Body.Close() if resp.StatusCode >= 400 { - return fmt.Errorf("Resend error: %s", resp.Status) + return fmt.Errorf("resend error: %s", resp.Status) } return nil @@ -152,13 +151,13 @@ func (m *Mailer) SendDeletionConfirmation(email string) error { body, err := json.Marshal(payload) if err != nil { - log.Printf("Marshal failed: %v", err) + m.Logger.Error("Marshal failed", "error", err) return err } req, err := http.NewRequest("POST", fmt.Sprintf("%s/emails", m.BaseURL), bytes.NewBuffer(body)) if err != nil { - log.Printf("Mailer NewRequest failed: %v", err) + m.Logger.Error("Mailer NewRequest failed", "error", err) return err } req.Header.Set("Authorization", "Bearer "+m.APIKey) @@ -166,13 +165,13 @@ func (m *Mailer) SendDeletionConfirmation(email string) error { resp, err := http.DefaultClient.Do(req) if err != nil { - log.Printf("Mailer Client Do failed: %v", err) + m.Logger.Error("Mailer Client Do failed", "error", err) return err } defer resp.Body.Close() if resp.StatusCode >= 400 { - return fmt.Errorf("Resend error: %s", resp.Status) + return fmt.Errorf("resend error: %s", resp.Status) } return nil diff --git a/token/service.go b/token/service.go index 22e8bae..f1c07f5 100644 --- a/token/service.go +++ b/token/service.go @@ -97,7 +97,7 @@ func ExtractUserIDFromToken(tokenString string) (int, error) { token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(tokenString *jwt.Token) (interface{}, error) { if _, ok := tokenString.Method.(*jwt.SigningMethodHMAC); !ok { - return 0, fmt.Errorf("Unauthorized") + return 0, fmt.Errorf("unauthorized") } return []byte(jwtSecret), nil }) @@ -116,5 +116,5 @@ func ExtractUserIDFromToken(tokenString string) (int, error) { return userID, nil } - return 0, fmt.Errorf("Unauthorized") + return 0, fmt.Errorf("unauthorized") }