Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
c06832d
converted athena endpoints to return filemaps
ekayandan Sep 4, 2025
2e196a4
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 4, 2025
9d00023
Merge remote-tracking branch 'origin' into chore/convert-athena-endpo…
ekayandan Sep 5, 2025
73c8a7d
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Sep 5, 2025
707637e
improvements on performance and tests
ekayandan Sep 5, 2025
0356671
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 5, 2025
6a0d3f2
added missing docstring
ekayandan Sep 5, 2025
0767f61
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Sep 5, 2025
f5f5a87
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 6, 2025
d2dc5f1
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 6, 2025
e218bd7
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 6, 2025
b5beb91
review comments
ekayandan Sep 8, 2025
51c8cca
Adapt tests and lint
ekayandan Sep 8, 2025
42dfaa4
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 10, 2025
0eecf38
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 15, 2025
1da53b5
Enhancement: Add feedback suggestion module validation in Athena serv…
ekayandan Sep 15, 2025
3eb6dde
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 15, 2025
f12077f
Merge remote-tracking branch 'origin' into chore/convert-athena-endpo…
ekayandan Sep 15, 2025
6fa9b85
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Sep 15, 2025
e437a1d
removed one of the last 2 GitRepositoryExportService mocks
ekayandan Sep 16, 2025
3c0d266
Merge branch 'chore/convert-athena-endpoints-to-filemap' into chore/p…
ekayandan Sep 17, 2025
4f00aeb
Merge remote-tracking branch 'origin' into chore/programming-exercise…
ekayandan Sep 17, 2025
28483ea
fixed other tests failing
ekayandan Sep 17, 2025
7945c00
Merge branch 'develop' into chore/programming-exercises/improve-expor…
ekayandan Sep 17, 2025
c1b326f
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 25, 2025
c366f1e
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 29, 2025
7e33edc
review comments
ekayandan Sep 29, 2025
1de24ff
fixed server test
ekayandan Sep 29, 2025
d5ba771
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Sep 29, 2025
4127676
converted var to type
ekayandan Sep 30, 2025
ae9ee78
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Sep 30, 2025
457508b
fixed tests & applied some of the comments
ekayandan Oct 1, 2025
9bbfc4b
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Oct 1, 2025
2e13784
remaining review comments
ekayandan Oct 1, 2025
ded36b3
changed naming to avoid confusions
ekayandan Oct 1, 2025
99efbf8
Update src/main/webapp/i18n/de/error.json
ekayandan Oct 3, 2025
52b19d2
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 3, 2025
f3f0c35
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 4, 2025
2911656
Merge remote-tracking branch 'origin/develop' into chore/convert-athe…
ekayandan Oct 20, 2025
8c0ee2c
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Oct 20, 2025
87ffab9
added even more client tests
ekayandan Oct 21, 2025
23b7eee
fixed test compile
ekayandan Oct 21, 2025
323374c
Merge remote-tracking branch 'origin/develop' into chore/convert-athe…
ekayandan Oct 21, 2025
efa540c
added server tests for athena
ekayandan Oct 21, 2025
e1c9444
fixed failing test
ekayandan Oct 22, 2025
dbc0913
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 22, 2025
df17fae
Merge remote-tracking branch 'origin/develop' into chore/convert-athe…
ekayandan Oct 23, 2025
8d31d0b
Added extra guardrails and test cases
ekayandan Oct 23, 2025
742197c
added missing javadoc
ekayandan Oct 23, 2025
ceb168d
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 23, 2025
6e4f64e
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
maximiliansoelch Oct 29, 2025
7cf6802
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 30, 2025
c0e91e5
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 31, 2025
83638d2
pr comments + fixed error keys
ekayandan Oct 31, 2025
4fd765f
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Oct 31, 2025
4e4f5d2
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Oct 31, 2025
464345f
Adapted tests for correct error keys
ekayandan Nov 1, 2025
33e85e6
Merge branch 'chore/convert-athena-endpoints-to-filemap' of github.co…
ekayandan Nov 1, 2025
c02029f
Merge branch 'develop' into chore/programming-exercises/improve-expor…
ekayandan Nov 1, 2025
6ba1957
resolved merge conflicts
ekayandan Nov 3, 2025
9cb68e7
resolved merge conflicts
ekayandan Nov 3, 2025
0d7ffe7
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Nov 4, 2025
82c3228
Merge branch 'develop' into chore/convert-athena-endpoints-to-filemap
ekayandan Nov 4, 2025
5a78006
Merge branch 'chore/convert-athena-endpoints-to-filemap' into chore/p…
ekayandan Nov 4, 2025
e176532
resolved merge conflicts
ekayandan Nov 4, 2025
cfee00a
added missing positive case
ekayandan Nov 5, 2025
0d81396
Created central export test util class and adapted suites using actua…
ekayandan Nov 6, 2025
a020143
converted ProgrammingExerciseGitIntegrationTest
ekayandan Nov 6, 2025
a495d44
converted some more
ekayandan Nov 8, 2025
4efbb43
conversion progressing
ekayandan Nov 8, 2025
c89b3e0
Merge develop
ekayandan Nov 8, 2025
e6db107
Converted auxiliary repo
ekayandan Nov 8, 2025
fa37d23
cleaned up code
ekayandan Nov 8, 2025
14486ea
Converted remaining from exerciseIntegration
ekayandan Nov 9, 2025
ace53f5
Merge remote-tracking branch 'origin/develop' into chore/programming-…
ekayandan Nov 9, 2025
95b7bd5
Fixed failing tests after merge develop
ekayandan Nov 9, 2025
4ccc8bf
Clean Auxiliary Tests
ekayandan Nov 9, 2025
2d3f429
cleaned ProgrammingExerciseIntegrationTestService
ekayandan Nov 9, 2025
2e00db0
removed gitservice mocks
ekayandan Nov 9, 2025
34f9707
Cleaned first(easier, but bigger) half of gitservice mock residues
ekayandan Nov 10, 2025
e5d9630
removing residues continued
ekayandan Nov 10, 2025
685b843
removed more mocks
ekayandan Nov 10, 2025
b2d3473
2 more batches to go
ekayandan Nov 11, 2025
431c075
Removed all mock related code. All passing
ekayandan Nov 11, 2025
c40ec2e
removed some more optional ones
ekayandan Nov 11, 2025
4ddeadd
fix some failing tests after merge
ekayandan Nov 12, 2025
ae24c88
Merge branch 'develop' into chore/programming-exercises/improve-expor…
ekayandan Nov 12, 2025
5753e41
Adapted RepositoryExportTestUtil
ekayandan Nov 13, 2025
3505f6f
Last mocks removed. Remaining are necessary
ekayandan Nov 13, 2025
120e9a9
centralize functionality
ekayandan Nov 13, 2025
db58d3c
move conversions and expansion of util class
ekayandan Nov 14, 2025
848138f
Merge branch 'chore/programming-exercises/improve-export-tests' of gi…
ekayandan Nov 14, 2025
1cc5e86
linte
ekayandan Nov 14, 2025
40ce916
removed last mocks
ekayandan Nov 15, 2025
906ed8e
introducing centralized auto cleaner
ekayandan Nov 16, 2025
47e8641
converted all classes to use repository tracker
ekayandan Nov 16, 2025
970f7bc
Merge branch 'develop' into chore/programming-exercises/improve-expor…
ekayandan Nov 16, 2025
1336f73
update disable reason
ekayandan Nov 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -263,17 +263,14 @@ private void validateAthenaEnabled(long exerciseId) {
}

/**
* GET public/programming-exercises/:exerciseId/submissions/:submissionId/repository : Get the repository as a zip file download
* GET public/programming-exercises/:exerciseId/submissions/:submissionId/repository : Deprecated, forwards to internal endpoint
*
* @param exerciseId the id of the exercise the submission belongs to
* @param submissionId the id of the submission to get the repository for
* @param auth the auth header value to check
* @param request the HTTP request
* @param response the HTTP response
* @throws ServletException if the forward fails
* @deprecated Use {@code api/athena/internal/programming-exercises/{exerciseId}/submissions/{submissionId}/repository} instead
* @param request the HTTP servlet request used for forwarding
* @param response the HTTP servlet response used for forwarding
*/
@Deprecated
@GetMapping("public/programming-exercises/{exerciseId}/submissions/{submissionId}/repository")
@EnforceNothing // We check the Athena secret and validation here
@ManualConfig
Expand All @@ -285,27 +282,7 @@ public void getRepository(@PathVariable long exerciseId, @PathVariable long subm
request.getRequestDispatcher("/api/athena/internal/programming-exercises/" + exerciseId + "/submissions/" + submissionId + "/repository").forward(request, response);
}

/**
* GET public/programming-exercises/:exerciseId/repository/template : Get the template repository as a zip file download
*
* @param exerciseId the id of the exercise
* @param auth the auth header value to check
* @param request the HTTP request
* @param response the HTTP response
* @throws ServletException if the forward fails
* @deprecated Use {@code api/athena/internal/programming-exercises/{exerciseId}/repository/template} instead
*/
@Deprecated
@GetMapping("public/programming-exercises/{exerciseId}/repository/template")
@EnforceNothing // We check the Athena secret and validation here
@ManualConfig
public void getTemplateRepository(@PathVariable long exerciseId, @RequestHeader(HttpHeaders.AUTHORIZATION) String auth, HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
log.debug("REST call to deprecated endpoint, forwarding to internal endpoint for exercise {}", exerciseId);
checkAthenaSecret(auth);
validateAthenaEnabled(exerciseId);
request.getRequestDispatcher("/api/athena/internal/programming-exercises/" + exerciseId + "/repository/template").forward(request, response);
}
// Removed legacy generic instructor repository endpoint in favor of specific endpoints below

/**
* GET public/programming-exercises/:exerciseId/repository/solution : Get the solution repository as a zip file download
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,10 @@ public Map<File, FileType> listFilesAndFolders(Repository repo, boolean omitBina
Iterator<java.io.File> itr = FileUtils.iterateFilesAndDirs(repo.getLocalPath().toFile(), filter, filter);
Map<File, FileType> files = new HashMap<>();

if (itr == null) {
return files;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider at least logging the failure, to make debugging locally easier

}

while (itr.hasNext()) {
File nextFile = new File(itr.next(), repo);
Path nextPath = nextFile.toPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ public void renameFile(Repository repository, FileMove fileMove) throws FileNotF
if (!repository.isValidFile(newFile)) {
throw new IllegalArgumentException("Existing path is not valid");
}
if (gitService.getFileByName(repository, newFile.getPath()).isPresent()) {
if (gitService.getFileByName(repository, newFile.toString()).isPresent()) {
throw new FileAlreadyExistsException("New path is not valid");
}
boolean isRenamed = existingFile.get().renameTo(newFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@

Path remoteDirPath = localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath);

if (Files.exists(remoteDirPath)) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 10 days ago

To fix the problem, user-tainted values like projectKey and repositorySlug must be validated before being used in path expressions. The best approach is to reject any input containing path separators ("/" or "") or sequences like "..". Since these are intended to be single name components (not paths), a whitelist of allowed characters (letters, digits, underscores, hyphens) can also be enforced. The key locations to validate are before creating paths with localVCBasePath.resolve(projectKey) and before computing repository paths using LocalVCRepositoryUri.

The implementation will be:

  • Add a static validation method to LocalVCService called validatePathComponent(String) that throws an IllegalArgumentException if the input is invalid.
  • Call this method for both projectKey and repositorySlug in createProjectForExercise and createRepository before constructing or using any paths.
  • Consider adding similar validation in the LocalVCRepositoryUri constructor for completeness (since it's called at multiple places).
  • The validation method checks for: null, empty, "..", "/", "", and optionally restricts to a specific pattern such as ^[a-zA-Z0-9_-]+$ (recommended for best security).

Changes are required in src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCService.java (for direct validation) and optionally in src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCRepositoryUri.java (for extra defense-in-depth).


Suggested changeset 2
src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCService.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCService.java
--- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCService.java
+++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCService.java
@@ -131,6 +131,7 @@
     @Override
     public void createProjectForExercise(ProgrammingExercise programmingExercise) {
         String projectKey = programmingExercise.getProjectKey();
+        validatePathComponent(projectKey, "projectKey");
         try {
             // Create a directory that will contain all repositories.
             Path projectPath = localVCBasePath.resolve(projectKey);
@@ -143,6 +144,18 @@
     }
 
     /**
+     * Validate that a string is a safe path component.
+     * Rejects null, empty, "..", path separators, and restricts to a safe pattern.
+     * Throws IllegalArgumentException if invalid.
+     */
+    private static void validatePathComponent(String input, String fieldName) {
+        if (input == null || input.isEmpty() || input.contains("..") || input.contains("/") || input.contains("\\") ||
+                !input.matches("^[a-zA-Z0-9_-]+$")) {
+            throw new IllegalArgumentException("Invalid " + fieldName + " for repository/project: " + input);
+        }
+    }
+
+    /**
      * Create a new repository for the given project key and repository slug
      *
      * @param projectKey     The project key of the parent project
@@ -151,6 +164,8 @@
      */
     @Override
     public void createRepository(String projectKey, String repositorySlug) {
+        validatePathComponent(projectKey, "projectKey");
+        validatePathComponent(repositorySlug, "repositorySlug");
         LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(localVCBaseUri, projectKey, repositorySlug);
 
         Path remoteDirPath = localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath);
EOF
@@ -131,6 +131,7 @@
@Override
public void createProjectForExercise(ProgrammingExercise programmingExercise) {
String projectKey = programmingExercise.getProjectKey();
validatePathComponent(projectKey, "projectKey");
try {
// Create a directory that will contain all repositories.
Path projectPath = localVCBasePath.resolve(projectKey);
@@ -143,6 +144,18 @@
}

/**
* Validate that a string is a safe path component.
* Rejects null, empty, "..", path separators, and restricts to a safe pattern.
* Throws IllegalArgumentException if invalid.
*/
private static void validatePathComponent(String input, String fieldName) {
if (input == null || input.isEmpty() || input.contains("..") || input.contains("/") || input.contains("\\") ||
!input.matches("^[a-zA-Z0-9_-]+$")) {
throw new IllegalArgumentException("Invalid " + fieldName + " for repository/project: " + input);
}
}

/**
* Create a new repository for the given project key and repository slug
*
* @param projectKey The project key of the parent project
@@ -151,6 +164,8 @@
*/
@Override
public void createRepository(String projectKey, String repositorySlug) {
validatePathComponent(projectKey, "projectKey");
validatePathComponent(repositorySlug, "repositorySlug");
LocalVCRepositoryUri localVCRepositoryUri = new LocalVCRepositoryUri(localVCBaseUri, projectKey, repositorySlug);

Path remoteDirPath = localVCRepositoryUri.getLocalRepositoryPath(localVCBasePath);
src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCRepositoryUri.java
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCRepositoryUri.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCRepositoryUri.java
--- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCRepositoryUri.java
+++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCRepositoryUri.java
@@ -39,6 +39,9 @@
      * @throws LocalVCInternalException If the project key or repository slug results in an invalid URI, encapsulating the {@link URISyntaxException}.
      */
     public LocalVCRepositoryUri(URI localVCBaseUri, String projectKey, String repositorySlug) {
+        if (!isSafePathComponent(projectKey) || !isSafePathComponent(repositorySlug)) {
+            throw new IllegalArgumentException("Invalid projectKey or repositorySlug.");
+        }
         this.uri = buildUri(localVCBaseUri, projectKey, repositorySlug);
         this.projectKey = projectKey;
         this.repositorySlug = repositorySlug;
@@ -46,6 +49,12 @@
         this.isPracticeRepository = isPracticeRepository(repositorySlug, projectKey);
     }
 
+    private static boolean isSafePathComponent(String input) {
+        return input != null && !input.isEmpty()
+                && !input.contains("..") && !input.contains("/") && !input.contains("\\")
+                && input.matches("^[a-zA-Z0-9_-]+$");
+    }
+
     private static URI buildUri(URI localVCBaseUri, String projectKey, String repositorySlug) {
         return UriComponentsBuilder.fromUri(localVCBaseUri).pathSegment("git", projectKey, repositorySlug + ".git").build().toUri();
     }
EOF
@@ -39,6 +39,9 @@
* @throws LocalVCInternalException If the project key or repository slug results in an invalid URI, encapsulating the {@link URISyntaxException}.
*/
public LocalVCRepositoryUri(URI localVCBaseUri, String projectKey, String repositorySlug) {
if (!isSafePathComponent(projectKey) || !isSafePathComponent(repositorySlug)) {
throw new IllegalArgumentException("Invalid projectKey or repositorySlug.");
}
this.uri = buildUri(localVCBaseUri, projectKey, repositorySlug);
this.projectKey = projectKey;
this.repositorySlug = repositorySlug;
@@ -46,6 +49,12 @@
this.isPracticeRepository = isPracticeRepository(repositorySlug, projectKey);
}

private static boolean isSafePathComponent(String input) {
return input != null && !input.isEmpty()
&& !input.contains("..") && !input.contains("/") && !input.contains("\\")
&& input.matches("^[a-zA-Z0-9_-]+$");
}

private static URI buildUri(URI localVCBaseUri, String projectKey, String repositorySlug) {
return UriComponentsBuilder.fromUri(localVCBaseUri).pathSegment("git", projectKey, repositorySlug + ".git").build().toUri();
}
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this also lead to a race condition if two users try to create it at once? Would both processes try to create it?

log.debug("Local git repo {} at {} already exists – skipping creation", repositorySlug, remoteDirPath);
return;
}

try {
Files.createDirectories(remoteDirPath);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.tum.cit.aet.artemis.assessment;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;

import java.time.ZonedDateTime;
import java.util.ArrayList;
Expand All @@ -12,14 +11,12 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jgit.lib.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentMatchers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.test.context.support.WithMockUser;
Expand Down Expand Up @@ -186,8 +183,6 @@ void setupTest() {
result2.setFeedbacks(feedbacks2);
result2.setAssessmentType(AssessmentType.SEMI_AUTOMATIC);

String dummyHash = "9b3a9bd71a0d80e5bbc42204c319ed3d1d4f0d6d";
doReturn(ObjectId.fromString(dummyHash)).when(gitService).getLastCommitHash(ArgumentMatchers.any());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
import static de.tum.cit.aet.artemis.core.connector.AthenaRequestMockProvider.ATHENA_RESTRICTED_MODULE_TEXT_TEST;
import static org.assertj.core.api.Assertions.assertThat;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -33,6 +37,7 @@
import de.tum.cit.aet.artemis.exercise.domain.InitializationState;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationFactory;
import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService;
import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository;
import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService;
import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise;
Expand All @@ -42,12 +47,17 @@
import de.tum.cit.aet.artemis.modeling.util.ModelingExerciseUtilService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;
import de.tum.cit.aet.artemis.programming.domain.RepositoryType;
import de.tum.cit.aet.artemis.programming.icl.LocalVCLocalCITestService;
import de.tum.cit.aet.artemis.programming.service.GitService;
import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository;
import de.tum.cit.aet.artemis.programming.util.LocalRepository;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseParticipationUtilService;
import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService;
import de.tum.cit.aet.artemis.programming.util.RepositoryExportTestUtil;
import de.tum.cit.aet.artemis.text.domain.TextExercise;
import de.tum.cit.aet.artemis.text.domain.TextSubmission;
import de.tum.cit.aet.artemis.text.repository.TextExerciseRepository;
Expand Down Expand Up @@ -100,6 +110,15 @@ class AthenaResourceIntegrationTest extends AbstractAthenaTest {
@Autowired
private LearnerProfileUtilService learnerProfileUtilService;

@Autowired
private LocalVCLocalCITestService localVCLocalCITestService;

@Autowired
private ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationTestRepository;

@Autowired
private ParticipationUtilService participationUtilService;

private TextExercise textExercise;

private TextSubmission textSubmission;
Expand Down Expand Up @@ -155,6 +174,11 @@ protected void initTestCase() {
modelingSubmissionRepository.save(modelingSubmission);
}

@AfterEach
void cleanupRepositories() {
RepositoryExportTestUtil.cleanupTrackedRepositories();
}

@Test
@WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR")
void testGetAvailableProgrammingModulesSuccess_EmptyModules() throws Exception {
Expand Down Expand Up @@ -391,13 +415,12 @@ void testRepositoryExportEndpoint(String urlSuffix) throws Exception {
programmingExerciseParticipationUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise);

// Seed a LocalVC bare repository with content
var sourceRepo = new LocalRepository(defaultBranch);
var sourceRepo = RepositoryExportTestUtil.trackRepository(new LocalRepository(defaultBranch));
sourceRepo.configureRepos(localVCBasePath, "athenaSrcLocalRepo", "athenaSrcOriginRepo");

// Ensure tests repository URI exists on the exercise
var testsSlug = programmingExercise.getProjectKey().toLowerCase() + "-tests";
var testsUri = new LocalVCRepositoryUri(localVCBaseUri, programmingExercise.getProjectKey(), testsSlug);
programmingExercise.setTestRepositoryUri(testsUri.toString());
var testsSlug = programmingExercise.getProjectKey().toLowerCase() + "-" + RepositoryType.TESTS.getName();
programmingExercise.setTestRepositoryUri(localVCLocalCITestService.buildLocalVCUri(null, null, programmingExercise.getProjectKey(), testsSlug));
programmingExerciseRepository.save(programmingExercise);

var sourceUri = new LocalVCRepositoryUri(localVCBaseUri, sourceRepo.remoteBareGitRepoFile.toPath());
Expand All @@ -422,6 +445,55 @@ void testRepositoryExportEndpoint(String urlSuffix) throws Exception {
assertThat(repoFiles).as("export returns exactly one file: README.md").isNotNull().hasSize(1).containsOnlyKeys("README.md").containsEntry("README.md", "Initial commit");
}

@Test
void testStudentRepositoryExportEndpoint() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think coderabbit's suggestion makes sense here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense. But also throwing generic exception in tests make sense since we would prefer tests to fail early, and gather as much context possible for the failure. It is a common pattern used in our test repo.

// Enable Athena for the exercise
programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST);
programmingExerciseRepository.save(programmingExercise);

// Create a programming student participation with a submission and result
var studentLogin = TEST_PREFIX + "student1";
var result = participationUtilService.addProgrammingParticipationWithResultForExercise(programmingExercise, studentLogin);
programmingSubmission = (ProgrammingSubmission) result.getSubmission();

// Prepare a LocalVC student repository and wire it to the participation referenced by the submission.
var projectKey = programmingExercise.getProjectKey();
String srcSlug = projectKey.toLowerCase() + "-athena-src";
var sourceRepo = RepositoryExportTestUtil.trackRepository(localVCLocalCITestService.createAndConfigureLocalRepository(projectKey, srcSlug));
// Remove default seed file (test.txt) from the working copy and commit deletion
var defaultSeed = sourceRepo.workingCopyGitRepoFile.toPath().resolve("test.txt");
if (Files.exists(defaultSeed)) {
Files.delete(defaultSeed);
sourceRepo.workingCopyGitRepo.add().addFilepattern(".").call();
GitService.commit(sourceRepo.workingCopyGitRepo).setMessage("Remove default seed").call();
sourceRepo.workingCopyGitRepo.push().setRemote("origin").call();
}
// Seed a README.md so the export contains the expected file
Path readme = sourceRepo.workingCopyGitRepoFile.toPath().resolve("README.md");
FileUtils.writeStringToFile(readme.toFile(), "Initial commit", java.nio.charset.StandardCharsets.UTF_8);
sourceRepo.workingCopyGitRepo.add().addFilepattern(".").call();
GitService.commit(sourceRepo.workingCopyGitRepo).setMessage("Initial commit").call();
sourceRepo.workingCopyGitRepo.push().setRemote("origin").call();
var studentRepoSlug = localVCLocalCITestService.getRepositorySlug(projectKey, studentLogin);
var studentLocalVCRepo = RepositoryExportTestUtil.seedLocalVcBareFrom(localVCLocalCITestService, projectKey, studentRepoSlug, sourceRepo);

// Persist repository URI on the participation
var programmingStudentParticipation = programmingExerciseStudentParticipationTestRepository.findById(programmingSubmission.getParticipation().getId()).orElseThrow();
programmingStudentParticipation.setRepositoryUri(localVCLocalCITestService.buildLocalVCUri(null, null, projectKey, studentRepoSlug));
programmingExerciseStudentParticipationTestRepository.save(programmingStudentParticipation);

// Call the internal endpoint with valid Athena auth and verify file map
var authHeaders = new HttpHeaders();
authHeaders.add(HttpHeaders.AUTHORIZATION, athenaSecret);

String json = request.get("/api/athena/internal/programming-exercises/" + programmingExercise.getId() + "/submissions/" + programmingSubmission.getId() + "/repository",
HttpStatus.OK, String.class, authHeaders);
Map<String, String> repoFiles = request.getObjectMapper().readValue(json, new TypeReference<Map<String, String>>() {
});
assertThat(repoFiles).as("student export returns exactly one file: README.md").isNotNull().hasSize(1).containsOnlyKeys("README.md").containsEntry("README.md",
"Initial commit");
}

@ParameterizedTest
@ValueSource(strings = { "repository/template", "repository/solution", "repository/tests" })
void testRepositoryExportEndpointsFailWhenAthenaNotEnabled(String urlSuffix) throws Exception {
Expand Down Expand Up @@ -458,4 +530,6 @@ void testRepositoryExportEndpointsFailWithInvalidRepositoryType(String urlSuffix

request.get("/api/athena/internal/programming-exercises/" + programmingExercise.getId() + "/" + urlSuffix, HttpStatus.NOT_FOUND, Result.class, authHeaders);
}

// Removed legacy public endpoint test that expected BAD_REQUEST for invalid repository type
}
Loading
Loading