summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pom.xml4
-rw-r--r--src/main/java/com/stileeducation/markr/controller/TestResultsController.java129
-rw-r--r--src/main/java/com/stileeducation/markr/dto/AggregateResponseDTO.java (renamed from src/main/java/com/stileeducation/markr/dto/AggregatedTestResultsDTO.java)4
-rw-r--r--src/main/java/com/stileeducation/markr/dto/ImportResponseDTO.java115
-rw-r--r--src/main/java/com/stileeducation/markr/dto/MCQTestResultDTO.java15
-rw-r--r--src/main/java/com/stileeducation/markr/dto/MCQTestResultsDTO.java2
-rw-r--r--src/main/java/com/stileeducation/markr/dto/SummaryMarksDTO.java12
-rw-r--r--src/main/java/com/stileeducation/markr/entity/Student.java192
-rw-r--r--src/main/java/com/stileeducation/markr/entity/Test.java166
-rw-r--r--src/main/java/com/stileeducation/markr/entity/TestResult.java187
-rw-r--r--src/main/java/com/stileeducation/markr/service/StudentService.java1
-rw-r--r--src/main/java/com/stileeducation/markr/service/TestResultsService.java94
-rw-r--r--src/main/java/com/stileeducation/markr/service/TestService.java12
-rw-r--r--src/test/java/com/stileeducation/markr/controller/TestResultsControllerTest.java266
-rw-r--r--src/test/resources/invalid-payload.xml10
15 files changed, 872 insertions, 337 deletions
diff --git a/pom.xml b/pom.xml
index 0422efd..cc5ce5d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,10 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+ <dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
diff --git a/src/main/java/com/stileeducation/markr/controller/TestResultsController.java b/src/main/java/com/stileeducation/markr/controller/TestResultsController.java
index 1376ecb..f5b6070 100644
--- a/src/main/java/com/stileeducation/markr/controller/TestResultsController.java
+++ b/src/main/java/com/stileeducation/markr/controller/TestResultsController.java
@@ -1,11 +1,8 @@
package com.stileeducation.markr.controller;
-import com.stileeducation.markr.dto.AggregatedTestResultsDTO;
-import com.stileeducation.markr.dto.MCQTestResultDTO;
+import com.stileeducation.markr.dto.AggregateResponseDTO;
+import com.stileeducation.markr.dto.ImportResponseDTO;
import com.stileeducation.markr.dto.MCQTestResultsDTO;
-import com.stileeducation.markr.entity.Student;
-import com.stileeducation.markr.entity.Test;
-import com.stileeducation.markr.entity.TestResult;
import com.stileeducation.markr.repository.TestRepository;
import com.stileeducation.markr.repository.TestResultRepository;
import com.stileeducation.markr.service.StudentService;
@@ -14,81 +11,77 @@ import com.stileeducation.markr.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/")
public class TestResultsController {
- public static final String IMPORT_ENDPOINT = "/import";
- public static final String AGGREGATE_ENDPOINT = "/results/{test-id}/aggregate";
+ public static final String IMPORT_ENDPOINT = "/import";
+ public static final String AGGREGATE_ENDPOINT = "/results/{test-id}/aggregate";
- @Autowired
- private StudentService studentService;
+ @Autowired
+ private StudentService studentService;
- @Autowired
- private TestService testService;
+ @Autowired
+ private TestService testService;
- @Autowired
- private TestResultsService testResultsService;
+ @Autowired
+ private TestResultsService testResultsService;
- @Autowired
- private TestRepository testRepository;
+ @Autowired
+ private TestRepository testRepository;
- @Autowired
- private TestResultRepository testResultRepository;
+ @Autowired
+ private TestResultRepository testResultRepository;
- public TestResultsController(TestResultsService testResultsService) {
- this.testResultsService = testResultsService;
- }
-
- // TODO consider return value
- @PostMapping(
- value = IMPORT_ENDPOINT,
- consumes = "text/xml+markr",
- produces = "application/json")
- public ResponseEntity<Void> handleXmlRequest(@RequestBody MCQTestResultsDTO testResults) {
-
- for (MCQTestResultDTO mcqTestResult : testResults.getMcqTestResults()) {
- Student student = studentService
- .findOrCreateStudent(
- mcqTestResult.getFirstName(),
- mcqTestResult.getLastName(),
- mcqTestResult.getStudentNumber());
-
- Test test = testService
- .findOrCreateTest(
- mcqTestResult.getTestId(),
- mcqTestResult.getSummaryMarks().getAvailable());
-
- if (test.getMarksAvailable() < mcqTestResult.getSummaryMarks().getAvailable()) {
- test.setMarksAvailable(mcqTestResult.getSummaryMarks().getAvailable());
- testRepository.save(test);
- }
-
- // Some edge cases to consider
- // obtained is higher than available (assumption?)
+ public TestResultsController(TestResultsService testResultsService) {
+ this.testResultsService = testResultsService;
+ }
- TestResult testResult = testResultsService
- .findOrCreateTestResult(
- student.getId(),
- test.getId(),
- mcqTestResult.getSummaryMarks().getObtained());
-
- if (testResult.getMarksAwarded() < mcqTestResult.getSummaryMarks().getObtained()) {
- testResult.setMarksAwarded(mcqTestResult.getSummaryMarks().getObtained());
- testResultRepository.save(testResult);
- }
- }
-
- return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ @PostMapping(
+ value = IMPORT_ENDPOINT,
+ consumes = "text/xml+markr",
+ produces = "application/json")
+ public ResponseEntity<ImportResponseDTO> postTestResults(@Validated @RequestBody MCQTestResultsDTO testResults) {
+ ImportResponseDTO response = testResultsService.processTestResults(testResults);
+ if ("failure".equals(response.getStatus())) {
+ return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
-
- @GetMapping(
- value = AGGREGATE_ENDPOINT,
- produces = "application/json")
- public AggregatedTestResultsDTO getAggregatedResults(@PathVariable("test-id") String testId) {
- return testResultsService.aggregateTestResults(testId);
- }
-
+ return new ResponseEntity<>(response, HttpStatus.OK);
+ }
+
+ @GetMapping(
+ value = AGGREGATE_ENDPOINT,
+ produces = "application/json")
+ public AggregateResponseDTO getAggregatedResults(@PathVariable("test-id") String testId) {
+ return testResultsService.aggregateTestResults(testId);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity<ImportResponseDTO> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
+ ImportResponseDTO response = new ImportResponseDTO();
+ response.setStatus("error");
+ response.setMessage("Invalid payload");
+ return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ public ResponseEntity<ImportResponseDTO> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
+ ImportResponseDTO response = new ImportResponseDTO();
+ response.setStatus("error");
+ response.setMessage("Invalid XML payload");
+ return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity<ImportResponseDTO> handleGenericException(Exception ex) {
+ ImportResponseDTO response = new ImportResponseDTO();
+ response.setStatus("error");
+ response.setMessage("An unexpected error occurred");
+ return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
}
diff --git a/src/main/java/com/stileeducation/markr/dto/AggregatedTestResultsDTO.java b/src/main/java/com/stileeducation/markr/dto/AggregateResponseDTO.java
index f5970c3..7232174 100644
--- a/src/main/java/com/stileeducation/markr/dto/AggregatedTestResultsDTO.java
+++ b/src/main/java/com/stileeducation/markr/dto/AggregateResponseDTO.java
@@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Objects;
@JsonInclude(JsonInclude.Include.NON_NULL)
-public class AggregatedTestResultsDTO {
+public class AggregateResponseDTO {
private double mean;
private double stddev;
@@ -85,7 +85,7 @@ public class AggregatedTestResultsDTO {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- AggregatedTestResultsDTO that = (AggregatedTestResultsDTO) o;
+ AggregateResponseDTO that = (AggregateResponseDTO) o;
return Double.compare(mean, that.mean) == 0
&& Double.compare(stddev, that.stddev) == 0
&& Double.compare(min, that.min) == 0
diff --git a/src/main/java/com/stileeducation/markr/dto/ImportResponseDTO.java b/src/main/java/com/stileeducation/markr/dto/ImportResponseDTO.java
new file mode 100644
index 0000000..8613865
--- /dev/null
+++ b/src/main/java/com/stileeducation/markr/dto/ImportResponseDTO.java
@@ -0,0 +1,115 @@
+package com.stileeducation.markr.dto;
+
+public class ImportResponseDTO {
+
+ private String status;
+ private String message;
+ private ImportData data;
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public ImportData getData() {
+ return data;
+ }
+
+ public void setData(ImportData data) {
+ this.data = data;
+ }
+
+ public static class ImportData {
+ private int studentsCreated = 0;
+ private int studentsUpdated = 0;
+
+ private int testsCreated = 0;
+ private int testsUpdated = 0;
+
+ private int testResultsCreated = 0;
+ private int testResultsUpdated = 0;
+
+ public int getStudentsCreated() {
+ return studentsCreated;
+ }
+
+ public void setStudentsCreated(int studentsCreated) {
+ this.studentsCreated = studentsCreated;
+ }
+
+ public int getStudentsUpdated() {
+ return studentsUpdated;
+ }
+
+ public void setStudentsUpdated(int studentsUpdated) {
+ this.studentsUpdated = studentsUpdated;
+ }
+
+ public int getTestsCreated() {
+ return testsCreated;
+ }
+
+ public void setTestsCreated(int testsCreated) {
+ this.testsCreated = testsCreated;
+ }
+
+ public int getTestsUpdated() {
+ return testsUpdated;
+ }
+
+ public void setTestsUpdated(int testsUpdated) {
+ this.testsUpdated = testsUpdated;
+ }
+
+ public int getTestResultsCreated() {
+ return testResultsCreated;
+ }
+
+ public void setTestResultsCreated(int testResultsCreated) {
+ this.testResultsCreated = testResultsCreated;
+ }
+
+ public int getTestResultsUpdated() {
+ return testResultsUpdated;
+ }
+
+ public void setTestResultsUpdated(int testResultsUpdated) {
+ this.testResultsUpdated = testResultsUpdated;
+ }
+
+ public void incrementStudentsCreated() {
+ this.studentsCreated++;
+ }
+
+ public void incrementStudentsUpdated() {
+ this.studentsUpdated++;
+ }
+
+ public void incrementTestsCreated() {
+ this.testsCreated++;
+ }
+
+ public void incrementTestsUpdated() {
+ this.testsUpdated++;
+ }
+
+ public void incrementTestResultsCreated() {
+ this.testResultsCreated++;
+ }
+
+ public void incrementTestResultsUpdated() {
+ this.testResultsUpdated++;
+ }
+ }
+}
diff --git a/src/main/java/com/stileeducation/markr/dto/MCQTestResultDTO.java b/src/main/java/com/stileeducation/markr/dto/MCQTestResultDTO.java
index b227fe3..da6a0e1 100644
--- a/src/main/java/com/stileeducation/markr/dto/MCQTestResultDTO.java
+++ b/src/main/java/com/stileeducation/markr/dto/MCQTestResultDTO.java
@@ -1,5 +1,8 @@
package com.stileeducation.markr.dto;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
@@ -9,11 +12,23 @@ import java.util.Objects;
@XmlRootElement(name = "mcq-test-result")
public class MCQTestResultDTO {
+ @NotBlank
private String scannedOn;
+
+ @NotBlank(message = "First name is mandatory")
private String firstName;
+
+ @NotBlank(message = "Last name is mandatory")
private String lastName;
+
+ @NotBlank(message = "Last name is mandatory")
private String studentNumber;
+
+ @NotBlank(message = "Test id is mandatory")
private String testId;
+
+ @Valid
+ @NotNull
private SummaryMarksDTO summaryMarks;
@XmlAttribute(name = "scanned-on")
diff --git a/src/main/java/com/stileeducation/markr/dto/MCQTestResultsDTO.java b/src/main/java/com/stileeducation/markr/dto/MCQTestResultsDTO.java
index e9ee8a7..fb77125 100644
--- a/src/main/java/com/stileeducation/markr/dto/MCQTestResultsDTO.java
+++ b/src/main/java/com/stileeducation/markr/dto/MCQTestResultsDTO.java
@@ -1,5 +1,6 @@
package com.stileeducation.markr.dto;
+import jakarta.validation.Valid;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
@@ -9,6 +10,7 @@ import java.util.Objects;
@XmlRootElement(name = "mcq-test-results")
public class MCQTestResultsDTO {
+ @Valid
private List<MCQTestResultDTO> mcqTestResults;
@XmlElement(name = "mcq-test-result")
diff --git a/src/main/java/com/stileeducation/markr/dto/SummaryMarksDTO.java b/src/main/java/com/stileeducation/markr/dto/SummaryMarksDTO.java
index a67d19c..788eea0 100644
--- a/src/main/java/com/stileeducation/markr/dto/SummaryMarksDTO.java
+++ b/src/main/java/com/stileeducation/markr/dto/SummaryMarksDTO.java
@@ -1,5 +1,7 @@
package com.stileeducation.markr.dto;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlRootElement;
@@ -7,8 +9,14 @@ import java.util.Objects;
@XmlRootElement(name = "summary-marks")
public class SummaryMarksDTO {
- private int available;
- private int obtained;
+
+ @NotNull(message = "Available marks must not be null")
+ @Min(value = 0, message = "Available marks must be non-negative")
+ private Integer available;
+
+ @NotNull(message = "Obtained marks must not be null")
+ @Min(value = 0, message = "Obtained marks must be non-negative")
+ private Integer obtained;
@XmlAttribute(name = "available")
public int getAvailable() {
diff --git a/src/main/java/com/stileeducation/markr/entity/Student.java b/src/main/java/com/stileeducation/markr/entity/Student.java
index 4e69eab..30a4c2b 100644
--- a/src/main/java/com/stileeducation/markr/entity/Student.java
+++ b/src/main/java/com/stileeducation/markr/entity/Student.java
@@ -10,87 +10,113 @@ import java.util.Set;
@Table(name = "students")
public class Student {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(name = "first_name", nullable = false)
- private String firstName;
-
- @Column(name = "last_name", nullable = false)
- private String lastName;
-
- @Column(name = "student_number", nullable = false, unique = true)
- private String studentNumber;
-
- @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
- private Set<TestResult> testResults = new HashSet<>();
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getFirstName() {
- return firstName;
- }
-
- public void setFirstName(String firstName) {
- this.firstName = firstName;
- }
-
- public String getLastName() {
- return lastName;
- }
-
- public void setLastName(String lastName) {
- this.lastName = lastName;
- }
-
- public String getStudentNumber() {
- return studentNumber;
- }
-
- public void setStudentNumber(String studentNumber) {
- this.studentNumber = studentNumber;
- }
-
- public Set<TestResult> getTestResults() {
- return testResults;
- }
-
- public void setTestResults(Set<TestResult> testResults) {
- this.testResults = testResults;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Student student = (Student) o;
- return Objects.equals(id, student.id) &&
- Objects.equals(firstName, student.firstName) &&
- Objects.equals(lastName, student.lastName) &&
- Objects.equals(studentNumber, student.studentNumber) &&
- Objects.equals(testResults, student.testResults);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, firstName, lastName, studentNumber, testResults);
- }
-
- @Override
- public String toString() {
- return "Student{" +
- "id=" + id +
- ", firstName='" + firstName + '\'' +
- ", lastName='" + lastName + '\'' +
- ", studentNumber='" + studentNumber + '\'' +
- ", testResults=" + testResults +
- '}';
- }
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "first_name", nullable = false)
+ private String firstName;
+
+ @Column(name = "last_name", nullable = false)
+ private String lastName;
+
+ @Column(name = "student_number", nullable = false, unique = true)
+ private String studentNumber;
+
+ @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
+ private Set<TestResult> testResults = new HashSet<>();
+
+ @Transient
+ private boolean created = false;
+
+ @Transient
+ private boolean updated = false;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getStudentNumber() {
+ return studentNumber;
+ }
+
+ public void setStudentNumber(String studentNumber) {
+ this.studentNumber = studentNumber;
+ }
+
+ public Set<TestResult> getTestResults() {
+ return testResults;
+ }
+
+ public void setTestResults(Set<TestResult> testResults) {
+ this.testResults = testResults;
+ }
+
+ public boolean isCreated() {
+ return created;
+ }
+
+ public void setCreated(boolean created) {
+ this.created = created;
+ }
+
+ public boolean isUpdated() {
+ return updated;
+ }
+
+ public void setUpdated(boolean updated) {
+ this.updated = updated;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Student student = (Student) o;
+ return Objects.equals(id, student.id) &&
+ Objects.equals(firstName, student.firstName) &&
+ Objects.equals(lastName, student.lastName) &&
+ Objects.equals(studentNumber, student.studentNumber) &&
+ Objects.equals(testResults, student.testResults) &&
+ Objects.equals(created, student.created) &&
+ Objects.equals(updated, student.updated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, firstName, lastName, studentNumber, testResults);
+ }
+
+ @Override
+ public String toString() {
+ return "Student{" +
+ "id=" + id +
+ ", firstName='" + firstName + '\'' +
+ ", lastName='" + lastName + '\'' +
+ ", studentNumber='" + studentNumber + '\'' +
+ ", testResults=" + testResults +
+ ", created=" + created +
+ ", updated=" + updated +
+ '}';
+ }
} \ No newline at end of file
diff --git a/src/main/java/com/stileeducation/markr/entity/Test.java b/src/main/java/com/stileeducation/markr/entity/Test.java
index 615d0e1..ff9088e 100644
--- a/src/main/java/com/stileeducation/markr/entity/Test.java
+++ b/src/main/java/com/stileeducation/markr/entity/Test.java
@@ -10,74 +10,100 @@ import java.util.Set;
@Table(name = "tests")
public class Test {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(name = "test_id", nullable = false, unique = true)
- private String testId;
-
- @Column(name = "marks_available", nullable = false)
- private Integer marksAvailable;
-
- @OneToMany(mappedBy = "test", cascade = CascadeType.ALL, orphanRemoval = true)
- private Set<TestResult> testResults = new HashSet<>();
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public String getTestId() {
- return testId;
- }
-
- public void setTestId(String testId) {
- this.testId = testId;
- }
-
- public Integer getMarksAvailable() {
- return marksAvailable;
- }
-
- public void setMarksAvailable(Integer marksAvailable) {
- this.marksAvailable = marksAvailable;
- }
-
- public Set<TestResult> getTestResults() {
- return testResults;
- }
-
- public void setTestResults(Set<TestResult> testResults) {
- this.testResults = testResults;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Test test = (Test) o;
- return Objects.equals(id, test.id) &&
- Objects.equals(testId, test.testId) &&
- Objects.equals(marksAvailable, test.marksAvailable) &&
- Objects.equals(testResults, test.testResults);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, testId, marksAvailable, testResults);
- }
-
- @Override
- public String toString() {
- return "Test{" +
- "id=" + id +
- ", testId='" + testId + '\'' +
- ", marksAvailable=" + marksAvailable +
- ", testResults=" + testResults +
- '}';
- }
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "test_id", nullable = false, unique = true)
+ private String testId;
+
+ @Column(name = "marks_available", nullable = false)
+ private Integer marksAvailable;
+
+ @OneToMany(mappedBy = "test", cascade = CascadeType.ALL, orphanRemoval = true)
+ private Set<TestResult> testResults = new HashSet<>();
+
+ @Transient
+ private boolean created = false;
+
+ @Transient
+ private boolean updated = false;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTestId() {
+ return testId;
+ }
+
+ public void setTestId(String testId) {
+ this.testId = testId;
+ }
+
+ public Integer getMarksAvailable() {
+ return marksAvailable;
+ }
+
+ public void setMarksAvailable(Integer marksAvailable) {
+ this.marksAvailable = marksAvailable;
+ }
+
+ public Set<TestResult> getTestResults() {
+ return testResults;
+ }
+
+ public void setTestResults(Set<TestResult> testResults) {
+ this.testResults = testResults;
+ }
+
+ public boolean isCreated() {
+ return created;
+ }
+
+ public void setCreated(boolean created) {
+ this.created = created;
+ }
+
+ public boolean isUpdated() {
+ return updated;
+ }
+
+ public void setUpdated(boolean updated) {
+ this.updated = updated;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Test test = (Test) o;
+ return Objects.equals(id, test.id) &&
+ Objects.equals(testId, test.testId) &&
+ Objects.equals(marksAvailable, test.marksAvailable) &&
+ Objects.equals(testResults, test.testResults) &&
+ Objects.equals(created, test.created) &&
+ Objects.equals(updated, test.updated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, testId, marksAvailable, testResults);
+ }
+
+ @Override
+ public String toString() {
+ return "Test{" +
+ "id=" + id +
+ ", testId='" + testId + '\'' +
+ ", marksAvailable=" + marksAvailable +
+ ", testResults=" + testResults +
+ ", created=" + created +
+ ", updated=" + updated +
+ '}';
+ }
}
diff --git a/src/main/java/com/stileeducation/markr/entity/TestResult.java b/src/main/java/com/stileeducation/markr/entity/TestResult.java
index 26b2346..bdbade4 100644
--- a/src/main/java/com/stileeducation/markr/entity/TestResult.java
+++ b/src/main/java/com/stileeducation/markr/entity/TestResult.java
@@ -8,83 +8,112 @@ import java.util.Objects;
@Table(name = "test_results")
public class TestResult {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "student_id", nullable = false)
- private Student student;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "test_id", nullable = false)
- private Test test;
- @Column(name = "marks_awarded", nullable = false)
- private Integer marksAwarded;
-
- public TestResult() {
- }
-
- public TestResult(Long id, Student student, Test test, Integer marksAwarded) {
- this.id = id;
- this.student = student;
- this.test = test;
- this.marksAwarded = marksAwarded;
- }
-
- public Long getId() {
- return id;
- }
-
- public void setId(Long id) {
- this.id = id;
- }
-
- public Student getStudent() {
- return student;
- }
-
- public void setStudent(Student student) {
- this.student = student;
- }
-
- public Test getTest() {
- return test;
- }
-
- public void setTest(Test test) {
- this.test = test;
- }
-
- public Integer getMarksAwarded() {
- return marksAwarded;
- }
-
- public void setMarksAwarded(Integer marksAwarded) {
- this.marksAwarded = marksAwarded;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TestResult that = (TestResult) o;
- return Objects.equals(id, that.id) &&
- Objects.equals(student, that.student) &&
- Objects.equals(test, that.test) &&
- Objects.equals(marksAwarded, that.marksAwarded);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, student, test, marksAwarded);
- }
-
- @Override
- public String toString() {
- return "TestResult{" +
- "id=" + id +
- ", student=" + student +
- ", test=" + test +
- ", marksAwarded=" + marksAwarded +
- '}';
- }
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "student_id", nullable = false)
+ private Student student;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "test_id", nullable = false)
+ private Test test;
+
+ @Column(name = "marks_awarded", nullable = false)
+ private Integer marksAwarded;
+
+ @Transient
+ private boolean created = false;
+
+ @Transient
+ private boolean updated = false;
+
+ public TestResult() {
+ }
+
+ public TestResult(Long id, Student student, Test test, Integer marksAwarded) {
+ this.id = id;
+ this.student = student;
+ this.test = test;
+ this.marksAwarded = marksAwarded;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Student getStudent() {
+ return student;
+ }
+
+ public void setStudent(Student student) {
+ this.student = student;
+ }
+
+ public Test getTest() {
+ return test;
+ }
+
+ public void setTest(Test test) {
+ this.test = test;
+ }
+
+ public Integer getMarksAwarded() {
+ return marksAwarded;
+ }
+
+ public void setMarksAwarded(Integer marksAwarded) {
+ this.marksAwarded = marksAwarded;
+ }
+
+ public boolean isCreated() {
+ return created;
+ }
+
+ public void setCreated(boolean created) {
+ this.created = created;
+ }
+
+ public boolean isUpdated() {
+ return updated;
+ }
+
+ public void setUpdated(boolean updated) {
+ this.updated = updated;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TestResult that = (TestResult) o;
+ return Objects.equals(id, that.id) &&
+ Objects.equals(student, that.student) &&
+ Objects.equals(test, that.test) &&
+ Objects.equals(marksAwarded, that.marksAwarded) &&
+ Objects.equals(created, that.created) &&
+ Objects.equals(updated, that.updated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, student, test, marksAwarded);
+ }
+
+ @Override
+ public String toString() {
+ return "TestResult{" +
+ "id=" + id +
+ ", student=" + student +
+ ", test=" + test +
+ ", marksAwarded=" + marksAwarded +
+ ", created=" + created +
+ ", updated=" + updated +
+ '}';
+ }
} \ No newline at end of file
diff --git a/src/main/java/com/stileeducation/markr/service/StudentService.java b/src/main/java/com/stileeducation/markr/service/StudentService.java
index 4af4d63..3ce28c0 100644
--- a/src/main/java/com/stileeducation/markr/service/StudentService.java
+++ b/src/main/java/com/stileeducation/markr/service/StudentService.java
@@ -22,6 +22,7 @@ public class StudentService {
student.setFirstName(firstName);
student.setLastName(lastName);
student.setStudentNumber(studentNumber);
+ student.setCreated(true);
return studentRepository.save(student);
}
}
diff --git a/src/main/java/com/stileeducation/markr/service/TestResultsService.java b/src/main/java/com/stileeducation/markr/service/TestResultsService.java
index 95b42f3..51efe95 100644
--- a/src/main/java/com/stileeducation/markr/service/TestResultsService.java
+++ b/src/main/java/com/stileeducation/markr/service/TestResultsService.java
@@ -1,11 +1,12 @@
package com.stileeducation.markr.service;
-import com.stileeducation.markr.dto.AggregatedTestResultsDTO;
+import com.stileeducation.markr.dto.AggregateResponseDTO;
+import com.stileeducation.markr.dto.ImportResponseDTO;
+import com.stileeducation.markr.dto.MCQTestResultDTO;
+import com.stileeducation.markr.dto.MCQTestResultsDTO;
import com.stileeducation.markr.entity.Student;
import com.stileeducation.markr.entity.Test;
import com.stileeducation.markr.entity.TestResult;
-import com.stileeducation.markr.repository.StudentRepository;
-import com.stileeducation.markr.repository.TestRepository;
import com.stileeducation.markr.repository.TestResultRepository;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
import org.apache.commons.math3.stat.descriptive.rank.Percentile;
@@ -19,27 +20,34 @@ import java.util.Optional;
public class TestResultsService {
public static final boolean IS_BIAS_CORRECTED = false;
+
@Autowired
private TestResultRepository testResultRepository;
@Autowired
- private StudentRepository studentRepository;
+ private StudentService studentService;
@Autowired
- private TestRepository testRepository;
-
- public TestResult findOrCreateTestResult(Long studentId, Long testId, Integer marksAwarded) {
- Student student = studentRepository.findById(studentId).orElseThrow(() -> new RuntimeException("Student not found"));
- Test test = testRepository.findById(testId).orElseThrow(() -> new RuntimeException("Test not found"));
+ private TestService testService;
+ public TestResult findOrCreateTestResult(Student student, Test test, Integer marksAwarded) {
Optional<TestResult> optionalTestResult = testResultRepository.findByStudentAndTest(student, test);
if (optionalTestResult.isPresent()) {
- return optionalTestResult.get();
+ TestResult testResult = optionalTestResult.get();
+ if (marksAwarded > testResult.getMarksAwarded()) {
+ testResult.setMarksAwarded(marksAwarded);
+ testResult.setUpdated(true);
+ testResultRepository.save(testResult);
+ } else {
+ testResult.setUpdated(false);
+ }
+ return testResult;
} else {
TestResult testResult = new TestResult();
testResult.setStudent(student);
testResult.setTest(test);
testResult.setMarksAwarded(marksAwarded);
+ testResult.setCreated(true);
return testResultRepository.save(testResult);
}
}
@@ -126,10 +134,10 @@ public class TestResultsService {
return new Percentile().evaluate(marks, 75.0);
}
- public AggregatedTestResultsDTO aggregateTestResults(String testId) {
+ public AggregateResponseDTO aggregateTestResults(String testId) {
List<TestResult> testResults = findAllByTestId(testId);
- AggregatedTestResultsDTO results = new AggregatedTestResultsDTO();
+ AggregateResponseDTO results = new AggregateResponseDTO();
results.setMean(calculateMeanOfTestResults(testResults));
results.setStddev(calculateStandardDeviationOfTestResults(testResults));
@@ -143,4 +151,66 @@ public class TestResultsService {
return results;
}
+ public ImportResponseDTO processTestResults(MCQTestResultsDTO testResults) {
+ ImportResponseDTO.ImportData importData = new ImportResponseDTO.ImportData();
+ boolean isValid = true;
+
+ for (MCQTestResultDTO mcqTestResult : testResults.getMcqTestResults()) {
+ try {
+
+ Student student = studentService
+ .findOrCreateStudent(
+ mcqTestResult.getFirstName(),
+ mcqTestResult.getLastName(),
+ mcqTestResult.getStudentNumber());
+
+ if (student.isCreated()) {
+ importData.incrementStudentsCreated();
+ }
+ if (student.isUpdated()) {
+ importData.incrementStudentsUpdated();
+ }
+
+ Test test = testService
+ .findOrCreateTest(
+ mcqTestResult.getTestId(),
+ mcqTestResult.getSummaryMarks().getAvailable());
+
+ if (test.isCreated()) {
+ importData.incrementTestsCreated();
+ }
+ if (test.isUpdated()) {
+ importData.incrementTestsUpdated();
+ }
+
+ TestResult testResult =
+ findOrCreateTestResult(
+ student,
+ test,
+ mcqTestResult.getSummaryMarks().getObtained());
+
+ if (testResult.isCreated()) {
+ importData.incrementTestResultsCreated();
+ }
+ if (testResult.isUpdated()) {
+ importData.incrementTestResultsUpdated();
+ }
+ } catch (Exception e) {
+ isValid = false;
+ }
+ }
+
+ ImportResponseDTO response = new ImportResponseDTO();
+ response.setData(importData);
+
+ if (isValid) {
+ response.setStatus("success");
+ response.setMessage("Import operation completed successfully.");
+ } else {
+ response.setStatus("failure");
+ response.setMessage("Data was invalid or processing failed.");
+ }
+
+ return response;
+ }
} \ No newline at end of file
diff --git a/src/main/java/com/stileeducation/markr/service/TestService.java b/src/main/java/com/stileeducation/markr/service/TestService.java
index f3ba98c..f3231b9 100644
--- a/src/main/java/com/stileeducation/markr/service/TestService.java
+++ b/src/main/java/com/stileeducation/markr/service/TestService.java
@@ -16,13 +16,21 @@ public class TestService {
public Test findOrCreateTest(String testId, Integer marksAvailable) {
Optional<Test> optionalTest = testRepository.findByTestId(testId);
if (optionalTest.isPresent()) {
- return optionalTest.get();
+ Test test = optionalTest.get();
+ if (test.getMarksAvailable() < marksAvailable) {
+ test.setMarksAvailable(marksAvailable);
+ test.setUpdated(true);
+ testRepository.save(test);
+ } else {
+ test.setUpdated(false);
+ }
+ return test;
} else {
Test test = new Test();
test.setTestId(testId);
test.setMarksAvailable(marksAvailable);
+ test.setCreated(true);
return testRepository.save(test);
}
}
-
}
diff --git a/src/test/java/com/stileeducation/markr/controller/TestResultsControllerTest.java b/src/test/java/com/stileeducation/markr/controller/TestResultsControllerTest.java
index b486812..c0358c9 100644
--- a/src/test/java/com/stileeducation/markr/controller/TestResultsControllerTest.java
+++ b/src/test/java/com/stileeducation/markr/controller/TestResultsControllerTest.java
@@ -2,11 +2,11 @@ package com.stileeducation.markr.controller;
import com.stileeducation.markr.MarkrApplication;
import com.stileeducation.markr.converter.XmlMarkrMessageConverter;
+import com.stileeducation.markr.dto.ImportResponseDTO;
import com.stileeducation.markr.dto.MCQTestResultsDTO;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
-import jakarta.xml.bind.Unmarshaller;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,29 +17,26 @@ import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import java.io.IOException;
-import java.io.InputStream;
import java.io.StringWriter;
import static com.stileeducation.markr.controller.TestResultsController.IMPORT_ENDPOINT;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = MarkrApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class TestResultsControllerTest {
- private static MCQTestResultsDTO sampleResults;
+ private static String validPayload;
+ private static String invalidPayload;
@Autowired
private TestRestTemplate restTemplate;
@BeforeAll
- static void setup() throws JAXBException, IOException {
- ClassPathResource resource = new ClassPathResource("sample-results.xml");
- try (InputStream inputStream = resource.getInputStream()) {
- JAXBContext jaxbContext = JAXBContext.newInstance(MCQTestResultsDTO.class);
- Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
- sampleResults = (MCQTestResultsDTO) unmarshaller.unmarshal(inputStream);
- }
+ static void setup() throws IOException {
+ validPayload = new String(new ClassPathResource("sample-results.xml").getInputStream().readAllBytes(), UTF_8);
+ invalidPayload = new String(new ClassPathResource("invalid-payload.xml").getInputStream().readAllBytes(), UTF_8);
}
private static String toXmlString(MCQTestResultsDTO mcqTestResultsDTO) throws JAXBException {
@@ -51,36 +48,267 @@ public class TestResultsControllerTest {
}
@Test
- void testPost() throws Exception {
+ void testSupportedMediaType() throws Exception {
// Given
- String requestXml = toXmlString(sampleResults);
-
HttpHeaders headers = new HttpHeaders();
headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(validPayload, headers);
// When
- HttpEntity<String> entity = new HttpEntity<>(requestXml, headers);
ResponseEntity<String> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, String.class);
// Then
- assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void testUnsupportedMediaType() throws Exception {
// Given
- MCQTestResultsDTO request = new MCQTestResultsDTO();
- String requestXml = toXmlString(request);
-
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
+ HttpEntity<String> entity = new HttpEntity<>(validPayload, headers);
// When
- HttpEntity<String> entity = new HttpEntity<>(requestXml, headers);
ResponseEntity<String> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, String.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
+ @Test
+ void testInvalidImport() throws Exception {
+ // Given
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<String> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, String.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ }
+
+ @Test
+ void testInvalidImport_MissingFirstName() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name></first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ <summary-marks available="20" obtained="13"/>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_MissingLastName() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name></last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ <summary-marks available="20" obtained="13"/>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_MissingStudentNumber() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name>Alysander</last-name>
+ <student-number></student-number>
+ <test-id>9863</test-id>
+ <summary-marks available="20" obtained="13"/>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_MissingTestId() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id></test-id>
+ <summary-marks available="20" obtained="13"/>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_MissingSummaryMarks() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_MissingSummaryMarksAvailable() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ <summary-marks obtained="13"/>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_MissingSummaryMarksObtained() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ <summary-marks available="20"/>
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid payload");
+ }
+
+ @Test
+ void testInvalidImport_InvalidXmlFormat() throws Exception {
+ // Given
+ String invalidPayload = """
+ <mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name>KJ</first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ <summary-marks available="20" obtained="1
+ </mcq-test-result>
+ </mcq-test-results>
+ """;
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(XmlMarkrMessageConverter.MEDIA_TYPE);
+ HttpEntity<String> entity = new HttpEntity<>(invalidPayload, headers);
+
+ // When
+ ResponseEntity<ImportResponseDTO> response = restTemplate.postForEntity(IMPORT_ENDPOINT, entity, ImportResponseDTO.class);
+
+ // Then
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().getMessage()).isEqualTo("Invalid XML payload");
+ }
} \ No newline at end of file
diff --git a/src/test/resources/invalid-payload.xml b/src/test/resources/invalid-payload.xml
new file mode 100644
index 0000000..02d3b76
--- /dev/null
+++ b/src/test/resources/invalid-payload.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<mcq-test-results>
+ <mcq-test-result scanned-on="2017-12-04T12:12:10+11:00">
+ <first-name></first-name>
+ <last-name>Alysander</last-name>
+ <student-number>002299</student-number>
+ <test-id>9863</test-id>
+ <summary-marks available="20" obtained="13" />
+ </mcq-test-result>
+</mcq-test-results> \ No newline at end of file