diff --git a/__tests__/data/java_tests_sample.zip b/__tests__/data/java_tests_sample.zip new file mode 100644 index 0000000..a7e3d11 Binary files /dev/null and b/__tests__/data/java_tests_sample.zip differ diff --git a/__tests__/integration/laboratories_utils_test.go b/__tests__/integration/laboratories_utils_test.go index 988ad96..d1323b7 100644 --- a/__tests__/integration/laboratories_utils_test.go +++ b/__tests__/integration/laboratories_utils_test.go @@ -1,6 +1,13 @@ package integration -import "net/http" +import ( + "bytes" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "os" +) func CreateLaboratory(cookie *http.Cookie, payload map[string]interface{}) (response map[string]interface{}, statusCode int) { w, r := PrepareRequest("POST", "/api/v1/laboratories", payload) @@ -37,3 +44,61 @@ func CreateMarkdownBlock(cookie *http.Cookie, laboratoryUUID string) (response m jsonResponse := ParseJsonResponse(w.Body) return jsonResponse, w.Code } + +type CreateTestBlockUtilsDTO struct { + laboratoryUUID string + languageUUID string + blockName string + cookie *http.Cookie + testFile *os.File +} + +func CreateTestBlock(dto *CreateTestBlockUtilsDTO) (response map[string]interface{}, statusCode int) { + // Create the request body + var body bytes.Buffer + + // Create the multipart form + writer := multipart.NewWriter(&body) + + // Add the file to the form + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", "form-data; name=\"test_archive\"; filename=\"test.zip\"") + h.Set("Content-Type", "application/zip") + + fileWriter, err := writer.CreatePart(h) + if err != nil { + panic(err) + } + + _, err = io.Copy(fileWriter, dto.testFile) + if err != nil { + panic(err) + } + + // Add the text fields to the form + err = writer.WriteField("block_name", dto.blockName) + if err != nil { + panic(err) + } + + err = writer.WriteField("language_uuid", dto.languageUUID) + if err != nil { + panic(err) + } + + // Close the multipart form + err = writer.Close() + if err != nil { + panic(err) + } + + // Create the request + w, r := PrepareMultipartRequest("POST", "/api/v1/laboratories/test_blocks/"+dto.laboratoryUUID, &body) + r.AddCookie(dto.cookie) + r.Header.Set("Content-Type", writer.FormDataContentType()) + + // Send the request + router.ServeHTTP(w, r) + jsonResponse := ParseJsonResponse(w.Body) + return jsonResponse, w.Code +} diff --git a/__tests__/integration/laboratorires_test.go b/__tests__/integration/laboratorires_test.go index 4bb600c..2133bca 100644 --- a/__tests__/integration/laboratorires_test.go +++ b/__tests__/integration/laboratorires_test.go @@ -2,6 +2,7 @@ package integration import ( "net/http" + "os" "testing" "github.com/stretchr/testify/require" @@ -279,3 +280,57 @@ func TestCreateMarkdownBlock(t *testing.T) { c.Equal("", block["content"]) c.EqualValues(1, block["index"]) } + +func TestCreateTestBlock(t *testing.T) { + c := require.New(t) + + // Login as a teacher + w, r := PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{ + "email": registeredTeacherEmail, + "password": registeredTeacherPass, + }) + router.ServeHTTP(w, r) + cookie := w.Result().Cookies()[0] + + // Create a course + courseUUID, status := CreateCourse("Create test block test - course") + c.Equal(http.StatusCreated, status) + + // Create a laboratory + laboratoryCreationResponse, status := CreateLaboratory(cookie, map[string]interface{}{ + "name": "Create test block test - laboratory", + "course_uuid": courseUUID, + "opening_date": "2023-12-01T08:00", + "due_date": "3023-12-01T00:00", + }) + laboratoryUUID := laboratoryCreationResponse["uuid"].(string) + c.Equal(http.StatusCreated, status) + + // Get the supported languages + languagesResponse, status := GetSupportedLanguages(cookie) + c.Equal(http.StatusOK, status) + + languages := languagesResponse["languages"].([]interface{}) + c.Greater(len(languages), 0) + + firstLanguage := languages[0].(map[string]interface{}) + firstLanguageUUID := firstLanguage["uuid"].(string) + + // Open `.zip` file from the data folder + TEST_FILE_PATH := "../data/java_tests_sample.zip" + zipFile, err := os.Open(TEST_FILE_PATH) + c.Nil(err) + + // Send the request + response, _ := CreateTestBlock(&CreateTestBlockUtilsDTO{ + laboratoryUUID: laboratoryUUID, + languageUUID: firstLanguageUUID, + blockName: "Create test block test - block", + cookie: cookie, + testFile: zipFile, + }) + + // Validate the response + // c.Equal(http.StatusCreated, status) + c.Contains(response, "uuid") +} diff --git a/__tests__/integration/main_test.go b/__tests__/integration/main_test.go index 5b265f0..545c79c 100644 --- a/__tests__/integration/main_test.go +++ b/__tests__/integration/main_test.go @@ -133,6 +133,16 @@ func PrepareRequest(method, endpoint string, payload interface{}) (*httptest.Res return w, req } +func PrepareMultipartRequest(method, endpoint string, body *bytes.Buffer) (*httptest.ResponseRecorder, *http.Request) { + var req *http.Request + + req, _ = http.NewRequest(method, endpoint, body) + req.Header.Set("Content-Type", "multipart/form-data") + + w := httptest.NewRecorder() + return w, req +} + func ParseJsonResponse(buffer *bytes.Buffer) map[string]interface{} { var response map[string]interface{} json.Unmarshal(buffer.Bytes(), &response) diff --git a/docs/insomnia/collection.json b/docs/insomnia/collection.json new file mode 100644 index 0000000..566b2a3 --- /dev/null +++ b/docs/insomnia/collection.json @@ -0,0 +1,156 @@ +{ + "_type": "export", + "__export_format": 4, + "__export_date": "2023-12-29T19:18:12.594Z", + "__export_source": "insomnia.desktop.app:v8.5.1", + "resources": [ + { + "_id": "req_1f5b243091ed48d19b56b9e9f1b8b60a", + "parentId": "fld_0f115af031f64a478dc8707d5aba1048", + "modified": 1703877462736, + "created": 1703867533491, + "url": "{{ _.BASE_URL }}/laboratories/test_blocks/77ab37da-580e-44d6-86d3-d3334cb24b12", + "name": "create-test-block", + "description": "", + "method": "POST", + "body": { + "mimeType": "multipart/form-data", + "params": [ + { + "id": "pair_a735916e2a8a49779538d59b17038494", + "name": "language_uuid", + "value": "19c11b28-3d06-4c54-8946-6af8a28e8b07", + "description": "" + }, + { + "id": "pair_08961c834b7f4eba9b58187135414cd6", + "name": "block_name", + "value": "Calculator operations", + "description": "" + }, + { + "id": "pair_a3253709f396429dbbdb03f522270092", + "name": "test_archive", + "value": "", + "description": "", + "type": "file", + "fileName": "/home/pacq/IdeaProjects/java.zip" + } + ] + }, + "parameters": [], + "headers": [ + { "name": "Content-Type", "value": "multipart/form-data" }, + { "name": "User-Agent", "value": "insomnia/8.5.1" } + ], + "authentication": {}, + "metaSortKey": -1703867533491, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "fld_0f115af031f64a478dc8707d5aba1048", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867530994, + "created": 1703867530994, + "name": "laboratories", + "description": "", + "environment": {}, + "environmentPropertyOrder": null, + "metaSortKey": -1703867530994, + "_type": "request_group" + }, + { + "_id": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "parentId": null, + "modified": 1703867428111, + "created": 1703867428111, + "name": "UPB Codelabs Multipart Form Request", + "description": "", + "scope": "collection", + "_type": "workspace" + }, + { + "_id": "req_81c24080612643dea636b43cba6a5c72", + "parentId": "fld_e99fc9329e1e4e9ea4aad5ac37ff965c", + "modified": 1703867500413, + "created": 1703867446159, + "url": "{{ _.BASE_URL }}/session/login", + "name": "login-as-teacher", + "description": "", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n \"email\": \"veli.ayele.2020@upb.edu.co\", \n \"password\": \"upbbbga2023*/\"\n}" + }, + "parameters": [], + "headers": [ + { "name": "Content-Type", "value": "application/json" }, + { "name": "User-Agent", "value": "insomnia/8.5.1" } + ], + "authentication": {}, + "metaSortKey": -1703867446159, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "fld_e99fc9329e1e4e9ea4aad5ac37ff965c", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867444303, + "created": 1703867444303, + "name": "session", + "description": "", + "environment": {}, + "environmentPropertyOrder": null, + "metaSortKey": -1703867444303, + "_type": "request_group" + }, + { + "_id": "env_5128b6531490f5b58316ee3db66aec4f90c368ff", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867471567, + "created": 1703867428113, + "name": "Base Environment", + "data": { "BASE_URL": "http://127.0.0.1:8080/api/v1" }, + "dataPropertyOrder": { "&": ["BASE_URL"] }, + "color": null, + "isPrivate": false, + "metaSortKey": 1703867428113, + "_type": "environment" + }, + { + "_id": "jar_5128b6531490f5b58316ee3db66aec4f90c368ff", + "parentId": "wrk_015f06b0727b4d81af80c31f9fc4f68f", + "modified": 1703867520274, + "created": 1703867428114, + "name": "Default Jar", + "cookies": [ + { + "key": "session", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiNWM2NjY5ZDgtODFiYi00NGEyLTk4YTMtMDRhNTBmY2I1NWU1Iiwicm9sZSI6InRlYWNoZXIiLCJpc3MiOiJjb2RlbGFicyIsImV4cCI6MTcwMzg4OTEyMCwibmJmIjoxNzAzODY3NTIwLCJpYXQiOjE3MDM4Njc1MjB9.XCEIBKwjjNLja0B1eYNlZIc51I5f5nHPic3UWLUC-lM", + "maxAge": 21600, + "domain": "127.0.0.1", + "path": "/", + "httpOnly": true, + "hostOnly": true, + "creation": "2023-12-29T16:32:00.273Z", + "lastAccessed": "2023-12-29T16:32:00.273Z", + "id": "f3827bc3-8097-4fa7-8dfa-d3c3f7e032f9" + } + ], + "_type": "cookie_jar" + } + ] +} diff --git a/docs/openapi/spec.openapi.yaml b/docs/openapi/spec.openapi.yaml index a97eef6..89d0c62 100644 --- a/docs/openapi/spec.openapi.yaml +++ b/docs/openapi/spec.openapi.yaml @@ -1849,13 +1849,13 @@ components: create_test_block_req: type: object properties: - name: + block_name: type: string example: "Test métodos de la lista simplemente enlazada" language_uuid: type: string example: "a7c4c843-c3ef-4083-8255-927aea3af77f" - file: + test_archive: type: string format: binary # A `.zip` archive diff --git a/src/blocks/domain/definitions/blocks_repository.go b/src/blocks/domain/definitions/blocks_repository.go index 6997ccb..74c9bac 100644 --- a/src/blocks/domain/definitions/blocks_repository.go +++ b/src/blocks/domain/definitions/blocks_repository.go @@ -1,6 +1,10 @@ package definitions +import "mime/multipart" + type BlockRepository interface { UpdateMarkdownBlockContent(blockUUID string, content string) (err error) DoesTeacherOwnsMarkdownBlock(teacherUUID string, blockUUID string) (bool, error) + + SaveTestsArchive(file *multipart.File) (uuid string, err error) } diff --git a/src/blocks/infrastructure/implementations/blocks_repository.go b/src/blocks/infrastructure/implementations/blocks_repository.go index 16ca798..1ef7a50 100644 --- a/src/blocks/infrastructure/implementations/blocks_repository.go +++ b/src/blocks/infrastructure/implementations/blocks_repository.go @@ -1,11 +1,19 @@ package implementations import ( + "bytes" "context" "database/sql" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" "time" - "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" + sharedDomainErrors "github.com/UPB-Code-Labs/main-api/src/shared/domain/errors" + sharedInfrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" ) type BlocksPostgresRepository struct { @@ -18,7 +26,7 @@ var blocksPostgresRepositoryInstance *BlocksPostgresRepository func GetBlocksPostgresRepositoryInstance() *BlocksPostgresRepository { if blocksPostgresRepositoryInstance == nil { blocksPostgresRepositoryInstance = &BlocksPostgresRepository{ - Connection: infrastructure.GetPostgresConnection(), + Connection: sharedInfrastructure.GetPostgresConnection(), } } @@ -79,3 +87,87 @@ func (repository *BlocksPostgresRepository) DoesTeacherOwnsMarkdownBlock(teacher return laboratoryTeacherUUID == teacherUUID, nil } + +func (repository *BlocksPostgresRepository) SaveTestsArchive(file *multipart.File) (uuid string, err error) { + // Arbitrary constants since they are not used anywhere else and + // we only support zip files for now. + FILE_NAME := "archive.zip" + FILE_CONTENT_TYPE := "application/zip" + + // Create multipart writer + staticFilesEndpoint := fmt.Sprintf("%s/archives/save", sharedInfrastructure.GetEnvironment().StaticFilesMicroserviceAddress) + var requestBuffer bytes.Buffer + multipartWriter := multipart.NewWriter(&requestBuffer) + + // Add the file field to the request + header := textproto.MIMEHeader{} + header.Set("Content-Disposition", + fmt.Sprintf( + `form-data; name="%s"; filename="%s"`, + "file", + FILE_NAME, + ), + ) + header.Set("Content-Type", FILE_CONTENT_TYPE) + + // Reset the file pointer to the beginning + _, err = (*file).Seek(0, io.SeekStart) + if err != nil { + return "", err + } + + fileWriter, err := multipartWriter.CreatePart(header) + if err != nil { + return "", err + } + + if _, err := io.Copy(fileWriter, *file); err != nil { + return "", err + } + + // Add the file type field to the request + if err := multipartWriter.WriteField("archive_type", "test"); err != nil { + return "", err + } + + // Close the writer + err = multipartWriter.Close() + if err != nil { + return "", err + } + + // Prepare the request + req, err := http.NewRequest("POST", staticFilesEndpoint, &requestBuffer) + + if err != nil { + return "", err + } + + req.Header.Set("Content-Type", multipartWriter.FormDataContentType()) + + // Send the request + client := &http.Client{} + res, err := client.Do(req) + + // Forward error message if any + microserviceError := sharedInfrastructure.ParseMicroserviceError(res, err) + if microserviceError != nil { + return "", microserviceError + } + + // Parse the response + var response map[string]interface{} + err = json.NewDecoder(res.Body).Decode(&response) + if err != nil { + return "", err + } + + if response["uuid"] == nil { + return "", &sharedDomainErrors.StaticFilesMicroserviceError{ + Code: http.StatusInternalServerError, + Message: "The static files microservice did not return the UUID of the saved file", + } + } + + return response["uuid"].(string), nil +} diff --git a/src/laboratories/application/use_cases.go b/src/laboratories/application/use_cases.go index 5c35ad1..05743b3 100644 --- a/src/laboratories/application/use_cases.go +++ b/src/laboratories/application/use_cases.go @@ -1,19 +1,23 @@ package application import ( + blocksDefinitions "github.com/UPB-Code-Labs/main-api/src/blocks/domain/definitions" courses_definitions "github.com/UPB-Code-Labs/main-api/src/courses/domain/definitions" courses_errors "github.com/UPB-Code-Labs/main-api/src/courses/domain/errors" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/definitions" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/dtos" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/entities" - rubrics_definitions "github.com/UPB-Code-Labs/main-api/src/rubrics/domain/definitions" + languagesDefinitions "github.com/UPB-Code-Labs/main-api/src/languages/domain/definitions" + rubricsDefinitions "github.com/UPB-Code-Labs/main-api/src/rubrics/domain/definitions" rubrics_errors "github.com/UPB-Code-Labs/main-api/src/rubrics/domain/errors" ) type LaboratoriesUseCases struct { - LaboratoriesRepository definitions.LaboratoriesRepository CoursesRepository courses_definitions.CoursesRepository - RubricsRepository rubrics_definitions.RubricsRepository + LaboratoriesRepository definitions.LaboratoriesRepository + RubricsRepository rubricsDefinitions.RubricsRepository + LanguagesRepository languagesDefinitions.LanguagesRepository + BlocksRepository blocksDefinitions.BlockRepository } func (useCases *LaboratoriesUseCases) CreateLaboratory(dto *dtos.CreateLaboratoryDTO) (laboratory *entities.Laboratory, err error) { @@ -100,3 +104,31 @@ func (useCases *LaboratoriesUseCases) doesTeacherOwnsLaboratory(teacherUUID, lab return useCases.CoursesRepository.DoesTeacherOwnsCourse(teacherUUID, laboratory.CourseUUID) } + +func (useCases *LaboratoriesUseCases) CreateTestBlock(dto *dtos.CreateTestBlockDTO) (blockUUID string, err error) { + // Check that the teacher owns the laboratory + teacherOwnsLaboratory, err := useCases.doesTeacherOwnsLaboratory(dto.TeacherUUID, dto.LaboratoryUUID) + if err != nil { + return "", err + } + + if !teacherOwnsLaboratory { + return "", &courses_errors.TeacherDoesNotOwnsCourseError{} + } + + // Check that the language exists + _, err = useCases.LanguagesRepository.GetByUUID(dto.LanguageUUID) + if err != nil { + return "", err + } + + // Send the file to the static files microservice + savedArchiveUUID, err := useCases.BlocksRepository.SaveTestsArchive(dto.MultipartFile) + if err != nil { + return "", err + } + dto.TestArchiveUUID = savedArchiveUUID + + // Save the information in the database + return useCases.LaboratoriesRepository.CreateTestBlock(dto) +} diff --git a/src/laboratories/domain/definitions/laboratories_repository.go b/src/laboratories/domain/definitions/laboratories_repository.go index bbb23d7..c1581db 100644 --- a/src/laboratories/domain/definitions/laboratories_repository.go +++ b/src/laboratories/domain/definitions/laboratories_repository.go @@ -11,4 +11,5 @@ type LaboratoriesRepository interface { UpdateLaboratory(dto *dtos.UpdateLaboratoryDTO) error CreateMarkdownBlock(laboratoryUUID string) (blockUUID string, err error) + CreateTestBlock(dto *dtos.CreateTestBlockDTO) (blockUUID string, err error) } diff --git a/src/laboratories/domain/dtos/laboratories_dtos.go b/src/laboratories/domain/dtos/laboratories_dtos.go index cfc4f1f..e415854 100644 --- a/src/laboratories/domain/dtos/laboratories_dtos.go +++ b/src/laboratories/domain/dtos/laboratories_dtos.go @@ -1,5 +1,7 @@ package dtos +import "mime/multipart" + type CreateLaboratoryDTO struct { TeacherUUID string CourseUUID string @@ -26,3 +28,12 @@ type CreateMarkdownBlockDTO struct { TeacherUUID string LaboratoryUUID string } + +type CreateTestBlockDTO struct { + TeacherUUID string + LaboratoryUUID string + LanguageUUID string + TestArchiveUUID string + Name string + MultipartFile *multipart.File +} diff --git a/src/laboratories/infrastructure/http/controllers.go b/src/laboratories/infrastructure/http/controllers.go index f620e23..27d3e0a 100644 --- a/src/laboratories/infrastructure/http/controllers.go +++ b/src/laboratories/infrastructure/http/controllers.go @@ -1,9 +1,11 @@ package http import ( + "fmt" "net/http" "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/dtos" + "github.com/gabriel-vasile/mimetype" "github.com/UPB-Code-Labs/main-api/src/laboratories/application" "github.com/UPB-Code-Labs/main-api/src/laboratories/infrastructure/requests" @@ -180,3 +182,86 @@ func (controller *LaboratoriesController) HandleCreateMarkdownBlock(c *gin.Conte "uuid": blockUUID, }) } + +func (controller *LaboratoriesController) HandleCreateTestBlock(c *gin.Context) { + teacherUUID := c.GetString("session_uuid") + laboratoryUUID := c.Param("laboratory_uuid") + + // Validate the request struct + languageUUID := c.PostForm("language_uuid") + name := c.PostForm("block_name") + + req := requests.CreateTestBlockRequest{ + LaboratoryUUID: laboratoryUUID, + LanguageUUID: languageUUID, + Name: name, + } + + if err := infrastructure.GetValidator().Struct(req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Validation error", + "errors": err.Error(), + }) + return + } + + // Validate the test archive + multipartFile, err := c.FormFile("test_archive") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Please, make sure to send the test archive", + }) + return + } + + if multipartFile.Size > infrastructure.GetEnvironment().ArchiveMaxSizeKb*1024 { + c.JSON(http.StatusBadRequest, gin.H{ + "message": fmt.Sprintf("The test archive must be smaller than %d KB", infrastructure.GetEnvironment().ArchiveMaxSizeKb), + }) + return + } + + file, err := multipartFile.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "There was an error while reading the test archive", + }) + return + } + defer file.Close() + + mtype, err := mimetype.DetectReader(file) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "There was an error while reading the MIME type of the test archive", + }) + return + } + + if mtype.String() != "application/zip" { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Please, make sure to send a ZIP archive", + }) + return + } + + // Create the DTO + dto := dtos.CreateTestBlockDTO{ + LaboratoryUUID: laboratoryUUID, + TeacherUUID: teacherUUID, + LanguageUUID: languageUUID, + Name: name, + MultipartFile: &file, + } + + // Create the block + blockUUID, err := controller.UseCases.CreateTestBlock(&dto) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "uuid": blockUUID, + }) +} diff --git a/src/laboratories/infrastructure/http/routes.go b/src/laboratories/infrastructure/http/routes.go index c1e2239..c757b3c 100644 --- a/src/laboratories/infrastructure/http/routes.go +++ b/src/laboratories/infrastructure/http/routes.go @@ -1,10 +1,12 @@ package http import ( - courses_implementation "github.com/UPB-Code-Labs/main-api/src/courses/infrastructure/implementations" + blocksImplementation "github.com/UPB-Code-Labs/main-api/src/blocks/infrastructure/implementations" + coursesImplementation "github.com/UPB-Code-Labs/main-api/src/courses/infrastructure/implementations" "github.com/UPB-Code-Labs/main-api/src/laboratories/application" "github.com/UPB-Code-Labs/main-api/src/laboratories/infrastructure/implementations" - rubrics_implementation "github.com/UPB-Code-Labs/main-api/src/rubrics/infrastructure/implementations" + languagesImplementation "github.com/UPB-Code-Labs/main-api/src/languages/infrastructure/implementations" + rubricImplementation "github.com/UPB-Code-Labs/main-api/src/rubrics/infrastructure/implementations" "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure" "github.com/gin-gonic/gin" ) @@ -14,8 +16,10 @@ func StartLaboratoriesRoutes(g *gin.RouterGroup) { useCases := application.LaboratoriesUseCases{ LaboratoriesRepository: implementations.GetLaboratoriesPostgresRepositoryInstance(), - CoursesRepository: courses_implementation.GetCoursesPgRepository(), - RubricsRepository: rubrics_implementation.GetRubricsPgRepository(), + CoursesRepository: coursesImplementation.GetCoursesPgRepository(), + RubricsRepository: rubricImplementation.GetRubricsPgRepository(), + LanguagesRepository: languagesImplementation.GetLanguagesRepositoryInstance(), + BlocksRepository: blocksImplementation.GetBlocksPostgresRepositoryInstance(), } controller := LaboratoriesController{ @@ -46,4 +50,10 @@ func StartLaboratoriesRoutes(g *gin.RouterGroup) { infrastructure.WithAuthorizationMiddleware([]string{"teacher"}), controller.HandleCreateMarkdownBlock, ) + laboratoriesGroup.POST( + "/test_blocks/:laboratory_uuid", + infrastructure.WithAuthenticationMiddleware(), + infrastructure.WithAuthorizationMiddleware([]string{"teacher"}), + controller.HandleCreateTestBlock, + ) } diff --git a/src/laboratories/infrastructure/implementations/laboratories_respository.go b/src/laboratories/infrastructure/implementations/laboratories_respository.go index 8f778f0..8546d54 100644 --- a/src/laboratories/infrastructure/implementations/laboratories_respository.go +++ b/src/laboratories/infrastructure/implementations/laboratories_respository.go @@ -215,3 +215,73 @@ func (repository *LaboratoriesPostgresRepository) CreateMarkdownBlock(laboratory // Return the new block UUID return blockUUID, nil } + +func (repository *LaboratoriesPostgresRepository) CreateTestBlock(dto *dtos.CreateTestBlockDTO) (blockUUID string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + // Start transaction + tx, err := repository.Connection.BeginTx(ctx, nil) + if err != nil { + return "", err + } + defer tx.Rollback() + + // Create block index + query := ` + INSERT INTO blocks_index (laboratory_id, block_position) + VALUES ( + $1, + ( SELECT COALESCE(MAX(block_position), 0) + 1 FROM blocks_index WHERE laboratory_id = $1 ) + ) + RETURNING id + ` + + row := tx.QueryRowContext(ctx, query, dto.LaboratoryUUID) + var dbBlockIndexUUID string + if err := row.Scan(&dbBlockIndexUUID); err != nil { + return "", err + } + + // Save the archive metadata + query = ` + INSERT INTO archives (file_id) + VALUES ($1) + RETURNING id + ` + + row = tx.QueryRowContext(ctx, query, dto.TestArchiveUUID) + var dbArchiveUUID string + if err := row.Scan(&dbArchiveUUID); err != nil { + return "", err + } + + // Create test block + query = ` + INSERT INTO test_blocks (language_id, test_archive_id, laboratory_id, block_index_id, name) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + ` + + row = tx.QueryRowContext( + ctx, + query, + dto.LanguageUUID, + dbArchiveUUID, + dto.LaboratoryUUID, + dbBlockIndexUUID, + dto.Name, + ) + + if err := row.Scan(&blockUUID); err != nil { + return "", err + } + + // Commit transaction + if err := tx.Commit(); err != nil { + return "", err + } + + // Return the new block UUID + return blockUUID, nil +} diff --git a/src/laboratories/infrastructure/requests/laboratories_requests.go b/src/laboratories/infrastructure/requests/laboratories_requests.go index 8fc2855..20470be 100644 --- a/src/laboratories/infrastructure/requests/laboratories_requests.go +++ b/src/laboratories/infrastructure/requests/laboratories_requests.go @@ -36,3 +36,9 @@ func (request *UpdateLaboratoryRequest) ToDTO(laboratoryUUID string, teacherUUID DueDate: request.DueDate, } } + +type CreateTestBlockRequest struct { + LaboratoryUUID string `validate:"required,uuid4"` + LanguageUUID string `validate:"required,uuid4"` + Name string `validate:"required,min=4,max=255"` +} diff --git a/src/languages/domain/definitions/languages_repository.go b/src/languages/domain/definitions/languages_repository.go index b61fa93..985e95c 100644 --- a/src/languages/domain/definitions/languages_repository.go +++ b/src/languages/domain/definitions/languages_repository.go @@ -4,6 +4,8 @@ import "github.com/UPB-Code-Labs/main-api/src/languages/domain/entities" type LanguagesRepository interface { GetAll() (languages []*entities.Language, err error) + GetByUUID(uuid string) (language *entities.Language, err error) + GetTemplateUUIDByLanguageUUID(uuid string) (templateUUID string, err error) GetTemplateBytes(uuid string) (template []byte, err error) } diff --git a/src/languages/domain/errors/languages_errors.go b/src/languages/domain/errors/languages_errors.go index efe42c8..7b97ecd 100644 --- a/src/languages/domain/errors/languages_errors.go +++ b/src/languages/domain/errors/languages_errors.go @@ -11,16 +11,3 @@ func (err *LangNotFoundError) Error() string { func (err *LangNotFoundError) StatusCode() int { return http.StatusNotFound } - -type StaticFilesMicroserviceError struct { - Code int - Message string -} - -func (err *StaticFilesMicroserviceError) Error() string { - return err.Message -} - -func (err *StaticFilesMicroserviceError) StatusCode() int { - return err.Code -} diff --git a/src/languages/infrastructure/implementations/languages_repository.go b/src/languages/infrastructure/implementations/languages_repository.go index fd8a877..f3f1e98 100644 --- a/src/languages/infrastructure/implementations/languages_repository.go +++ b/src/languages/infrastructure/implementations/languages_repository.go @@ -78,6 +78,10 @@ func (repository *LanguagesRepository) GetByUUID(uuid string) (language *entitie language = &entities.Language{} err = row.Scan(&language.UUID, &language.TemplateArchiveUUID, &language.Name) if err != nil { + if err == sql.ErrNoRows { + return nil, &errors.LangNotFoundError{} + } + return nil, err } diff --git a/src/shared/domain/errors/errors.go b/src/shared/domain/errors/errors.go index 09851c6..2349e7d 100644 --- a/src/shared/domain/errors/errors.go +++ b/src/shared/domain/errors/errors.go @@ -4,3 +4,16 @@ type DomainError interface { Error() string StatusCode() int } + +type StaticFilesMicroserviceError struct { + Code int + Message string +} + +func (err *StaticFilesMicroserviceError) Error() string { + return err.Message +} + +func (err *StaticFilesMicroserviceError) StatusCode() int { + return err.Code +} diff --git a/src/shared/infrastructure/environment.go b/src/shared/infrastructure/environment.go index d4960f5..8347345 100644 --- a/src/shared/infrastructure/environment.go +++ b/src/shared/infrastructure/environment.go @@ -7,13 +7,23 @@ import ( ) type EnvironmentSpec struct { - Environment string `split_words:"true" default:"development"` + // Execution environment + Environment string `split_words:"true" default:"development"` + + // Connection strings DbConnectionString string `split_words:"true" default:"postgres://postgres:postgres@localhost:5432/codelabs?sslmode=disable"` - DbMigrationsPath string `split_words:"true" default:"file://sql/migrations"` - JwtSecret string `split_words:"true" default:"default"` - JwtExpirationHours int `split_words:"true" default:"6"` WebClientUrl string `split_words:"true" default:"http://localhost:5173"` StaticFilesMicroserviceAddress string `split_words:"true" default:"http://localhost:8081"` + + // PgSQL migration files + DbMigrationsPath string `split_words:"true" default:"file://sql/migrations"` + + // JWT parameters + JwtSecret string `split_words:"true" default:"default"` + JwtExpirationHours int `split_words:"true" default:"6"` + + // Configuration parameters + ArchiveMaxSizeKb int64 `split_words:"true" default:"1024"` } var environment *EnvironmentSpec diff --git a/src/shared/infrastructure/utils.go b/src/shared/infrastructure/utils.go index 361689b..c07852c 100644 --- a/src/shared/infrastructure/utils.go +++ b/src/shared/infrastructure/utils.go @@ -2,10 +2,11 @@ package infrastructure import ( "encoding/json" + "fmt" "net/http" "time" - languages_domain_errors "github.com/UPB-Code-Labs/main-api/src/languages/domain/errors" + sharedDomainErrors "github.com/UPB-Code-Labs/main-api/src/shared/domain/errors" ) func ParseISODate(date string) (time.Time, error) { @@ -14,7 +15,10 @@ func ParseISODate(date string) (time.Time, error) { } func ParseMicroserviceError(resp *http.Response, err error) error { - if err != nil || resp.StatusCode != http.StatusOK { + statusStr := fmt.Sprintf("%d", resp.StatusCode) + isInTwoHundredsGroup := statusStr[0] == '2' + + if err != nil || !isInTwoHundredsGroup { defaultErrorMessage := "There was an error while requesting the archives microservice" errorMessage := defaultErrorMessage @@ -22,7 +26,10 @@ func ParseMicroserviceError(resp *http.Response, err error) error { var responseJSON map[string]interface{} err := json.NewDecoder(resp.Body).Decode(&responseJSON) if err != nil { - return err + return &sharedDomainErrors.StaticFilesMicroserviceError{ + Code: http.StatusBadRequest, + Message: defaultErrorMessage, + } } // Get the error message @@ -32,7 +39,7 @@ func ParseMicroserviceError(resp *http.Response, err error) error { } // Return the error - return &languages_domain_errors.StaticFilesMicroserviceError{ + return &sharedDomainErrors.StaticFilesMicroserviceError{ Code: resp.StatusCode, Message: errorMessage, }