diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f8d012 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +userprofile-api + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/controllers/user_controller_test.go b/controllers/user_controller_test.go new file mode 100644 index 0000000..e227ebd --- /dev/null +++ b/controllers/user_controller_test.go @@ -0,0 +1,399 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "userprofile-api/models" +) + +// setupTestRouter creates a new Gin router for testing +func setupTestRouter() *gin.Engine { + gin.SetMode(gin.TestMode) + router := gin.Default() + return router +} + +// resetUsers resets the users slice to a known state for testing +func resetUsers() { + users = []models.UserProfile{ + {ID: "1", FullName: "John Doe", Emoji: "😀"}, + {ID: "2", FullName: "Jane Smith", Emoji: "🚀"}, + {ID: "3", FullName: "Robert Johnson", Emoji: "🎸"}, + } +} + +// TestHomePageHandler tests the HomePageHandler function +func TestHomePageHandler(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.LoadHTMLGlob("../templates/*") + router.GET("/", HomePageHandler) + + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) + } + + // Check that the response contains HTML + if w.Header().Get("Content-Type") != "text/html; charset=utf-8" { + t.Errorf("Expected Content-Type text/html; charset=utf-8, got %s", w.Header().Get("Content-Type")) + } + + // Check that the response body contains some expected content + body := w.Body.String() + if len(body) == 0 { + t.Error("Expected non-empty response body") + } +} + +// TestGetUsers tests the GetUsers function +func TestGetUsers(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.GET("/api/v1/users", GetUsers) + + req, err := http.NewRequest("GET", "/api/v1/users", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) + } + + var result []models.UserProfile + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != 3 { + t.Errorf("Expected 3 users, got %d", len(result)) + } + + // Verify the first user + if result[0].ID != "1" || result[0].FullName != "John Doe" { + t.Errorf("Expected first user to be John Doe with ID 1, got %v", result[0]) + } +} + +// TestGetUserSuccess tests the GetUser function with a valid ID +func TestGetUserSuccess(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.GET("/api/v1/users/:id", GetUser) + + req, err := http.NewRequest("GET", "/api/v1/users/2", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) + } + + var result models.UserProfile + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result.ID != "2" { + t.Errorf("Expected user ID 2, got %s", result.ID) + } + + if result.FullName != "Jane Smith" { + t.Errorf("Expected user FullName 'Jane Smith', got %s", result.FullName) + } + + if result.Emoji != "🚀" { + t.Errorf("Expected user Emoji '🚀', got %s", result.Emoji) + } +} + +// TestGetUserNotFound tests the GetUser function with a non-existent ID +func TestGetUserNotFound(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.GET("/api/v1/users/:id", GetUser) + + req, err := http.NewRequest("GET", "/api/v1/users/999", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d, got %d", http.StatusNotFound, w.Code) + } + + var result map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result["error"] != "User not found" { + t.Errorf("Expected error message 'User not found', got %s", result["error"]) + } +} + +// TestCreateUserSuccess tests the CreateUser function with valid input +func TestCreateUserSuccess(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.POST("/api/v1/users", CreateUser) + + newUser := models.UserProfile{ + ID: "4", + FullName: "Alice Johnson", + Emoji: "🌟", + } + + jsonData, err := json.Marshal(newUser) + if err != nil { + t.Fatalf("Failed to marshal user: %v", err) + } + + req, err := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Errorf("Expected status code %d, got %d", http.StatusCreated, w.Code) + } + + var result models.UserProfile + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result.ID != "4" { + t.Errorf("Expected user ID 4, got %s", result.ID) + } + + if result.FullName != "Alice Johnson" { + t.Errorf("Expected user FullName 'Alice Johnson', got %s", result.FullName) + } + + if result.Emoji != "🌟" { + t.Errorf("Expected user Emoji '🌟', got %s", result.Emoji) + } + + // Verify the user was added to the users slice + if len(users) != 4 { + t.Errorf("Expected 4 users after creation, got %d", len(users)) + } +} + +// TestCreateUserInvalidJSON tests the CreateUser function with invalid JSON +func TestCreateUserInvalidJSON(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.POST("/api/v1/users", CreateUser) + + invalidJSON := []byte(`{"id": "4", "fullName": "Alice Johnson", "emoji":`) + + req, err := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(invalidJSON)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code) + } + + var result map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result["error"] == "" { + t.Error("Expected an error message, got empty string") + } + + // Verify the users slice was not modified + if len(users) != 3 { + t.Errorf("Expected 3 users after invalid creation, got %d", len(users)) + } +} + +// TestUpdateUserSuccess tests the UpdateUser function with valid input +func TestUpdateUserSuccess(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.PUT("/api/v1/users/:id", UpdateUser) + + updatedUser := models.UserProfile{ + ID: "2", + FullName: "Jane Doe", + Emoji: "✨", + } + + jsonData, err := json.Marshal(updatedUser) + if err != nil { + t.Fatalf("Failed to marshal user: %v", err) + } + + req, err := http.NewRequest("PUT", "/api/v1/users/2", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) + } + + var result models.UserProfile + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result.ID != "2" { + t.Errorf("Expected user ID 2, got %s", result.ID) + } + + if result.FullName != "Jane Doe" { + t.Errorf("Expected user FullName 'Jane Doe', got %s", result.FullName) + } + + if result.Emoji != "✨" { + t.Errorf("Expected user Emoji '✨', got %s", result.Emoji) + } + + // Verify the user was updated in the users slice + found := false + for _, user := range users { + if user.ID == "2" { + if user.FullName != "Jane Doe" || user.Emoji != "✨" { + t.Errorf("User was not properly updated in the users slice") + } + found = true + break + } + } + + if !found { + t.Error("Updated user not found in users slice") + } +} + +// TestUpdateUserNotFound tests the UpdateUser function with a non-existent ID +func TestUpdateUserNotFound(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.PUT("/api/v1/users/:id", UpdateUser) + + updatedUser := models.UserProfile{ + ID: "999", + FullName: "Non Existent", + Emoji: "❌", + } + + jsonData, err := json.Marshal(updatedUser) + if err != nil { + t.Fatalf("Failed to marshal user: %v", err) + } + + req, err := http.NewRequest("PUT", "/api/v1/users/999", bytes.NewBuffer(jsonData)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("Expected status code %d, got %d", http.StatusNotFound, w.Code) + } + + var result map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result["error"] != "User not found" { + t.Errorf("Expected error message 'User not found', got %s", result["error"]) + } +} + +// TestUpdateUserInvalidJSON tests the UpdateUser function with invalid JSON +func TestUpdateUserInvalidJSON(t *testing.T) { + resetUsers() + + router := setupTestRouter() + router.PUT("/api/v1/users/:id", UpdateUser) + + invalidJSON := []byte(`{"id": "2", "fullName": "Jane Doe", "emoji":`) + + req, err := http.NewRequest("PUT", "/api/v1/users/2", bytes.NewBuffer(invalidJSON)) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code) + } + + var result map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result["error"] == "" { + t.Error("Expected an error message, got empty string") + } + + // Verify the user was not modified + for _, user := range users { + if user.ID == "2" { + if user.FullName != "Jane Smith" || user.Emoji != "🚀" { + t.Errorf("User should not have been modified") + } + break + } + } +} diff --git a/go.mod b/go.mod index 12fb3df..9839098 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,14 @@ module userprofile-api go 1.24.2 +require github.com/gin-gonic/gin v1.10.0 + require ( github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect diff --git a/go.sum b/go.sum index 2ce8cf0..37ffc5c 100644 --- a/go.sum +++ b/go.sum @@ -5,9 +5,9 @@ github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCN github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= @@ -15,6 +15,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -23,6 +25,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -41,6 +45,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -50,6 +55,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -65,11 +72,13 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=