Skip to content

Commit f01ba09

Browse files
authored
Merge pull request #75 from CS3219-AY2425S1/solomon/add-create-test
feat: add create, update, delete test
2 parents bb8cc46 + 780aa3d commit f01ba09

File tree

16 files changed

+1033
-55
lines changed

16 files changed

+1033
-55
lines changed

apps/execution-service/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,43 @@ The following json format will be returned:
160160
]
161161
```
162162

163+
`POST /tests`
164+
165+
To create a new test case, run the following command:
166+
167+
```bash
168+
curl -X POST http://localhost:8083/tests \
169+
-H "Content-Type: application/json" \
170+
-d '{
171+
"questionDocRefId": "sampleDocRefId123",
172+
"questionTitle": "Sample Question Title",
173+
"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH",
174+
"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba"
175+
}'
176+
```
177+
178+
`PUT /tests/{questionDocRefId}`
179+
180+
To update an existing test case from an existing question, run the following command:
181+
182+
```bash
183+
curl -X PUT http://localhost:8083/tests/{questionDocRefId} \
184+
-H "Content-Type: application/json" \
185+
-d '{
186+
"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH",
187+
"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba"
188+
}'
189+
```
190+
191+
`DELETE /tests/{questionDocRefId}`
192+
193+
To delete an existing test case from an existing question, run the following command:
194+
195+
```bash
196+
curl -X DELETE http://localhost:8083/tests/{questionDocRefId} \
197+
-H "Content-Type: application/json"
198+
```
199+
163200
`POST /tests/{questionDocRefId}/execute`
164201

165202
To execute test cases via a question ID without custom test cases, run the following command, with custom code and language:

apps/execution-service/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/go-chi/chi/v5 v5.1.0
99
github.com/go-chi/cors v1.2.1
1010
github.com/joho/godotenv v1.5.1
11+
github.com/rabbitmq/amqp091-go v1.10.0
1112
github.com/traefik/yaegi v0.16.1
1213
google.golang.org/api v0.203.0
1314
)
@@ -31,7 +32,6 @@ require (
3132
github.com/google/uuid v1.6.0 // indirect
3233
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
3334
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
34-
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
3535
go.opencensus.io v0.24.0 // indirect
3636
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
3737
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect

apps/execution-service/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHy
111111
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
112112
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
113113
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
114+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
115+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
114116
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
115117
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
116118
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package handlers
2+
3+
import (
4+
"cloud.google.com/go/firestore"
5+
"encoding/json"
6+
"execution-service/models"
7+
"execution-service/utils"
8+
"google.golang.org/api/iterator"
9+
"net/http"
10+
)
11+
12+
func (s *Service) CreateTest(w http.ResponseWriter, r *http.Request) {
13+
ctx := r.Context()
14+
15+
var test models.Test
16+
if err := utils.DecodeJSONBody(w, r, &test); err != nil {
17+
http.Error(w, err.Error(), http.StatusBadRequest)
18+
return
19+
}
20+
21+
// Basic validation for question title and question ID
22+
if test.QuestionDocRefId == "" || test.QuestionTitle == "" {
23+
http.Error(w, "QuestionDocRefId and QuestionTitle are required", http.StatusBadRequest)
24+
return
25+
}
26+
27+
// Normalise test cases
28+
test.VisibleTestCases = utils.NormaliseTestCaseFormat(test.VisibleTestCases)
29+
test.HiddenTestCases = utils.NormaliseTestCaseFormat(test.HiddenTestCases)
30+
31+
// Automatically populate validation for input and output in test case
32+
test.InputValidation = utils.GetDefaultValidation()
33+
test.OutputValidation = utils.GetDefaultValidation()
34+
35+
// Validate test case format
36+
if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, test.InputValidation,
37+
test.OutputValidation); err != nil {
38+
http.Error(w, err.Error(), http.StatusBadRequest)
39+
return
40+
}
41+
if _, err := utils.ValidateTestCaseFormat(test.HiddenTestCases, test.InputValidation,
42+
test.OutputValidation); err != nil {
43+
http.Error(w, err.Error(), http.StatusBadRequest)
44+
return
45+
}
46+
47+
// Check if a test already exists for the question
48+
iter := s.Client.Collection("tests").Where("questionDocRefId", "==", test.QuestionDocRefId).Documents(ctx)
49+
for {
50+
_, err := iter.Next()
51+
if err == iterator.Done {
52+
break
53+
}
54+
if err != nil {
55+
http.Error(w, "Error fetching test", http.StatusInternalServerError)
56+
return
57+
}
58+
http.Error(w, "Test already exists for the question", http.StatusConflict)
59+
return
60+
}
61+
defer iter.Stop()
62+
63+
// Save test to Firestore
64+
docRef, _, err := s.Client.Collection("tests").Add(ctx, map[string]interface{}{
65+
"questionDocRefId": test.QuestionDocRefId,
66+
"questionTitle": test.QuestionTitle,
67+
"visibleTestCases": test.VisibleTestCases,
68+
"hiddenTestCases": test.HiddenTestCases,
69+
"inputValidation": test.InputValidation,
70+
"outputValidation": test.OutputValidation,
71+
"createdAt": firestore.ServerTimestamp,
72+
"updatedAt": firestore.ServerTimestamp,
73+
})
74+
if err != nil {
75+
http.Error(w, err.Error(), http.StatusInternalServerError)
76+
return
77+
}
78+
79+
// Get data
80+
doc, err := docRef.Get(ctx)
81+
if err != nil {
82+
if err != iterator.Done {
83+
http.Error(w, "Test not found", http.StatusInternalServerError)
84+
return
85+
}
86+
http.Error(w, "Failed to get test", http.StatusInternalServerError)
87+
return
88+
}
89+
90+
// Map data
91+
if err := doc.DataTo(&test); err != nil {
92+
http.Error(w, err.Error(), http.StatusInternalServerError)
93+
return
94+
}
95+
96+
w.Header().Set("Content-Type", "application/json")
97+
w.WriteHeader(http.StatusCreated)
98+
json.NewEncoder(w).Encode(test)
99+
}
100+
101+
// Manual test cases
102+
103+
//curl -X POST http://localhost:8083/tests \
104+
//-H "Content-Type: application/json" \
105+
//-d '{
106+
//"questionDocRefId": "sampleDocRefId123",
107+
//"questionTitle": "Sample Question Title",
108+
//"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH",
109+
//"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba"
110+
//}'
111+
112+
//curl -X POST http://localhost:8083/tests \
113+
//-H "Content-Type: application/json" \
114+
//-d "{
115+
//\"questionDocRefId\": \"sampleDocRefId12345\",
116+
//\"questionTitle\": \"Sample Question Title\",
117+
//\"visibleTestCases\": \"2\\nhello\\nolleh\\nHannah\\nhannaH\",
118+
//\"hiddenTestCases\": \"2\\nHannah\\nhannaH\\nabcdefg\\ngfedcba\"
119+
//}"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package handlers
2+
3+
import (
4+
"github.com/go-chi/chi/v5"
5+
"google.golang.org/api/iterator"
6+
"net/http"
7+
)
8+
9+
func (s *Service) DeleteTest(w http.ResponseWriter, r *http.Request) {
10+
ctx := r.Context()
11+
12+
// Parse request
13+
docRefID := chi.URLParam(r, "questionDocRefId")
14+
15+
docRef := s.Client.Collection("tests").Where("questionDocRefId", "==", docRefID).Limit(1).Documents(ctx)
16+
doc, err := docRef.Next()
17+
if err != nil {
18+
if err == iterator.Done {
19+
http.Error(w, "Test not found", http.StatusNotFound)
20+
return
21+
}
22+
http.Error(w, err.Error(), http.StatusInternalServerError)
23+
return
24+
}
25+
defer docRef.Stop()
26+
27+
_, err = doc.Ref.Delete(ctx)
28+
if err != nil {
29+
http.Error(w, "Error deleting test", http.StatusInternalServerError)
30+
return
31+
}
32+
33+
w.Header().Set("Content-Type", "application/json")
34+
w.WriteHeader(http.StatusOK)
35+
}
36+
37+
// Manual test cases
38+
39+
//curl -X DELETE http://localhost:8083/tests/sampleDocRefId123 \
40+
//-H "Content-Type: application/json"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"execution-service/models"
6+
"execution-service/utils"
7+
"net/http"
8+
9+
"github.com/go-chi/chi/v5"
10+
"google.golang.org/api/iterator"
11+
)
12+
13+
func (s *Service) ReadAllTests(w http.ResponseWriter, r *http.Request) {
14+
ctx := r.Context()
15+
16+
questionDocRefId := chi.URLParam(r, "questionDocRefId")
17+
if questionDocRefId == "" {
18+
http.Error(w, "questionDocRefId is required", http.StatusBadRequest)
19+
return
20+
}
21+
22+
iter := s.Client.Collection("tests").Where("questionDocRefId", "==", questionDocRefId).Limit(1).Documents(ctx)
23+
doc, err := iter.Next()
24+
if err != nil {
25+
if err == iterator.Done {
26+
http.Error(w, "Test not found", http.StatusNotFound)
27+
return
28+
}
29+
http.Error(w, err.Error(), http.StatusInternalServerError)
30+
return
31+
}
32+
defer iter.Stop()
33+
34+
var test models.Test
35+
if err := doc.DataTo(&test); err != nil {
36+
http.Error(w, err.Error(), http.StatusInternalServerError)
37+
return
38+
}
39+
40+
_, hiddenTestCases, err := utils.GetTestLengthAndUnexecutedCases(test.HiddenTestCases)
41+
42+
var hiddenTests []models.HiddenTest
43+
for _, hiddenTestCase := range hiddenTestCases {
44+
hiddenTests = append(hiddenTests, models.HiddenTest{
45+
Input: hiddenTestCase.Input,
46+
Expected: hiddenTestCase.Expected,
47+
})
48+
}
49+
50+
_, visibleTestCases, err := utils.GetTestLengthAndUnexecutedCases(test.VisibleTestCases)
51+
52+
var visibleTests []models.VisibleTest
53+
for _, visibleTestCase := range visibleTestCases {
54+
visibleTests = append(visibleTests, models.VisibleTest{
55+
Input: visibleTestCase.Input,
56+
Expected: visibleTestCase.Expected,
57+
})
58+
}
59+
60+
allTests := models.AllTests{
61+
VisibleTests: visibleTests,
62+
HiddenTests: hiddenTests,
63+
}
64+
65+
w.Header().Set("Content-Type", "application/json")
66+
w.WriteHeader(http.StatusOK)
67+
json.NewEncoder(w).Encode(allTests)
68+
}
69+
70+
//curl -X GET http://localhost:8083/tests/bmzFyLMeSOoYU99pi4yZ/ \
71+
//-H "Content-Type: application/json"
File renamed without changes.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package handlers
2+
3+
import (
4+
"cloud.google.com/go/firestore"
5+
"encoding/json"
6+
"execution-service/models"
7+
"execution-service/utils"
8+
"github.com/go-chi/chi/v5"
9+
"google.golang.org/api/iterator"
10+
"net/http"
11+
)
12+
13+
func (s *Service) UpdateTest(w http.ResponseWriter, r *http.Request) {
14+
ctx := r.Context()
15+
16+
// get param questionDocRefId
17+
questionDocRefId := chi.URLParam(r, "questionDocRefId")
18+
19+
var test models.Test
20+
if err := utils.DecodeJSONBody(w, r, &test); err != nil {
21+
http.Error(w, err.Error(), http.StatusBadRequest)
22+
return
23+
}
24+
25+
// Normalise test cases
26+
test.VisibleTestCases = utils.NormaliseTestCaseFormat(test.VisibleTestCases)
27+
test.HiddenTestCases = utils.NormaliseTestCaseFormat(test.HiddenTestCases)
28+
29+
// Only test cases will be updated
30+
// Validate test case format with default validation
31+
if _, err := utils.ValidateTestCaseFormat(test.VisibleTestCases, utils.GetDefaultValidation(),
32+
utils.GetDefaultValidation()); err != nil {
33+
http.Error(w, err.Error(), http.StatusBadRequest)
34+
return
35+
}
36+
if _, err := utils.ValidateTestCaseFormat(test.HiddenTestCases, utils.GetDefaultValidation(),
37+
utils.GetDefaultValidation()); err != nil {
38+
http.Error(w, err.Error(), http.StatusBadRequest)
39+
return
40+
}
41+
42+
// Update test in Firestore
43+
docRef := s.Client.Collection("tests").Where("questionDocRefId", "==", questionDocRefId).Limit(1).Documents(ctx)
44+
doc, err := docRef.Next()
45+
if err != nil {
46+
if err == iterator.Done {
47+
http.Error(w, "Test not found", http.StatusNotFound)
48+
return
49+
}
50+
http.Error(w, err.Error(), http.StatusInternalServerError)
51+
return
52+
}
53+
defer docRef.Stop()
54+
55+
// Update database
56+
updates := []firestore.Update{
57+
{Path: "visibleTestCases", Value: test.VisibleTestCases},
58+
{Path: "hiddenTestCases", Value: test.HiddenTestCases},
59+
{Path: "updatedAt", Value: firestore.ServerTimestamp},
60+
}
61+
_, err = doc.Ref.Update(ctx, updates)
62+
if err != nil {
63+
http.Error(w, "Error updating test", http.StatusInternalServerError)
64+
return
65+
}
66+
67+
// Get data
68+
doc, err = doc.Ref.Get(ctx)
69+
if err != nil {
70+
if err != iterator.Done {
71+
http.Error(w, "Test not found", http.StatusNotFound)
72+
return
73+
}
74+
http.Error(w, "Failed to get test", http.StatusInternalServerError)
75+
return
76+
}
77+
78+
// Map data
79+
if err = doc.DataTo(&test); err != nil {
80+
http.Error(w, "Failed to map test data", http.StatusInternalServerError)
81+
return
82+
}
83+
84+
w.Header().Set("Content-Type", "application/json")
85+
w.WriteHeader(http.StatusOK)
86+
json.NewEncoder(w).Encode(test)
87+
}
88+
89+
// Manual test cases
90+
91+
//curl -X PUT http://localhost:8083/tests/sampleDocRefId123 \
92+
//-H "Content-Type: application/json" \
93+
//-d '{
94+
//"visibleTestCases": "2\nhello\nolleh\nHannah\nhannaH",
95+
//"hiddenTestCases": "2\nHannah\nhannaH\nabcdefg\ngfedcba"
96+
//}'

0 commit comments

Comments
 (0)