monorepo/cloud/maplefile-backend/pkg/httperror/rfc9457_test.go

357 lines
9.4 KiB
Go

package httperror
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewValidationError(t *testing.T) {
fieldErrors := map[string]string{
"email": "Email is required",
"password": "Password must be at least 8 characters",
}
problem := NewValidationError(fieldErrors)
if problem.Type != TypeValidationError {
t.Errorf("Expected type %s, got %s", TypeValidationError, problem.Type)
}
if problem.Status != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, problem.Status)
}
if problem.Title != "Validation Failed" {
t.Errorf("Expected title 'Validation Failed', got '%s'", problem.Title)
}
if len(problem.Errors) != 2 {
t.Errorf("Expected 2 field errors, got %d", len(problem.Errors))
}
if problem.Errors["email"] != "Email is required" {
t.Errorf("Expected email error, got '%s'", problem.Errors["email"])
}
if problem.Timestamp == "" {
t.Error("Expected timestamp to be set")
}
}
func TestNewValidationError_Empty(t *testing.T) {
problem := NewValidationError(map[string]string{})
if problem.Detail != "Validation failed." {
t.Errorf("Expected detail 'Validation failed.', got '%s'", problem.Detail)
}
}
func TestNewBadRequestError(t *testing.T) {
detail := "Invalid request payload"
problem := NewBadRequestError(detail)
if problem.Type != TypeBadRequest {
t.Errorf("Expected type %s, got %s", TypeBadRequest, problem.Type)
}
if problem.Status != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, problem.Status)
}
if problem.Detail != detail {
t.Errorf("Expected detail '%s', got '%s'", detail, problem.Detail)
}
}
func TestNewUnauthorizedError(t *testing.T) {
detail := "Invalid token"
problem := NewUnauthorizedError(detail)
if problem.Type != TypeUnauthorized {
t.Errorf("Expected type %s, got %s", TypeUnauthorized, problem.Type)
}
if problem.Status != http.StatusUnauthorized {
t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, problem.Status)
}
if problem.Detail != detail {
t.Errorf("Expected detail '%s', got '%s'", detail, problem.Detail)
}
}
func TestNewUnauthorizedError_DefaultMessage(t *testing.T) {
problem := NewUnauthorizedError("")
if problem.Detail != "Authentication is required to access this resource." {
t.Errorf("Expected default detail message, got '%s'", problem.Detail)
}
}
func TestNewForbiddenError(t *testing.T) {
detail := "Insufficient permissions"
problem := NewForbiddenError(detail)
if problem.Type != TypeForbidden {
t.Errorf("Expected type %s, got %s", TypeForbidden, problem.Type)
}
if problem.Status != http.StatusForbidden {
t.Errorf("Expected status %d, got %d", http.StatusForbidden, problem.Status)
}
}
func TestNewNotFoundError(t *testing.T) {
problem := NewNotFoundError("User")
if problem.Type != TypeNotFound {
t.Errorf("Expected type %s, got %s", TypeNotFound, problem.Type)
}
if problem.Status != http.StatusNotFound {
t.Errorf("Expected status %d, got %d", http.StatusNotFound, problem.Status)
}
if problem.Detail != "User not found." {
t.Errorf("Expected detail 'User not found.', got '%s'", problem.Detail)
}
}
func TestNewConflictError(t *testing.T) {
detail := "Email already exists"
problem := NewConflictError(detail)
if problem.Type != TypeConflict {
t.Errorf("Expected type %s, got %s", TypeConflict, problem.Type)
}
if problem.Status != http.StatusConflict {
t.Errorf("Expected status %d, got %d", http.StatusConflict, problem.Status)
}
}
func TestNewInternalServerError(t *testing.T) {
detail := "Database connection failed"
problem := NewInternalServerError(detail)
if problem.Type != TypeInternalError {
t.Errorf("Expected type %s, got %s", TypeInternalError, problem.Type)
}
if problem.Status != http.StatusInternalServerError {
t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, problem.Status)
}
}
func TestNewServiceUnavailableError(t *testing.T) {
problem := NewServiceUnavailableError("")
if problem.Type != TypeServiceUnavailable {
t.Errorf("Expected type %s, got %s", TypeServiceUnavailable, problem.Type)
}
if problem.Status != http.StatusServiceUnavailable {
t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, problem.Status)
}
}
func TestWithInstance(t *testing.T) {
problem := NewBadRequestError("Test")
instance := "/api/v1/test"
problem.WithInstance(instance)
if problem.Instance != instance {
t.Errorf("Expected instance '%s', got '%s'", instance, problem.Instance)
}
}
func TestWithTraceID(t *testing.T) {
problem := NewBadRequestError("Test")
traceID := "trace-123"
problem.WithTraceID(traceID)
if problem.TraceID != traceID {
t.Errorf("Expected traceID '%s', got '%s'", traceID, problem.TraceID)
}
}
func TestWithError(t *testing.T) {
problem := NewBadRequestError("Test")
problem.WithError("email", "Email is required")
problem.WithError("password", "Password is required")
if len(problem.Errors) != 2 {
t.Errorf("Expected 2 errors, got %d", len(problem.Errors))
}
if problem.Errors["email"] != "Email is required" {
t.Errorf("Expected email error, got '%s'", problem.Errors["email"])
}
}
func TestProblemDetailError(t *testing.T) {
detail := "Test detail"
problem := NewBadRequestError(detail)
if problem.Error() != detail {
t.Errorf("Expected Error() to return detail, got '%s'", problem.Error())
}
// Test with no detail
problem2 := &ProblemDetail{
Title: "Test Title",
}
if problem2.Error() != "Test Title" {
t.Errorf("Expected Error() to return title, got '%s'", problem2.Error())
}
}
func TestExtractRequestID_FromContext(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
// Note: In real code, request ID would be set by middleware
// For testing, we'll test the empty case
requestID := ExtractRequestID(req)
// Should return empty string when no request ID is present
if requestID != "" {
t.Errorf("Expected empty string, got '%s'", requestID)
}
}
func TestExtractRequestID_FromHeader(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("X-Request-ID", "test-request-123")
requestID := ExtractRequestID(req)
if requestID != "test-request-123" {
t.Errorf("Expected 'test-request-123', got '%s'", requestID)
}
}
func TestRespondWithProblem(t *testing.T) {
problem := NewValidationError(map[string]string{
"email": "Email is required",
})
problem.WithInstance("/api/v1/test")
problem.WithTraceID("trace-123")
w := httptest.NewRecorder()
RespondWithProblem(w, problem)
// Check status code
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
}
// Check content type
contentType := w.Header().Get("Content-Type")
if contentType != "application/problem+json" {
t.Errorf("Expected Content-Type 'application/problem+json', got '%s'", contentType)
}
// Check JSON response
var response ProblemDetail
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
if response.Type != TypeValidationError {
t.Errorf("Expected type %s, got %s", TypeValidationError, response.Type)
}
if response.Instance != "/api/v1/test" {
t.Errorf("Expected instance '/api/v1/test', got '%s'", response.Instance)
}
if response.TraceID != "trace-123" {
t.Errorf("Expected traceID 'trace-123', got '%s'", response.TraceID)
}
if len(response.Errors) != 1 {
t.Errorf("Expected 1 error, got %d", len(response.Errors))
}
}
func TestRespondWithError_ProblemDetail(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/test", nil)
w := httptest.NewRecorder()
problem := NewBadRequestError("Test error")
RespondWithError(w, req, problem)
// Check status code
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
}
// Check that instance was set
var response ProblemDetail
json.NewDecoder(w.Body).Decode(&response)
if response.Instance != "/api/v1/test" {
t.Errorf("Expected instance to be set automatically, got '%s'", response.Instance)
}
}
func TestRespondWithError_StandardError(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/test", nil)
w := httptest.NewRecorder()
err := &customError{message: "Custom error"}
RespondWithError(w, req, err)
// Check status code (should be 500 for standard errors)
if w.Code != http.StatusInternalServerError {
t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, w.Code)
}
// Check that it was wrapped in a ProblemDetail
var response ProblemDetail
json.NewDecoder(w.Body).Decode(&response)
if response.Type != TypeInternalError {
t.Errorf("Expected type %s, got %s", TypeInternalError, response.Type)
}
if response.Detail != "Custom error" {
t.Errorf("Expected detail 'Custom error', got '%s'", response.Detail)
}
}
// Helper type for testing standard error handling
type customError struct {
message string
}
func (e *customError) Error() string {
return e.message
}
func TestChaining(t *testing.T) {
// Test method chaining
problem := NewBadRequestError("Test").
WithInstance("/api/v1/test").
WithTraceID("trace-123").
WithError("field1", "error1").
WithError("field2", "error2")
if problem.Instance != "/api/v1/test" {
t.Error("Instance not set correctly through chaining")
}
if problem.TraceID != "trace-123" {
t.Error("TraceID not set correctly through chaining")
}
if len(problem.Errors) != 2 {
t.Error("Errors not set correctly through chaining")
}
}