From b65f2380f61252f7a236a0b041a80be3da98ea49 Mon Sep 17 00:00:00 2001 From: mp-dg <57503145+MP-DG@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:32:19 +0100 Subject: [PATCH 1/2] added filtering for submitted files --- .../controller/SubscriptionController.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java index 117953a..dde800a 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java @@ -137,6 +137,8 @@ public Mono onContentProgressedEvent(@RequestBody CloudEvent for reactive processing @@ -159,6 +161,14 @@ public Mono onStudentCodeSubmittedEvent(@RequestBody CloudEvent filteredFiles = filterJavaFiles(event.getFiles()); + + log.info("Filtered {} files down to {} Java files for student {} on assignment {}", + event.getFiles() != null ? event.getFiles().size() : 0, + filteredFiles.size(), + event.getStudentId(), + event.getAssignmentId()); + studentCodeSubmissionService.saveCodeSubmission( event.getStudentId(), event.getAssignmentId(), @@ -166,7 +176,7 @@ public Mono onStudentCodeSubmittedEvent(@RequestBody CloudEvent onStudentCodeSubmittedEvent(@RequestBody CloudEvent filterJavaFiles(Map files) { + if (files == null) { + return new HashMap<>(); + } + + Map filteredFiles = new HashMap<>(); + + files.forEach((filename, content) -> { + if (filename != null && + content != null && + filename.endsWith(".java")) { + filteredFiles.put(filename, content); + } + }); + + return filteredFiles; + } } From f633689b351891ad17097030e015888ecf034b82 Mon Sep 17 00:00:00 2001 From: mp-dg <57503145+MP-DG@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:42:30 +0100 Subject: [PATCH 2/2] added option to chose which file endings get saved in properties --- .../config/StudentCodeSubmissionConfig.java | 24 ++ .../controller/SubscriptionController.java | 28 ++- src/main/resources/application.properties | 3 + .../SubscriptionControllerTest.java | 238 +++++++++++++++++- 4 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/StudentCodeSubmissionConfig.java diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/StudentCodeSubmissionConfig.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/StudentCodeSubmissionConfig.java new file mode 100644 index 0000000..bea2054 --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/StudentCodeSubmissionConfig.java @@ -0,0 +1,24 @@ +package de.unistuttgart.iste.meitrex.tutor_service.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * Configuration properties for student code submission filtering. + */ +@Configuration +@ConfigurationProperties(prefix = "student.code.submission") +@Getter +@Setter +public class StudentCodeSubmissionConfig { + + /** + * List of file endings to save from student code submissions. + * Example: [".java", ".kt", ".py"] + */ + private List fileEndings = List.of(".java"); +} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java index dde800a..39ff51b 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionController.java @@ -5,6 +5,7 @@ import de.unistuttgart.iste.meitrex.common.event.StudentCodeSubmittedEvent; import de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent; import de.unistuttgart.iste.meitrex.common.event.skilllevels.UserSkillLevelChangedEvent; +import de.unistuttgart.iste.meitrex.tutor_service.config.StudentCodeSubmissionConfig; import de.unistuttgart.iste.meitrex.tutor_service.service.ProactiveFeedbackService; import de.unistuttgart.iste.meitrex.tutor_service.service.StudentCodeSubmissionService; import de.unistuttgart.iste.meitrex.tutor_service.service.UserPlayerTypeService; @@ -34,6 +35,7 @@ public class SubscriptionController { private final UserSkillLevelService userSkillLevelService; private final ProactiveFeedbackService proactiveFeedbackService; private final StudentCodeSubmissionService studentCodeSubmissionService; + private final StudentCodeSubmissionConfig studentCodeSubmissionConfig; /** * Handles the user-hexad-player-type-set event. @@ -161,9 +163,9 @@ public Mono onStudentCodeSubmittedEvent(@RequestBody CloudEvent filteredFiles = filterJavaFiles(event.getFiles()); + Map filteredFiles = filterFiles(event.getFiles()); - log.info("Filtered {} files down to {} Java files for student {} on assignment {}", + log.info("Filtered {} files down to {} valid files for student {} on assignment {}", event.getFiles() != null ? event.getFiles().size() : 0, filteredFiles.size(), event.getStudentId(), @@ -187,12 +189,13 @@ public Mono onStudentCodeSubmittedEvent(@RequestBody CloudEvent filterJavaFiles(Map files) { + private Map filterFiles(Map files) { if (files == null) { return new HashMap<>(); } @@ -200,13 +203,22 @@ private Map filterJavaFiles(Map files) { Map filteredFiles = new HashMap<>(); files.forEach((filename, content) -> { - if (filename != null && - content != null && - filename.endsWith(".java")) { + if (filename != null && content != null && hasAllowedFileEnding(filename)) { filteredFiles.put(filename, content); } }); return filteredFiles; } + + /** + * Checks if a filename ends with one of the configured file endings. + * + * @param filename the filename to check + * @return true if the filename ends with an allowed ending, false otherwise + */ + private boolean hasAllowedFileEnding(String filename) { + return studentCodeSubmissionConfig.getFileEndings().stream() + .anyMatch(filename::endsWith); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d6b4e30..351a299 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -40,3 +40,6 @@ spring.jpa.open-in-view=false content_service.url=http://app-content:4001/graphql gamification_service.url=http://app-gamification:1201/graphql docproc.url=http://app-docprocai:9901/graphql/ + +# File endings to save from student code submissions (comma-separated) +student.code.submission.file-endings=.java,.kt,.py diff --git a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionControllerTest.java b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionControllerTest.java index cf2231b..0514d0e 100644 --- a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionControllerTest.java +++ b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/controller/SubscriptionControllerTest.java @@ -5,6 +5,7 @@ import de.unistuttgart.iste.meitrex.common.event.StudentCodeSubmittedEvent; import de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent; import de.unistuttgart.iste.meitrex.common.event.skilllevels.UserSkillLevelChangedEvent; +import de.unistuttgart.iste.meitrex.tutor_service.config.StudentCodeSubmissionConfig; import de.unistuttgart.iste.meitrex.tutor_service.service.ProactiveFeedbackService; import de.unistuttgart.iste.meitrex.tutor_service.service.StudentCodeSubmissionService; import de.unistuttgart.iste.meitrex.tutor_service.service.UserPlayerTypeService; @@ -19,6 +20,7 @@ import java.time.OffsetDateTime; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -46,6 +48,9 @@ class SubscriptionControllerTest { @Mock private StudentCodeSubmissionService studentCodeSubmissionService; + @Mock + private StudentCodeSubmissionConfig studentCodeSubmissionConfig; + @InjectMocks private SubscriptionController subscriptionController; @@ -66,6 +71,9 @@ void setUp() { contentId = UUID.randomUUID(); assignmentId = UUID.randomUUID(); courseId = UUID.randomUUID(); + + lenient().when(studentCodeSubmissionConfig.getFileEndings()) + .thenReturn(List.of(".java", ".kt", ".py")); } @Test @@ -236,7 +244,9 @@ void testOnStudentCodeSubmittedEvent_SavesCodeSubmission() { eq(repositoryUrl), eq(commitSha), eq(commitTimestamp), - eq(files), + argThat(filteredFiles -> filteredFiles.size() == 2 + && filteredFiles.containsKey("src/Main.java") + && filteredFiles.containsKey("src/Helper.java")), eq(branch) ); } @@ -309,4 +319,230 @@ void testOnContentProgressedEvent_NullEventData_HandlesGracefully() { verify(proactiveFeedbackService, never()).generateFeedback(any()); } + + @Test + void testFileFiltering_OnlyJavaFiles_AllSaved() { + Map files = new HashMap<>(); + files.put("src/Main.java", "public class Main {}"); + files.put("src/Helper.java", "public class Helper {}"); + files.put("src/Utils.java", "public class Utils {}"); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> filteredFiles.size() == 3), + any() + ); + } + + @Test + void testFileFiltering_MixedFileTypes_OnlyAllowedSaved() { + Map files = new HashMap<>(); + files.put("src/Main.java", "public class Main {}"); + files.put("src/Helper.kt", "class Helper {}"); + files.put("script.py", "print('hello')"); + files.put("README.md", "# Documentation"); + files.put("package.json", "{}"); + files.put(".gitignore", "*.class"); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> + filteredFiles.size() == 3 + && filteredFiles.containsKey("src/Main.java") + && filteredFiles.containsKey("src/Helper.kt") + && filteredFiles.containsKey("script.py") + && !filteredFiles.containsKey("README.md") + && !filteredFiles.containsKey("package.json")), + any() + ); + } + + @Test + void testFileFiltering_NullFilename_Filtered() { + Map files = new HashMap<>(); + files.put("src/Main.java", "public class Main {}"); + files.put(null, "some content"); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> filteredFiles.size() == 1 && filteredFiles.containsKey("src/Main.java")), + any() + ); + } + + @Test + void testFileFiltering_NullContent_Filtered() { + Map files = new HashMap<>(); + files.put("src/Main.java", "public class Main {}"); + files.put("src/Empty.java", null); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> filteredFiles.size() == 1 && filteredFiles.containsKey("src/Main.java")), + any() + ); + } + + @Test + void testFileFiltering_NoMatchingFileEndings_EmptyMap() { + Map files = new HashMap<>(); + files.put("README.md", "# Documentation"); + files.put("package.json", "{}"); + files.put("Dockerfile", "FROM openjdk"); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(Map::isEmpty), + any() + ); + } + + @Test + void testFileFiltering_NullFilesMap_EmptyMap() { + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(null); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(Map::isEmpty), + any() + ); + } + + @Test + void testFileFiltering_EmptyFilesMap_EmptyMap() { + Map files = new HashMap<>(); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(Map::isEmpty), + any() + ); + } + + @Test + void testFileFiltering_SingleFileEndingConfiguration_OnlyThatTypeSaved() { + // Configure to only accept .java files + when(studentCodeSubmissionConfig.getFileEndings()).thenReturn(List.of(".java")); + + Map files = new HashMap<>(); + files.put("src/Main.java", "public class Main {}"); + files.put("src/Helper.kt", "class Helper {}"); + files.put("script.py", "print('hello')"); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> + filteredFiles.size() == 1 + && filteredFiles.containsKey("src/Main.java")), + any() + ); + } + + @Test + void testFileFiltering_CaseSensitiveFileEndings() { + Map files = new HashMap<>(); + files.put("src/Main.java", "public class Main {}"); + files.put("src/Helper.JAVA", "public class Helper {}"); // uppercase extension + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> + filteredFiles.size() == 1 + && filteredFiles.containsKey("src/Main.java") + && !filteredFiles.containsKey("src/Helper.JAVA")), + any() + ); + } + + @Test + void testFileFiltering_MultipleDotsInFilename() { + Map files = new HashMap<>(); + files.put("src/Main.test.java", "public class MainTest {}"); + files.put("src/config.prod.py", "config = {}"); + files.put("src/data.backup.json", "{}"); + + StudentCodeSubmittedEvent event = createCodeSubmissionEvent(files); + CloudEvent cloudEvent = mock(CloudEvent.class); + when(cloudEvent.getData()).thenReturn(event); + + assertDoesNotThrow(() -> subscriptionController.onStudentCodeSubmittedEvent(cloudEvent, headers).block()); + + verify(studentCodeSubmissionService, times(1)).saveCodeSubmission( + any(), any(), any(), any(), any(), any(), + argThat(filteredFiles -> + filteredFiles.size() == 2 + && filteredFiles.containsKey("src/Main.test.java") + && filteredFiles.containsKey("src/config.prod.py") + && !filteredFiles.containsKey("src/data.backup.json")), + any() + ); + } + + /** + * Helper method to create a StudentCodeSubmittedEvent with the given files. + */ + private StudentCodeSubmittedEvent createCodeSubmissionEvent(Map files) { + return StudentCodeSubmittedEvent.builder() + .studentId(userId) + .assignmentId(assignmentId) + .courseId(courseId) + .repositoryUrl("https://github.com/student/repo") + .commitSha("abc123") + .commitTimestamp(OffsetDateTime.now()) + .files(files) + .branch("main") + .build(); + } }