CourseStaffCSVController.java

1
package edu.ucsb.cs.scaffold.controller;
2
3
import com.opencsv.CSVReader;
4
import com.opencsv.exceptions.CsvException;
5
import edu.ucsb.cs.scaffold.entity.Course;
6
import edu.ucsb.cs.scaffold.entity.CourseStaff;
7
import edu.ucsb.cs.scaffold.errors.EntityNotFoundException;
8
import edu.ucsb.cs.scaffold.repository.CourseRepository;
9
import edu.ucsb.cs.scaffold.repository.CourseStaffRepository;
10
import edu.ucsb.cs.scaffold.services.UpdateUserService;
11
import io.swagger.v3.oas.annotations.Operation;
12
import io.swagger.v3.oas.annotations.Parameter;
13
import io.swagger.v3.oas.annotations.tags.Tag;
14
import java.io.BufferedInputStream;
15
import java.io.IOException;
16
import java.io.InputStream;
17
import java.io.InputStreamReader;
18
import java.util.Map;
19
import lombok.extern.slf4j.Slf4j;
20
import org.springframework.beans.factory.annotation.Autowired;
21
import org.springframework.http.HttpStatus;
22
import org.springframework.security.access.prepost.PreAuthorize;
23
import org.springframework.web.bind.annotation.PostMapping;
24
import org.springframework.web.bind.annotation.RequestMapping;
25
import org.springframework.web.bind.annotation.RequestParam;
26
import org.springframework.web.bind.annotation.RestController;
27
import org.springframework.web.multipart.MultipartFile;
28
import org.springframework.web.server.ResponseStatusException;
29
30
@Tag(name = "CourseStaff")
31
@RequestMapping("/api/coursestaff")
32
@RestController("CourseStaffCSVController")
33
@Slf4j
34
public class CourseStaffCSVController extends ApiController {
35
36
  @Autowired private CourseStaffRepository courseStaffRepository;
37
38
  @Autowired private CourseRepository courseRepository;
39
40
  @Autowired private UpdateUserService updateUserService;
41
42
  public static final String STAFF_CSV_HEADERS = "firstName,lastName,email";
43
44
  @Operation(summary = "Upload Course Staff from CSV")
45
  @PreAuthorize("@CourseSecurity.hasInstructorPermissions(#root, #courseId)")
46
  @PostMapping(
47
      value = "/upload/csv",
48
      consumes = {"multipart/form-data"})
49
  public Map<String, Integer> uploadStaffCSV(
50
      @Parameter(name = "courseId") @RequestParam Long courseId,
51
      @Parameter(name = "file") @RequestParam("file") MultipartFile file)
52
      throws IOException, CsvException {
53
54
    Course course =
55
        courseRepository
56
            .findById(courseId)
57 1 1. lambda$uploadStaffCSV$0 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CourseStaffCSVController::lambda$uploadStaffCSV$0 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId.toString()));
58
59
    int count = 0;
60
61
    try (InputStream inputStream = new BufferedInputStream(file.getInputStream());
62
        InputStreamReader reader = new InputStreamReader(inputStream);
63
        CSVReader csvReader = new CSVReader(reader)) {
64
65
      String[] headers = csvReader.readNext();
66 1 1. uploadStaffCSV : negated conditional → KILLED
      if (headers == null) {
67
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "CSV file is empty");
68
      }
69
70 1 1. uploadStaffCSV : removed call to edu/ucsb/cs/scaffold/controller/CourseStaffCSVController::validateHeaders → KILLED
      validateHeaders(headers);
71
72
      String[] row;
73 1 1. uploadStaffCSV : negated conditional → KILLED
      while ((row = csvReader.readNext()) != null) {
74 2 1. uploadStaffCSV : changed conditional boundary → KILLED
2. uploadStaffCSV : negated conditional → KILLED
        if (row.length < 3) {
75
          throw new ResponseStatusException(
76
              HttpStatus.BAD_REQUEST,
77
              String.format(
78
                  "CSV row does not have enough columns. Expected at least 3, got %d", row.length));
79
        }
80
81
        CourseStaff courseStaff =
82
            CourseStaff.builder()
83
                .firstName(row[0].trim())
84
                .lastName(row[1].trim())
85
                .email(row[2].trim())
86
                .course(course)
87
                .build();
88
89
        CourseStaff savedCourseStaff = courseStaffRepository.save(courseStaff);
90 1 1. uploadStaffCSV : removed call to edu/ucsb/cs/scaffold/services/UpdateUserService::attachUserToCourseStaff → KILLED
        updateUserService.attachUserToCourseStaff(savedCourseStaff);
91 1 1. uploadStaffCSV : Changed increment from 1 to -1 → KILLED
        count++;
92
      }
93
    }
94
95 1 1. uploadStaffCSV : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/CourseStaffCSVController::uploadStaffCSV → KILLED
    return Map.of("count", count);
96
  }
97
98
  static void validateHeaders(String[] headers) {
99
    String[] expectedHeaders = STAFF_CSV_HEADERS.split(",");
100 2 1. validateHeaders : negated conditional → KILLED
2. validateHeaders : changed conditional boundary → KILLED
    if (headers.length < expectedHeaders.length) {
101
      throw new ResponseStatusException(
102
          HttpStatus.BAD_REQUEST,
103
          String.format(
104
              "CSV headers do not match expected format. Expected: %s", STAFF_CSV_HEADERS));
105
    }
106 2 1. validateHeaders : negated conditional → KILLED
2. validateHeaders : changed conditional boundary → KILLED
    for (int i = 0; i < expectedHeaders.length; i++) {
107 1 1. validateHeaders : negated conditional → KILLED
      if (!expectedHeaders[i].trim().equalsIgnoreCase(headers[i].trim())) {
108
        throw new ResponseStatusException(
109
            HttpStatus.BAD_REQUEST,
110
            String.format(
111
                "CSV headers do not match expected format. Expected: %s", STAFF_CSV_HEADERS));
112
      }
113
    }
114
  }
115
}

Mutations

57

1.1
Location : lambda$uploadStaffCSV$0
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_not_found_for_missing_course()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CourseStaffCSVController::lambda$uploadStaffCSV$0 → KILLED

66

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_empty_csv()]
negated conditional → KILLED

70

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_invalid_headers()]
removed call to edu/ucsb/cs/scaffold/controller/CourseStaffCSVController::validateHeaders → KILLED

73

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_short_row()]
negated conditional → KILLED

74

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:instructor_can_upload_staff_csv()]
changed conditional boundary → KILLED

2.2
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_short_row()]
negated conditional → KILLED

90

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:instructor_can_upload_staff_csv()]
removed call to edu/ucsb/cs/scaffold/services/UpdateUserService::attachUserToCourseStaff → KILLED

91

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:instructor_can_upload_staff_csv()]
Changed increment from 1 to -1 → KILLED

95

1.1
Location : uploadStaffCSV
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:instructor_can_upload_staff_csv()]
replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/CourseStaffCSVController::uploadStaffCSV → KILLED

100

1.1
Location : validateHeaders
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_too_few_headers()]
negated conditional → KILLED

2.2
Location : validateHeaders
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:instructor_can_upload_staff_csv()]
changed conditional boundary → KILLED

106

1.1
Location : validateHeaders
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_invalid_headers()]
negated conditional → KILLED

2.2
Location : validateHeaders
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_short_row()]
changed conditional boundary → KILLED

107

1.1
Location : validateHeaders
Killed by : edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CourseStaffCSVControllerTests]/[method:upload_returns_bad_request_for_invalid_headers()]
negated conditional → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0