diff --git a/build.gradle b/build.gradle index 08cc16d..0b39757 100644 --- a/build.gradle +++ b/build.gradle @@ -13,11 +13,16 @@ jar { enabled = false } -//java { -// toolchain { -// languageVersion = JavaLanguageVersion.of(23) -// } -//} +java { + toolchain { + languageVersion = JavaLanguageVersion.of(23) + } +} + +test { + maxHeapSize = "512m" + forkEvery = 1 +} repositories { mavenCentral() @@ -47,9 +52,12 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'com.h2database:h2' + testImplementation 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' + } tasks.named('test') { diff --git a/src/main/java/com/codzilla/backend/Submissions/RedisSubmissionRepository.java b/src/main/java/com/codzilla/backend/Submissions/RedisSubmissionRepository.java deleted file mode 100644 index 307829f..0000000 --- a/src/main/java/com/codzilla/backend/Submissions/RedisSubmissionRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.codzilla.backend.Submissions; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; -import tools.jackson.databind.ObjectMapper; - -import java.util.Optional; -import java.util.UUID; - -@Repository -public class RedisSubmissionRepository { - - @Autowired - protected ObjectMapper objectMapper; - - @Autowired - RedisTemplate redisTemplate; - - - public void save(Submission submission) { - redisTemplate.opsForValue().set("submission:" + submission.id(), submission); - } - - - public Optional get(UUID id) { - Object value = redisTemplate.opsForValue().get("submission:"+id); - if (value == null) return Optional.empty(); - return Optional.of(objectMapper.convertValue(value, Submission.class)); - } -} diff --git a/src/main/java/com/codzilla/backend/Submissions/Submission.java b/src/main/java/com/codzilla/backend/Submissions/Submission.java deleted file mode 100644 index cf90a5d..0000000 --- a/src/main/java/com/codzilla/backend/Submissions/Submission.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.codzilla.backend.Submissions; - -import java.util.UUID; -import com.codzilla.backend.Submissions.Language; - -public record Submission( - UUID id, - UUID userId, - String s3Path, - Language language -) { -} - -enum Language {CPP, PY, JAVA} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/Submissions/SubmissionConfiguration.java b/src/main/java/com/codzilla/backend/Submissions/SubmissionConfiguration.java deleted file mode 100644 index 1e779de..0000000 --- a/src/main/java/com/codzilla/backend/Submissions/SubmissionConfiguration.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.codzilla.backend.Submissions; - - -import com.codzilla.backend.S3.S3Settings; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; - - -@Slf4j -@Configuration -public class SubmissionConfiguration { - - @Bean - RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - var redisTemplate = new RedisTemplate(); - redisTemplate.setConnectionFactory(redisConnectionFactory); - - redisTemplate.setKeySerializer(new StringRedisSerializer()); - - JacksonJsonRedisSerializer serializer = new JacksonJsonRedisSerializer<>(Object.class); - - redisTemplate.setValueSerializer(serializer); - return redisTemplate; - } - -} diff --git a/src/main/java/com/codzilla/backend/Submissions/SubmissionService.java b/src/main/java/com/codzilla/backend/Submissions/SubmissionService.java deleted file mode 100644 index ff397d3..0000000 --- a/src/main/java/com/codzilla/backend/Submissions/SubmissionService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.codzilla.backend.Submissions; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Limit; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; - -import java.util.List; - -@Slf4j -@Service -public class SubmissionService { -} diff --git a/src/main/java/com/codzilla/backend/User/UserRepository.java b/src/main/java/com/codzilla/backend/User/UserRepository.java index e7d2582..71446d6 100644 --- a/src/main/java/com/codzilla/backend/User/UserRepository.java +++ b/src/main/java/com/codzilla/backend/User/UserRepository.java @@ -1,6 +1,8 @@ package com.codzilla.backend.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; import java.util.UUID; @@ -9,4 +11,7 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); boolean existsByNickname(String nickname); Optional findByEmail(String email); + + @Query("SELECT u.id FROM User u WHERE u.email = :email") + Optional findIdByEmail(@Param("email") String email); } diff --git a/src/main/java/com/codzilla/backend/User/UserService.java b/src/main/java/com/codzilla/backend/User/UserService.java index cd4dc93..e6b8ebd 100644 --- a/src/main/java/com/codzilla/backend/User/UserService.java +++ b/src/main/java/com/codzilla/backend/User/UserService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.NoSuchElementException; import java.util.UUID; @Slf4j @@ -26,6 +27,14 @@ public UserService(UserRepository userRepository, PasswordEncoder passwordEncode this.passwordEncoder = passwordEncoder; } + public UUID getIdByEmail(String email) { + try { + return userRepository.findIdByEmail(email).get(); + } catch (NoSuchElementException e) { + throw new UserNotFoundException(); + } + } + public void registerUser(RegisterRequestDTO dto) throws UserAlreadyExistsException { if (userRepository.existsByEmail(dto.email())) { throw new UserAlreadyExistsException(); diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java b/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java index 987ccc2..4098d73 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/judge0/Judge0Client.java @@ -17,33 +17,48 @@ public class Judge0Client { private final ObjectMapper objectMapper = new ObjectMapper(); public Judge0Client(@Value("${judge0.base-url}") String baseUrl) { + this.restClient = RestClient.builder() - .baseUrl(baseUrl) - .build(); + .baseUrl(baseUrl) + .build(); } - - public String submitAsync(String sourceCode, int languageId, String stdin, String expectedOutput) { + public String submitAsync(String sourceCode, int languageId, String stdin, + String expectedOutput) { try { String body = objectMapper.writeValueAsString( - new SubmissionRequest(sourceCode, languageId, stdin, expectedOutput) + new SubmissionRequest( + sourceCode, + languageId, + stdin, + expectedOutput + ) ); String raw = restClient.post() - .uri("/submissions?base64_encoded=false") - .contentType(MediaType.APPLICATION_JSON) - .body(body) - .retrieve() - .body(String.class); - - - TokenResponse tokenResponse = objectMapper.readValue(raw, TokenResponse.class); + .uri("/submissions?base64_encoded=false&fields=stdout") + .contentType(MediaType.APPLICATION_JSON) + .body(body) + .retrieve() + .body(String.class); + + TokenResponse tokenResponse = objectMapper.readValue( + raw, + TokenResponse.class + ); + log.info( + "Submission get token: {}", + tokenResponse.getToken() + ); return tokenResponse.getToken(); } catch (Exception e) { - log.error("Judge0 submission failed", e); + log.error( + "Judge0 submission failed", + e + ); return null; } } @@ -51,13 +66,22 @@ public String submitAsync(String sourceCode, int languageId, String stdin, Strin public SubmissionResponse getSubmissionStatus(String token) { try { String raw = restClient.get() - .uri("/submissions/" + token + "?base64_encoded=false") - .retrieve() - .body(String.class); - - return objectMapper.readValue(raw, SubmissionResponse.class); + .uri("/submissions/" + token + "?base64_encoded=false") + .retrieve() + .body(String.class); + log.info( + "Raw data from judge: {}", + raw + ); + return objectMapper.readValue( + raw, + SubmissionResponse.class + ); } catch (Exception e) { - log.error("Failed to fetch status for token: " + token, e); + log.error( + "Failed to fetch status for token: " + token, + e + ); return null; } } @@ -74,14 +98,14 @@ public static class SubmissionRequest { public String source_code; public Integer language_id; public String stdin; - String expectedOutput; + public String expected_output; - public SubmissionRequest(String sourceCode, int languageId, String stdin, String expectedOutput) { + public SubmissionRequest(String sourceCode, int languageId, String stdin, + String expectedOutput) { this.source_code = sourceCode; this.language_id = languageId; this.stdin = stdin; - this.expectedOutput = expectedOutput; - + this.expected_output = expectedOutput; } } diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java index fed9d9d..cfae201 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/CreateProblemRequest.java @@ -1,9 +1,11 @@ package com.codzilla.backend.controller.Sandbox.polygon; import com.codzilla.backend.controller.Sandbox.problem.Problem; +import lombok.Builder; import lombok.Data; import java.util.List; + @Data public class CreateProblemRequest { private String name; @@ -15,5 +17,10 @@ public class CreateProblemRequest { public static class TestCase { private String input; private String output; + +// public TestCase(String input, String output) { +// this.input = input; +// this.output = output; +// } } } \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java index 3e3c14d..b7c473b 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonClient.java @@ -1,5 +1,6 @@ package com.codzilla.backend.controller.Sandbox.polygon; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -35,10 +36,11 @@ public class PolygonClient { @Value("${polygon.api.secret}") private String apiSecret; - private final RestClient restClient; + private RestClient restClient; private final ObjectMapper objectMapper = new ObjectMapper(); - public PolygonClient() { + @PostConstruct + public void init() { this.restClient = RestClient.builder() .baseUrl(baseUrl) .defaultHeader("Accept", "application/json") @@ -48,14 +50,14 @@ public PolygonClient() { public PolygonProblem getProblemTests(String problemId) { + log.info("Polygon api URL:{}", baseUrl); var params = new TreeMap(); params.put("problemId", problemId); params.put("testset", "tests"); - String url = buildSignedUrl("problem.tests", params); - + String fullUrl = buildSignedUrl("problem.tests", params); String raw = restClient.get() - .uri(url) + .uri(fullUrl) .retrieve() .onStatus(status -> true, (req, res) -> {}) .body(String.class); diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblemService.java b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblemService.java index b68ec17..b0d9315 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblemService.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/polygon/PolygonProblemService.java @@ -22,6 +22,7 @@ public class PolygonProblemService { public List getTests(String polygonId) { + try { return cache.get(polygonId, () -> { log.info("Fetching fresh tests for problem {} from Polygon", polygonId); diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java index a89ee61..2c07daf 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemController.java @@ -1,10 +1,17 @@ package com.codzilla.backend.controller.Sandbox.problem; +import com.codzilla.backend.User.User; +import com.codzilla.backend.User.UserRepository; +import com.codzilla.backend.User.UserService; import com.codzilla.backend.controller.Sandbox.polygon.CreateProblemRequest; +import com.codzilla.backend.controller.Sandbox.polygon.PolygonClient; +import com.codzilla.backend.controller.Sandbox.submission.SubmissionRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -12,12 +19,15 @@ import java.nio.charset.StandardCharsets; import java.util.UUID; +@Slf4j @RestController @RequestMapping("/problems") @RequiredArgsConstructor public class ProblemController { private final ProblemService problemService; + private final UserService userService; + private final SubmissionRepository submissionRepository; @PostMapping("/create") public ResponseEntity createProblem(@RequestBody CreateProblemRequest request) { @@ -25,23 +35,45 @@ public ResponseEntity createProblem(@RequestBody CreateProblemRequest r return ResponseEntity.ok(saved); } - @PostMapping(value = "/{id}/submit/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "submit/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity submitFile( - @PathVariable Long id, + @AuthenticationPrincipal User user, + @RequestParam Long problemId, @RequestParam int languageId, - @RequestParam("file") MultipartFile file) throws IOException { - String sourceCode = new String(file.getBytes(), StandardCharsets.UTF_8); - String result = problemService.submitSolution(1L, id, sourceCode, languageId); + @RequestParam MultipartFile file + ) throws IOException { + String sourceCode = new String( + file.getBytes(), + StandardCharsets.UTF_8 + ); + log.info("File content: {}", sourceCode); + UUID userId = userService.getIdByEmail(user.getEmail()); + String result = problemService.submitSolution( + userId, + problemId, + sourceCode, + languageId + ); return ResponseEntity.ok(result); } + @GetMapping("/submissions/{id}/status") + public ResponseEntity getStatus(@PathVariable Long id) { + return submissionRepository.findById(id) + .map(sub -> { + String details = sub.getResultDetails() != null ? ": " + sub.getResultDetails() : ""; + return ResponseEntity.ok(sub.getStatus().name() + details); + }) + .orElse(ResponseEntity.notFound().build()); + } @PostMapping("/{id}/submit") public ResponseEntity submit( @PathVariable Long id, @RequestParam int languageId, @RequestBody String sourceCode) { - String result = problemService.submitSolution(1L, id, sourceCode, languageId); + String result = problemService.submitSolution(UUID.randomUUID(), id, sourceCode, languageId); return ResponseEntity.ok(result); } + } \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java index 24c6b6e..7a2dc5c 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemService.java @@ -1,16 +1,21 @@ package com.codzilla.backend.controller.Sandbox.problem; +import com.codzilla.backend.S3.S3Repository; import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; import com.codzilla.backend.controller.Sandbox.polygon.*; import com.codzilla.backend.controller.Sandbox.submission.Submission; import com.codzilla.backend.controller.Sandbox.submission.SubmissionRepository; +import com.codzilla.backend.controller.Sandbox.submission.SubmissionTest; +import com.codzilla.backend.controller.Sandbox.submission.SubmissionTestRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; +import java.util.UUID; + @Service @RequiredArgsConstructor @Slf4j @@ -20,36 +25,79 @@ public class ProblemService { private final Judge0Client judge0Client; private final PolygonProblemService polygonProblemService; private final SubmissionRepository submissionRepository; + private final PolygonClient polygonClient; + private final S3Repository s3Repository; + private final SubmissionTestRepository submissionTestRepository; + private final ProblemTestRepository problemTestRepository; public Problem createProblem(CreateProblemRequest request) { + String polygonId = polygonClient.createProblem(request.getName()); Problem problem = new Problem(); - - problem.setPolygonToken(request.getName()); + problem.setPolygonToken(polygonId); problem.setType(request.getType()); problem.setLevel(request.getLevel()); + Problem saved = problemRepository.save(problem); + + if (request.getTests() != null) { + for (int i = 0; i < request.getTests().size(); i++) { + var t = request.getTests().get(i); + + polygonClient.saveTest(polygonId, i + 1, t.getInput(), t.getOutput()); + - return problemRepository.save(problem); + ProblemTest pt = new ProblemTest(); + pt.setProblemId(saved.getId()); + pt.setTestIndex(i + 1); + pt.setInput(t.getInput()); + pt.setExpectedOutput(t.getOutput()); + problemTestRepository.save(pt); + } + } + + return saved; } - public String submitSolution(Long userId, Long problemId, String sourceCode, int languageId) { + public String submitSolution(UUID userId, Long problemId, String sourceCode, int languageId) { Problem problem = problemRepository.findById(problemId) .orElseThrow(() -> new RuntimeException("Problem not found: " + problemId)); - List tests = polygonProblemService.getTests(problem.getPolygonToken()); - PolygonProblem.Test mainTest = tests.get(0); - String token = judge0Client.submitAsync(sourceCode, languageId, mainTest.getInput(), mainTest.getOutput()); + List tests = problemTestRepository + .findAllByProblemIdOrderByTestIndex(problemId); + if (tests.isEmpty()) { + throw new RuntimeException("No tests found for problem " + problemId); + } Submission sub = new Submission(); sub.setProblemId(problemId); - sub.setSourceCode(sourceCode); + sub.setUserId(userId); + sub.setLanguageId(languageId); - sub.setJudge0Token(token); sub.setStatus(Submission.Status.IN_QUEUE); - submissionRepository.save(sub); + Submission saved = submissionRepository.save(sub); + s3Repository.save(sourceCode.getBytes(), "submissions/" + saved.getId()); + for (int i = 0; i < tests.size(); i++) { + ProblemTest test = tests.get(i); + + String token = judge0Client.submitAsync( + sourceCode, languageId, test.getInput(), null + ); + + if (token == null) { + throw new RuntimeException("Judge0 unavailable"); + } + + SubmissionTest subTest = new SubmissionTest(); + subTest.setSubmissionId(saved.getId()); + subTest.setTestIndex(i + 1); + subTest.setJudge0Token(token); + subTest.setExpectedOutput(test.getExpectedOutput().trim()); + subTest.setStatus(SubmissionTest.Status.IN_QUEUE); + submissionTestRepository.save(subTest); + } - return "Submitted! Your token: " + token; + return saved.getId().toString(); } } \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemTest.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemTest.java new file mode 100644 index 0000000..71bfc30 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemTest.java @@ -0,0 +1,23 @@ +package com.codzilla.backend.controller.Sandbox.problem; + +import jakarta.persistence.*; +import lombok.Data; + +@Data +@Entity +@Table(name = "problem_tests") +public class ProblemTest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long problemId; + private int testIndex; + + @Column(columnDefinition = "TEXT") + private String input; + + @Column(columnDefinition = "TEXT") + private String expectedOutput; +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemTestRepository.java b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemTestRepository.java new file mode 100644 index 0000000..f023ef7 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/problem/ProblemTestRepository.java @@ -0,0 +1,8 @@ +package com.codzilla.backend.controller.Sandbox.problem; + +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface ProblemTestRepository extends JpaRepository { + List findAllByProblemIdOrderByTestIndex(Long problemId); +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java index 377f9be..9630521 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/Submission.java @@ -3,11 +3,14 @@ import jakarta.persistence.*; import lombok.Data; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; @Entity @Table(name = "submissions") @@ -19,11 +22,14 @@ public class Submission { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private int retryCount = 0; private Long problemId; - private Long userId; - @Column(columnDefinition = "TEXT") + @JdbcTypeCode(SqlTypes.UUID) + private UUID userId; + + @Transient private String sourceCode; private Integer languageId; diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java index dc51dd1..12dd8ec 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionPollingService.java @@ -1,15 +1,11 @@ package com.codzilla.backend.controller.Sandbox.submission; import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; -import com.codzilla.backend.controller.Sandbox.polygon.PolygonProblemService; -import com.codzilla.backend.controller.Sandbox.problem.Problem; -import com.codzilla.backend.controller.Sandbox.problem.ProblemRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; import java.util.List; @Slf4j @@ -18,40 +14,86 @@ public class SubmissionPollingService { private final SubmissionRepository submissionRepository; + private final SubmissionTestRepository submissionTestRepository; private final Judge0Client judge0Client; - final private PolygonProblemService polygonProblemService; - final private ProblemRepository problemRepository; @Scheduled(fixedDelay = 2000) public void pollStatuses() { - List pending = submissionRepository.findAllByStatus(Submission.Status.IN_QUEUE); - for (Submission sub : pending) { - var response = judge0Client.getSubmissionStatus(sub.getJudge0Token()); - if (response != null && response.getStatus() != null && response.getStatus().getId() > 2) { - updateSubmissionStatus(sub, response); - } + List pending = submissionTestRepository + .findAllByStatus(SubmissionTest.Status.IN_QUEUE); + + for (SubmissionTest subTest : pending) { + var response = judge0Client.getSubmissionStatus(subTest.getJudge0Token()); + if (response == null || response.getStatus() == null) continue; + if (response.getStatus().getId() <= 2) continue; // ещё обрабатывается + + updateTestStatus(subTest, response); + updateSubmissionStatus(subTest.getSubmissionId()); } } - private void updateSubmissionStatus(Submission sub, Judge0Client.SubmissionResponse response) { - String description = response.getStatus().getDescription(); + private void updateTestStatus(SubmissionTest subTest, Judge0Client.SubmissionResponse response) { + int statusId = response.getStatus().getId(); + String actual = response.getStdout() == null ? "" : response.getStdout().trim(); - if ("Accepted".equals(description)) { - sub.setStatus(Submission.Status.ACCEPTED); - } else if (description.contains("Compile Error")) { - sub.setStatus(Submission.Status.COMPILE_ERROR); - } else if (description.contains("Wrong Answer")) { - sub.setStatus(Submission.Status.WRONG_ANSWER); + subTest.setActualOutput(actual); + + if (statusId == 6) { + subTest.setStatus(SubmissionTest.Status.COMPILE_ERROR); + } else if (statusId >= 7 && statusId <= 12) { + subTest.setStatus(SubmissionTest.Status.RUNTIME_ERROR); + } else if (statusId == 3) { + String expected = subTest.getExpectedOutput() == null ? "" : subTest.getExpectedOutput().trim(); + if (actual.equals(expected)) { + subTest.setStatus(SubmissionTest.Status.ACCEPTED); + } else { + subTest.setStatus(SubmissionTest.Status.WRONG_ANSWER); + } } else { - sub.setStatus(Submission.Status.RUNTIME_ERROR); + subTest.setStatus(SubmissionTest.Status.RUNTIME_ERROR); } - sub.setResultDetails(description); - submissionRepository.save(sub); + submissionTestRepository.save(subTest); + log.info("Test {} of submission {} → {}", + subTest.getTestIndex(), subTest.getSubmissionId(), subTest.getStatus()); + } - log.info("Submission {} finished with verdict: {}", sub.getId(), description); + private void updateSubmissionStatus(Long submissionId) { + List allTests = submissionTestRepository + .findAllBySubmissionIdOrderByTestIndex(submissionId); + boolean allDone = allTests.stream() + .allMatch(t -> t.getStatus() != SubmissionTest.Status.IN_QUEUE); + if (!allDone) return; + // ищем первый провальный тест + SubmissionTest firstFailed = allTests.stream() + .filter(t -> t.getStatus() != SubmissionTest.Status.ACCEPTED) + .findFirst() + .orElse(null); + + Submission sub = submissionRepository.findById(submissionId).orElse(null); + if (sub == null) return; + + if (firstFailed == null) { + sub.setStatus(Submission.Status.ACCEPTED); + sub.setResultDetails("All " + allTests.size() + " tests passed"); + } else { + Submission.Status status = switch (firstFailed.getStatus()) { + case WRONG_ANSWER -> Submission.Status.WRONG_ANSWER; + case COMPILE_ERROR -> Submission.Status.COMPILE_ERROR; + default -> Submission.Status.RUNTIME_ERROR; + }; + sub.setStatus(status); + sub.setResultDetails( + "Failed on test " + firstFailed.getTestIndex() + + "\nExpected: " + firstFailed.getExpectedOutput() + + "\nGot: " + firstFailed.getActualOutput() + ); + } + + submissionRepository.save(sub); + log.info("Submission {} final verdict: {}", submissionId, sub.getStatus()); } } \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java index e9cce27..6ce17d5 100644 --- a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionRepository.java @@ -4,8 +4,12 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface SubmissionRepository extends JpaRepository { + Optional findByJudge0Token(String token); List findAllByStatus(Submission.Status status); +// Submission findById(Long id); + } diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionTest.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionTest.java new file mode 100644 index 0000000..8d17198 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionTest.java @@ -0,0 +1,31 @@ +package com.codzilla.backend.controller.Sandbox.submission; + +import jakarta.persistence.*; +import lombok.Data; + +@Data +@Entity +@Table(name = "submission_tests") +public class SubmissionTest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long submissionId; + private int testIndex; + private String judge0Token; + + @Column(columnDefinition = "TEXT") + private String expectedOutput; + + @Enumerated(EnumType.STRING) + private Status status = Status.IN_QUEUE; + + @Column(columnDefinition = "TEXT") + private String actualOutput; + + public enum Status { + IN_QUEUE, PROCESSING, ACCEPTED, WRONG_ANSWER, COMPILE_ERROR, RUNTIME_ERROR + } +} \ No newline at end of file diff --git a/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionTestRepository.java b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionTestRepository.java new file mode 100644 index 0000000..027b289 --- /dev/null +++ b/src/main/java/com/codzilla/backend/controller/Sandbox/submission/SubmissionTestRepository.java @@ -0,0 +1,9 @@ +package com.codzilla.backend.controller.Sandbox.submission; + +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SubmissionTestRepository extends JpaRepository { + List findAllBySubmissionIdOrderByTestIndex(Long submissionId); + List findAllByStatus(SubmissionTest.Status status); +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/Sandbox/SandboxTest.java b/src/test/java/com/codzilla/backend/Sandbox/SandboxTest.java new file mode 100644 index 0000000..334affb --- /dev/null +++ b/src/test/java/com/codzilla/backend/Sandbox/SandboxTest.java @@ -0,0 +1,219 @@ +package com.codzilla.backend.Sandbox; + +import com.codzilla.backend.S3.S3Repository; +import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; +import com.codzilla.backend.controller.Sandbox.polygon.*; +import com.codzilla.backend.controller.Sandbox.problem.*; +import com.codzilla.backend.controller.Sandbox.submission.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class SandboxTest { + @Mock + private ProblemTestRepository problemTestRepository; + + @Mock + private SubmissionTestRepository submissionTestRepository; + + @Mock + private ProblemRepository problemRepository; + + @Mock + private Judge0Client judge0Client; + + @Mock + private PolygonProblemService polygonProblemService; + + @Mock + private SubmissionRepository submissionRepository; + + @Mock + private PolygonClient polygonClient; + + @Mock + private S3Repository s3Repository; + + @InjectMocks + private ProblemService problemService; + + private Problem problem; + private PolygonProblem.Test test; + + @BeforeEach + void setUp() { + problem = new Problem(); + problem.setId(1L); + problem.setPolygonToken("test-problem-1"); + problem.setType(Problem.ProblemType.ALGORITHM); + problem.setLevel(Problem.ProblemLevel.EASY); + + test = new PolygonProblem.Test(); + test.setIndex(1); + test.setInput("1 2"); + test.setOutput("3"); + } + + + @Test + void createProblem_shouldSaveProblem() { + CreateProblemRequest request = new CreateProblemRequest(); + request.setName("test-problem-1"); + request.setType(Problem.ProblemType.ALGORITHM); + request.setLevel(Problem.ProblemLevel.EASY); + + when(polygonClient.createProblem(anyString())).thenReturn("517936"); + when(problemRepository.save(any())) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Problem result = problemService.createProblem(request); + + assertThat(result).isNotNull(); + assertThat(result.getPolygonToken()).isEqualTo("517936"); + verify(problemRepository, times(1)).save(any()); + } + + @Test + void createProblem_shouldCallPolygonAPI() { + when(polygonClient.createProblem(anyString())).thenReturn("517936"); + when(problemRepository.save(any())).thenReturn(problem); + + CreateProblemRequest request = new CreateProblemRequest(); + request.setName("test-problem-1"); + request.setType(Problem.ProblemType.ALGORITHM); + request.setLevel(Problem.ProblemLevel.EASY); + + problemService.createProblem(request); + + verify(polygonClient, times(1)).createProblem("test-problem-1"); + } + + @Test + void submitSolution_shouldThrowWhenProblemNotFound() { + when(problemRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> problemService.submitSolution(UUID.randomUUID(), 99L, "code", 71)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Problem not found"); + } + + @Test + void judge0Client_expectedOutputFieldNameShouldBeCorrect() throws Exception { + com.fasterxml.jackson.databind.ObjectMapper mapper = + new com.fasterxml.jackson.databind.ObjectMapper(); + + Judge0Client.SubmissionRequest request = + new Judge0Client.SubmissionRequest("print(3)", 71, "1 2", "3"); + + String json = mapper.writeValueAsString(request); + + assertThat(json).contains("\"expected_output\""); + assertThat(json).contains("\"3\""); + } + @Test + void submitSolution_shouldReturnToken() { + Submission savedSub = new Submission(); + savedSub.setId(42L); + savedSub.setStatus(Submission.Status.IN_QUEUE); + + when(problemRepository.findById(1L)).thenReturn(Optional.of(problem)); + when(problemTestRepository.findAllByProblemIdOrderByTestIndex(1L)) + .thenReturn(List.of(toProblemTest(test, 1L))); + when(judge0Client.submitAsync(anyString(), anyInt(), anyString(), isNull())) + .thenReturn("judge0-token-123"); + when(submissionRepository.save(any())).thenReturn(savedSub); + when(submissionTestRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + String result = problemService.submitSolution(UUID.randomUUID(), 1L, "print(3)", 71); + + assertThat(result).isEqualTo("42"); + } + + @Test + void submitSolution_shouldRunAllTests() { + Submission savedSub = new Submission(); + savedSub.setId(42L); + + ProblemTest pt1 = toProblemTest(test, 1L); + ProblemTest pt2 = new ProblemTest(); + pt2.setProblemId(1L); + pt2.setTestIndex(2); + pt2.setInput("5 10"); + pt2.setExpectedOutput("15"); + + when(problemRepository.findById(1L)).thenReturn(Optional.of(problem)); + when(problemTestRepository.findAllByProblemIdOrderByTestIndex(1L)) + .thenReturn(List.of(pt1, pt2)); + when(judge0Client.submitAsync(anyString(), anyInt(), anyString(), isNull())) + .thenReturn("token"); + when(submissionRepository.save(any())).thenReturn(savedSub); + when(submissionTestRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + problemService.submitSolution(UUID.randomUUID(), 1L, "print(3)", 71); + + verify(judge0Client, times(2)).submitAsync(anyString(), anyInt(), anyString(), isNull()); + } + + @Test + void submitSolution_shouldSaveSubmissionWithInQueueStatus() { + Submission[] saved = new Submission[1]; + Submission savedSub = new Submission(); + savedSub.setId(42L); + + when(problemRepository.findById(1L)).thenReturn(Optional.of(problem)); + when(problemTestRepository.findAllByProblemIdOrderByTestIndex(1L)) + .thenReturn(List.of(toProblemTest(test, 1L))); + when(judge0Client.submitAsync(anyString(), anyInt(), anyString(), isNull())) + .thenReturn("token-123"); + when(submissionRepository.save(any())).thenAnswer(inv -> { + saved[0] = inv.getArgument(0); + saved[0].setId(42L); + return saved[0]; + }); + when(submissionTestRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + problemService.submitSolution(UUID.randomUUID(), 1L, "print(3)", 71); + + assertThat(saved[0]).isNotNull(); + assertThat(saved[0].getStatus()).isEqualTo(Submission.Status.IN_QUEUE); + assertThat(saved[0].getProblemId()).isEqualTo(1L); + } + + @Test + void submitSolution_shouldHandleJudge0Failure() { + Submission savedSub = new Submission(); + savedSub.setId(42L); + + when(problemRepository.findById(1L)).thenReturn(Optional.of(problem)); + when(problemTestRepository.findAllByProblemIdOrderByTestIndex(1L)) + .thenReturn(List.of(toProblemTest(test, 1L))); + when(judge0Client.submitAsync(anyString(), anyInt(), anyString(), isNull())) + .thenReturn(null); + when(submissionRepository.save(any())).thenReturn(savedSub); + + assertThatThrownBy(() -> problemService.submitSolution(UUID.randomUUID(), 1L, "print(3)", 71)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Judge0 unavailable"); + } + + private ProblemTest toProblemTest(PolygonProblem.Test t, Long problemId) { + ProblemTest pt = new ProblemTest(); + pt.setProblemId(problemId); + pt.setTestIndex(t.getIndex()); + pt.setInput(t.getInput()); + pt.setExpectedOutput(t.getOutput()); + return pt; + } +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/Sandbox/controller/ProblemControllerFileTest.java b/src/test/java/com/codzilla/backend/Sandbox/controller/ProblemControllerFileTest.java new file mode 100644 index 0000000..1b64ed6 --- /dev/null +++ b/src/test/java/com/codzilla/backend/Sandbox/controller/ProblemControllerFileTest.java @@ -0,0 +1,85 @@ +package com.codzilla.backend.Sandbox.controller; + +import com.codzilla.backend.User.User; +import com.codzilla.backend.User.UserService; +import com.codzilla.backend.controller.Sandbox.problem.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.mock.web.MockMultipartFile; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest( + controllers = ProblemController.class, + excludeAutoConfiguration = { + org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration.class, + org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration.class, + org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration.class + } +) +class ProblemControllerFileTest { + + @MockitoBean + private com.codzilla.backend.Authentication.JWTRequestFilter.JWTRequestFilter jwtRequestFilter; + + @MockitoBean + private com.codzilla.backend.Authentication.JWTUtils.JWTUtils jwtUtils; + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ProblemService problemService; + + @MockitoBean + private UserService userService; + + @MockitoBean + private com.codzilla.backend.controller.Sandbox.submission.SubmissionRepository submissionRepository; + + private User mockUser; + + @BeforeEach + void setUp() { + mockUser = User.builder() + .email("test@mail.com") + .password("password") + .nickname("tester") + .build(); + } + + + @Test + void submitFile_shouldWork() throws Exception { + MockMultipartFile file = new MockMultipartFile( + "file", + "solution.py", + MediaType.TEXT_PLAIN_VALUE, + "print(3)".getBytes() + ); + + when(userService.getIdByEmail("test@mail.com")) + .thenReturn(UUID.randomUUID()); + + when(problemService.submitSolution(any(UUID.class), anyLong(), anyString(), anyInt())) + .thenReturn("Submitted!"); + + mockMvc.perform(multipart("/problems/submit/file") + .file(file) + .param("problemId", "1") + .param("languageId", "71") + .with(user(mockUser))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/Sandbox/controller/ProblemControllerTest.java b/src/test/java/com/codzilla/backend/Sandbox/controller/ProblemControllerTest.java new file mode 100644 index 0000000..7a0b3c4 --- /dev/null +++ b/src/test/java/com/codzilla/backend/Sandbox/controller/ProblemControllerTest.java @@ -0,0 +1,95 @@ +package com.codzilla.backend.Sandbox.controller; + +import com.codzilla.backend.User.User; +import com.codzilla.backend.User.UserService; +import com.codzilla.backend.controller.Sandbox.problem.*; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.mock.web.MockMultipartFile; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@WebMvcTest( + controllers = ProblemController.class, + excludeAutoConfiguration = { + org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration.class, + org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration.class, + org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration.class + } +) +@AutoConfigureMockMvc(addFilters = false) +class ProblemControllerTest { + + @MockitoBean + private com.codzilla.backend.Authentication.JWTRequestFilter.JWTRequestFilter jwtRequestFilter; + + @MockitoBean + private com.codzilla.backend.Authentication.JWTUtils.JWTUtils jwtUtils; + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ProblemService problemService; + + @MockitoBean + private UserService userService; + + @MockitoBean + private com.codzilla.backend.controller.Sandbox.submission.SubmissionRepository submissionRepository; + + + @Test + void createProblem_shouldReturnSavedProblem() throws Exception { + + Problem problem = new Problem(); + problem.setId(1L); + problem.setPolygonToken("517936"); + problem.setType(Problem.ProblemType.ALGORITHM); + problem.setLevel(Problem.ProblemLevel.EASY); + + when(problemService.createProblem(any())).thenReturn(problem); + + String json = """ + { + "name": "test-problem", + "type": "ALGORITHM", + "level": "EASY" + } + """; + + mockMvc.perform(post("/problems/create") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.polygonToken").value("517936")); + } + + + @Test + void submit_shouldReturnResult() throws Exception { + + when(problemService.submitSolution(any(UUID.class), anyLong(), anyString(), anyInt())) + .thenReturn("Submitted! token-123"); + + mockMvc.perform(post("/problems/1/submit") + .param("languageId", "71") + .content("print(3)") + .contentType(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("token-123"))); + } + +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java b/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java new file mode 100644 index 0000000..0c58003 --- /dev/null +++ b/src/test/java/com/codzilla/backend/Sandbox/integration/ProblemIntegrationTest.java @@ -0,0 +1,137 @@ +package com.codzilla.backend.Sandbox.integration; + +import com.codzilla.backend.S3.S3Initialization; +import com.codzilla.backend.User.UserRepository; + +import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; +import com.codzilla.backend.controller.Sandbox.polygon.PolygonClient; +import com.codzilla.backend.controller.Sandbox.polygon.PolygonProblem; +import com.codzilla.backend.controller.Sandbox.problem.Problem; +import com.codzilla.backend.controller.Sandbox.problem.ProblemRepository; +import com.codzilla.backend.controller.Sandbox.submission.Submission; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + + +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.MOCK, + properties = { + "spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.username=sa", + "spring.datasource.password=", + "spring.jpa.database-platform=org.hibernate.dialect.H2Dialect", + "spring.jpa.hibernate.ddl-auto=create-drop", + + "app.s3.enabled=false", + + "spring.autoconfigure.exclude=" + + "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration," + + "org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration" + } +) +@AutoConfigureMockMvc(addFilters = false) +class ProblemIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private com.codzilla.backend.controller.Sandbox.problem.ProblemTestRepository problemTestRepository; + + @MockitoBean + private com.codzilla.backend.controller.Sandbox.submission.SubmissionTestRepository submissionTestRepository; + + @MockitoBean + private ProblemRepository problemRepository; + @MockitoBean + private S3Initialization s3Initialization; + + @MockitoBean + private PolygonClient polygonClient; + + @MockitoBean + private Judge0Client judge0Client; + + @MockitoBean + private UserRepository userRepository; + @MockitoBean + private com.codzilla.backend.controller.Sandbox.polygon.PolygonProblemService polygonProblemService; + + @MockitoBean + private com.codzilla.backend.controller.Sandbox.submission.SubmissionRepository submissionRepository; + + @Test + void fullFlow_createAndSubmit() throws Exception { + Problem problem = new Problem(); + problem.setId(1L); + problem.setPolygonToken("533472"); + problem.setType(Problem.ProblemType.ALGORITHM); + problem.setLevel(Problem.ProblemLevel.EASY); + + + when(polygonClient.createProblem(anyString())).thenReturn("533472"); + + + when(problemRepository.save(any())).thenReturn(problem); + + + when(problemRepository.findById(1L)).thenReturn(Optional.of(problem)); + + + PolygonProblem.Test test = new PolygonProblem.Test(); + test.setIndex(1); + test.setInput("1 2"); + test.setOutput("3"); + + when(polygonProblemService.getTests("517936")).thenReturn(List.of(test)); + when(judge0Client.submitAsync(any(), anyInt(), any(), any())).thenReturn("token-123"); + com.codzilla.backend.controller.Sandbox.problem.ProblemTest pt = + new com.codzilla.backend.controller.Sandbox.problem.ProblemTest(); + pt.setProblemId(1L); + pt.setTestIndex(1); + pt.setInput("1 2"); + pt.setExpectedOutput("3"); + + when(problemTestRepository.findAllByProblemIdOrderByTestIndex(1L)) + .thenReturn(List.of(pt)); + + Submission savedSub = new Submission(); + savedSub.setId(42L); + savedSub.setStatus(Submission.Status.IN_QUEUE); + when(submissionRepository.save(any())).thenReturn(savedSub); + when(submissionTestRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + mockMvc.perform(post("/problems/create") + .contentType("application/json") + .content(""" + { + "name": "test-problem-check-1", + "type": "ALGORITHM", + "level": "EASY", + "tests": [{"input": "1 2", "output": "3"}] + } + """)) + .andExpect(status().isOk()); + + mockMvc.perform(post("/problems/1/submit") + .param("languageId", "71") + .content("print(3)") + .contentType("text/plain")) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/com/codzilla/backend/Sandbox/submission/SubmissionPollingTest.java b/src/test/java/com/codzilla/backend/Sandbox/submission/SubmissionPollingTest.java new file mode 100644 index 0000000..877d0f1 --- /dev/null +++ b/src/test/java/com/codzilla/backend/Sandbox/submission/SubmissionPollingTest.java @@ -0,0 +1,68 @@ + package com.codzilla.backend.Sandbox.submission; + + import com.codzilla.backend.controller.Sandbox.judge0.Judge0Client; + import com.codzilla.backend.controller.Sandbox.submission.*; + import org.junit.jupiter.api.Test; + import org.mockito.InjectMocks; + import org.mockito.Mock; + import org.mockito.junit.jupiter.MockitoExtension; + + import java.util.List; + import java.util.Optional; + + import static org.assertj.core.api.Assertions.assertThat; + import static org.mockito.ArgumentMatchers.any; + import static org.mockito.Mockito.when; + + @org.junit.jupiter.api.extension.ExtendWith(MockitoExtension.class) + class SubmissionPollingTest { + + @Mock + private SubmissionRepository submissionRepository; + + @Mock + private Judge0Client judge0Client; + + @InjectMocks + private SubmissionPollingService pollingService; + @Mock + private SubmissionTestRepository submissionTestRepository; + + @Test + void polling_shouldUpdateStatus() { + SubmissionTest subTest = new SubmissionTest(); + subTest.setId(1L); + subTest.setSubmissionId(1L); + subTest.setTestIndex(1); + subTest.setJudge0Token("token"); + subTest.setExpectedOutput("3"); + subTest.setStatus(SubmissionTest.Status.IN_QUEUE); + + when(submissionTestRepository.findAllByStatus(SubmissionTest.Status.IN_QUEUE)) + .thenReturn(List.of(subTest)); + + Judge0Client.SubmissionResponse response = new Judge0Client.SubmissionResponse(); + Judge0Client.SubmissionResponse.Status status = new Judge0Client.SubmissionResponse.Status(); + status.setId(3); + status.setDescription("Accepted"); + response.setStatus(status); + response.setStdout("3\n"); + + when(judge0Client.getSubmissionStatus("token")).thenReturn(response); + + Submission submission = new Submission(); + submission.setId(1L); + submission.setStatus(Submission.Status.IN_QUEUE); + + when(submissionTestRepository.findAllBySubmissionIdOrderByTestIndex(1L)) + .thenReturn(List.of(subTest)); + when(submissionRepository.findById(1L)).thenReturn(Optional.of(submission)); + when(submissionTestRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + when(submissionRepository.save(any())).thenAnswer(i -> i.getArgument(0)); + + pollingService.pollStatuses(); + + assertThat(subTest.getStatus()).isEqualTo(SubmissionTest.Status.ACCEPTED); + assertThat(submission.getStatus()).isEqualTo(Submission.Status.ACCEPTED); + } + } \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 9383beb..ad92b53 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -27,4 +27,5 @@ spring.data.redis.password=redis-password logging.level.org.springframework.security=DEBUG polygon.api.key=e74e00bbcd8a98522b9e44c87f7508f5b9d1ad75 polygon.api.secret=3de0ad5bd9459d7e69633148859acd7c0ba2f1f0 -judge0.base-url=http://localhost:2358 \ No newline at end of file +polygon.api.url=${POLYGON_API_URL:https://polygon.codeforces.com/api/} +judge0.base-url=https://ce.judge0.com \ No newline at end of file