diff --git a/.gitignore b/.gitignore index dcf2de13..3613b12d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,8 @@ out/ .vscode/ backend/app/data/* backend/data/* +backend/tmp/* +backend/app/tmp/* ### Secrets ### backend/app/src/main/resources/application-secrets.properties diff --git a/backend/app/build.gradle b/backend/app/build.gradle index 8ab8a618..ee3da405 100644 --- a/backend/app/build.gradle +++ b/backend/app/build.gradle @@ -57,12 +57,15 @@ dependencies { task unitTests (type: Test){ - exclude '**/DockerSubmissionTestTest.java' + exclude '**/docker' + + useJUnitPlatform() maxHeapSize = '1G' testLogging { events "passed" } + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java index 53058ab6..7beebc7d 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/ProjectController.java @@ -311,7 +311,7 @@ public ResponseEntity getGroupsOfProject(@PathVariable Long projectId, Auth a * @return ResponseEntity with the status, no content */ @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectId}") - @Roles({UserRole.teacher}) + @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity deleteProjectById(@PathVariable long projectId, Auth auth) { CheckResult projectCheck = projectUtil.getProjectIfAdmin(projectId, auth.getUserEntity()); if (projectCheck.getStatus() != HttpStatus.OK) { diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java index a943ce41..1028825c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/SubmissionController.java @@ -6,11 +6,17 @@ import com.ugent.pidgeon.model.json.GroupJson; import com.ugent.pidgeon.model.json.LastGroupSubmissionJson; import com.ugent.pidgeon.model.json.SubmissionJson; +import com.ugent.pidgeon.model.submissionTesting.DockerOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; import com.ugent.pidgeon.model.submissionTesting.SubmissionTemplateModel; import com.ugent.pidgeon.postgre.models.*; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -25,12 +31,11 @@ import java.nio.file.Path; import java.time.OffsetDateTime; import java.util.List; -import java.util.function.Function; import java.util.logging.Logger; import java.util.zip.ZipFile; @RestController -public class SubmissionController { +public class SubmissionController { @Autowired private GroupRepository groupRepository; @@ -55,23 +60,62 @@ public class SubmissionController { private EntityToJsonConverter entityToJsonConverter; @Autowired private CommonDatabaseActions commonDatabaseActions; + @Autowired + private TestUtil testUtil; - private SubmissionTemplateModel.SubmissionResult runStructureTest(ZipFile file, TestEntity testEntity) throws IOException { - // Get the test file from the server - FileEntity testfileEntity = fileRepository.findById(testEntity.getStructureTestId()).orElse(null); - if (testfileEntity == null) { + private SubmissionTemplateModel.SubmissionResult runStructureTest(ZipFile file, TestEntity testEntity) throws IOException { + // There is no structure test for this project + if(testEntity.getStructureTemplate() == null){ return null; } - String testfile = Filehandler.getStructureTestString(Path.of(testfileEntity.getPath())); + String structureTemplateString = testEntity.getStructureTemplate(); // Parse the file SubmissionTemplateModel model = new SubmissionTemplateModel(); - model.parseSubmissionTemplate(testfile); - + model.parseSubmissionTemplate(structureTemplateString); return model.checkSubmission(file); } + private DockerOutput runDockerTest(ZipFile file, TestEntity testEntity, Path outputPath) throws IOException { + + // Get the test file from the server + String testScript = testEntity.getDockerTestScript(); + String testTemplate = testEntity.getDockerTestTemplate(); + String image = testEntity.getDockerImage(); + + // The first script must always be null, otherwise there is nothing to run on the container + if (testScript == null) { + return null; + } + + // Init container and add input files + DockerSubmissionTestModel model = new DockerSubmissionTestModel(image); + model.addZipInputFiles(file); + DockerOutput output; + + if (testTemplate == null) { + // This docker test is configured in the simple mode (store test console logs) + output = model.runSubmission(testScript); + } else { + // This docker test is configured in the template mode (store json with feedback) + output = model.runSubmissionWithTemplate(testScript, testTemplate); + } + // Get list of artifact files generated on submission + List artifacts = model.getArtifacts(); + + // Copy all files as zip into the output directory + if (artifacts != null && !artifacts.isEmpty()) { + Filehandler.copyFilesAsZip(artifacts, outputPath); + } + + // Cleanup garbage files and container + model.cleanUp(); + + return output; + + } + /** * Function to get a submission by its ID * @@ -93,8 +137,8 @@ public ResponseEntity getSubmission(@PathVariable("submissionid") long submis SubmissionEntity submission = checkResult.getData(); SubmissionJson submissionJson = entityToJsonConverter.getSubmissionJson(submission); - return ResponseEntity.ok(submissionJson); - } + return ResponseEntity.ok(submissionJson); + } /** * Function to get all submissions @@ -170,7 +214,6 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P long groupId = checkResult.getData(); - //TODO: execute the docker tests onces these are implemented try { //Save the file entry in the database to get the id FileEntity fileEntity = new FileEntity("", "", userId); @@ -200,35 +243,78 @@ public ResponseEntity submitFile(@RequestParam("file") MultipartFile file, @P fileEntity.setPath(pathname); fileRepository.save(fileEntity); + // Run structure tests + TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null); + SubmissionTemplateModel.SubmissionResult structureTestResult; + if (testEntity == null) { + Logger.getLogger("SubmissionController").info("no tests"); + submission.setStructureFeedback("No specific structure requested for this project."); + submission.setStructureAccepted(true); + } else { + + // Check file structure + structureTestResult = runStructureTest(new ZipFile(savedFile), testEntity); + if (structureTestResult == null) { + submission.setStructureFeedback( + "No specific structure requested for this project."); + submission.setStructureAccepted(true); + } else { + submission.setStructureAccepted(structureTestResult.passed); + submission.setStructureFeedback(structureTestResult.feedback); + } - // Run structure tests - TestEntity testEntity = testRepository.findByProjectId(projectid).orElse(null); - SubmissionTemplateModel.SubmissionResult testresult; - if (testEntity == null) { - Logger.getLogger("SubmissionController").info("no test"); - testresult = new SubmissionTemplateModel.SubmissionResult(true, "No structure requirements for this project."); - } else { - testresult = runStructureTest(new ZipFile(savedFile), testEntity); - } - if (testresult == null) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while running tests: test files not found"); - } - submissionRepository.save(submissionEntity); - // Update the submission with the test resultsetAccepted - submission.setStructureAccepted(testresult.passed); - submission = submissionRepository.save(submission); + if (testEntity.getDockerTestTemplate() != null) { + submission.setDockerType(DockerTestType.TEMPLATE); + } else if (testEntity.getDockerTestScript() != null) { + submission.setDockerType(DockerTestType.SIMPLE); + } else { + submission.setDockerType(DockerTestType.NONE); + } - // Update the submission with the test feedbackfiles - submission.setDockerFeedback("TEMP DOCKER FEEDBACK"); - submission.setStructureFeedback(testresult.feedback); - submissionRepository.save(submission); + // save the first feedback, without docker feedback + submissionRepository.save(submission); + + if (testEntity.getDockerTestScript() != null) { + // Define docker test as running + submission.setDockerTestState(DockerTestState.running); + // run docker tests in background + File finalSavedFile = savedFile; + CompletableFuture.runAsync(() -> { + try { + // Check if docker tests succeed + DockerOutput dockerOutput = runDockerTest(new ZipFile(finalSavedFile), testEntity, + Filehandler.getSubmissionAritfactPath(projectid, groupId, submission.getId())); + if (dockerOutput == null) { + throw new RuntimeException("Error while running docker tests."); + } + // Representation of dockerOutput, this will be a json(easily displayable in frontend) if it is a template test + // or a string if it is a simple test + submission.setDockerFeedback(dockerOutput.getFeedbackAsString()); + submission.setDockerAccepted(dockerOutput.isAllowed()); + + submission.setDockerTestState(DockerTestState.finished); + submissionRepository.save(submission); + } catch (Exception e) { + /* Log error */ + Logger.getLogger("SubmissionController").log(Level.SEVERE, e.getMessage(), e); + + submission.setDockerFeedback(""); + submission.setDockerAccepted(false); + + submission.setDockerTestState(DockerTestState.aborted); + submissionRepository.save(submission); - return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submissionEntity)); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving file: " + e.getMessage()); + } + }); } + } + return ResponseEntity.ok(entityToJsonConverter.getSubmissionJson(submissionEntity)); + } catch (IOException ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to save submissions on file server."); } + } /** * Function to get a submission file @@ -258,7 +344,10 @@ public ResponseEntity getSubmissionFile(@PathVariable("submissionid") long su // Get the file from the server try { - Resource zipFile = Filehandler.getSubmissionAsResource(Path.of(file.getPath())); + Resource zipFile = Filehandler.getFileAsResource(Path.of(file.getPath())); + if (zipFile == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found."); + } // Set headers for the response HttpHeaders headers = new HttpHeaders(); @@ -273,54 +362,35 @@ public ResponseEntity getSubmissionFile(@PathVariable("submissionid") long su } } - - public ResponseEntity getFeedbackReponseEntity(long submissionid, Auth auth, Function feedbackGetter) { - + @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/artifacts") //Route to get a submission + @Roles({UserRole.teacher, UserRole.student}) + public ResponseEntity getSubmissionArtifacts(@PathVariable("submissionid") long submissionid, Auth auth) { CheckResult checkResult = submissionUtil.canGetSubmission(submissionid, auth.getUserEntity()); if (!checkResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); } SubmissionEntity submission = checkResult.getData(); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.TEXT_PLAIN)); - return ResponseEntity.ok().headers(headers).body(feedbackGetter.apply(submission)); - } + // Get the file from the server + try { + Resource zipFile = Filehandler.getFileAsResource(Filehandler.getSubmissionAritfactPath(submission.getProjectId(), submission.getGroupId(), submission.getId())); + if (zipFile == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No artifacts found for this submission."); + } + // Set headers for the response + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + zipFile.getFilename()); + headers.add(HttpHeaders.CONTENT_TYPE, "application/zip"); - /** - * Function to get the structure feedback of a submission - * - * @param submissionid ID of the submission to get the feedback from - * @param auth authentication object of the requesting user - * @return ResponseEntity with the feedback - * @ApiDog apiDog documentation - * @HttpMethod GET - * @AllowedRoles teacher, student - * @ApiPath /api/submissions/{submissionid}/structurefeedback - */ - @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/structurefeedback") - //Route to get the structure feedback - @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getStructureFeedback(@PathVariable("submissionid") long submissionid, Auth auth) { - return getFeedbackReponseEntity(submissionid, auth, SubmissionEntity::getStructureFeedback); + return ResponseEntity.ok() + .headers(headers) + .body(zipFile); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } } - /** - * Function to get the docker feedback of a submission - * - * @param submissionid ID of the submission to get the feedback from - * @param auth authentication object of the requesting user - * @return ResponseEntity with the feedback - * @ApiDog apiDog documentation - * @HttpMethod GET - * @AllowedRoles teacher, student - * @ApiPath /api/submissions/{submissionid}/dockerfeedback - */ - @GetMapping(ApiRoutes.SUBMISSION_BASE_PATH + "/{submissionid}/dockerfeedback") //Route to get the docker feedback - @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getDockerFeedback(@PathVariable("submissionid") long submissionid, Auth auth) { - return getFeedbackReponseEntity(submissionid, auth, SubmissionEntity::getDockerFeedback); - } + /** diff --git a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java index 39bc514c..06f6ddc6 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/controllers/TestController.java @@ -3,16 +3,19 @@ import com.ugent.pidgeon.auth.Roles; import com.ugent.pidgeon.model.Auth; import com.ugent.pidgeon.model.json.TestJson; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; import com.ugent.pidgeon.postgre.models.*; import com.ugent.pidgeon.postgre.models.types.UserRole; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.io.*; import java.nio.file.Path; import java.util.Optional; @@ -36,6 +39,8 @@ public class TestController { private CommonDatabaseActions commonDatabaseActions; @Autowired private EntityToJsonConverter entityToJsonConverter; + @Autowired + private ProjectUtil projectUtil; /** * Function to update the tests of a project @@ -53,34 +58,37 @@ public class TestController { @PostMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity updateTests( - @RequestParam(name = "dockerimage", required = false) String dockerImage, - @RequestParam(name = "dockertest", required = false) MultipartFile dockerTest, - @RequestParam(name = "structuretest", required = false) MultipartFile structureTest, - @PathVariable("projectid") long projectId, - Auth auth) { - return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, structureTest, HttpMethod.POST); + @RequestParam(name = "dockerimage", required = false) String dockerImage, + @RequestParam(name = "dockerscript", required = false) String dockerTest, + @RequestParam(name = "dockertemplate", required = false) String dockerTemplate, + @RequestParam(name = "structuretest", required = false) String structureTest, + @PathVariable("projectid") long projectId, + Auth auth) { + return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, dockerTemplate, structureTest, HttpMethod.POST); } @PatchMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity patchTests( - @RequestParam(name = "dockerimage", required = false) String dockerImage, - @RequestParam(name = "dockertest", required = false) MultipartFile dockerTest, - @RequestParam(name = "structuretest", required = false) MultipartFile structureTest, - @PathVariable("projectid") long projectId, - Auth auth) { - return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, structureTest, HttpMethod.PATCH); + @RequestParam(name = "dockerimage", required = false) String dockerImage, + @RequestParam(name = "dockerscript", required = false) String dockerTest, + @RequestParam(name = "dockertemplate", required = false) String dockerTemplate, + @RequestParam(name = "structuretest", required = false) String structureTest, + @PathVariable("projectid") long projectId, + Auth auth) { + return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, dockerTemplate, structureTest, HttpMethod.PATCH); } @PutMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity putTests( @RequestParam(name = "dockerimage", required = false) String dockerImage, - @RequestParam(name = "dockertest", required = false) MultipartFile dockerTest, - @RequestParam(name = "structuretest", required = false) MultipartFile structureTest, + @RequestParam(name = "dockerscript", required = false) String dockerTest, + @RequestParam(name = "dockertemplate", required = false) String dockerTemplate, + @RequestParam(name = "structuretest", required = false) String structureTest, @PathVariable("projectid") long projectId, Auth auth) { - return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, structureTest, HttpMethod.PUT); + return alterTests(projectId, auth.getUserEntity(), dockerImage, dockerTest, dockerTemplate, structureTest, HttpMethod.PUT); } @@ -88,48 +96,91 @@ private ResponseEntity alterTests( long projectId, UserEntity user, String dockerImage, - MultipartFile dockerTest, - MultipartFile structureTest, + String dockerScript, + String dockerTemplate, + String structureTemplate, HttpMethod httpMethod ) { - CheckResult> checkResult = testUtil.checkForTestUpdate(projectId, user, dockerImage, dockerTest, structureTest, httpMethod); - if (!checkResult.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(checkResult.getStatus()).body(checkResult.getMessage()); + + + + if (dockerImage != null && dockerImage.isBlank()) { + dockerImage = null; + } + if (dockerScript != null && dockerScript.isBlank()) { + dockerScript = null; + } + if (dockerTemplate != null && dockerTemplate.isBlank()) { + dockerTemplate = null; + } + if (structureTemplate != null && structureTemplate.isBlank()) { + structureTemplate = null; } - TestEntity testEntity = checkResult.getData().getFirst(); - ProjectEntity projectEntity = checkResult.getData().getSecond(); - - try { - - // Save the files on server - long dockertestFileEntityId; - long structuretestFileEntityId; - if (dockerTest != null) { - Path dockerTestPath = Filehandler.saveTest(dockerTest, projectId); - FileEntity dockertestFileEntity = fileUtil.saveFileEntity(dockerTestPath, projectId, user.getId()); - dockertestFileEntityId = dockertestFileEntity.getId(); - } else { - dockertestFileEntityId = testEntity.getDockerTestId(); - } - if (structureTest != null) { - Path structureTestPath = Filehandler.saveTest(structureTest, projectId); - FileEntity structuretestFileEntity = fileUtil.saveFileEntity(structureTestPath, projectId, user.getId()); - structuretestFileEntityId = structuretestFileEntity.getId(); - } else { - structuretestFileEntityId = testEntity.getStructureTestId(); - } + /* LOg arguments even if null */ + System.out.println("dockerImage: " + dockerImage); + System.out.println("dockerScript: " + dockerScript); + System.out.println("dockerTemplate: " + dockerTemplate); + System.out.println("structureTemplate: " + structureTemplate); + + CheckResult> updateCheckResult = testUtil.checkForTestUpdate(projectId, user, dockerImage, dockerScript, dockerTemplate, httpMethod); - // Create/update test entity - TestEntity test = new TestEntity(dockerImage, dockertestFileEntityId, structuretestFileEntityId); - test = testRepository.save(test); - projectEntity.setTestId(test.getId()); - projectRepository.save(projectEntity); - return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(test, projectId)); - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving files: " + e.getMessage()); + + if (!updateCheckResult.getStatus().equals(HttpStatus.OK)) { + return ResponseEntity.status(updateCheckResult.getStatus()).body(updateCheckResult.getMessage()); + } + + TestEntity testEntity = updateCheckResult.getData().getFirst(); + ProjectEntity projectEntity = updateCheckResult.getData().getSecond(); + + // Creating a test entry + if(httpMethod.equals(HttpMethod.POST)){ + testEntity = new TestEntity(); } + + // Docker test + if(!(dockerImage == null && dockerScript == null && dockerTemplate == null)) { + + // update/install image if possible, do so in a seperate thread to reduce wait time. + String finalDockerImage = dockerImage; + CompletableFuture.runAsync(() -> { + if (finalDockerImage != null) { + DockerSubmissionTestModel.installImage(finalDockerImage); + } + }); + + //Update fields + if (dockerImage != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerImage(dockerImage); + if (!testRepository.imageIsUsed(dockerImage)) { + // Do it on a different thread + String finalDockerImage1 = dockerImage; + CompletableFuture.runAsync(() -> { + DockerSubmissionTestModel.removeDockerImage( + finalDockerImage1); + }); + } + } + + if (dockerScript != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestScript(dockerScript); + } + if (dockerTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setDockerTestTemplate(dockerTemplate); + } + } + + if (structureTemplate != null || !httpMethod.equals(HttpMethod.PATCH)) { + testEntity.setStructureTemplate(structureTemplate); + } + // save test entity + testEntity = testRepository.save(testEntity); + projectEntity.setTestId(testEntity.getId()); + projectRepository.save(projectEntity); // make sure to update test id in project + + return ResponseEntity.ok(entityToJsonConverter.testEntityToTestJson(testEntity, projectId)); + } @@ -147,66 +198,19 @@ private ResponseEntity alterTests( @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity getTests(@PathVariable("projectid") long projectId, Auth auth) { - CheckResult projectCheck = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); + CheckResult> projectCheck = testUtil.getTestWithAdminStatus(projectId, auth.getUserEntity()); if (!projectCheck.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(projectCheck.getStatus()).body(projectCheck.getMessage()); } - TestEntity test = projectCheck.getData(); + TestEntity test = projectCheck.getData().getFirst(); + if (!projectCheck.getData().getSecond()) { // user is not an admin, hide script and image + test.setDockerTestScript(null); + test.setDockerImage(null); + } TestJson res = entityToJsonConverter.testEntityToTestJson(test, projectId); return ResponseEntity.ok(res); } - /** - * Function to get the structure test file of a project - * @param projectId the id of the project to get the structure test file for - * @param auth the authentication object of the requesting user - * @HttpMethod GET - * @ApiDog apiDog documentation - * @AllowedRoles teacher, student - * @ApiPath /api/projects/{projectid}/tests/structuretest - * @return ResponseEntity with the structure test file - */ - @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/structuretest") - @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getStructureTestFile(@PathVariable("projectid") long projectId, Auth auth) { - return getTestFileResponseEnity(projectId, auth, TestEntity::getStructureTestId); - } - - /** - * Function to get the docker test file of a project - * @param projectId the id of the project to get the docker test file for - * @param auth the authentication object of the requesting user - * @HttpMethod GET - * @ApiDog apiDog documentation - * @AllowedRoles teacher, student - * @ApiPath /api/projects/{projectid}/tests/dockertest - * @return ResponseEntity with the docker test file - */ - @GetMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests/dockertest") - @Roles({UserRole.teacher, UserRole.student}) - public ResponseEntity getDockerTestFile(@PathVariable("projectid") long projectId, Auth auth) { - return getTestFileResponseEnity(projectId, auth, TestEntity::getDockerTestId); - } - - public ResponseEntity getTestFileResponseEnity(long projectId, Auth auth, Function testFileIdGetter) { - CheckResult projectCheck = testUtil.getTestIfAdmin(projectId, auth.getUserEntity()); - if (!projectCheck.getStatus().equals(HttpStatus.OK)) { - return ResponseEntity.status(projectCheck.getStatus()).body(projectCheck.getMessage()); - } - TestEntity testEntity = projectCheck.getData(); - - long testFileId = testFileIdGetter.apply(testEntity); - Optional fileEntity = fileRepository.findById(testFileId); - if (fileEntity.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No file found for test with id: " + testFileId); - } - Resource file = Filehandler.getFileAsResource(Path.of(fileEntity.get().getPath())); - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileEntity.get().getName()); - headers.add(HttpHeaders.CONTENT_TYPE, String.valueOf(MediaType.TEXT_PLAIN)); - return ResponseEntity.ok().headers(headers).body(file); - } - /** * Function to delete the tests of a project * @param projectId the id of the test to delete @@ -220,10 +224,11 @@ public ResponseEntity getTestFileResponseEnity(long projectId, Auth auth, Fun @DeleteMapping(ApiRoutes.PROJECT_BASE_PATH + "/{projectid}/tests") @Roles({UserRole.teacher, UserRole.student}) public ResponseEntity deleteTestById(@PathVariable("projectid") long projectId, Auth auth) { - CheckResult> updateCheckResult = testUtil.checkForTestUpdate(projectId, auth.getUserEntity(), null, null, null, HttpMethod.DELETE); + CheckResult> updateCheckResult = testUtil.checkForTestUpdate(projectId, auth.getUserEntity(), null, null, null, HttpMethod.DELETE); if (!updateCheckResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(updateCheckResult.getStatus()).body(updateCheckResult.getMessage()); } + ProjectEntity projectEntity = updateCheckResult.getData().getSecond(); TestEntity testEntity = updateCheckResult.getData().getFirst(); @@ -231,7 +236,6 @@ public ResponseEntity deleteTestById(@PathVariable("projectid") long projectI if (!deleteResult.getStatus().equals(HttpStatus.OK)) { return ResponseEntity.status(deleteResult.getStatus()).body(deleteResult.getMessage()); } - return ResponseEntity.ok().build(); } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/DockerTestFeedbackJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/DockerTestFeedbackJson.java new file mode 100644 index 00000000..1fae8f07 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/DockerTestFeedbackJson.java @@ -0,0 +1,14 @@ +package com.ugent.pidgeon.model.json; + + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; + +@JsonSerialize(using = DockerTestFeedbackJsonSerializer.class) +public record DockerTestFeedbackJson( + DockerTestType type, + String feedback, + boolean allowed +) { + +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/DockerTestFeedbackJsonSerializer.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/DockerTestFeedbackJsonSerializer.java new file mode 100644 index 00000000..29e541ad --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/DockerTestFeedbackJsonSerializer.java @@ -0,0 +1,24 @@ +package com.ugent.pidgeon.model.json; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; +import java.io.IOException; + +public class DockerTestFeedbackJsonSerializer extends JsonSerializer { + + @Override + public void serialize(DockerTestFeedbackJson value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeStringField("type", value.type().toString()); + if (value.type() == DockerTestType.TEMPLATE) { + gen.writeFieldName("feedback"); + gen.writeRawValue(value.feedback().replace("\n", "\\n")); + } else { + gen.writeStringField("feedback", value.feedback()); + } + gen.writeBooleanField("allowed", value.allowed()); + gen.writeEndObject(); + } +} + diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java index 49096d42..1128783a 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/SubmissionJson.java @@ -14,29 +14,26 @@ public class SubmissionJson { private String fileUrl; private Boolean structureAccepted; - private Boolean dockerAccepted; + private String dockerStatus; @JsonSerialize(using = OffsetDateTimeSerializer.class) private OffsetDateTime submissionTime; - private String structureFeedbackUrl; + private String structureFeedback; - public String getDockerFeedbackUrl() { - return dockerFeedbackUrl; - } - public void setDockerFeedbackUrl(String dockerFeedbackUrl) { - this.dockerFeedbackUrl = dockerFeedbackUrl; - } + private DockerTestFeedbackJson dockerFeedback; + private String artifactUrl; + - private String dockerFeedbackUrl; public SubmissionJson() { } public SubmissionJson( long id, String projectUrl, String groupUrl, Long projectId, Long groupId, String fileUrl, - Boolean structureAccepted, OffsetDateTime submissionTime, Boolean dockerAccepted, String structureFeedbackUrl, String dockerFeedbackUrl) { + Boolean structureAccepted, OffsetDateTime submissionTime, String structureFeedback, DockerTestFeedbackJson dockerFeedback, String dockerStatus, + String artifactUrl) { this.submissionId = id; this.projectUrl = projectUrl; this.groupUrl = groupUrl; @@ -45,9 +42,10 @@ public SubmissionJson( this.fileUrl = fileUrl; this.structureAccepted = structureAccepted; this.submissionTime = submissionTime; - this.dockerAccepted = dockerAccepted; - this.structureFeedbackUrl = structureFeedbackUrl; - this.dockerFeedbackUrl = dockerFeedbackUrl; + this.dockerFeedback = dockerFeedback; + this.structureFeedback = structureFeedback; + this.dockerStatus = dockerStatus; + this.artifactUrl = artifactUrl; } public long getSubmissionId() { @@ -98,20 +96,14 @@ public void setSubmissionTime(OffsetDateTime submissionTime) { this.submissionTime = submissionTime; } - public Boolean getDockerAccepted() { - return dockerAccepted; - } - public void setDockerAccepted(Boolean dockerAccepted) { - this.dockerAccepted = dockerAccepted; - } - public String getStructureFeedbackUrl() { - return structureFeedbackUrl; + public String getStructureFeedback() { + return structureFeedback; } - public void setStructureFeedbackUrl(String structureFeedbackUrl) { - this.structureFeedbackUrl = structureFeedbackUrl; + public void setStructureFeedback(String structureFeedback) { + this.structureFeedback = structureFeedback; } public Long getProjectId() { @@ -129,4 +121,28 @@ public Long getGroupId() { public void setGroupId(Long groupId) { this.groupId = groupId; } + + public String getDockerStatus() { + return dockerStatus; + } + + public void setDockerStatus(String dockerStatus) { + this.dockerStatus = dockerStatus; + } + + public DockerTestFeedbackJson getDockerFeedback() { + return dockerFeedback; + } + + public void setDockerFeedback(DockerTestFeedbackJson dockerFeedback) { + this.dockerFeedback = dockerFeedback; + } + + public String getArtifactUrl() { + return artifactUrl; + } + + public void setArtifactUrl(String artifactUrl) { + this.artifactUrl = artifactUrl; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java b/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java index 35c5e039..e2c0d034 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/json/TestJson.java @@ -3,17 +3,20 @@ public class TestJson { private String projectUrl; private String dockerImage; - private String dockerTestUrl; - private String structureTestUrl; + private String dockerScript; + private String dockerTemplate; + private String structureTest; public TestJson() { } - public TestJson(String projectUrl, String dockerImage, String dockerTestUrl, String structureTestUrl) { + public TestJson(String projectUrl, String dockerImage, String dockerScript, + String dockerTemplate, String structureTest) { this.projectUrl = projectUrl; this.dockerImage = dockerImage; - this.dockerTestUrl = dockerTestUrl; - this.structureTestUrl = structureTestUrl; + this.dockerScript = dockerScript; + this.dockerTemplate = dockerTemplate; + this.structureTest = structureTest; } public String getProjectUrl() { @@ -32,19 +35,27 @@ public void setDockerImage(String dockerImage) { this.dockerImage = dockerImage; } - public String getDockerTestUrl() { - return dockerTestUrl; + public String getDockerScript() { + return dockerScript; } - public void setDockerTestUrl(String dockerTestUrl) { - this.dockerTestUrl = dockerTestUrl; + public void setDockerScript(String dockerScript) { + this.dockerScript = dockerScript; } - public String getStructureTestUrl() { - return structureTestUrl; + public String getStructureTest() { + return structureTest; } - public void setStructureTestUrl(String structureTestUrl) { - this.structureTestUrl = structureTestUrl; + public void setStructureTest(String structureTest) { + this.structureTest = structureTest; + } + + public String getDockerTemplate() { + return dockerTemplate; + } + + public void setDockerTemplate(String dockerTemplate) { + this.dockerTemplate = dockerTemplate; } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerOutput.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerOutput.java new file mode 100644 index 00000000..4fc07377 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerOutput.java @@ -0,0 +1,8 @@ +package com.ugent.pidgeon.model.submissionTesting; + +import java.util.List; + +public interface DockerOutput { + public boolean isAllowed(); + public String getFeedbackAsString(); +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java index 872e9afa..7e67c969 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubmissionTestModel.java @@ -16,7 +16,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Enumeration; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.apache.commons.io.FileUtils; public class DockerSubmissionTestModel { @@ -48,6 +51,11 @@ public DockerSubmissionTestModel(String dockerImage) { createFolder(); // Create the folder after we// generate tmp folder of project // Configure container with volume bindings container.withHostConfig(new HostConfig().withBinds(new Bind(localMountFolder, sharedVolume))); + + // Init directories in the shared folder + new File(localMountFolder + "input/").mkdirs(); + new File(localMountFolder + "output/").mkdirs(); + new File(localMountFolder + "artifacts/").mkdirs(); } @@ -63,31 +71,50 @@ private void removeFolder() { // clear shared folder } } - public DockerTestOutput runSubmission(String script) throws InterruptedException { - return runSubmission(script, new File[0]); + // function for deleting shared docker files, only use after catching the artifacts + public void cleanUp() { + removeFolder(); } - private void runContainer(String script, File[] inputFiles, ResultCallback.Adapter callback) { - - // Init directories in the shared folder - new File(localMountFolder + "input/").mkdirs(); - new File(localMountFolder + "output/").mkdirs(); - - // Copy input files to the shared folder - for (File file : inputFiles) { + public void addInputFiles(File[] files) { + for (File file : files) { try { FileUtils.copyFileToDirectory(file, new File(localMountFolder + "input/")); } catch (IOException e) { e.printStackTrace(); } } + } + + public void addZipInputFiles(ZipFile zipFile) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File entryDestination = new File(localMountFolder + "input/", entry.getName()); + if (entry.isDirectory()) { + entryDestination.mkdirs(); + } else { + File parent = entryDestination.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try { + FileUtils.copyInputStreamToFile(zipFile.getInputStream(entry), entryDestination); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private void runContainer(String script, ResultCallback.Adapter callback) { // Configure and start the container container.withCmd("/bin/sh", "-c", script); CreateContainerResponse responseContainer = container.exec(); String executionContainerID = responseContainer.getId(); // Use correct ID for operations dockerClient.startContainerCmd(executionContainerID).exec(); - try{ + try { dockerClient.logContainerCmd(executionContainerID) .withStdOut(true) .withStdErr(true) @@ -95,7 +122,7 @@ private void runContainer(String script, File[] inputFiles, ResultCallback.Adapt .withTailAll() .exec(callback) .awaitCompletion(); - }catch (InterruptedException e){ + } catch (InterruptedException e) { System.err.println("Failed to read output file. Push is denied."); } @@ -107,8 +134,7 @@ private void runContainer(String script, File[] inputFiles, ResultCallback.Adapt } - public DockerTestOutput runSubmission(String script, File[] inputFiles) - { + public DockerTestOutput runSubmission(String script) { List consoleLogs = new ArrayList<>(); ResultCallback.Adapter callback = new ResultCallback.Adapter<>() { @@ -117,7 +143,7 @@ public void onNext(Frame item) { consoleLogs.add(new String(item.getPayload())); } }; - runContainer(script, inputFiles, callback); + runContainer(script, callback); boolean allowPush; @@ -133,15 +159,12 @@ public void onNext(Frame item) { allowPush = false; } - // Cleanup - removeFolder(); return new DockerTestOutput(consoleLogs, allowPush); } - public DockerTemplateTestResult runSubmissionWithTemplate(String script, String template, - File[] inputFiles) throws InterruptedException { + public DockerTemplateTestOutput runSubmissionWithTemplate(String script, String template) { - runContainer(script, inputFiles, new Adapter<>()); + runContainer(script, new Adapter<>()); // execute dockerClient and await @@ -171,9 +194,6 @@ public DockerTemplateTestResult runSubmissionWithTemplate(String script, String } } - // Cleanup - removeFolder(); - // Check if allowed boolean allowed = true; for (DockerSubtestResult result : results) { @@ -183,7 +203,7 @@ public DockerTemplateTestResult runSubmissionWithTemplate(String script, String } } - return new DockerTemplateTestResult(results, allowed); + return new DockerTemplateTestOutput(results, allowed); } private static DockerSubtestResult getDockerSubtestResult(String entry) { @@ -217,15 +237,29 @@ private static DockerSubtestResult getDockerSubtestResult(String entry) { templateEntry.setRequired(true); } else if (currentOption.equalsIgnoreCase(">Optional")) { templateEntry.setRequired(false); - } else if (currentOption.substring(0, 12).equalsIgnoreCase(">Description")) { + } else if (currentOption.length() >=13 && currentOption.substring(0, 13).equalsIgnoreCase(">Description=")) { templateEntry.setTestDescription(currentOption.split("=\"")[1].split("\"")[0]); } } - templateEntry.setCorrect(entry.substring(lineIterator)); + String substring = entry.substring(lineIterator); + if (substring.endsWith("\n")) { + substring = substring.substring(0, substring.length() - 1); + } + templateEntry.setCorrect(substring); return templateEntry; } - public static void addDocker(String imageName) { + public List getArtifacts() { + List files = new ArrayList<>(); + File[] filesInFolder = new File(localMountFolder + "artifacts/").listFiles(); + if (filesInFolder != null) { + files.addAll(Arrays.asList(filesInFolder)); + } + return files; + } + + + public static void installImage(String imageName) { DockerClient dockerClient = DockerClientInstance.getInstance(); // Pull the Docker image (if not already present) @@ -247,4 +281,50 @@ public static void removeDockerImage(String imageName) { System.out.println("Failed removing docker image: " + e.getMessage()); } } + + public static boolean imageExists(String image) { + DockerClient dockerClient = DockerClientInstance.getInstance(); + try { + dockerClient.inspectImageCmd(image).exec(); + } catch (Exception e) { + return false; + } + return true; + } + + public static boolean isValidTemplate(String template) { + // lines with @ should be the first of a string + // @ is always the first character + // ">" options under the template should be "required, optional or description="..." + boolean atLeastOne = false; // Template should not be empty + String[] lines = template.split("\n"); + if (lines[0].charAt(0) != '@') { + return false; + } + boolean isConfigurationLine = false; + for (String line : lines) { + if(line.length() == 0){ // skip line if empty + continue; + } + if (line.charAt(0) == '@') { + atLeastOne = true; + isConfigurationLine = true; + continue; + } + if (isConfigurationLine) { + if (line.charAt(0) == '>') { + boolean isDescription = line.length() >= 13 && line.substring(0, 13).equalsIgnoreCase(">Description="); + // option lines + if (!line.equalsIgnoreCase(">Required") && !line.equalsIgnoreCase(">Optional") + && !isDescription) { + return false; + } + } else { + isConfigurationLine = false; + } + } + } + return atLeastOne; + } + } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java index 43869cc9..c0c70fcd 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerSubtestResult.java @@ -1,6 +1,6 @@ package com.ugent.pidgeon.model.submissionTesting; -public class DockerSubtestResult { +public class DockerSubtestResult implements DockerOutput { private String correct; private String output; private String testName; @@ -59,4 +59,17 @@ public boolean isRequired() { public void setRequired(boolean required) { this.required = required; } + + @Override + public boolean isAllowed() { + return correct.equals(output); + } + + @Override + public String getFeedbackAsString() { + // Display feedback as a json, only display testName and testDescription if they are not empty + String testDescription = this.testDescription.isEmpty() ? "" : "\",\"testDescription\":\"" + this.testDescription; + //TODO add allowed to json + return "{\"testName\":\"" + testName + testDescription + "\",\"correct\":\"" + correct + "\",\"output\":\"" + output + "\", \"required\":" + required + ", \"succes\": " + isAllowed() + "}"; + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestOutput.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestOutput.java new file mode 100644 index 00000000..78d19c4d --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestOutput.java @@ -0,0 +1,32 @@ +package com.ugent.pidgeon.model.submissionTesting; + +import java.util.List; + +public class DockerTemplateTestOutput implements DockerOutput{ + private List subtestResults; + private boolean allowed; + + public List getSubtestResults() { + return subtestResults; + } + + @Override + public boolean isAllowed() { + return allowed; + } + + public DockerTemplateTestOutput(List subtestResults, boolean allowed) { + this.subtestResults = subtestResults; + this.allowed = allowed; + } + @Override + public String getFeedbackAsString(){ + //json representation of the tests + StringBuilder feedback = new StringBuilder("{\"subtests\": ["); + for (DockerSubtestResult subtestResult : subtestResults) { + feedback.append(subtestResult.getFeedbackAsString()).append(","); + } + feedback.append("]"); + return feedback.toString(); + } +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestResult.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestResult.java deleted file mode 100644 index 1ece985d..00000000 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTemplateTestResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ugent.pidgeon.model.submissionTesting; - -import java.util.List; - -public class DockerTemplateTestResult { - private List subtestResults; - private boolean allowed; - - public List getSubtestResults() { - return subtestResults; - } - - public boolean isAllowed() { - return allowed; - } - - public DockerTemplateTestResult(List subtestResults, boolean allowed) { - this.subtestResults = subtestResults; - this.allowed = allowed; - } -} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java index 25e27dc5..6b9e520a 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/model/submissionTesting/DockerTestOutput.java @@ -2,7 +2,7 @@ import java.util.List; -public class DockerTestOutput { +public class DockerTestOutput implements DockerOutput { public List logs; public Boolean allowed; @@ -11,4 +11,13 @@ public DockerTestOutput(List logs, Boolean allowed) { this.allowed = allowed; } + @Override + public boolean isAllowed() { + return allowed; + } + + @Override + public String getFeedbackAsString() { + return String.join("", logs); + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java index 65803ff0..2ecdcd8e 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/SubmissionEntity.java @@ -1,5 +1,7 @@ package com.ugent.pidgeon.postgre.models; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; import jakarta.persistence.*; import java.time.OffsetDateTime; @@ -36,6 +38,12 @@ public class SubmissionEntity { @Column(name="docker_feedback") private String dockerFeedback; + @Column(name="docker_test_state") + private String dockerTestState; + + @Column(name="docker_type") + private String dockerType; + public SubmissionEntity() { } @@ -116,4 +124,35 @@ public String getDockerFeedback() { public void setDockerFeedback(String dockerFeedbackFileId) { this.dockerFeedback = dockerFeedbackFileId; } + public DockerTestState getDockerTestState() { + if(dockerTestState == null) { + return DockerTestState.no_test; + } + return switch (dockerTestState) { + case "running" -> DockerTestState.running; + case "finished" -> DockerTestState.finished; + case "aborted" -> DockerTestState.aborted; + default -> null; + }; + } + + public void setDockerTestState(DockerTestState dockerTestState) { + this.dockerTestState = dockerTestState.toString(); + } + + public DockerTestType getDockerTestType() { + if (dockerType == null) { + return DockerTestType.NONE; + } + return switch (dockerType) { + case "SIMPLE" -> DockerTestType.SIMPLE; + case "TEMPLATE" -> DockerTestType.TEMPLATE; + case "NONE" -> DockerTestType.NONE; + default -> null; + }; + } + + public void setDockerType(DockerTestType dockerType) { + this.dockerType = dockerType.toString(); + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java index f0aac3e0..885c6a49 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/TestEntity.java @@ -15,30 +15,44 @@ public class TestEntity { @Column(name = "docker_image") private String dockerImage; - @Column(name = "docker_test") - private long dockerTestId; + @Column(name = "docker_test_script") + private String dockerTestScript; - @Column(name = "structure_test_id") - private long structureTestId; + @Column(name = "docker_test_template") + private String dockerTestTemplate; - public TestEntity() { - } + @Column(name = "structure_template") + private String structureTemplate; - public TestEntity(String dockerImage, long dockerTestId, long structureTestId) { + public TestEntity(String dockerImage, String docker_test_script, + String dockerTestTemplate, + String structureTemplate) { this.dockerImage = dockerImage; - this.dockerTestId = dockerTestId; - this.structureTestId = structureTestId; + this.dockerTestScript = docker_test_script; + this.dockerTestTemplate = dockerTestTemplate; + this.structureTemplate = structureTemplate; } + public TestEntity() { + + } - public void setId(Long id) { - this.id = id; + public String getDockerTestScript() { + return dockerTestScript; } - public Long getId() { + public void setDockerTestScript(String docker_test_script) { + this.dockerTestScript = docker_test_script; + } + + public long getId() { return id; } + public void setId(long id) { + this.id = id; + } + public String getDockerImage() { return dockerImage; } @@ -47,19 +61,19 @@ public void setDockerImage(String dockerImage) { this.dockerImage = dockerImage; } - public long getDockerTestId() { - return dockerTestId; + public String getDockerTestTemplate() { + return dockerTestTemplate; } - public void setDockerTestId(long dockerTest) { - this.dockerTestId = dockerTest; + public void setDockerTestTemplate(String dockerTestTemplate) { + this.dockerTestTemplate = dockerTestTemplate; } - public long getStructureTestId() { - return structureTestId; + public String getStructureTemplate() { + return structureTemplate; } - public void setStructureTestId(long structureTestId) { - this.structureTestId = structureTestId; + public void setStructureTemplate(String structureTemplate) { + this.structureTemplate = structureTemplate; } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestState.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestState.java new file mode 100644 index 00000000..e1a84616 --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestState.java @@ -0,0 +1,9 @@ +package com.ugent.pidgeon.postgre.models.types; + +public enum DockerTestState { + running, + finished, + aborted, + + no_test +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestType.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestType.java new file mode 100644 index 00000000..eefd122c --- /dev/null +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/models/types/DockerTestType.java @@ -0,0 +1,7 @@ +package com.ugent.pidgeon.postgre.models.types; + +public enum DockerTestType { + SIMPLE, + TEMPLATE, + NONE +} diff --git a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java index 7a0c5fb6..744d016c 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/postgre/repository/TestRepository.java @@ -7,6 +7,14 @@ import java.util.Optional; public interface TestRepository extends JpaRepository { + @Query(value = """ + SELECT CASE WHEN EXISTS (SELECT t FROM TestEntity t WHERE t.dockerImage = ?1) + THEN true + ELSE false + END + """) + boolean imageIsUsed(String image); + @Query(value ="SELECT t FROM ProjectEntity p JOIN TestEntity t ON p.testId = t.id WHERE p.id = ?1") Optional findByProjectId(long projectId); diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java index d05bf63e..a4e64a58 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/CommonDatabaseActions.java @@ -1,6 +1,8 @@ package com.ugent.pidgeon.util; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; import com.ugent.pidgeon.postgre.models.*; import com.ugent.pidgeon.postgre.models.types.CourseRelation; import com.ugent.pidgeon.postgre.repository.*; @@ -131,8 +133,6 @@ public CheckResult deleteProject(long projectId) { } } - projectRepository.delete(projectEntity); - if (projectEntity.getTestId() != null) { TestEntity testEntity = testRepository.findById(projectEntity.getTestId()).orElse(null); if (testEntity == null) { @@ -142,7 +142,7 @@ public CheckResult deleteProject(long projectId) { return delRes; } - + projectRepository.delete(projectEntity); return new CheckResult<>(HttpStatus.OK, "", null); } catch (Exception e) { @@ -177,16 +177,13 @@ public CheckResult deleteSubmissionById(long submissionId) { * @return CheckResult with the status of the deletion */ public CheckResult deleteTestById(ProjectEntity projectEntity, TestEntity testEntity) { - try { - testRepository.deleteById(testEntity.getId()) ; - CheckResult checkAndDeleteRes = fileUtil.deleteFileById(testEntity.getStructureTestId()); - if (!checkAndDeleteRes.getStatus().equals(HttpStatus.OK)) { - return checkAndDeleteRes; - } - return fileUtil.deleteFileById(testEntity.getDockerTestId()); - } catch (Exception e) { - return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while deleting test", null); + projectEntity.setTestId(null); + projectRepository.save(projectEntity); + testRepository.deleteById(testEntity.getId()); + if(!testRepository.imageIsUsed(testEntity.getDockerImage())){ + DockerSubmissionTestModel.removeDockerImage(testEntity.getDockerImage()); } + return new CheckResult<>(HttpStatus.OK, "", null); } /** @@ -318,7 +315,7 @@ public CheckResult copyProject(ProjectEntity project, long course if (project.getTestId() != null) { TestEntity test = testRepository.findById(project.getTestId()).orElse(null); if (test != null) { - CheckResult checkResult = copyTest(test, newProject.getId()); + CheckResult checkResult = copyTest(test); if (!checkResult.getStatus().equals(HttpStatus.OK)) { return new CheckResult<>(checkResult.getStatus(), checkResult.getMessage(), null); } @@ -334,62 +331,20 @@ public CheckResult copyProject(ProjectEntity project, long course } /** - * Copy a test and all its related data. Assumes that permissions are already checked + * Copy a test and all its related data. Assumes that permissions are already checked and that the parameters are valid. * @param test test that needs to be copied - * @param projectId id of the project the test is linked to * @return CheckResult with the status of the copy and the new test */ - public CheckResult copyTest(TestEntity test, long projectId) { + public CheckResult copyTest(TestEntity test) { // Copy the test TestEntity newTest = new TestEntity( test.getDockerImage(), - test.getDockerTestId(), - test.getStructureTestId() + test.getDockerTestScript(), + test.getDockerTestTemplate(), + test.getStructureTemplate() ); - // Copy the files linked to the test - try { - FileEntity dockerFile = fileRepository.findById(test.getDockerTestId()).orElse(null); - FileEntity structureFile = fileRepository.findById(test.getStructureTestId()).orElse(null); - if (dockerFile == null || structureFile == null) { - return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying test", null); - } - - CheckResult copyDockRes = copyTestFile(dockerFile, projectId); - if (!copyDockRes.getStatus().equals(HttpStatus.OK)) { - return new CheckResult<>(copyDockRes.getStatus(), copyDockRes.getMessage(), null); - } - newTest.setDockerTestId(copyDockRes.getData().getId()); - - CheckResult copyStructRes = copyTestFile(structureFile, projectId); - if (!copyStructRes.getStatus().equals(HttpStatus.OK)) { - return new CheckResult<>(copyStructRes.getStatus(), copyStructRes.getMessage(), null); - } - newTest.setStructureTestId(copyStructRes.getData().getId()); - } catch (Exception e) { - return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying test", null); - } - newTest = testRepository.save(newTest); return new CheckResult<>(HttpStatus.OK, "", newTest); } - - - /** - * Copy a file and all its related data. Assumes that permissions are already checked - * @param file file to copy - * @param projectId id of the project the file is linked to - * @return CheckResult with the status of the copy and the new file - */ - public CheckResult copyTestFile(FileEntity file, long projectId) { - // Copy the file - try { - Path newPath = Filehandler.copyTest(Path.of(file.getPath()), projectId); - FileEntity newFile = new FileEntity(newPath.getFileName().toString(), newPath.toString(), file.getUploadedBy()); - newFile = fileRepository.save(newFile); - return new CheckResult<>(HttpStatus.OK, "", newFile); - } catch (Exception e) { - return new CheckResult<>(HttpStatus.INTERNAL_SERVER_ERROR, "Error while copying file", null); - } - } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java b/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java index 4ee77c8b..a4813f59 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/DockerClientInstance.java @@ -21,8 +21,8 @@ private DockerClientInstance() { public static synchronized DockerClient getInstance() { if (dockerClient == null) { - DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder() - .withDockerHost("tcp://10.5.0.4:2375").build(); + + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() .dockerHost(config.getDockerHost()) .sslConfig(config.getSSLConfig()) diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java index 9056821a..02b98182 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/EntityToJsonConverter.java @@ -6,10 +6,10 @@ import com.ugent.pidgeon.model.json.*; import com.ugent.pidgeon.postgre.models.*; import com.ugent.pidgeon.postgre.models.types.CourseRelation; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; import com.ugent.pidgeon.postgre.repository.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import java.util.List; @@ -35,9 +35,13 @@ public class EntityToJsonConverter { private SubmissionRepository submissionRepository; @Autowired private ClusterUtil clusterUtil; + @Autowired + private TestUtil testUtil; + @Autowired + private TestRepository testRepository; - public GroupJson groupEntityToJson(GroupEntity groupEntity) { + public GroupJson groupEntityToJson(GroupEntity groupEntity) { GroupClusterEntity cluster = groupClusterRepository.findById(groupEntity.getClusterId()).orElse(null); GroupJson group = new GroupJson(cluster.getMaxSize(), groupEntity.getId(), groupEntity.getName(), ApiRoutes.CLUSTER_BASE_PATH + "/" + groupEntity.getClusterId()); if (cluster != null && cluster.getGroupAmount() > 1){ @@ -224,6 +228,18 @@ public CourseReferenceJson courseEntityToCourseReference(CourseEntity course) { public SubmissionJson getSubmissionJson(SubmissionEntity submission) { + DockerTestFeedbackJson feedback; + TestEntity test = testRepository.findByProjectId(submission.getProjectId()).orElse(null); + if (submission.getDockerTestState().equals(DockerTestState.running)) { + feedback = null; + } else if (submission.getDockerTestType().equals(DockerTestType.NONE)) { + feedback = new DockerTestFeedbackJson(DockerTestType.NONE, "", true); + } + else if (submission.getDockerTestType().equals(DockerTestType.SIMPLE)) { + feedback = new DockerTestFeedbackJson(DockerTestType.SIMPLE, submission.getDockerFeedback(), submission.getDockerAccepted()); + } else { + feedback = new DockerTestFeedbackJson(DockerTestType.TEMPLATE, submission.getDockerFeedback(), submission.getDockerAccepted()); + } return new SubmissionJson( submission.getId(), ApiRoutes.PROJECT_BASE_PATH + "/" + submission.getProjectId(), @@ -233,18 +249,20 @@ public SubmissionJson getSubmissionJson(SubmissionEntity submission) { ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/file", submission.getStructureAccepted(), submission.getSubmissionTime(), - submission.getDockerAccepted(), - ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/structurefeedback", - ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/dockerfeedback" + submission.getStructureFeedback(), + feedback, + submission.getDockerTestState().toString(), + ApiRoutes.SUBMISSION_BASE_PATH + "/" + submission.getId() + "/artifacts" ); } public TestJson testEntityToTestJson(TestEntity testEntity, long projectId) { return new TestJson( ApiRoutes.PROJECT_BASE_PATH + "/" + projectId, - testEntity.getDockerImage(), - ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/tests/dockertest", - ApiRoutes.PROJECT_BASE_PATH + "/" + projectId + "/tests/structuretest" + testEntity.getDockerImage(), + testEntity.getDockerTestScript(), + testEntity.getDockerTestTemplate(), + testEntity.getStructureTemplate() ); } } \ No newline at end of file diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java index 3051422a..3475b43d 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java @@ -2,6 +2,9 @@ import com.ugent.pidgeon.postgre.models.FileEntity; import com.ugent.pidgeon.postgre.repository.FileRepository; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.apache.tika.Tika; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.InputStreamResource; @@ -123,6 +126,10 @@ static public Path getSubmissionPath(long projectid, long groupid, long submissi return Path.of(BASEPATH,"projects", String.valueOf(projectid), String.valueOf(groupid), String.valueOf(submissionid)); } + static public Path getSubmissionAritfactPath(long projectid, long groupid, long submissionid) { + return getSubmissionPath(projectid, groupid, submissionid).resolve("artifacts.zip"); + } + /** * Get the path were a test is stored * @param projectid id of the project @@ -138,6 +145,9 @@ static public Path getTestPath(long projectid) { * @return the file as a resource */ public static Resource getFileAsResource(Path path) { + if (!Files.exists(path)) { + return null; + } File file = path.toFile(); return new FileSystemResource(file); } @@ -161,15 +171,6 @@ public static boolean isZipFile(File file) throws IOException { } - /** - * Get a submission as a resource - * @param path path of the submission - * @return the submission as a resource - * @throws IOException if an error occurs while getting the submission - */ - public static Resource getSubmissionAsResource(Path path) throws IOException { - return new InputStreamResource(new FileInputStream(path.toFile())); - } /** * Save a file to the server @@ -238,4 +239,39 @@ public static String getStructureTestString(Path path) throws IOException { throw new IOException("Error while reading testfile: " + e.getMessage()); } } + + /** + * A function for copying internally made lists of files, to a required path. + * @param files list of files to copy + * @param path path to copy the files to + * @throws IOException if an error occurs while copying the files + */ + public static void copyFilesAsZip(List files, Path path) throws IOException { + // Write directly to a zip file in the path variable + File zipFile = new File(path.toString()); + System.out.println(zipFile.getAbsolutePath()); + Logger.getGlobal().info("Filexists: " + zipFile.exists()); + if (zipFile.exists() && !zipFile.canWrite()) { + Logger.getGlobal().info("Setting writable"); + boolean res = zipFile.setWritable(true); + if (!res) { + throw new IOException("Cannot write to zip file"); + } + } + + try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) { + for (File file : files) { + // add file to zip + zipOutputStream.putNextEntry(new ZipEntry(file.getName())); + FileInputStream fileInputStream = new FileInputStream(file); + byte[] buffer = new byte[1024]; + int len; + while ((len = fileInputStream.read(buffer)) > 0) { + zipOutputStream.write(buffer, 0, len); + } + fileInputStream.close(); + zipOutputStream.closeEntry(); + } + } + } } diff --git a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java index bf359735..d663dfd7 100644 --- a/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java +++ b/backend/app/src/main/java/com/ugent/pidgeon/util/TestUtil.java @@ -2,10 +2,13 @@ import com.ugent.pidgeon.controllers.ApiRoutes; import com.ugent.pidgeon.model.json.TestJson; +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; import com.ugent.pidgeon.postgre.models.ProjectEntity; import com.ugent.pidgeon.postgre.models.TestEntity; import com.ugent.pidgeon.postgre.models.UserEntity; import com.ugent.pidgeon.postgre.repository.TestRepository; +import java.util.logging.Level; +import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -31,12 +34,12 @@ public TestEntity getTestIfExists(long projectId) { } /** - * Check if a user can get update a test + * Check if a user can update a test * @param projectId id of the project * @param user user that wants to update the test * @param dockerImage docker image for the test - * @param dockerTest docker test file - * @param structureTest structure test file + * @param dockerScript docker script for the test + * @param dockerTemplate docker template for the test * @param httpMethod http method used to update the test * @return CheckResult with the status of the check and the test and project */ @@ -44,10 +47,20 @@ public CheckResult> checkForTestUpdate( long projectId, UserEntity user, String dockerImage, - MultipartFile dockerTest, - MultipartFile structureTest, + String dockerScript, + String dockerTemplate, HttpMethod httpMethod ) { + /* Log arguments */ + Logger logger = Logger.getGlobal(); + logger.log(Level.INFO, "=========="); + logger.log(Level.INFO, "projectId: " + projectId); + logger.log(Level.INFO, "user: " + user); + logger.log(Level.INFO, "dockerImage: " + dockerImage); + logger.log(Level.INFO, "dockerScript: " + dockerScript); + logger.log(Level.INFO, "dockerTemplate: " + dockerTemplate); + logger.log(Level.INFO, "httpMethod: " + httpMethod); + CheckResult projectCheck = projectUtil.getProjectIfAdmin(projectId, user); if (!projectCheck.getStatus().equals(HttpStatus.OK)) { return new CheckResult<>(projectCheck.getStatus(), projectCheck.getMessage(), null); @@ -67,10 +80,28 @@ public CheckResult> checkForTestUpdate( return new CheckResult<>(HttpStatus.CONFLICT, "Tests already exist for this project", null); } - if (!httpMethod.equals(HttpMethod.PATCH)) { - if (dockerImage == null || dockerTest == null || structureTest == null) { - return new CheckResult<>(HttpStatus.BAD_REQUEST, "Missing parameters: dockerimage (string), dockertest (file), structuretest (file) are required", null); - } + if(!httpMethod.equals(HttpMethod.PATCH) && dockerImage != null && dockerScript == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A test script is required in a docker test.", null); + } + + if(!httpMethod.equals(HttpMethod.PATCH) && dockerScript != null && dockerImage == null && DockerSubmissionTestModel.imageExists(dockerImage)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A valid docker image is required in a docker test.", null); + } + + if (!httpMethod.equals(HttpMethod.PATCH) && dockerTemplate != null && dockerScript == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "A test script and image are required in a docker template test.", null); + } + + if(httpMethod.equals(HttpMethod.PATCH) && dockerScript != null && testEntity.getDockerImage() == null && dockerImage == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "No docker image is configured for this test", null); + } + + if(httpMethod.equals(HttpMethod.PATCH) && dockerImage != null && testEntity.getDockerTestScript() == null && dockerScript == null) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "No docker test script is configured for this test", null); + } + + if(dockerTemplate != null && !DockerSubmissionTestModel.isValidTemplate(dockerTemplate)) { + return new CheckResult<>(HttpStatus.BAD_REQUEST, "Invalid docker template", null); } return new CheckResult<>(HttpStatus.OK, "", new Pair<>(testEntity, projectEntity)); @@ -96,5 +127,26 @@ public CheckResult getTestIfAdmin(long projectId, UserEntity user) { return new CheckResult<>(HttpStatus.OK, "", testEntity); } + public CheckResult> getTestWithAdminStatus(long projectId, UserEntity user) { + TestEntity testEntity = getTestIfExists(projectId); + if (testEntity == null) { + return new CheckResult<>(HttpStatus.NOT_FOUND, "No tests found for project with id: " + projectId, null); + } + + boolean userPartOfProject = projectUtil.userPartOfProject(projectId, user.getId()); + if (!userPartOfProject) { + return new CheckResult<>(HttpStatus.FORBIDDEN, "You are not part of this project", null); + } + + boolean admin = false; + CheckResult isProjectAdmin = projectUtil.isProjectAdmin(projectId, user); + if (isProjectAdmin.getStatus().equals(HttpStatus.OK)) { + admin = true; + } else if (!isProjectAdmin.getStatus().equals(HttpStatus.FORBIDDEN)){ + return new CheckResult<>(isProjectAdmin.getStatus(), isProjectAdmin.getMessage(), null); + } + + return new CheckResult<>(HttpStatus.OK, "", new Pair<>(testEntity, admin)); + } } diff --git a/backend/app/src/main/resources/application.properties b/backend/app/src/main/resources/application.properties index ff099426..7b44679e 100644 --- a/backend/app/src/main/resources/application.properties +++ b/backend/app/src/main/resources/application.properties @@ -25,6 +25,6 @@ server.port=8080 # TODO: this is just temporary, we will need to think of an actual limit at some point -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=10MB +spring.servlet.multipart.max-file-size=50MB +spring.servlet.multipart.max-request-size=50MB diff --git a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java index e3db0139..01112557 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/controllers/SubmissionControllerTest.java @@ -1,5 +1,6 @@ package com.ugent.pidgeon.controllers; +import com.ugent.pidgeon.model.json.DockerTestFeedbackJson; import com.ugent.pidgeon.model.json.GroupFeedbackJson; import com.ugent.pidgeon.model.json.GroupJson; import com.ugent.pidgeon.model.json.LastGroupSubmissionJson; @@ -8,6 +9,8 @@ import com.ugent.pidgeon.postgre.models.GroupEntity; import com.ugent.pidgeon.postgre.models.GroupFeedbackEntity; import com.ugent.pidgeon.postgre.models.SubmissionEntity; +import com.ugent.pidgeon.postgre.models.types.DockerTestState; +import com.ugent.pidgeon.postgre.models.types.DockerTestType; import com.ugent.pidgeon.postgre.repository.*; import com.ugent.pidgeon.util.*; import org.junit.jupiter.api.BeforeEach; @@ -84,8 +87,8 @@ public void setup() { submission = new SubmissionEntity(1L, 1L, 1L, OffsetDateTime.MIN, true, true); groupIds = List.of(1L); submissionJson = new SubmissionJson(1L, "projecturl", "groupurl", 1L, - 1L, "fileurl", true, OffsetDateTime.MIN, true, - "structurefeedbackurl", "dockerfeedbackurl"); + 1L, "fileurl", true, OffsetDateTime.MIN, "structureFeedback", + new DockerTestFeedbackJson(DockerTestType.NONE, "", true), DockerTestState.running.toString(), "artifacturl"); groupJson = new GroupJson(1, 1L, "groupname", "groupclusterurl"); groupFeedbackJson = new GroupFeedbackJson(0F, "feedback", 1L, 1L); groupEntity = new GroupEntity("groupname", 1L); @@ -150,40 +153,40 @@ public void testSubmitFile() throws Exception { .andExpect(status().isInternalServerError()); } - @Test - public void testGetSubmissionFile() throws Exception { - //TODO: dit ook een correcte test laten uitvoeren met dummyfiles - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - when(fileRepository.findById(anyLong())).thenReturn(Optional.of(fileEntity)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) - .andExpect(status().isInternalServerError()); - - when(fileRepository.findById(anyLong())).thenReturn(Optional.empty()); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) - .andExpect(status().isNotFound()); - - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) - .andExpect(status().isForbidden()); - } - - @Test - public void testGetStructureFeedback() throws Exception { - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/structurefeedback")) - .andExpect(status().isOk()); - - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/structurefeedback")) - .andExpect(status().isIAmATeapot()); - } - - @Test - public void testGetDockerFeedback() throws Exception { - when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); - mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/dockerfeedback")) - .andExpect(status().isOk()); - } +// @Test +// public void testGetSubmissionFile() throws Exception { +// //TODO: dit ook een correcte test laten uitvoeren met dummyfiles +// when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); +// when(fileRepository.findById(anyLong())).thenReturn(Optional.of(fileEntity)); +// mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) +// .andExpect(status().isInternalServerError()); +// +// when(fileRepository.findById(anyLong())).thenReturn(Optional.empty()); +// mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) +// .andExpect(status().isNotFound()); +// +// when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.FORBIDDEN, "", null)); +// mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/file")) +// .andExpect(status().isForbidden()); +// } + +// @Test +// public void testGetStructureFeedback() throws Exception { +// when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); +// mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/structurefeedback")) +// .andExpect(status().isOk()); +// +// when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.I_AM_A_TEAPOT, "", null)); +// mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/structurefeedback")) +// .andExpect(status().isIAmATeapot()); +// } + +// @Test +// public void testGetDockerFeedback() throws Exception { +// when(submissionUtil.canGetSubmission(anyLong(), any())).thenReturn(new CheckResult<>(HttpStatus.OK, "", submission)); +// mockMvc.perform(MockMvcRequestBuilders.get(ApiRoutes.SUBMISSION_BASE_PATH + "/1/dockerfeedback")) +// .andExpect(status().isOk()); +// } @Test public void testDeleteSubmissionById() throws Exception { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java new file mode 100644 index 00000000..54a91551 --- /dev/null +++ b/backend/app/src/test/java/com/ugent/pidgeon/docker/DockerSubmissionTestTest.java @@ -0,0 +1,205 @@ +package com.ugent.pidgeon.docker; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; +import com.ugent.pidgeon.model.submissionTesting.DockerSubtestResult; +import com.ugent.pidgeon.model.submissionTesting.DockerTemplateTestOutput; +import com.ugent.pidgeon.model.submissionTesting.DockerTestOutput; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +public class DockerSubmissionTestTest { + + File initTestFile(String text, String fileName) { + String localFileLocation = System.getProperty("user.dir") + "/tmp/test/" + fileName; + File file = new File(localFileLocation); + try { + file.getParentFile().mkdirs(); + file.createNewFile(); + FileUtils.writeStringToFile(file, text, "UTF-8"); + } catch (Exception e) { + e.printStackTrace(); + } + return file; + } + + // Check if we can catch the console output of a script. + @Test + void scriptSucceeds() throws InterruptedException { + DockerSubmissionTestModel.installImage("fedora:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); + // Run script + DockerTestOutput to = stm.runSubmission("echo 'PUSH ALLOWED' > /shared//output/testOutput"); + assertTrue(to.allowed); + stm.cleanUp(); + } + + @Test + void scriptFails() throws InterruptedException { + //make sure docker image is installed + DockerSubmissionTestModel.installImage("fedora:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); + // Run script + // Example for running a bash script correctly + DockerTestOutput to = stm.runSubmission("echo 'PUSH DENIED' > /shared/output/testOutput"); + assertFalse(to.allowed); + stm.cleanUp(); + } + + @Test + void catchesConsoleLogs() throws InterruptedException { + DockerSubmissionTestModel.installImage("alpine:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); + // Run script + // Example for running a bash script correctly + DockerTestOutput to = stm.runSubmission( + "echo 'Woopdie Woop Scoop! ~ KW'; echo 'PUSH ALLOWED' > /shared/output/testOutput"); + + assertTrue(to.allowed); + assertEquals(to.logs.get(0), "Woopdie Woop Scoop! ~ KW\n"); + stm.cleanUp(); + } + + @Test + void correctlyReceivesInputFiles() throws InterruptedException { + DockerSubmissionTestModel.installImage("alpine:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); + + // Create an input file in tmp/test/input + File file = initTestFile("This is a test input file\n", "testInput"); + stm.addInputFiles(new File[]{file}); + // Run script + // Example for running a bash script correctly + DockerTestOutput to = stm.runSubmission( + "cat /shared/input/testInput; echo PUSH ALLOWED > /shared/output/testOutput"); + assertEquals(to.logs.get(0), "This is a test input file\n"); + stm.cleanUp(); + } + + @Test + void templateTest() throws InterruptedException { + String testOne = "@HelloWorld\n" + + ">Description=\"Test for hello world!\"\n" + + ">Required\n" + + "HelloWorld!\n"; + String testTwo = "@HelloWorld2\n" + + ">Optional\n" + + "HelloWorld2!\n"; + String template = testOne + "\n" + testTwo + "\n"; + + File[] files = new File[]{initTestFile("#!/bin/sh\necho 'HelloWorld!'", "HelloWorld.sh"), + initTestFile("#!/bin/sh\necho 'HelloWorld2!'", "HelloWorld2.sh")}; + + String script = + "chmod +x /shared/input/HelloWorld.sh;" + + "chmod +x /shared/input/HelloWorld2.sh;" + + "/shared/input/HelloWorld.sh > /shared/output/HelloWorld;" + + "/shared/input/HelloWorld2.sh > /shared/output/HelloWorld2"; + + DockerSubmissionTestModel.installImage("alpine:latest"); + // Load docker container + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + stm.addInputFiles(files); + DockerTemplateTestOutput result = stm.runSubmissionWithTemplate(script, template); + + // Extract subtests + List results = result.getSubtestResults(); + + // Testing for the template parser capabilities + assertEquals(results.size(), 2); + + assertTrue(results.get(0).isRequired()); + assertFalse(results.get(1).isRequired()); + + assertEquals(results.get(0).getCorrect(), "HelloWorld!\n"); + assertEquals(results.get(1).getCorrect(), "HelloWorld2!\n"); + + assertEquals(results.get(0).getTestDescription(), "Test for hello world!"); + assertEquals(results.get(1).getTestDescription(), ""); + + // Test the docker output + assertEquals(results.get(0).getOutput(), "HelloWorld!\n"); + assertEquals(results.get(1).getOutput(), "HelloWorld2!\n"); + + assertTrue(result.isAllowed()); + stm.cleanUp(); + } + + @Test + void artifactTest() throws IOException { + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + String script = + "echo 'HelloWorld!' > /shared/artifacts/HelloWorld"; + + DockerTestOutput to = stm.runSubmission(script); + assertFalse(to.allowed); + // check file properties + List files = stm.getArtifacts(); + assertEquals(files.size(), 1); + assertEquals(files.get(0).getName(), "HelloWorld"); + // check file contents + assertEquals("HelloWorld!\n", FileUtils.readFileToString(files.get(0), "UTF-8")); + stm.cleanUp(); + } + + @Test + void zipFileInputTest() throws IOException { + // construct zip with hello world contents + StringBuilder sb = new StringBuilder(); + sb.append("Hello Happy World!"); + + File f = new File("src/test/test-cases/DockerSubmissionTestTest/d__test.zip"); + ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f)); + ZipEntry e = new ZipEntry("helloworld.txt"); + out.putNextEntry(e); + + byte[] data = sb.toString().getBytes(); + out.write(data, 0, data.length); + out.closeEntry(); + out.close(); + + DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine:latest"); + // get zipfile + stm.addZipInputFiles(new ZipFile(f)); + DockerTestOutput output = stm.runSubmission("cat /shared/input/helloworld.txt"); + // run and check if zipfile was properly received + assertEquals( "Hello Happy World!", output.logs.get(0)); + + } + @Test + void dockerImageDoesNotExist(){ + assertFalse(DockerSubmissionTestModel.imageExists("BADUBADUBADUBADUBADUBADUB")); + assertTrue(DockerSubmissionTestModel.imageExists("alpine:latest")); + } + + @Test + void isValidTemplate(){ + assertFalse(DockerSubmissionTestModel.isValidTemplate("This is not a valid template")); + assertTrue(DockerSubmissionTestModel.isValidTemplate("@HelloWorld\n" + + ">Description=\"Test for hello world!\"\n" + + ">Required\n" + + "HelloWorld!")); + assertTrue(DockerSubmissionTestModel.isValidTemplate("@helloworld\n" + + ">required\n" + + ">description=\"Helloworldtest\"\n" + + "Hello World\n" + + "\n" + + "@helloworld2\n" + + "bruh\n")); + } + +} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java b/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java deleted file mode 100644 index 3a6dd5ee..00000000 --- a/backend/app/src/test/java/com/ugent/pidgeon/model/DockerSubmissionTestTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.ugent.pidgeon.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.ugent.pidgeon.model.submissionTesting.DockerSubmissionTestModel; -import com.ugent.pidgeon.model.submissionTesting.DockerSubtestResult; -import com.ugent.pidgeon.model.submissionTesting.DockerTemplateTestResult; -import com.ugent.pidgeon.model.submissionTesting.DockerTestOutput; -import java.io.File; -import java.util.List; -import org.apache.commons.io.FileUtils; -import org.junit.jupiter.api.Test; - -public class DockerSubmissionTestTest { - -// File initTestFile(String text, String fileName) { -// String localFileLocation = System.getProperty("user.dir") + "/tmp/test/" + fileName; -// File file = new File(localFileLocation); -// try { -// file.getParentFile().mkdirs(); -// file.createNewFile(); -// FileUtils.writeStringToFile(file, text, "UTF-8"); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// return file; -// } -// -// // Check if we can catch the console output of a script. -// @Test -// void scriptSucceeds() throws InterruptedException { -// DockerSubmissionTestModel.addDocker("fedora:latest"); -// // Load docker container -// DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); -// // Run script -// DockerTestOutput to = stm.runSubmission("echo 'PUSH ALLOWED' > /shared//output/testOutput"); -// assertTrue(to.allowed); -// } -// -// @Test -// void scriptFails() throws InterruptedException { -// //make sure docker image is installed -// DockerSubmissionTestModel.addDocker("fedora:latest"); -// // Load docker container -// DockerSubmissionTestModel stm = new DockerSubmissionTestModel("fedora"); -// // Run script -// // Example for running a bash script correctly -// DockerTestOutput to = stm.runSubmission("echo 'PUSH DENIED' > /shared/output/testOutput"); -// assertFalse(to.allowed); -// } -// -// @Test -// void catchesConsoleLogs() throws InterruptedException { -// DockerSubmissionTestModel.addDocker("alpine:latest"); -// // Load docker container -// DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); -// // Run script -// // Example for running a bash script correctly -// DockerTestOutput to = stm.runSubmission("echo 'Woopdie Woop Scoop! ~ KW'; echo 'PUSH ALLOWED' > /shared/output/testOutput"); -// -// assertTrue(to.allowed); -// assertEquals(to.logs.get(0), "Woopdie Woop Scoop! ~ KW\n"); -// } -// -// @Test -// void correctlyReceivesInputFiles() throws InterruptedException { -// DockerSubmissionTestModel.addDocker("alpine:latest"); -// // Load docker container -// DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); -// -// // Create an input file in tmp/test/input -// File file = initTestFile("This is a test input file\n", "testInput"); -// -// // Run script -// // Example for running a bash script correctly -// DockerTestOutput to = stm.runSubmission("cat /shared/input/testInput; echo PUSH ALLOWED > /shared/output/testOutput", new File[]{file}); -// assertEquals(to.logs.get(0), "This is a test input file\n"); -// } -// -// @Test -// void templateTest() throws InterruptedException { -// String testOne = "@HelloWorld\n" + -// ">Description=\"Test for hello world!\"\n" + -// ">Required\n" + -// "HelloWorld!"; -// String testTwo = "@HelloWorld2\n" + -// ">Optional\n" + -// "HelloWorld2!\n"; -// String template = testOne + "\n" + testTwo; -// -// File[] files = new File[]{initTestFile("#!/bin/sh\necho 'HelloWorld!'", "HelloWorld.sh"), -// initTestFile("#!/bin/sh\necho 'HelloWorld2!'", "HelloWorld2.sh")}; -// -// String script = -// "chmod +x /shared/input/HelloWorld.sh;" + -// "chmod +x /shared/input/HelloWorld2.sh;" + -// "/shared/input/HelloWorld.sh > /shared/output/HelloWorld;" + -// "/shared/input/HelloWorld2.sh > /shared/output/HelloWorld2"; -// -// DockerSubmissionTestModel.addDocker("alpine:latest"); -// // Load docker container -// DockerSubmissionTestModel stm = new DockerSubmissionTestModel("alpine"); -// DockerTemplateTestResult result = stm.runSubmissionWithTemplate(script, template, files); -// -// // Extract subtests -// List results = result.getSubtestResults(); -// -// // Testing for the template parser capabilities -// assertEquals(results.size(), 2); -// -// assertTrue(results.get(0).isRequired()); -// assertFalse(results.get(1).isRequired()); -// -// assertEquals(results.get(0).getCorrect(), "HelloWorld!\n"); -// assertEquals(results.get(1).getCorrect(), "HelloWorld2!\n"); -// -// assertEquals(results.get(0).getTestDescription(), "Test for hello world!"); -// assertEquals(results.get(1).getTestDescription(), ""); -// -// // Test the docker output -// assertEquals(results.get(0).getOutput(), "HelloWorld!\n"); -// assertEquals(results.get(1).getOutput(), "HelloWorld2!\n"); -// -// assertTrue(result.isAllowed()); -// -// } - - -} diff --git a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java index ac9158c6..f18211bc 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/postgre/models/TestEntityTest.java @@ -29,27 +29,32 @@ public void testDockerImage() { } @Test - public void testDockerTestId() { - long dockerTestId = 1L; - testEntity.setDockerTestId(dockerTestId); - assertEquals(dockerTestId, testEntity.getDockerTestId()); + public void testDockerTestScript() { + String dockerTestScript = "Docker Test Script"; + testEntity.setDockerTestScript(dockerTestScript); + assertEquals(dockerTestScript, testEntity.getDockerTestScript()); } @Test public void testStructureTestId() { - long structureTestId = 1L; - testEntity.setStructureTestId(structureTestId); - assertEquals(structureTestId, testEntity.getStructureTestId()); + String template = "@Testone\nHello World!"; + testEntity.setStructureTemplate(template); + assertEquals(template, testEntity.getStructureTemplate()); } @Test public void testConstructor() { - String dockerImage = "Docker Image"; - long dockerTestId = 1L; - long structureTestId = 1L; - TestEntity test = new TestEntity(dockerImage, dockerTestId, structureTestId); + String dockerImage = "Docker image"; + String dockerTestScript = "echo 'hello'"; + String dockerTestTemplate = "@testone\nHello World!"; + String structureTestId = "src/"; + + TestEntity test = new TestEntity(dockerImage, dockerTestScript, dockerTestTemplate, structureTestId); + assertEquals(dockerImage, test.getDockerImage()); - assertEquals(dockerTestId, test.getDockerTestId()); - assertEquals(structureTestId, test.getStructureTestId()); + assertEquals(dockerTestScript, test.getDockerTestScript()); + assertEquals(dockerTestTemplate, test.getDockerTestTemplate()); + assertEquals(structureTestId, test.getStructureTemplate()); + } } \ No newline at end of file diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java index b199d564..1dd71d81 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/EntityToJsonConverterTest.java @@ -142,12 +142,12 @@ public void testProjectEntityToProjectResponseJson() { assertEquals(projectEntity.getName(), result.name()); } - @Test - public void testGetSubmissionJson() { - SubmissionJson result = entityToJsonConverter.getSubmissionJson(submissionEntity); - assertEquals(submissionEntity.getId(), result.getSubmissionId()); - assertEquals(submissionEntity.getProjectId(), result.getProjectId()); - } +// @Test +// public void testGetSubmissionJson() { +// SubmissionJson result = entityToJsonConverter.getSubmissionJson(submissionEntity); +// assertEquals(submissionEntity.getId(), result.getSubmissionId()); +// assertEquals(submissionEntity.getProjectId(), result.getProjectId()); +// } @Test public void testTestEntityToTestJson() { diff --git a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java index 7ad5e4d3..2ab022a2 100644 --- a/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java +++ b/backend/app/src/test/java/com/ugent/pidgeon/util/TestUtilTest.java @@ -66,12 +66,9 @@ public void testCheckForTestUpdate() { // Mock the testRepository.findByProjectId method to return an Optional of testEntity when(testRepository.findByProjectId(anyLong())).thenReturn(Optional.of(testEntity)); - // Create a mock MultipartFile - MultipartFile mockFile = mock(MultipartFile.class); - // Call the checkForTestUpdate method CheckResult> result = testUtil.checkForTestUpdate(1L, - userEntity, "dockerImage", mockFile, mockFile, HttpMethod.POST); + userEntity, "dockerImage", "", null, HttpMethod.POST); // Assert the result assertEquals(HttpStatus.OK, result.getStatus()); diff --git a/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip new file mode 100644 index 00000000..7f6b6645 Binary files /dev/null and b/backend/app/src/test/test-cases/DockerSubmissionTestTest/d__test.zip differ diff --git a/backend/database/start_database.sql b/backend/database/start_database.sql index 1e076839..41cffe31 100644 --- a/backend/database/start_database.sql +++ b/backend/database/start_database.sql @@ -49,11 +49,14 @@ CREATE TABLE files ( ); -- A id for the docker test and an id for the file test id +-- docker test is enabled if script is not null +-- docker test is in simple mode if template is null CREATE TABLE tests ( test_id SERIAL PRIMARY KEY, docker_image VARCHAR(256), - docker_test INT REFERENCES files(file_id), - structure_test_id INT REFERENCES files(file_id) + docker_test_script TEXT, + docker_test_template TEXT, + structure_template TEXT ); @@ -105,6 +108,8 @@ CREATE TABLE submissions ( docker_accepted BOOLEAN NOT NULL, structure_feedback TEXT, docker_feedback TEXT, + docker_test_state VARCHAR(10) DEFAULT "running", + docker_type VARCHAR(10) DEFAULT "simple", submission_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4f6b5ac8..8fc8899b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -763,11 +763,10 @@ "version": "0.7.5", "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -780,9 +779,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -795,9 +794,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -810,9 +809,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -825,25 +824,24 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -856,9 +854,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -871,9 +869,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -886,9 +884,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -901,9 +899,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -916,9 +914,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -931,9 +929,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -946,9 +944,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -961,9 +959,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -976,9 +974,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -991,9 +989,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -1006,9 +1004,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -1021,9 +1019,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -1036,9 +1034,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -1051,9 +1049,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -1066,9 +1064,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -1081,9 +1079,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -1096,9 +1094,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -1974,188 +1972,6 @@ "node": ">=14.0.0" } }, - - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.2.tgz", - "integrity": "sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.2.tgz", - "integrity": "sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.2.tgz", - "integrity": "sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.2.tgz", - "integrity": "sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.2.tgz", - "integrity": "sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.2.tgz", - "integrity": "sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.2.tgz", - "integrity": "sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.2.tgz", - "integrity": "sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.2.tgz", - "integrity": "sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.2.tgz", - "integrity": "sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.2.tgz", - "integrity": "sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.2.tgz", - "integrity": "sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.2.tgz", - "integrity": "sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.2.tgz", - "integrity": "sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.2.tgz", - "integrity": "sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -9067,39 +8883,6 @@ "node": ">=10" } }, - "node_modules/rollup": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.2.tgz", - "integrity": "sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.2", - "@rollup/rollup-android-arm64": "4.14.2", - "@rollup/rollup-darwin-arm64": "4.14.2", - "@rollup/rollup-darwin-x64": "4.14.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.2", - "@rollup/rollup-linux-arm64-gnu": "4.14.2", - "@rollup/rollup-linux-arm64-musl": "4.14.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.2", - "@rollup/rollup-linux-riscv64-gnu": "4.14.2", - "@rollup/rollup-linux-s390x-gnu": "4.14.2", - "@rollup/rollup-linux-x64-gnu": "4.14.2", - "@rollup/rollup-linux-x64-musl": "4.14.2", - "@rollup/rollup-win32-arm64-msvc": "4.14.2", - "@rollup/rollup-win32-ia32-msvc": "4.14.2", - "@rollup/rollup-win32-x64-msvc": "4.14.2", - "fsevents": "~2.3.2" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9940,7 +9723,162 @@ "node": ">=14.17" } }, - + "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/vite/node_modules/rollup": { "version": "4.13.0", "license": "MIT",