diff --git a/.editorconfig b/.editorconfig index 01b5b05b..ce472594 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,8 +16,7 @@ ij_java_continuation_indent_size = 4 ij_java_class_count_to_use_import_on_demand = 999 ij_java_names_count_to_use_import_on_demand = 999 # Import order to match Checkstyle groups: java, javax, org, com -ij_java_imports_layout = import java.*; import javax.*; import org.*; import com.*; blank_line; import all other imports; blank_line; import static all other static imports +ij_java_imports_layout = $*,java.**,javax.**,org.**,com.**,* [*.{yml,yaml}] indent_size = 2 - diff --git a/.github/workflows/deploy-application.yaml b/.github/workflows/deploy-application.yaml new file mode 100644 index 00000000..fe65cddc --- /dev/null +++ b/.github/workflows/deploy-application.yaml @@ -0,0 +1,30 @@ +name: deploy-to-k8s + +on: + push: + branches: + - "**" + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v5 + + - name: Build & Publish Docker image + id: build + uses: Agile-Software-Engineering-25/build-and-publish-image@v1.0.0 + with: + # optional: set extra tags if needed + extra_tags: ${{ github.sha }} + + - name: Deploy to Namespace + uses: Agile-Software-Engineering-25/deploy-to-k8s@v1.0.1 + with: + kubeconfig: ${{ secrets.KUBECONFIG }} + namespace: ${{ vars.K8S_NAMESPACE }} + deployment: grading-service + image: ghcr.io/agile-software-engineering-25/team-12-backend-exagrad-lecturer-service + tag: ${{ github.sha }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..98af8c8a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,65 @@ +name: CI - Test + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test (Java ${{ matrix.java }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [ '21', '22' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Temurin JDK ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: maven + + - name: Show Java and Maven versions + run: | + java -version + mvn -v + + - name: Build and run tests + run: mvn -B -ntp clean verify + + - name: Upload test reports (Surefire) + if: always() + uses: actions/upload-artifact@v4 + with: + name: surefire-reports-java-${{ matrix.java }} + path: | + **/target/surefire-reports/** + if-no-files-found: ignore + retention-days: 14 + + - name: Upload integration test reports (Failsafe) + if: always() + uses: actions/upload-artifact@v4 + with: + name: failsafe-reports-java-${{ matrix.java }} + path: | + **/target/failsafe-reports/** + if-no-files-found: ignore + retention-days: 14 diff --git a/.gitignore b/.gitignore index 7a6a576f..28564dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ build/ ### VS Code ### .vscode/ - +/data/ ### MacOs ### -*/.DS_STORE \ No newline at end of file +*/.DS_STORE diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..78dd770e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Stage 1: build the jar +FROM maven:3.9.9-eclipse-temurin-21-alpine AS builder +WORKDIR /app + +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src src +RUN mvn clean package -DskipTests -B + +# Stage 2: run the jar +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Copy the fat jar (Spring Boot creates a runnable jar under target/) +COPY --from=builder /app/target/*.jar app.jar +RUN chown -R 1000:1000 /app + +# Run as non-root +USER 1000:1000 +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/checkstyle.xml b/checkstyle.xml index 0d7ee386..89298d65 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -73,7 +73,7 @@ - + @@ -143,9 +143,9 @@ - + - + diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 00000000..9fe698cb --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grading-service + namespace: ase-12 + labels: + app.kubernetes.io/name: grading-service + app.kubernetes.io/instance: grading-service +spec: + replicas: 2 + revisionHistoryLimit: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: grading-service + app.kubernetes.io/instance: grading-service + template: + metadata: + labels: + app.kubernetes.io/name: grading-service + app.kubernetes.io/instance: grading-service + spec: + serviceAccountName: default + terminationGracePeriodSeconds: 30 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: app + image: ghcr.io/agile-software-engineering-25/team-12-backend-exagrad-lecturer-service:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + # Enable graceful shutdown at app level (align with terminationGracePeriodSeconds) + - name: SERVER_SERVLET_CONTEXT_PATH + value: "/exa-grad/grading-service" + - name: SERVER_SHUTDOWN + value: "graceful" + + readinessProbe: + httpGet: + path: /exa-grad/grading-service/actuator/health/readiness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 6 + timeoutSeconds: 1 + livenessProbe: + httpGet: + path: /exa-grad/grading-service/actuator/health/liveness + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 1 + startupProbe: + httpGet: + path: /exa-grad/grading-service/actuator/health + port: 8080 + periodSeconds: 5 + failureThreshold: 30 + + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1" + memory: "1024Mi" + + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true # Immutable root FS → mount /tmp as writable + capabilities: + drop: [ "ALL" ] + + volumeMounts: + - name: data + mountPath: /app/data + - name: tmp + mountPath: /tmp + + volumes: + - name: tmp + emptyDir: { } + - name: data + emptyDir: { } diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 00000000..1dfa9064 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: grading-service + namespace: ase-12 + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure # send traffic over HTTPS +spec: + ingressClassName: traefik + tls: + - hosts: ["sau-portal.de"] + secretName: tls-to-come + rules: + - host: sau-portal.de + http: + paths: + - path: /exa-grad/grading-service # Public URL prefix for this service + pathType: Prefix + backend: + service: + name: grading-service + port: + number: 80 diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 00000000..a67b402e --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: ase-12 #e.g ase-1 +resources: + - deployment.yaml + - service.yaml + - ingress.yaml diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 00000000..45b819cf --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: grading-service + namespace: ase-12 +spec: + selector: + app.kubernetes.io/name: grading-service # Must match Deployment template labels + ports: + - name: http + port: 80 + targetPort: 8080 diff --git a/pom.xml b/pom.xml index e26b2f8a..e1073c4c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,16 +31,13 @@ - ch.bildspur - artnet4j - 0.6.2 + org.springframework.boot + spring-boot-starter-actuator - org.springframework.boot spring-boot-starter-web - org.projectlombok lombok @@ -65,4 +62,22 @@ runtime + + + + org.springframework.boot + spring-boot-maven-plugin + + com.ase.lecturerservice.Application + + + + + repackage + + + + + + diff --git a/src/main/java/com/ase/userservice/Application.java b/src/main/java/com/ase/lecturerservice/Application.java similarity index 91% rename from src/main/java/com/ase/userservice/Application.java rename to src/main/java/com/ase/lecturerservice/Application.java index fecb641b..2bfb7667 100644 --- a/src/main/java/com/ase/userservice/Application.java +++ b/src/main/java/com/ase/lecturerservice/Application.java @@ -1,4 +1,4 @@ -package com.ase.userservice; +package com.ase.lecturerservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/com/ase/userservice/components/.gitkeep b/src/main/java/com/ase/lecturerservice/components/.gitkeep similarity index 100% rename from src/main/java/com/ase/userservice/components/.gitkeep rename to src/main/java/com/ase/lecturerservice/components/.gitkeep diff --git a/src/main/java/com/ase/lecturerservice/config/CorsConfig.java b/src/main/java/com/ase/lecturerservice/config/CorsConfig.java new file mode 100644 index 00000000..9bc320e6 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/config/CorsConfig.java @@ -0,0 +1,47 @@ +package com.ase.lecturerservice.config; + +import java.util.Arrays; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties(CorsConfig.CorsConfigurationProperties.class) +public class CorsConfig { + + private final CorsConfigurationProperties corsConfigurationProperties; + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + corsConfiguration.setAllowCredentials(true); + Arrays.stream(corsConfigurationProperties.allowedOrigins) + .forEach(corsConfiguration::addAllowedOrigin); + log.info("Allowed origins:"); + Arrays.stream(corsConfigurationProperties.allowedOrigins).forEach(System.out::println); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + return source; + } + + @Bean + CorsFilter corsFilter() { + return new CorsFilter(corsConfigurationSource()); + } + + @ConfigurationProperties(prefix = "app.cors") + record CorsConfigurationProperties(String[] allowedOrigins) { + + } +} diff --git a/src/main/java/com/ase/lecturerservice/controllers/BaseController.java b/src/main/java/com/ase/lecturerservice/controllers/BaseController.java new file mode 100644 index 00000000..ca0e250e --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/controllers/BaseController.java @@ -0,0 +1,5 @@ +package com.ase.lecturerservice.controllers; + +public interface BaseController { + String BASE_PATH = "/api/v1"; +} diff --git a/src/main/java/com/ase/lecturerservice/controllers/ExamController.java b/src/main/java/com/ase/lecturerservice/controllers/ExamController.java new file mode 100644 index 00000000..e16cc6f2 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/controllers/ExamController.java @@ -0,0 +1,37 @@ +package com.ase.lecturerservice.controllers; + +import static com.ase.lecturerservice.controllers.BaseController.BASE_PATH; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ase.lecturerservice.dtos.ExamDto; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.services.ExamService; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequestMapping(BASE_PATH + "/exams") +@RequiredArgsConstructor +public class ExamController { + private final ExamService examService; + private final ObjectMapper objectMapper; + + @GetMapping + public ResponseEntity> getExams(@RequestParam String lecturerUuid) + throws IllegalArgumentException { + List exams = examService.getExamsByLecturer(lecturerUuid); + + List examDtoList = exams.stream() + .map(exam -> objectMapper.convertValue(exam, ExamDto.class)) + .collect(Collectors.toList()); + + return ResponseEntity.ok(examDtoList); + } +} diff --git a/src/main/java/com/ase/lecturerservice/controllers/FeedbackController.java b/src/main/java/com/ase/lecturerservice/controllers/FeedbackController.java new file mode 100644 index 00000000..1e74eec2 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/controllers/FeedbackController.java @@ -0,0 +1,32 @@ +package com.ase.lecturerservice.controllers; + +import static com.ase.lecturerservice.controllers.BaseController.BASE_PATH; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ase.lecturerservice.entities.Feedback; +import com.ase.lecturerservice.services.FeedbackService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(BASE_PATH + "/feedback") +@RequiredArgsConstructor +public class FeedbackController { + private final FeedbackService feedbackService; + + @GetMapping + public ResponseEntity getFeedbackFromExam( + @RequestParam String examUuid, @RequestParam String studentUuid) { + return ResponseEntity.ok(feedbackService.getFeedbackExam(studentUuid, examUuid)); + } + + @GetMapping("/for-lecturer/{lecturerUuid}") + public ResponseEntity> getFeedbacksForLecturer(@PathVariable String lecturerUuid) { + return ResponseEntity.ok(feedbackService.getFeedbackForLecturer(lecturerUuid)); + } + +} diff --git a/src/main/java/com/ase/lecturerservice/controllers/SubmissionController.java b/src/main/java/com/ase/lecturerservice/controllers/SubmissionController.java new file mode 100644 index 00000000..df688e95 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/controllers/SubmissionController.java @@ -0,0 +1,29 @@ +package com.ase.lecturerservice.controllers; + +import static com.ase.lecturerservice.controllers.BaseController.BASE_PATH; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ase.lecturerservice.entities.Submission; +import com.ase.lecturerservice.services.SubmissionService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(BASE_PATH + "/submissions") +@RequiredArgsConstructor +public class SubmissionController { + + private final SubmissionService submissionService; + + @GetMapping("/for-lecturer/{lecturerUuid}") + public ResponseEntity> getRelevantSubmissions( + @PathVariable String lecturerUuid + ) { + return ResponseEntity.ok( + submissionService.getAllAccessibleSubmissionsForLecturer(lecturerUuid) + ); + } +} diff --git a/src/main/java/com/ase/lecturerservice/dtos/ExamDto.java b/src/main/java/com/ase/lecturerservice/dtos/ExamDto.java new file mode 100644 index 00000000..b0db3be9 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/dtos/ExamDto.java @@ -0,0 +1,37 @@ +package com.ase.lecturerservice.dtos; + +import java.time.LocalDate; +import java.util.List; +import org.antlr.v4.runtime.misc.NotNull; +import com.ase.lecturerservice.entities.ExamType; +import com.ase.lecturerservice.entities.user.Student; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ExamDto { + @NotNull + private String uuid; + + @NotNull + private String name; + + @NotNull + private LocalDate date; + + @NotNull + private String module; + + @NotNull + private ExamType examType; + + @NotNull + private List assignedStudents; + + @NotNull + private int time; + + @NotNull + private int totalPoints; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/Exam.java b/src/main/java/com/ase/lecturerservice/entities/Exam.java new file mode 100644 index 00000000..55ebf302 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/Exam.java @@ -0,0 +1,43 @@ +package com.ase.lecturerservice.entities; + +import java.time.LocalDate; +import java.util.List; +import com.ase.lecturerservice.entities.user.Student; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Exam { + private String uuid; + + private String name; + + private int totalPoints; + + private ExamType examType; + + private LocalDate date; + + private int time; + + private String allowedResources; + + private int attempt; + + private int etcs; + + private String room; + + private String module; + + private String lecturerUuid; + + private List assignedStudents; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/ExamType.java b/src/main/java/com/ase/lecturerservice/entities/ExamType.java new file mode 100644 index 00000000..2f441523 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/ExamType.java @@ -0,0 +1,9 @@ +package com.ase.lecturerservice.entities; + +public enum ExamType { + EXAM, + PRESENTATION, + ORAL, + PROJECT, + OTHERS +} diff --git a/src/main/java/com/ase/lecturerservice/entities/Feedback.java b/src/main/java/com/ase/lecturerservice/entities/Feedback.java new file mode 100644 index 00000000..c3d710a8 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/Feedback.java @@ -0,0 +1,69 @@ +package com.ase.lecturerservice.entities; + +import java.time.LocalDate; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Entity +public class Feedback { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String uuid; + + @Column(name = "graded_at") + private LocalDate gradedAt; + + @Column(name = "graded_exam") + @JsonProperty("examUuid") + private String examUuid; + + @Column(name = "graded_by") + @JsonProperty("lecturerUuid") + private String lecturerUuid; + + @Column(name = "grade_for") + @JsonProperty("studentUuid") + private String studentUuid; + + @Column(name = "graded_submission") + @JsonProperty("submissionUuid") + private String submissionUuid; + + @Column(name = "comment") + @JsonProperty("comment") + private String comment; + + @ElementCollection + @JsonProperty("fileReference") + @CollectionTable( + name = "grade_file_references", + joinColumns = @JoinColumn(name = "grade_id") + ) + private List fileReference; + + @Column(name = "points") + @JsonProperty("points") + private int points; + + @Column(name = "grade") + @JsonProperty("grade") + private float grade; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/FileReference.java b/src/main/java/com/ase/lecturerservice/entities/FileReference.java new file mode 100644 index 00000000..db1544c3 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/FileReference.java @@ -0,0 +1,21 @@ +package com.ase.lecturerservice.entities; + +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@NoArgsConstructor +@SuperBuilder +@Embeddable +public class FileReference { + private String fileUuid; + + private String filename; + + private String downloadLink; + +} diff --git a/src/main/java/com/ase/lecturerservice/entities/Submission.java b/src/main/java/com/ase/lecturerservice/entities/Submission.java new file mode 100644 index 00000000..ecb9cd1c --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/Submission.java @@ -0,0 +1,14 @@ +package com.ase.lecturerservice.entities; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class Submission { + private String id; + private String examId; + private String studentId; + private String submissionDate; + private FileReference fileUpload; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/user/Lecturer.java b/src/main/java/com/ase/lecturerservice/entities/user/Lecturer.java new file mode 100644 index 00000000..df34aeea --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/user/Lecturer.java @@ -0,0 +1,15 @@ +package com.ase.lecturerservice.entities.user; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@NoArgsConstructor +@SuperBuilder +public class Lecturer extends UserEntity { + + private final UserType type = UserType.LECTURER; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/user/Student.java b/src/main/java/com/ase/lecturerservice/entities/user/Student.java new file mode 100644 index 00000000..844f5728 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/user/Student.java @@ -0,0 +1,19 @@ +package com.ase.lecturerservice.entities.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class Student extends UserEntity { + + private String matriculationNumber; + + private UserType type = UserType.STUDENT; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/user/UserEntity.java b/src/main/java/com/ase/lecturerservice/entities/user/UserEntity.java new file mode 100644 index 00000000..3a4e05b4 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/user/UserEntity.java @@ -0,0 +1,30 @@ +package com.ase.lecturerservice.entities.user; + +import org.hibernate.annotations.UuidGenerator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public abstract class UserEntity { + protected String email; + + @UuidGenerator + private String uuid; + + @JsonProperty("first_name") + private String firstName; + + @JsonProperty("last_name") + private String lastName; + + @JsonProperty("type") + private UserType type; +} diff --git a/src/main/java/com/ase/lecturerservice/entities/user/UserType.java b/src/main/java/com/ase/lecturerservice/entities/user/UserType.java new file mode 100644 index 00000000..ead31a66 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/entities/user/UserType.java @@ -0,0 +1,6 @@ +package com.ase.lecturerservice.entities.user; + +public enum UserType { + LECTURER, + STUDENT +} diff --git a/src/main/java/com/ase/userservice/config/.gitkeep b/src/main/java/com/ase/lecturerservice/exception/.gitkeep similarity index 100% rename from src/main/java/com/ase/userservice/config/.gitkeep rename to src/main/java/com/ase/lecturerservice/exception/.gitkeep diff --git a/src/main/java/com/ase/lecturerservice/exception/CustomExceptionHandler.java b/src/main/java/com/ase/lecturerservice/exception/CustomExceptionHandler.java new file mode 100644 index 00000000..bd82db89 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/exception/CustomExceptionHandler.java @@ -0,0 +1,31 @@ +package com.ase.lecturerservice.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.resource.NoResourceFoundException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ControllerAdvice +public class CustomExceptionHandler { + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException( + IllegalArgumentException illegalArgumentException) { + return ResponseEntity.badRequest().body(illegalArgumentException.getMessage()); + } + + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handleNoResourceFoundException( + NoResourceFoundException noResourceFoundException + ) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("This page could not be found"); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity internalErrorHandler(Exception exception) { + log.error(exception.getMessage(), exception); + return ResponseEntity.internalServerError().body("Internal Server Error"); + } +} diff --git a/src/main/java/com/ase/lecturerservice/mockvalues/MockValues.java b/src/main/java/com/ase/lecturerservice/mockvalues/MockValues.java new file mode 100644 index 00000000..5bd35854 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/mockvalues/MockValues.java @@ -0,0 +1,76 @@ +package com.ase.lecturerservice.mockvalues; + +import lombok.Getter; + +public class MockValues { + + @Getter + public enum UuidMocks { + EXAM_UUID("550e8400-e29b-41d4-a716-446655440000"), + EXAM_UUID2("550e8400-e29b-41d4-a716-446655440001"), + EXAM_UUID3("550e8400-e29b-41d4-a716-446655440002"), + EXAM_UUID4("550e8400-e29b-41d4-a716-446655440003"), + EXAM_UUID5("550e8400-e29b-41d4-a716-446655440004"), + EXAM_UUID6("550e8400-e29b-41d4-a716-446655440005"), + + STUDENT_UUID("d1c27c4f-e7d7-45b8-bc4e-6f634e7c5e8f"), + STUDENT_UUID2("f2a26e3f-3b50-44ac-a7f9-02fe3b41cf6a"), + STUDENT_UUID3("7283a092-2b64-4bfa-bf92-4242448b740a"), + STUDENT_UUID4("a9f5d8b5-2632-42b5-8520-1db4010fc80d"), + STUDENT_UUID5("be7f4234-cd28-4b29-9b09-5d1a38d3c67a"), + + GRADE_UUID("ea3f2b67-5ed0-4d89-bc2c-28533a210ae2"), + GRADE_UUID2("27d211f8-e45e-4f5a-b264-e7b4f51e8f95"), + GRADE_UUID3("cc28b1f6-3b5b-44e1-963f-0793b742a6d4"), + GRADE_UUID4("23428b1f6-3b5b-44e1-963f-0793b742a53"), + GRADE_UUID5("5318b1fs6-3b5b-46e1-963f-079dw742a53"), + GRADE_UUID6("5318b1fs6-a6ds-46e1-963f-079dw742a53"), + GRADE_UUID7("ac28b1f6-3b5b-44e1-963f-9033b742a6d4"), + + LECTURER_UUID("12345678-62hj-jhj2-h23j-901234567890"), + LECTURER_UUID2("3f8a9c12-7b4e-4d21-9c8a-2e6b7d9f1a23"), + LECTURER_UUID3("c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f"), + LECTURER_UUID4("a0b1c2d3-e4f5-4a67-8b9c-d0e1f2a3b4c5"), + LECTURER_UUID5("9d8c7b6a-5e4f-4a3b-9c8d-7e6f5a4b3c2d"), + LECTURER_UUID6("f0e1d2c3-b4a5-49c6-8d7e-6f5a4b3c2d1e"); + + + private final String value; + + UuidMocks(String value) { + this.value = value; + } + } + + @Getter + public enum FloatMocks { + GRADE(1.3f), + AVERAGE_GRADE(2.0f); + + private final float value; + + FloatMocks(float value) { + this.value = value; + } + } + + @Getter + public enum IntMocks { + SUBMISSIONS(0), + TOTAL_POINTS(100), + ACHIEVED_POINTS(95), + TIME_SECONDS(5400), + TIME_MIN(90), + ATTEMPT(1), + ETCS(5), + DATE_YEAR(2015), + DATE_MONTH(10), + DATE_DAY(25); + + private final int value; + + IntMocks(int value) { + this.value = value; + } + } +} diff --git a/src/main/java/com/ase/userservice/entities/.gitkeep b/src/main/java/com/ase/lecturerservice/repositories/.gitkeep similarity index 100% rename from src/main/java/com/ase/userservice/entities/.gitkeep rename to src/main/java/com/ase/lecturerservice/repositories/.gitkeep diff --git a/src/main/java/com/ase/lecturerservice/services/DummyData.java b/src/main/java/com/ase/lecturerservice/services/DummyData.java new file mode 100644 index 00000000..2820173c --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/services/DummyData.java @@ -0,0 +1,262 @@ +package com.ase.lecturerservice.services; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.entities.ExamType; +import com.ase.lecturerservice.entities.Feedback; +import com.ase.lecturerservice.entities.FileReference; +import com.ase.lecturerservice.entities.Submission; +import com.ase.lecturerservice.entities.user.Student; +import com.ase.lecturerservice.mockvalues.MockValues; + +public class DummyData { + public static List SUBMISSIONS = List.of( + Submission.builder() + .studentId(MockValues.UuidMocks.STUDENT_UUID.getValue()) + .examId(MockValues.UuidMocks.EXAM_UUID.getValue()) + .submissionDate("2025-09-01T10:30:00Z") + .fileUpload(FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("submission1.pdf") + .downloadLink("https://example.com/submission1.pdf") + .build()) + .build(), + Submission.builder() + .studentId(MockValues.UuidMocks.STUDENT_UUID2.getValue()) + .examId(MockValues.UuidMocks.EXAM_UUID.getValue()) + .submissionDate("2025-09-01T11:10:00Z") + .fileUpload(FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("submission2.pdf") + .downloadLink("https://example.com/submission2.pdf") + .build()) + .build(), + Submission.builder() + .studentId(MockValues.UuidMocks.STUDENT_UUID3.getValue()) + .examId(MockValues.UuidMocks.EXAM_UUID2.getValue()) + .submissionDate("2025-09-02T09:15:00Z") + .fileUpload(FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("physics_midterm_attempt1.zip") + .downloadLink("https://example.com/physics_midterm_attempt1.zip") + .build()) + .build(), + Submission.builder() + .studentId(MockValues.UuidMocks.STUDENT_UUID.getValue()) + .examId(MockValues.UuidMocks.EXAM_UUID6.getValue()) + .submissionDate("2025-09-03T14:45:00Z") + .fileUpload(FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("software_project_demo.mp4") + .downloadLink("https://example.com/software_project_demo.mp4") + .build()) + .build(), + Submission.builder() + .studentId(MockValues.UuidMocks.STUDENT_UUID2.getValue()) + .examId(MockValues.UuidMocks.EXAM_UUID4.getValue()) + .submissionDate("2025-09-04T08:05:00Z") + .fileUpload(FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("chemistry_lab_report.docx") + .downloadLink("https://example.com/chemistry_lab_report.docx") + .build()) + .build() + ); + static LocalDate date = LocalDate.of( + MockValues.IntMocks.DATE_YEAR.getValue(), + MockValues.IntMocks.DATE_MONTH.getValue(), + MockValues.IntMocks.DATE_DAY.getValue()); + static List studentList = List.of( + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID.getValue()).matriculationNumber("D725").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID2.getValue()).matriculationNumber("D755").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID3.getValue()).matriculationNumber("D735").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID4.getValue()).matriculationNumber("D729").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID5.getValue()).matriculationNumber("D726").build() + ); + static List studentList2 = List.of( + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID.getValue()).matriculationNumber("D725").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID2.getValue()).matriculationNumber("D755").build() + ); + public static List EXAMS = List.of( + Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .name("Mathematics Final Exam") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.PRESENTATION) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Calculator, Formula Sheet") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Room A101") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID.getValue()) + .module("Mathe") + .assignedStudents(studentList) + .build(), + Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID2.getValue()) + .name("Physics Midterm") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.EXAM) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("None") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Room B202") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID.getValue()) + .module("Physics") + .assignedStudents(studentList2) + .build(), + Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID3.getValue()) + .name("Computer Science Project") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.OTHERS) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Laptop, IDE") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Online Submission") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID2.getValue()) + .module("CS I") + .assignedStudents(studentList) + .build(), + Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID4.getValue()) + .name("Chemistry Lab Exam") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.ORAL) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Lab Equipment, Safety Manual") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Online Submission") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID2.getValue()) + .module("Chemistry") + .assignedStudents(studentList2) + .build(), + Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID6.getValue()) + .name("Software Development Project") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.PRESENTATION) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Notes, Textbook") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Room C303") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID2.getValue()) + .assignedStudents(studentList2) + .module("History I") + .build() + ); + static List fileReferencesList = List.of( + FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("dummy_file") + .build(), + FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename("dummy_file2") + .build() + ); + public static List Feedbacks = List.of( + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .comment("Excellent work on the assignment.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(), + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID2.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID2.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .comment("Great effort! Check feedback in files.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(), + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID3.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID3.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .comment("Incomplete submission. Please review guidelines.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(), + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID4.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID6.getValue()) + .comment("Great effort! Check feedback in files.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(), + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID5.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID2.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID6.getValue()) + .comment("Great effort! Check feedback in files.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(), + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID6.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID2.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID2.getValue()) + .comment("Great effort! Check feedback in files.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(), + Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID7.getValue()) + .gradedAt(date) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID4.getValue()) + .comment("Great effort! Check feedback in files.") + .fileReference(fileReferencesList) + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build() + ); +} diff --git a/src/main/java/com/ase/lecturerservice/services/ExamService.java b/src/main/java/com/ase/lecturerservice/services/ExamService.java new file mode 100644 index 00000000..e371f398 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/services/ExamService.java @@ -0,0 +1,28 @@ +package com.ase.lecturerservice.services; + +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import com.ase.lecturerservice.entities.Exam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ExamService { + + public List getExamsByLecturer(String lecturerUuid) { + if (lecturerUuid == null || lecturerUuid.isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, + "Lecturer name is required"); + } + + // TODO: change this to a webclient call, when the API is ready + log.info("The Exams from {} has been requested", lecturerUuid); + return DummyData.EXAMS.stream() + .filter(exam -> exam.getLecturerUuid().equals(lecturerUuid)) + .toList(); + } +} diff --git a/src/main/java/com/ase/lecturerservice/services/FeedbackService.java b/src/main/java/com/ase/lecturerservice/services/FeedbackService.java new file mode 100644 index 00000000..fc6efdbe --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/services/FeedbackService.java @@ -0,0 +1,38 @@ +package com.ase.lecturerservice.services; + +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Service; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.entities.Feedback; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FeedbackService { + + // TODO: Adjust this when saving data is implemented + public Feedback getFeedbackExam(String studentUuid, String examUuid) { + return DummyData.Feedbacks.stream() + .filter(feedback -> feedback.getStudentUuid().equals(studentUuid) + && feedback.getExamUuid().equals(examUuid)) + .findFirst() + .orElse(null); + } + + public List getFeedbackForLecturer(String lecturerUuid) { + return DummyData.Feedbacks.stream().filter(feedback -> + Optional.ofNullable(getExam(feedback.getExamUuid())) + .map(exam -> exam.getLecturerUuid().equals(lecturerUuid)) + .orElse(false) + ).toList(); + } + + public Exam getExam(String uuid) { + return DummyData.EXAMS.stream() + .filter(exam -> exam.getUuid().equals(uuid)) + .findFirst().orElse(null); + } +} diff --git a/src/main/java/com/ase/lecturerservice/services/SubmissionService.java b/src/main/java/com/ase/lecturerservice/services/SubmissionService.java new file mode 100644 index 00000000..68985294 --- /dev/null +++ b/src/main/java/com/ase/lecturerservice/services/SubmissionService.java @@ -0,0 +1,40 @@ +package com.ase.lecturerservice.services; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.entities.Submission; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SubmissionService { + + private final ExamService examService; + + public List getSubmissionsForExam(String examId) { + return DummyData.SUBMISSIONS.stream() + .filter(submission -> submission.getExamId().equals(examId)) + .toList(); + } + + public List getSubmissionsForStudent(String studentId) { + return DummyData.SUBMISSIONS.stream() + .filter(submission -> submission.getStudentId().equals(studentId)) + .toList(); + } + + public List getAllAccessibleSubmissionsForLecturer(String lecturerUuid) { + Set examsOfLecturer = examService.getExamsByLecturer(lecturerUuid) + .stream() + .map(Exam::getUuid) + .collect(Collectors.toSet()); + + return DummyData.SUBMISSIONS.stream() + .filter(submission -> examsOfLecturer.contains(submission.getExamId())) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/ase/userservice/controllers/RootController.java b/src/main/java/com/ase/userservice/controllers/RootController.java deleted file mode 100644 index 34e21cb6..00000000 --- a/src/main/java/com/ase/userservice/controllers/RootController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ase.userservice.controllers; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class RootController { - - @GetMapping("/") - public ResponseEntity root() { - return ResponseEntity.ok("API Root: /api/v1/"); - } - -} diff --git a/src/main/java/com/ase/userservice/services/.gitkeep b/src/main/java/com/ase/userservice/services/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4b5711b9..3965cd82 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -17,3 +17,18 @@ spring: server: error: include-message: always +management: + endpoints: + web: + exposure: + include: health,readiness,liveness,prometheus + base-path: /actuator + endpoint: + health: + probes: + enabled: true +app: + cors: + allowed-origins: + - http://localhost:5173 + - https://sau-portal.de diff --git a/src/test/java/com/ase/userservice/ApplicationTests.java b/src/test/java/com/ase/lecturerservice/ApplicationTests.java similarity index 67% rename from src/test/java/com/ase/userservice/ApplicationTests.java rename to src/test/java/com/ase/lecturerservice/ApplicationTests.java index be5b4249..688544d4 100644 --- a/src/test/java/com/ase/userservice/ApplicationTests.java +++ b/src/test/java/com/ase/lecturerservice/ApplicationTests.java @@ -1,4 +1,4 @@ -package com.ase.userservice; +package com.ase.lecturerservice; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -6,8 +6,8 @@ @SpringBootTest class ApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } diff --git a/src/test/java/com/ase/lecturerservice/MockValues.java b/src/test/java/com/ase/lecturerservice/MockValues.java new file mode 100644 index 00000000..aeb7364e --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/MockValues.java @@ -0,0 +1,71 @@ +package com.ase.lecturerservice; + +import lombok.Getter; + +public class MockValues { + + @Getter + public enum UuidMocks { + EXAM_UUID("550e8400-e29b-41d4-a716-446655440000"), + EXAM_UUID2("550e8400-e29b-41d4-a716-446655440001"), + EXAM_UUID3("550e8400-e29b-41d4-a716-446655440002"), + EXAM_UUID4("550e8400-e29b-41d4-a716-446655440003"), + EXAM_UUID5("550e8400-e29b-41d4-a716-446655440004"), + + STUDENT_UUID("d1c27c4f-e7d7-45b8-bc4e-6f634e7c5e8f"), + STUDENT_UUID2("f2a26e3f-3b50-44ac-a7f9-02fe3b41cf6a"), + STUDENT_UUID3("7283a092-2b64-4bfa-bf92-4242448b740a"), + STUDENT_UUID4("a9f5d8b5-2632-42b5-8520-1db4010fc80d"), + STUDENT_UUID5("be7f4234-cd28-4b29-9b09-5d1a38d3c67a"), + + GRADE_UUID("ea3f2b67-5ed0-4d89-bc2c-28533a210ae2"), + GRADE_UUID2("27d211f8-e45e-4f5a-b264-e7b4f51e8f95"), + GRADE_UUID3("cc28b1f6-3b5b-44e1-963f-0793b742a6d4"), + + LECTURER_UUID("12345678-62hj-jhj2-h23j-901234567890"), + LECTURER_UUID2("3f8a9c12-7b4e-4d21-9c8a-2e6b7d9f1a23"), + LECTURER_UUID3("c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f"), + LECTURER_UUID4("a0b1c2d3-e4f5-4a67-8b9c-d0e1f2a3b4c5"), + LECTURER_UUID5("9d8c7b6a-5e4f-4a3b-9c8d-7e6f5a4b3c2d"), + LECTURER_UUID6("f0e1d2c3-b4a5-49c6-8d7e-6f5a4b3c2d1e"); + + + private final String value; + + UuidMocks(String value) { + this.value = value; + } + } + + @Getter + public enum FloatMocks { + GRADE(1.3f), + AVERAGE_GRADE(2.0f); + + private final float value; + + FloatMocks(float value) { + this.value = value; + } + } + + @Getter + public enum IntMocks { + SUBMISSIONS(0), + TOTAL_POINTS(100), + ACHIEVED_POINTS(95), + TIME_SECONDS(5400), + TIME_MIN(90), + ATTEMPT(1), + ETCS(5), + DATE_YEAR(2015), + DATE_MONTH(10), + DATE_DAY(25); + + private final int value; + + IntMocks(int value) { + this.value = value; + } + } +} diff --git a/src/test/java/com/ase/lecturerservice/controllers/ExamControllerTest.java b/src/test/java/com/ase/lecturerservice/controllers/ExamControllerTest.java new file mode 100644 index 00000000..7a7dfbc7 --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/controllers/ExamControllerTest.java @@ -0,0 +1,100 @@ +package com.ase.lecturerservice.controllers; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import com.ase.lecturerservice.MockValues; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.entities.ExamType; +import com.ase.lecturerservice.entities.user.Student; +import com.ase.lecturerservice.services.ExamService; + +@ExtendWith(SpringExtension.class) +@WebMvcTest(ExamController.class) +public class ExamControllerTest { + private static final List STUDENT_LIST = List.of( + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID.getValue()).matriculationNumber("D725").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID2.getValue()).matriculationNumber("D755").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID3.getValue()).matriculationNumber("D735").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID4.getValue()).matriculationNumber("D729").build(), + Student.builder().uuid( + MockValues.UuidMocks.STUDENT_UUID5.getValue()).matriculationNumber("D726").build() + ); + private static Exam exam; + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ExamService examService; + + @BeforeAll + public static void setup() { + LocalDate date = LocalDate.of( + MockValues.IntMocks.DATE_YEAR.getValue(), + MockValues.IntMocks.DATE_MONTH.getValue(), + MockValues.IntMocks.DATE_DAY.getValue()); + + exam = Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .name("Test") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.EXAM) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Calculator, Formula Sheet") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Room A101") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID.getValue()) + .module("Test") + .assignedStudents(STUDENT_LIST) + .build(); + } + + @Test + void fetchExamsShouldReturnExamDtos() throws Exception { + when(examService.getExamsByLecturer("Tom")).thenReturn(List.of(exam)); + + mockMvc.perform(get("/api/v1/exams?lecturerUuid={lecturerUuid}", "Tom") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].uuid") + .value(MockValues.UuidMocks.EXAM_UUID.getValue())) + .andExpect(jsonPath("$[0].name").value("Test")) + .andExpect(jsonPath("$[0].date").value("2015-10-25")) + .andExpect(jsonPath("$[0].module").value("Test")) + .andExpect(jsonPath("$[0].time").value(MockValues.IntMocks.TIME_SECONDS.getValue())) + .andExpect(jsonPath("$[0].examType").value(ExamType.EXAM.toString())) + .andExpect(jsonPath("$[0].assignedStudents").isNotEmpty()); + } + + @Test + void fetchExamsShouldThrowException() throws Exception { + when(examService.getExamsByLecturer(" ")) + .thenThrow(new IllegalArgumentException("Lecturer cannot be empty")); + + mockMvc.perform(get("/api/v1/exams?lecturerUuid={lecturerUuid}", " ") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("Lecturer cannot be empty")); + } +} diff --git a/src/test/java/com/ase/lecturerservice/controllers/FeedbackControllerTest.java b/src/test/java/com/ase/lecturerservice/controllers/FeedbackControllerTest.java new file mode 100644 index 00000000..c2863771 --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/controllers/FeedbackControllerTest.java @@ -0,0 +1,71 @@ +package com.ase.lecturerservice.controllers; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.time.LocalDate; +import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import com.ase.lecturerservice.MockValues; +import com.ase.lecturerservice.entities.Feedback; +import com.ase.lecturerservice.services.FeedbackService; + +@ExtendWith(SpringExtension.class) +@WebMvcTest(FeedbackController.class) +public class FeedbackControllerTest { + private static final LocalDate DATE = LocalDate.of( + MockValues.IntMocks.DATE_YEAR.getValue(), + MockValues.IntMocks.DATE_MONTH.getValue(), + MockValues.IntMocks.DATE_DAY.getValue()); + private static Feedback feedback; + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private FeedbackService feedbackService; + + @BeforeAll + public static void setup() { + feedback = Feedback.builder() + .uuid(MockValues.UuidMocks.GRADE_UUID.getValue()) + .gradedAt(DATE) + .lecturerUuid(UUID.randomUUID().toString()) + .studentUuid(MockValues.UuidMocks.STUDENT_UUID.getValue()) + .submissionUuid(UUID.randomUUID().toString()) + .examUuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .comment("Excellent work on the assignment.") + .points(MockValues.IntMocks.ACHIEVED_POINTS.getValue()) + .grade(MockValues.FloatMocks.GRADE.getValue()) + .build(); + } + + @Test + void getGradedExamShouldReturnGradeWhenValidUuids() throws Exception { + String studentUuid = MockValues.UuidMocks.STUDENT_UUID.getValue(); + String examUuid = MockValues.UuidMocks.EXAM_UUID.getValue(); + + when(feedbackService.getFeedbackExam(studentUuid, examUuid)) + .thenReturn(feedback); + + mockMvc.perform(get("/api/v1/feedback") + .param("studentUuid", studentUuid) + .param("examUuid", examUuid) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.gradedAt").value(DATE.toString())) + .andExpect(jsonPath("$.studentUuid").value(studentUuid)) + .andExpect(jsonPath("$.examUuid").value(examUuid)) + .andExpect(jsonPath("$.comment").value("Excellent work on the assignment.")) + .andExpect(jsonPath("$.grade").value(feedback.getGrade())) + .andExpect(jsonPath("$.points").value(feedback.getPoints())); + } +} diff --git a/src/main/java/com/ase/userservice/repositories/.gitkeep b/src/test/java/com/ase/lecturerservice/controllers/LecturerControllerTest.java similarity index 100% rename from src/main/java/com/ase/userservice/repositories/.gitkeep rename to src/test/java/com/ase/lecturerservice/controllers/LecturerControllerTest.java diff --git a/src/test/java/com/ase/lecturerservice/controllers/SubmissionControllerTest.java b/src/test/java/com/ase/lecturerservice/controllers/SubmissionControllerTest.java new file mode 100644 index 00000000..08fc8114 --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/controllers/SubmissionControllerTest.java @@ -0,0 +1,105 @@ +package com.ase.lecturerservice.controllers; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import com.ase.lecturerservice.MockValues; +import com.ase.lecturerservice.entities.FileReference; +import com.ase.lecturerservice.entities.Submission; +import com.ase.lecturerservice.services.SubmissionService; + +@ExtendWith(SpringExtension.class) +@WebMvcTest(SubmissionController.class) +public class SubmissionControllerTest { + private static final String LECTURER_UUID = "lecturer-uuid-123"; + private static final String STUDENT_UUID = MockValues.UuidMocks.STUDENT_UUID.getValue(); + private static final String EXAM_UUID = MockValues.UuidMocks.EXAM_UUID.getValue(); + private static final String SUBMISSION_DATE = "2025-09-01T10:30:00Z"; + private static final String FILENAME = "submission1.pdf"; + private static final String DOWNLOAD_LINK = "https://example.com/submission1.pdf"; + + private static Submission submission; + private static List submissionList; + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private SubmissionService submissionService; + + @BeforeAll + public static void setup() { + FileReference fileReference = FileReference.builder() + .fileUuid(UUID.randomUUID().toString()) + .filename(FILENAME) + .downloadLink(DOWNLOAD_LINK) + .build(); + + submission = Submission.builder() + .id(UUID.randomUUID().toString()) + .examId(EXAM_UUID) + .studentId(STUDENT_UUID) + .submissionDate(SUBMISSION_DATE) + .fileUpload(fileReference) + .build(); + + submissionList = List.of(submission); + } + + @Test + void getRelevantSubmissionsShouldReturnSubmissionsForLecturer() throws Exception { + when(submissionService.getAllAccessibleSubmissionsForLecturer(LECTURER_UUID)) + .thenReturn(submissionList); + + mockMvc.perform(get("/api/v1/submissions/for-lecturer/{lecturerUuid}", LECTURER_UUID) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$[0].examId").value(EXAM_UUID)) + .andExpect(jsonPath("$[0].studentId").value(STUDENT_UUID)) + .andExpect(jsonPath("$[0].submissionDate").value(SUBMISSION_DATE)) + .andExpect(jsonPath("$[0].fileUpload.filename").value(FILENAME)) + .andExpect(jsonPath("$[0].fileUpload.downloadLink").value(DOWNLOAD_LINK)); + } + + @Test + void getRelevantSubmissionsShouldReturnEmptyListWhenNoSubmissions() throws Exception { + when(submissionService.getAllAccessibleSubmissionsForLecturer(LECTURER_UUID)) + .thenReturn(List.of()); + + mockMvc.perform(get("/api/v1/submissions/for-lecturer/{lecturerUuid}", LECTURER_UUID) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + } + + @Test + void getRelevantSubmissionsShouldHandleInvalidLecturerUuid() throws Exception { + String invalidLecturerUuid = "invalid-uuid"; + when(submissionService.getAllAccessibleSubmissionsForLecturer(invalidLecturerUuid)) + .thenReturn(List.of()); + + mockMvc.perform(get("/api/v1/submissions/for-lecturer/{lecturerUuid}", invalidLecturerUuid) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$").isEmpty()); + } +} diff --git a/src/test/java/com/ase/lecturerservice/services/ExamServiceTest.java b/src/test/java/com/ase/lecturerservice/services/ExamServiceTest.java new file mode 100644 index 00000000..1487fe64 --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/services/ExamServiceTest.java @@ -0,0 +1,90 @@ +package com.ase.lecturerservice.services; + +import java.time.LocalDate; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import com.ase.lecturerservice.MockValues; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.entities.ExamType; +import com.ase.lecturerservice.entities.user.Lecturer; +import com.ase.lecturerservice.entities.user.UserType; + +@SpringBootTest +public class ExamServiceTest { + @Autowired + private ExamService examService; + + private Lecturer lecturer; + private LocalDate date; + + @BeforeEach + public void setUpLecturer() { + lecturer = Lecturer.builder() + .uuid(MockValues.UuidMocks.LECTURER_UUID.getValue()) + .email("lecturer@example.com") + .type(UserType.LECTURER) + .firstName("John") + .lastName("Doe") + .build(); + + date = LocalDate.of( + MockValues.IntMocks.DATE_YEAR.getValue(), + MockValues.IntMocks.DATE_MONTH.getValue(), + MockValues.IntMocks.DATE_DAY.getValue()); + } + + @Test + void fetchExamsByLecturerShouldGetExams() { + DummyData.EXAMS = List.of(Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .name("Mathematics Final Exam") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.PRESENTATION) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Calculator, Formula Sheet") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Room A101") + .lecturerUuid(MockValues.UuidMocks.LECTURER_UUID.getValue()) + .module("Mathe") + .build()); + + List exams = examService.getExamsByLecturer(lecturer.getUuid()); + Exam exam = exams.getFirst(); + + Assertions.assertThat(exams).isNotEmpty(); + Assertions.assertThat(exam.getUuid()) + .isEqualTo(MockValues.UuidMocks.EXAM_UUID.getValue()); + Assertions.assertThat(exam.getName()) + .isEqualTo("Mathematics Final Exam"); + Assertions.assertThat(exam.getTotalPoints()) + .isEqualTo(MockValues.IntMocks.TOTAL_POINTS.getValue()); + Assertions.assertThat(exam.getExamType()).isEqualTo(ExamType.PRESENTATION); + Assertions.assertThat(exam.getDate()).isEqualTo(date); + Assertions.assertThat(exam.getTime()) + .isEqualTo(MockValues.IntMocks.TIME_SECONDS.getValue()); + Assertions.assertThat(exam.getAllowedResources()) + .isEqualTo("Calculator, Formula Sheet"); + Assertions.assertThat(exam.getAttempt()) + .isEqualTo(MockValues.IntMocks.ATTEMPT.getValue()); + Assertions.assertThat(exam.getEtcs()) + .isEqualTo(MockValues.IntMocks.ETCS.getValue()); + Assertions.assertThat(exam.getRoom()).isEqualTo("Room A101"); + Assertions.assertThat(exam.getLecturerUuid()).isEqualTo(lecturer.getUuid()); + Assertions.assertThat(exam.getModule()).isEqualTo("Mathe"); + } + + @Test + void fetchExamsByLecturerShouldNotGetExams() { + DummyData.EXAMS = List.of(); + + List exams = examService.getExamsByLecturer("Test"); + + Assertions.assertThat(exams).isEmpty(); + } +} diff --git a/src/test/java/com/ase/lecturerservice/services/FeedbackServiceTest.java b/src/test/java/com/ase/lecturerservice/services/FeedbackServiceTest.java new file mode 100644 index 00000000..d19e41c5 --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/services/FeedbackServiceTest.java @@ -0,0 +1,8 @@ +package com.ase.lecturerservice.services; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class FeedbackServiceTest { + +} diff --git a/src/test/java/com/ase/lecturerservice/services/SubmissionServiceTest.java b/src/test/java/com/ase/lecturerservice/services/SubmissionServiceTest.java new file mode 100644 index 00000000..2a7ae0e7 --- /dev/null +++ b/src/test/java/com/ase/lecturerservice/services/SubmissionServiceTest.java @@ -0,0 +1,122 @@ +package com.ase.lecturerservice.services; + +import java.time.LocalDate; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import com.ase.lecturerservice.MockValues; +import com.ase.lecturerservice.entities.Exam; +import com.ase.lecturerservice.entities.ExamType; +import com.ase.lecturerservice.entities.Submission; +import com.ase.lecturerservice.entities.user.Lecturer; +import com.ase.lecturerservice.entities.user.UserType; + +@SpringBootTest +public class SubmissionServiceTest { + @Autowired + private SubmissionService submissionService; + + private Lecturer lecturer; + private LocalDate date; + + @BeforeEach + public void setUpLecturer() { + lecturer = Lecturer.builder() + .uuid(MockValues.UuidMocks.LECTURER_UUID.getValue()) + .email("lecturer@example.com") + .type(UserType.LECTURER) + .firstName("John") + .lastName("Doe") + .build(); + + date = LocalDate.of( + MockValues.IntMocks.DATE_YEAR.getValue(), + MockValues.IntMocks.DATE_MONTH.getValue(), + MockValues.IntMocks.DATE_DAY.getValue()); + } + + @Test + void getSubmissionsForExamShouldReturnSubmissionsForSpecificExam() { + String examId = MockValues.UuidMocks.EXAM_UUID.getValue(); + + List submissions = submissionService.getSubmissionsForExam(examId); + + Assertions.assertThat(submissions).isNotEmpty(); + Assertions.assertThat(submissions) + .allMatch(submission -> submission.getExamId().equals(examId)); + } + + @Test + void getSubmissionsForStudentShouldReturnSubmissionsForSpecificStudent() { + String studentId = MockValues.UuidMocks.STUDENT_UUID.getValue(); + + List submissions = submissionService.getSubmissionsForStudent(studentId); + + Assertions.assertThat(submissions).isNotEmpty(); + Assertions.assertThat(submissions) + .allMatch(submission -> submission.getStudentId().equals(studentId)); + } + + @Test + void getAllAccessibleSubmissionsForLecturerShouldReturnOnlyLecturerSubmissions() { + // Setup exam for lecturer + Exam exam = Exam.builder() + .uuid(MockValues.UuidMocks.EXAM_UUID.getValue()) + .name("Test Exam") + .totalPoints(MockValues.IntMocks.TOTAL_POINTS.getValue()) + .examType(ExamType.EXAM) + .date(date) + .time(MockValues.IntMocks.TIME_SECONDS.getValue()) + .allowedResources("Calculator") + .attempt(MockValues.IntMocks.ATTEMPT.getValue()) + .etcs(MockValues.IntMocks.ETCS.getValue()) + .room("Room A101") + .lecturerUuid(lecturer.getUuid()) + .module("Test Module") + .build(); + + DummyData.EXAMS = List.of(exam); + + List submissions = submissionService + .getAllAccessibleSubmissionsForLecturer(lecturer.getUuid()); + + Assertions.assertThat(submissions).isNotEmpty(); + // All submissions should be for exams belonging to this lecturer + Assertions.assertThat(submissions).allMatch(submission -> + DummyData.EXAMS.stream() + .anyMatch(ex -> ex.getUuid().equals(submission.getExamId()) + && ex.getLecturerUuid().equals(lecturer.getUuid())) + ); + } + + @Test + void getAllAccessibleSubmissionsForLecturerShouldReturnEmptyListForUnknownLecturer() { + String unknownLecturerUuid = "unknown-lecturer-uuid"; + + List submissions = submissionService + .getAllAccessibleSubmissionsForLecturer(unknownLecturerUuid); + + Assertions.assertThat(submissions).isEmpty(); + } + + @Test + void getSubmissionsForExamShouldReturnEmptyListForUnknownExam() { + String unknownExamId = "unknown-exam-id"; + + List submissions = submissionService.getSubmissionsForExam(unknownExamId); + + Assertions.assertThat(submissions).isEmpty(); + } + + @Test + void getSubmissionsForStudentShouldReturnEmptyListForUnknownStudent() { + String unknownStudentId = "unknown-student-id"; + + List submissions = submissionService.getSubmissionsForStudent(unknownStudentId); + + Assertions.assertThat(submissions).isEmpty(); + } +}