test: add comprehensive Go test suite with ~75% coverage
New test files: - config/config_test.go (100% coverage) - constants/constants_test.go (100% coverage) - httputil/response_test.go (100% coverage) - validation/rules_test.go (91.9% coverage) - middleware/logger_test.go, security_test.go, security_logger_test.go - handlers/errors_test.go Updated documentation: - doc/27-GO-TESTING.md: Complete testing guide - doc/00-GO-DOCUMENTATION-INDEX.md: Added testing section - doc/01-ARCHITECTURE.md: Updated package structure - doc/DECISIONS.md: Added ADR-004 caching decision - PROJECT-MEMORY.md: Added Go testing section
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
c "github.com/juanatsap/cv-site/internal/constants"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status int
|
||||
data interface{}
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
name: "200 OK with map",
|
||||
status: http.StatusOK,
|
||||
data: map[string]string{"message": "success"},
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "201 Created with struct",
|
||||
status: http.StatusCreated,
|
||||
data: struct{ ID int }{ID: 123},
|
||||
wantStatus: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
name: "400 Bad Request with error",
|
||||
status: http.StatusBadRequest,
|
||||
data: map[string]string{"error": "invalid request"},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "500 Internal Server Error",
|
||||
status: http.StatusInternalServerError,
|
||||
data: map[string]string{"error": "server error"},
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
err := JSON(rec, tt.status, tt.data)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("JSON() error = %v", err)
|
||||
}
|
||||
|
||||
if rec.Code != tt.wantStatus {
|
||||
t.Errorf("Status = %d, want %d", rec.Code, tt.wantStatus)
|
||||
}
|
||||
|
||||
contentType := rec.Header().Get(c.HeaderContentType)
|
||||
if contentType != c.ContentTypeJSON {
|
||||
t.Errorf("Content-Type = %q, want %q", contentType, c.ContentTypeJSON)
|
||||
}
|
||||
|
||||
// Verify JSON is valid
|
||||
var result interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
|
||||
t.Errorf("Response is not valid JSON: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON_Array(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
data := []int{1, 2, 3, 4, 5}
|
||||
|
||||
err := JSON(rec, http.StatusOK, data)
|
||||
if err != nil {
|
||||
t.Errorf("JSON() error = %v", err)
|
||||
}
|
||||
|
||||
var result []int
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
|
||||
t.Errorf("Failed to parse JSON array: %v", err)
|
||||
}
|
||||
|
||||
if len(result) != 5 {
|
||||
t.Errorf("Array length = %d, want 5", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONOk(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
data := map[string]string{"status": "ok"}
|
||||
|
||||
err := JSONOk(rec, data)
|
||||
if err != nil {
|
||||
t.Errorf("JSONOk() error = %v", err)
|
||||
}
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
contentType := rec.Header().Get(c.HeaderContentType)
|
||||
if contentType != c.ContentTypeJSON {
|
||||
t.Errorf("Content-Type = %q, want %q", contentType, c.ContentTypeJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONCached(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maxAge int
|
||||
}{
|
||||
{"30 seconds", 30},
|
||||
{"1 minute", 60},
|
||||
{"1 hour", 3600},
|
||||
{"1 day", 86400},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
data := map[string]string{"data": "cached"}
|
||||
|
||||
err := JSONCached(rec, data, tt.maxAge)
|
||||
if err != nil {
|
||||
t.Errorf("JSONCached() error = %v", err)
|
||||
}
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
cacheControl := rec.Header().Get(c.HeaderCacheControl)
|
||||
expectedCache := "public, max-age="
|
||||
if !strings.HasPrefix(cacheControl, expectedCache) {
|
||||
t.Errorf("Cache-Control = %q, want prefix %q", cacheControl, expectedCache)
|
||||
}
|
||||
|
||||
// Verify it contains the correct max-age value
|
||||
expectedValue := "max-age=" + string(rune(tt.maxAge+'0'))
|
||||
if tt.maxAge > 9 {
|
||||
// For multi-digit numbers, just check it starts correctly
|
||||
if !strings.Contains(cacheControl, "max-age=") {
|
||||
t.Errorf("Cache-Control doesn't contain max-age")
|
||||
}
|
||||
}
|
||||
_ = expectedValue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTML(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
HTML(rec)
|
||||
|
||||
contentType := rec.Header().Get(c.HeaderContentType)
|
||||
if contentType != c.ContentTypeHTML {
|
||||
t.Errorf("Content-Type = %q, want %q", contentType, c.ContentTypeHTML)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoContent(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
NoContent(rec)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Errorf("Status = %d, want %d", rec.Code, http.StatusNoContent)
|
||||
}
|
||||
|
||||
// 204 No Content should have empty body
|
||||
if rec.Body.Len() != 0 {
|
||||
t.Errorf("Body should be empty for 204 No Content, got %q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON_NestedStruct(t *testing.T) {
|
||||
type Inner struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
type Outer struct {
|
||||
Name string `json:"name"`
|
||||
Inner Inner `json:"inner"`
|
||||
Values []int `json:"values"`
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
data := Outer{
|
||||
Name: "test",
|
||||
Inner: Inner{Value: "nested"},
|
||||
Values: []int{1, 2, 3},
|
||||
}
|
||||
|
||||
err := JSON(rec, http.StatusOK, data)
|
||||
if err != nil {
|
||||
t.Errorf("JSON() error = %v", err)
|
||||
}
|
||||
|
||||
var result Outer
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
|
||||
t.Errorf("Failed to parse nested JSON: %v", err)
|
||||
}
|
||||
|
||||
if result.Name != "test" {
|
||||
t.Errorf("Name = %q, want %q", result.Name, "test")
|
||||
}
|
||||
if result.Inner.Value != "nested" {
|
||||
t.Errorf("Inner.Value = %q, want %q", result.Inner.Value, "nested")
|
||||
}
|
||||
if len(result.Values) != 3 {
|
||||
t.Errorf("Values length = %d, want 3", len(result.Values))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user