From 55a424b3d0fd4aa0f3dc54758c9493aff4de648e Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 12 Nov 2024 18:15:28 +0800 Subject: [PATCH 01/16] Add create test --- apps/execution-service/go.mod | 2 +- apps/execution-service/go.sum | 2 + apps/execution-service/handlers/create.go | 90 +++++++++++++++++++++++ apps/execution-service/main.go | 2 +- apps/execution-service/utils/populate.go | 6 ++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 apps/execution-service/handlers/create.go diff --git a/apps/execution-service/go.mod b/apps/execution-service/go.mod index aff484d2a7..fa974a5550 100644 --- a/apps/execution-service/go.mod +++ b/apps/execution-service/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 github.com/joho/godotenv v1.5.1 + github.com/rabbitmq/amqp091-go v1.10.0 github.com/traefik/yaegi v0.16.1 google.golang.org/api v0.203.0 ) @@ -31,7 +32,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect - github.com/rabbitmq/amqp091-go v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect diff --git a/apps/execution-service/go.sum b/apps/execution-service/go.sum index 8a595690fa..4733d88abb 100644 --- a/apps/execution-service/go.sum +++ b/apps/execution-service/go.sum @@ -111,6 +111,8 @@ go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHy go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= diff --git a/apps/execution-service/handlers/create.go b/apps/execution-service/handlers/create.go new file mode 100644 index 0000000000..5b9839fc04 --- /dev/null +++ b/apps/execution-service/handlers/create.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "execution-service/models" + "execution-service/utils" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var test models.Test + if err := utils.DecodeJSONBody(w, r, &test); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Basic validation for question title and question ID + if test.QuestionDocRefId == "" || test.QuestionTitle == "" { + http.Error(w, "QuestionDocRefId and QuestionTitle are required", http.StatusBadRequest) + return + } + + // Automatically populate validation for input and output in test case + test.InputValidation = utils.GetDefaultValidation() + test.OutputValidation = utils.GetDefaultValidation() + + // Validate test case format + if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, test.InputValidation, + test.OutputValidation); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, err := utils.ValidateTestCaseFormat(test.HiddenTestCases, test.InputValidation, + test.OutputValidation); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Save test to Firestore + docRef, _, err := s.Client.Collection("tests").Add(ctx, map[string]interface{}{ + "questionDocRefId": test.QuestionDocRefId, + "questionTitle": test.QuestionTitle, + "visibleTestCases": test.VisibleTestCases, + "hiddenTestCases": test.HiddenTestCases, + "inputValidation": test.InputValidation, + "outputValidation": test.OutputValidation, + "createdAt": firestore.ServerTimestamp, + "updatedAt": firestore.ServerTimestamp, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "Test not found", http.StatusInternalServerError) + return + } + http.Error(w, "Failed to get test", http.StatusInternalServerError) + return + } + + // Map data + if err := doc.DataTo(&test); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(test) +} + +// Manual test cases + +//curl -X POST http://localhost:8083/tests \ +//-H "Content-Type: application/json" \ +//-d '{ +//"questionDocRefId": "sampleDocRefId123", +//"questionTitle": "Sample Question Title", +//"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +//"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +//}' diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go index e2da2948be..1173bfe2ff 100644 --- a/apps/execution-service/main.go +++ b/apps/execution-service/main.go @@ -83,7 +83,7 @@ func registerRoutes(r *chi.Mux, service *handlers.Service) { // Re: CreateTest // Current: Unused, since testcases are populated via script // Future extension: can be created by admin - //r.Post("/", service.CreateTest) + r.Post("/", service.CreateTest) r.Post("/populate", service.PopulateTests) r.Route("/{questionDocRefId}", func(r chi.Router) { diff --git a/apps/execution-service/utils/populate.go b/apps/execution-service/utils/populate.go index fc23098712..767b565af2 100644 --- a/apps/execution-service/utils/populate.go +++ b/apps/execution-service/utils/populate.go @@ -673,3 +673,9 @@ func validateInputOrOutput(inputOrOutput string) bool { } `, importCode.String(), functionBody) } + +func GetDefaultValidation() string { + return getPackagesAndFunction([]string{}, ` +return len(inputOrOutput) > 0 +`) +} From 60966abe1ff67733dd77e45fef4130677fb309a3 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Tue, 12 Nov 2024 18:21:35 +0800 Subject: [PATCH 02/16] Update readme for create test --- apps/execution-service/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index c53dc3ab9f..68d4031e37 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -106,6 +106,21 @@ The following json format will be returned: ] ``` +`POST /tests` + +To create a new test case, run the following command: + +```bash +curl -X POST http://localhost:8083/tests \ +-H "Content-Type: application/json" \ +-d '{ +"questionDocRefId": "sampleDocRefId123", +"questionTitle": "Sample Question Title", +"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +}' +``` + `POST /tests/{questionDocRefId}/execute` To execute test cases via a question ID without custom test cases, run the following command, with custom code and language: From b3e1d6100dcf94451362a4c300eb3a8eeec84ce6 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Wed, 13 Nov 2024 10:23:44 +0800 Subject: [PATCH 03/16] Add update test --- apps/execution-service/README.md | 15 ++++ apps/execution-service/handlers/create.go | 15 ++++ apps/execution-service/handlers/update.go | 92 +++++++++++++++++++++++ apps/execution-service/main.go | 2 +- 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 apps/execution-service/handlers/update.go diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index 68d4031e37..95b4302d05 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -121,6 +121,21 @@ curl -X POST http://localhost:8083/tests \ }' ``` +`PUT /tests/{questionDocRefId}` + +To update an existing test case from an existing question, run the following command: + +```bash +curl -X PUT http://localhost:8083/tests/{questionDocRefId} \ +-H "Content-Type: application/json" \ +-d '{ +"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +}' +``` + +```bash + `POST /tests/{questionDocRefId}/execute` To execute test cases via a question ID without custom test cases, run the following command, with custom code and language: diff --git a/apps/execution-service/handlers/create.go b/apps/execution-service/handlers/create.go index 5b9839fc04..a81422b898 100644 --- a/apps/execution-service/handlers/create.go +++ b/apps/execution-service/handlers/create.go @@ -40,6 +40,21 @@ func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) { return } + // Check if a test already exists for the question + iter := s.Client.Collection("tests").Where("questionDocRefId", "==", test.QuestionDocRefId).Documents(ctx) + for { + _, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + http.Error(w, "Error fetching test", http.StatusInternalServerError) + return + } + http.Error(w, "Test already exists for the question", http.StatusConflict) + return + } + // Save test to Firestore docRef, _, err := s.Client.Collection("tests").Add(ctx, map[string]interface{}{ "questionDocRefId": test.QuestionDocRefId, diff --git a/apps/execution-service/handlers/update.go b/apps/execution-service/handlers/update.go new file mode 100644 index 0000000000..1636456f96 --- /dev/null +++ b/apps/execution-service/handlers/update.go @@ -0,0 +1,92 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "execution-service/models" + "execution-service/utils" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) UpdateTest(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // get param questionDocRefId + questionDocRefId := chi.URLParam(r, "questionDocRefId") + + var test models.Test + if err := utils.DecodeJSONBody(w, r, &test); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Only test cases will be updated + // Validate test case format with default validation + if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, utils.GetDefaultValidation(), + utils.GetDefaultValidation()); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, err := utils.ValidateTestCaseFormat(test.HiddenTestCases, utils.GetDefaultValidation(), + utils.GetDefaultValidation()); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Update test in Firestore + docRef := s.Client.Collection("tests").Where("questionDocRefId", "==", questionDocRefId).Limit(1).Documents(ctx) + doc, err := docRef.Next() + if err != nil { + if err == iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer docRef.Stop() + + // Update database + updates := []firestore.Update{ + {Path: "visibleTestCases", Value: test.VisibleTestCases}, + {Path: "hiddenTestCases", Value: test.HiddenTestCases}, + {Path: "updatedAt", Value: firestore.ServerTimestamp}, + } + _, err = doc.Ref.Update(ctx, updates) + if err != nil { + http.Error(w, "Error updating test", http.StatusInternalServerError) + return + } + + // Get data + doc, err = doc.Ref.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get test", http.StatusInternalServerError) + return + } + + // Map data + if err = doc.DataTo(&test); err != nil { + http.Error(w, "Failed to map test data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(test) +} + +// Manual test cases + +//curl -X PUT http://localhost:8083/tests/sampleDocRefId123 \ +//-H "Content-Type: application/json" \ +//-d '{ +//"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", +//"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" +//}' diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go index 1173bfe2ff..3b79fbf527 100644 --- a/apps/execution-service/main.go +++ b/apps/execution-service/main.go @@ -90,7 +90,7 @@ func registerRoutes(r *chi.Mux, service *handlers.Service) { // Re: UpdateTest, DeleteTest // Current: Unused, since testcases are executed within service and not exposed // Future extension: can be read by admin to view testcases - //r.Put("/", service.UpdateTest) + r.Put("/", service.UpdateTest) //r.Delete("/", service.DeleteTest) r.Get("/", service.ReadVisibleTests) r.Post("/execute", service.ExecuteVisibleAndCustomTests) From 8a31a6f39eb75f57a68e97b3b182407541d1ebf9 Mon Sep 17 00:00:00 2001 From: solomonng2001 Date: Wed, 13 Nov 2024 11:06:38 +0800 Subject: [PATCH 04/16] Add delete and update tests --- apps/execution-service/README.md | 7 ++++ apps/execution-service/handlers/create.go | 14 +++++++ apps/execution-service/handlers/delete.go | 40 +++++++++++++++++++ apps/execution-service/handlers/update.go | 4 ++ apps/execution-service/main.go | 2 +- .../utils/validateTestCaseFormat.go | 4 ++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 apps/execution-service/handlers/delete.go diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index 95b4302d05..d99ba7b92b 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -134,7 +134,14 @@ curl -X PUT http://localhost:8083/tests/{questionDocRefId} \ }' ``` +`DELETE /tests/{questionDocRefId}` + +To delete an existing test case from an existing question, run the following command: + ```bash +curl -X DELETE http://localhost:8083/tests/{questionDocRefId} \ +-H "Content-Type: application/json" +``` `POST /tests/{questionDocRefId}/execute` diff --git a/apps/execution-service/handlers/create.go b/apps/execution-service/handlers/create.go index a81422b898..6a0d9d0324 100644 --- a/apps/execution-service/handlers/create.go +++ b/apps/execution-service/handlers/create.go @@ -24,6 +24,10 @@ func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) { return } + // Normalise test cases + test.VisibleTestCases = utils.NormaliseTestCaseFormat(test.VisibleTestCases) + test.HiddenTestCases = utils.NormaliseTestCaseFormat(test.HiddenTestCases) + // Automatically populate validation for input and output in test case test.InputValidation = utils.GetDefaultValidation() test.OutputValidation = utils.GetDefaultValidation() @@ -54,6 +58,7 @@ func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) { http.Error(w, "Test already exists for the question", http.StatusConflict) return } + defer iter.Stop() // Save test to Firestore docRef, _, err := s.Client.Collection("tests").Add(ctx, map[string]interface{}{ @@ -103,3 +108,12 @@ func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) { //"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH", //"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba" //}' + +//curl -X POST http://localhost:8083/tests \ +//-H "Content-Type: application/json" \ +//-d "{ +//\"questionDocRefId\": \"sampleDocRefId12345\", +//\"questionTitle\": \"Sample Question Title\", +//\"visibleTestCases\": \"2\\nhello\\nolleh\\nHannah\\nhannaH\", +//\"hiddenTestCases\": \"2\\nHannah\\nhannaH\\nabcdefg\\ngfedcba\" +//}" diff --git a/apps/execution-service/handlers/delete.go b/apps/execution-service/handlers/delete.go new file mode 100644 index 0000000000..79b9987d3a --- /dev/null +++ b/apps/execution-service/handlers/delete.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "net/http" +) + +func (s *Service) DeleteTest(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + docRefID := chi.URLParam(r, "questionDocRefId") + + docRef := s.Client.Collection("tests").Where("questionDocRefId", "==", docRefID).Limit(1).Documents(ctx) + doc, err := docRef.Next() + if err != nil { + if err == iterator.Done { + http.Error(w, "Test not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer docRef.Stop() + + _, err = doc.Ref.Delete(ctx) + if err != nil { + http.Error(w, "Error deleting test", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +// Manual test cases + +//curl -X DELETE http://localhost:8083/tests/sampleDocRefId123 \ +//-H "Content-Type: application/json" diff --git a/apps/execution-service/handlers/update.go b/apps/execution-service/handlers/update.go index 1636456f96..b713a52c2c 100644 --- a/apps/execution-service/handlers/update.go +++ b/apps/execution-service/handlers/update.go @@ -22,6 +22,10 @@ func (s *Service) UpdateTest(w http.ResponseWriter, r *http.Request) { return } + // Normalise test cases + test.VisibleTestCases = utils.NormaliseTestCaseFormat(test.VisibleTestCases) + test.HiddenTestCases = utils.NormaliseTestCaseFormat(test.HiddenTestCases) + // Only test cases will be updated // Validate test case format with default validation if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, utils.GetDefaultValidation(), diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go index 3b79fbf527..df0dab53b2 100644 --- a/apps/execution-service/main.go +++ b/apps/execution-service/main.go @@ -91,7 +91,7 @@ func registerRoutes(r *chi.Mux, service *handlers.Service) { // Current: Unused, since testcases are executed within service and not exposed // Future extension: can be read by admin to view testcases r.Put("/", service.UpdateTest) - //r.Delete("/", service.DeleteTest) + r.Delete("/", service.DeleteTest) r.Get("/", service.ReadVisibleTests) r.Post("/execute", service.ExecuteVisibleAndCustomTests) r.Post("/submit", service.ExecuteVisibleAndHiddenTestsAndSubmit) diff --git a/apps/execution-service/utils/validateTestCaseFormat.go b/apps/execution-service/utils/validateTestCaseFormat.go index e38e0881b4..5410a0e4f0 100644 --- a/apps/execution-service/utils/validateTestCaseFormat.go +++ b/apps/execution-service/utils/validateTestCaseFormat.go @@ -77,3 +77,7 @@ package main result := validateFunc.Interface().(func(string) bool)(inputOrOutput) return result, nil } + +func NormaliseTestCaseFormat(testCase string) string { + return strings.ReplaceAll(testCase, "\\n", "\n") +} From 014d5b895b5afcac3962f8c4ca0c0bcd919dc90f Mon Sep 17 00:00:00 2001 From: Ryan Chia Date: Wed, 13 Nov 2024 14:13:49 +0800 Subject: [PATCH 05/16] add create_test.go, update_test.go, delete_test.go --- apps/question-service/go.mod | 11 +- apps/question-service/go.sum | 1 + apps/question-service/tests/common_test.go | 65 ++++++++++ apps/question-service/tests/create_test.go | 144 +++++++++++++++++++++ apps/question-service/tests/delete_test.go | 47 +++++++ apps/question-service/tests/read_test.go | 51 +------- apps/question-service/tests/update_test.go | 88 +++++++++++++ 7 files changed, 358 insertions(+), 49 deletions(-) create mode 100644 apps/question-service/tests/common_test.go create mode 100644 apps/question-service/tests/create_test.go create mode 100644 apps/question-service/tests/delete_test.go create mode 100644 apps/question-service/tests/update_test.go 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()) + }) +} From 1255bb138a5c4d4d36868cfad20f10ca9df481c8 Mon Sep 17 00:00:00 2001 From: tituschewxj Date: Wed, 13 Nov 2024 17:41:25 +0800 Subject: [PATCH 06/16] feat: update frontend ui for crud testcases --- apps/frontend/src/app/question/page.tsx | 132 ++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/app/question/page.tsx b/apps/frontend/src/app/question/page.tsx index cb88d0bf3f..a65f3e5097 100644 --- a/apps/frontend/src/app/question/page.tsx +++ b/apps/frontend/src/app/question/page.tsx @@ -14,12 +14,17 @@ import { Tag, Modal, Form, + Tabs, + Checkbox, + Tooltip, + Card, } from "antd"; import { Content } from "antd/es/layout/layout"; import { DeleteOutlined, EditOutlined, PlusCircleOutlined, + PlusOutlined, SearchOutlined, } from "@ant-design/icons"; import "./styles.scss"; @@ -40,6 +45,7 @@ import { import Link from "next/link"; import TextArea from "antd/es/input/TextArea"; import { ValidateUser, VerifyTokenResponseType } from "../services/user"; +import TabPane from "antd/es/tabs/TabPane"; /** * defines the State of the page whe a user is deleing an object. Has 3 general states: @@ -120,10 +126,9 @@ export default function QuestionListPage() { const [isAdmin, setIsAdmin] = useState(undefined); useLayoutEffect(() => { - ValidateUser() - .then((data: VerifyTokenResponseType) => { - setIsAdmin(data.data.isAdmin); - }) + ValidateUser().then((data: VerifyTokenResponseType) => { + setIsAdmin(data.data.isAdmin); + }); }, []); const handleEditClick = (index: number, question: Question) => { @@ -248,6 +253,34 @@ export default function QuestionListPage() { return () => clearTimeout(timeout); }, [search]); + const [visibleTests, setVisibleTests] = useState([ + { key: "1", title: "Visible Test 1" }, + ]); + const [hiddenTests, setHiddenTests] = useState([]); // FIXME: fix the type!! + const [tabIndex, setTabIndex] = useState(2); + + const handleAddVisibleTest = () => { + const newKey = `${Date.now()}`; // Use unique timestamp as key + setVisibleTests([...visibleTests, { key: newKey }]); + }; + + const handleAddHiddenTest = () => { + const newKey = `${Date.now()}`; // Use unique timestamp as key + setHiddenTests([...hiddenTests, { key: newKey }]); + }; + + const handleRemoveVisibleTest = (targetKey: string) => { + if (visibleTests.length > 1) { + setVisibleTests( + visibleTests.filter((test: any) => test.key !== targetKey) + ); + } + }; + + const handleRemoveHiddenTest = (targetKey: string) => { + setHiddenTests(hiddenTests.filter((test: any) => test.key !== targetKey)); + }; + // Table column specification var columns: TableProps["columns"]; if (isAdmin) { @@ -343,7 +376,7 @@ export default function QuestionListPage() { }, ]} > -