diff --git a/apps/question-service/go.mod b/apps/question-service/go.mod index 481f8efbb7..0b9136c217 100644 --- a/apps/question-service/go.mod +++ b/apps/question-service/go.mod @@ -10,7 +10,16 @@ require ( google.golang.org/grpc v1.67.1 ) -require github.com/joho/godotenv v1.5.1 +require ( + github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) require ( cloud.google.com/go v0.115.1 // indirect diff --git a/apps/question-service/go.sum b/apps/question-service/go.sum index 02ba516f91..0e6b4278c5 100644 --- a/apps/question-service/go.sum +++ b/apps/question-service/go.sum @@ -203,6 +203,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= diff --git a/apps/question-service/tests/common_test.go b/apps/question-service/tests/common_test.go new file mode 100644 index 0000000000..948982b12f --- /dev/null +++ b/apps/question-service/tests/common_test.go @@ -0,0 +1,65 @@ +package tests + +import ( + "context" + "log" + "os" + "question-service/handlers" + "question-service/utils" + "testing" + + "cloud.google.com/go/firestore" +) + +var service *handlers.Service +var ctx = context.Background() + +func TestMain(m *testing.M) { + // Set FIRESTORE_EMULATOR_HOST environment variable. + err := os.Setenv("FIRESTORE_EMULATOR_HOST", "127.0.0.1:8080") + if err != nil { + log.Fatalf("could not set env %v", err) + } + // Create client. + client, err := firestore.NewClient(ctx, "my-project-id") + service = &handlers.Service{Client: client} + + if err != nil { + log.Fatalf("could not create client %v", err) + } + defer client.Close() + + m.Run() + os.Exit(0) +} + +// Sets up the firestore emulator with the sample questions +// This repopulates the db +// Returns the docref of one of the questions if a test need it +func setupDb(t *testing.T) string { + // Repopulate document + utils.Populate(service.Client, false) + + coll := service.Client.Collection("questions") + if coll == nil { + t.Fatalf("Failed to get CollectionRef") + } + docRef, err := coll.DocumentRefs(ctx).Next() + if err != nil { + t.Fatalf("Failed to get DocRef: %v", err) + } + return docRef.ID +} + +func getCount(t *testing.T) int64 { + counterDocRef, err := service.Client.Collection("counters").Doc("questions").Get(context.Background()) + if err != nil { + t.Fatal(err) + } + fields := counterDocRef.Data() + if err != nil { + t.Fatal(err) + } + count := fields["count"].(int64) + return count +} diff --git a/apps/question-service/tests/create_test.go b/apps/question-service/tests/create_test.go new file mode 100644 index 0000000000..b959993a3a --- /dev/null +++ b/apps/question-service/tests/create_test.go @@ -0,0 +1,144 @@ +package tests + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "question-service/models" + + "github.com/stretchr/testify/assert" +) + +// tests partially generated using Github Copilot + +func createCreateRequestWithData(_ *testing.T, body []byte) *http.Request { + req := httptest.NewRequest(http.MethodPost, "http://localhost:12345/questions", bytes.NewBuffer(body)) + + return req +} + +func TestCreateQuestion(t *testing.T) { + t.Run("Create new question", func(t *testing.T) { + var err error + + newQuestion := models.Question{ + Title: "New Question", + Description: "New Description", + Complexity: models.Medium, + Categories: []string{"Category1"}, + + DocRefID: "a-doc-ref-id", + } + + setupDb(t) + beforeCount := getCount(t) + + w := httptest.NewRecorder() + data, err := json.Marshal(newQuestion) + assert.NoError(t, err) + req := createCreateRequestWithData(t, data) + service.CreateQuestion(w, req) + afterCount := getCount(t) + // Check response + assert.Equal(t, http.StatusOK, w.Code) + var response models.Question + err = json.NewDecoder(w.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, newQuestion.Title, response.Title) + assert.Equal(t, newQuestion.Description, response.Description) + assert.Equal(t, newQuestion.Complexity, response.Complexity) + assert.Equal(t, newQuestion.Categories, response.Categories) + assert.Equal(t, beforeCount+1, afterCount) + }) + + t.Run("Create question with missing title", func(t *testing.T) { + newQuestion := models.Question{ + Description: "New Description", + Complexity: models.Medium, + Categories: []string{"Category1"}, + } + + setupDb(t) + beforeCount := getCount(t) + + w := httptest.NewRecorder() + data, _ := json.Marshal(newQuestion) + req := createCreateRequestWithData(t, data) + service.CreateQuestion(w, req) + + // Check response + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "Title is required") + assert.Equal(t, beforeCount, getCount(t)) + }) + + t.Run("Create question with duplicate title", func(t *testing.T) { + newQuestion := models.Question{ + Title: "Duplicate Title", + Description: "New Description", + Complexity: models.Medium, + Categories: []string{"Category1"}, + } + + setupDb(t) + + // Create the first question + w := httptest.NewRecorder() + data, _ := json.Marshal(newQuestion) + req := createCreateRequestWithData(t, data) + service.CreateQuestion(w, req) + assert.Equal(t, http.StatusOK, w.Code) + + // Try to create the second question with the same title + w = httptest.NewRecorder() + req = createCreateRequestWithData(t, data) + service.CreateQuestion(w, req) + + // Check response + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "Question title already exists") + }) + + t.Run("Create question with empty description", func(t *testing.T) { + newQuestion := models.Question{ + Title: "New Question", + Description: "", + Complexity: models.Medium, + Categories: []string{"Category1"}, + } + + setupDb(t) + + w := httptest.NewRecorder() + data, _ := json.Marshal(newQuestion) + req := createCreateRequestWithData(t, data) + service.CreateQuestion(w, req) + + // Check response + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "Description is required") + }) + + t.Run("Create question with nil title", func(t *testing.T) { + newQuestion := models.Question{ + // Title: "New Question", + Description: "New Description", + Complexity: models.Medium, + Categories: []string{"Category1"}, + } + + setupDb(t) + + w := httptest.NewRecorder() + data, _ := json.Marshal(newQuestion) + req := createCreateRequestWithData(t, data) + service.CreateQuestion(w, req) + + // Check response + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "Title is required") + }) +} diff --git a/apps/question-service/tests/delete_test.go b/apps/question-service/tests/delete_test.go new file mode 100644 index 0000000000..98da9a50d3 --- /dev/null +++ b/apps/question-service/tests/delete_test.go @@ -0,0 +1,47 @@ +package tests + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +// tests partially generated using Github Copilot + +func createDeleteRequestWithId(docRefID string) *http.Request { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("docRefID", docRefID) + + req := httptest.NewRequest(http.MethodDelete, "/questions/"+docRefID, nil) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + return req +} + +func TestDeleteQuestion(t *testing.T) { + + t.Run("Delete existing question", func(t *testing.T) { + docRefID := setupDb(t) + req := createDeleteRequestWithId(docRefID) + res := httptest.NewRecorder() + + service.DeleteQuestion(res, req) + + assert.Equal(t, http.StatusOK, res.Code) + assert.Equal(t, res.Body.String(), "Question with ID "+docRefID+" deleted successfully") + }) + + t.Run("Delete non-existing question", func(t *testing.T) { + nonExistentDocRefID := "non-existent-id" + req := createDeleteRequestWithId(nonExistentDocRefID) + res := httptest.NewRecorder() + + service.DeleteQuestion(res, req) + + assert.Equal(t, http.StatusNotFound, res.Code) + assert.Equal(t, res.Body.String(), "Question not found\n") + }) +} diff --git a/apps/question-service/tests/read_test.go b/apps/question-service/tests/read_test.go index 769cb1ded9..8018d16b0e 100644 --- a/apps/question-service/tests/read_test.go +++ b/apps/question-service/tests/read_test.go @@ -2,60 +2,15 @@ package tests import ( "context" - "log" "net/http" "net/http/httptest" - "os" - "question-service/handlers" - "question-service/utils" "strings" "testing" - "cloud.google.com/go/firestore" "github.com/go-chi/chi/v5" ) -var service *handlers.Service -var ctx = context.Background() - -func TestMain(m *testing.M) { - // Set FIRESTORE_EMULATOR_HOST environment variable. - err := os.Setenv("FIRESTORE_EMULATOR_HOST", "127.0.0.1:8080") - if err != nil { - log.Fatalf("could not set env %v", err) - } - // Create client. - client, err := firestore.NewClient(ctx, "my-project-id") - service = &handlers.Service{Client: client} - - if err != nil { - log.Fatalf("could not create client %v", err) - } - defer client.Close() - - m.Run() - os.Exit(0) -} - -// Sets up the firestore emulator with the sample questions -// This repopulates the db -// Returns the docref of one of the questions if a test need it -func setupDb(t *testing.T) string { - // Repopulate document - utils.Populate(service.Client, false) - - coll := service.Client.Collection("questions") - if coll == nil { - t.Fatalf("Failed to get CollectionRef") - } - docRef, err := coll.DocumentRefs(ctx).Next() - if err != nil { - t.Fatalf("Failed to get DocRef: %v", err) - } - return docRef.ID -} - -func ReadRequestWithId(id string) *http.Request { +func readRequestWithId(id string) *http.Request { // adds chi context // https://stackoverflow.com/questions/54580582/testing-chi-routes-w-path-variables rctx := chi.NewRouteContext() @@ -69,7 +24,7 @@ func Test_Read(t *testing.T) { id := setupDb(t) res := httptest.NewRecorder() - req := ReadRequestWithId(id) + req := readRequestWithId(id) service.ReadQuestion(res, req) @@ -82,7 +37,7 @@ func Test_ReadNotFound(t *testing.T) { setupDb(t) res := httptest.NewRecorder() - req := ReadRequestWithId("invalid-docref") + req := readRequestWithId("invalid-docref") service.ReadQuestion(res, req) diff --git a/apps/question-service/tests/update_test.go b/apps/question-service/tests/update_test.go new file mode 100644 index 0000000000..d228607804 --- /dev/null +++ b/apps/question-service/tests/update_test.go @@ -0,0 +1,88 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "question-service/models" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +func createUpdateRequestWithIdAndData(_ *testing.T, id string, body []byte) *http.Request { + // adds chi context + // https://stackoverflow.com/questions/54580582/testing-chi-routes-w-path-variables + rctx := chi.NewRouteContext() + rctx.URLParams.Add("docRefID", id) + + req := httptest.NewRequest(http.MethodPut, "http://localhost:12345/questions/"+id, bytes.NewBuffer(body)) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + return req +} +func TestUpdateQuestion(t *testing.T) { + t.Run("Update existing question", func(t *testing.T) { + var err error + + expected := models.Question{ + Title: "Updated Title", + Description: "Updated Description", + Complexity: models.Medium, + Categories: []string{"Category2"}, + } + + id := setupDb(t) + + w := httptest.NewRecorder() + data, err := json.Marshal(expected) + assert.NoError(t, err) + req := createUpdateRequestWithIdAndData(t, id, data) + service.UpdateQuestion(w, req) + + // Check response + assert.Equal(t, http.StatusOK, w.Code) + var response models.Question + err = json.NewDecoder(w.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, expected.Title, response.Title) + assert.Equal(t, expected.Description, response.Description) + assert.Equal(t, expected.Complexity, response.Complexity) + assert.Equal(t, expected.Categories, response.Categories) + }) + t.Run("Update non-existing question", func(t *testing.T) { + // Prepare update data + updatedQuestion := models.Question{ + Title: "Updated Title", + Description: "Updated Description", + Complexity: models.Medium, + Categories: []string{"Category2"}, + } + body, _ := json.Marshal(updatedQuestion) + req := createUpdateRequestWithIdAndData(t, "non-existing-id", body) + w := httptest.NewRecorder() + + // Call UpdateQuestion handler + service.UpdateQuestion(w, req) + + // Check response + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "Question not found") + }) + + t.Run("Invalid request body", func(t *testing.T) { + req := createUpdateRequestWithIdAndData(t, "some-id", []byte("invalid body")) + w := httptest.NewRecorder() + + // Call UpdateQuestion handler + service.UpdateQuestion(w, req) + + t.Log(w) + // Check response + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Equal(t, []byte("Invalid request payload: invalid character 'i' looking for beginning of value\n"), w.Body.Bytes()) + }) +}