StaffController.java

1
package edu.ucsb.cs156.happiercows.controllers;
2
3
import edu.ucsb.cs156.happiercows.entities.Staff;
4
import edu.ucsb.cs156.happiercows.errors.EntityNotFoundException;
5
import edu.ucsb.cs156.happiercows.models.CsvUploadResult;
6
import edu.ucsb.cs156.happiercows.models.StaffDTO;
7
import edu.ucsb.cs156.happiercows.repositories.StaffRepository;
8
import io.swagger.v3.oas.annotations.tags.Tag;
9
import io.swagger.v3.oas.annotations.Operation;
10
import io.swagger.v3.oas.annotations.Parameter;
11
import lombok.extern.slf4j.Slf4j;
12
import org.apache.commons.csv.CSVFormat;
13
import org.apache.commons.csv.CSVParser;
14
import org.apache.commons.csv.CSVRecord;
15
import org.springframework.beans.factory.annotation.Autowired;
16
import org.springframework.http.MediaType;
17
import org.springframework.security.access.prepost.PreAuthorize;
18
import org.springframework.web.bind.annotation.*;
19
import org.springframework.web.multipart.MultipartFile;
20
21
import java.io.IOException;
22
import java.io.InputStreamReader;
23
import java.nio.charset.StandardCharsets;
24
import java.util.ArrayList;
25
import java.util.Iterator;
26
import java.util.List;
27
28
@Slf4j
29
@Tag(name = "Staff")
30
@RequestMapping("/api/staff")
31
@RestController
32
public class StaffController extends ApiController {
33
    @Autowired
34
    private StaffRepository staffRepository;
35
36
    @Operation(summary = "List all course staff")
37
    @PreAuthorize("hasRole('ROLE_ADMIN')")
38
    @GetMapping("/all")
39
    public Iterable<Staff> allStaff() {
40 1 1. allStaff : replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StaffController::allStaff → KILLED
        return staffRepository.findAll();
41
    }
42
43
    @Operation(summary = "List the staff for a single course")
44
    @PreAuthorize("hasRole('ROLE_ADMIN')")
45
    @GetMapping("/course/{courseId}")
46
    public Iterable<Staff> staffForCourse(
47
            @Parameter(name = "courseId") @PathVariable Long courseId) {
48 1 1. staffForCourse : replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StaffController::staffForCourse → KILLED
        return staffRepository.findByCourseId(courseId);
49
    }
50
51
    @Operation(summary = "Get a staff member by id")
52
    @PreAuthorize("hasRole('ROLE_ADMIN')")
53
    @GetMapping("/{id}")
54
    public Staff getStaffById(
55
            @Parameter(name = "id") @PathVariable Long id) {
56 1 1. getStaffById : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::getStaffById → KILLED
        return staffRepository.findById(id)
57 1 1. lambda$getStaffById$0 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$getStaffById$0 → KILLED
                .orElseThrow(() -> new EntityNotFoundException(Staff.class, id));
58
    }
59
60
    @Operation(summary = "Add a staff member to a course")
61
    @PreAuthorize("hasRole('ROLE_ADMIN')")
62
    @PostMapping("")
63
    public Staff postStaff(
64
            @Parameter(name = "staff") @RequestBody StaffDTO staffDTO) {
65 1 1. postStaff : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::postStaff → KILLED
        return staffRepository.save(staffDTO.toStaff());
66
    }
67
68
    @Operation(summary = "Update a single staff member")
69
    @PreAuthorize("hasRole('ROLE_ADMIN')")
70
    @PutMapping("/{id}")
71
    public Staff updateStaff(
72
            @Parameter(name = "id") @PathVariable Long id, @RequestBody StaffDTO staffDTO) {
73
        Staff staff = staffRepository.findById(id)
74 1 1. lambda$updateStaff$1 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$updateStaff$1 → KILLED
                .orElseThrow(() -> new EntityNotFoundException(Staff.class, id));
75
76 1 1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setLastName → KILLED
        staff.setLastName(staffDTO.getLastName());
77 1 1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setFirstMiddleName → KILLED
        staff.setFirstMiddleName(staffDTO.getFirstMiddleName());
78 1 1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setEmail → KILLED
        staff.setEmail(staffDTO.getEmail());
79 1 1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setCourseId → KILLED
        staff.setCourseId(staffDTO.getCourseId());
80
81
        staffRepository.save(staff);
82
83 1 1. updateStaff : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::updateStaff → KILLED
        return staff;
84
    }
85
86
    @Operation(summary = "Remove a staff member from a course")
87
    @PreAuthorize("hasRole('ROLE_ADMIN')")
88
    @DeleteMapping("/{id}")
89
    public Object deleteStaff(
90
            @Parameter(name = "id") @PathVariable Long id) {
91
        Staff staff = staffRepository.findById(id)
92 1 1. lambda$deleteStaff$2 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$deleteStaff$2 → KILLED
                .orElseThrow(() -> new EntityNotFoundException(Staff.class, id));
93
94 1 1. deleteStaff : removed call to edu/ucsb/cs156/happiercows/repositories/StaffRepository::delete → KILLED
        staffRepository.delete(staff);
95 1 1. deleteStaff : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::deleteStaff → KILLED
        return genericMessage("Staff with id %s deleted".formatted(id));
96
    }
97
98
    private static final List<String> STAFF_CSV_HEADERS = List.of("lastName", "firstMiddleName", "email");
99
100
    /**
101
     * Bulk-adds staff to a course from an uploaded CSV file, with a single
102
     * fixed header format ({@code lastName,firstMiddleName,email}). Rows
103
     * whose email is already on the course's staff list are skipped rather
104
     * than duplicated.
105
     *
106
     * @param courseId the course to add the staff to
107
     * @param file the uploaded CSV file
108
     * @return a summary of how many staff were created, and which emails
109
     *         were skipped as already-on-roster
110
     */
111
    @Operation(summary = "Upload a CSV file of staff for a course")
112
    @PreAuthorize("hasRole('ROLE_ADMIN')")
113
    @PostMapping(value = "/upload/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
114
    public CsvUploadResult uploadStaffCsv(
115
            @Parameter(name = "courseId") @RequestParam Long courseId,
116
            @Parameter(name = "file") @RequestParam("file") MultipartFile file) throws IOException {
117
118
        List<CSVRecord> records;
119
        try (CSVParser parser = CSVFormat.DEFAULT.parse(
120
                new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
121
            records = parser.getRecords();
122
        }
123
124 1 1. uploadStaffCsv : negated conditional → KILLED
        if (records.isEmpty()) {
125
            throw new IllegalArgumentException("CSV file is empty");
126
        }
127
128
        Iterator<CSVRecord> rows = records.iterator();
129
130
        List<String> headerValues = new ArrayList<>();
131
        for (String value : rows.next()) {
132
            headerValues.add(value.trim());
133
        }
134
135 1 1. uploadStaffCsv : negated conditional → KILLED
        if (headerValues.size() != STAFF_CSV_HEADERS.size()) {
136
            throw new IllegalArgumentException("Unrecognized CSV header format");
137
        }
138 2 1. uploadStaffCsv : negated conditional → KILLED
2. uploadStaffCsv : changed conditional boundary → KILLED
        for (int i = 0; i < STAFF_CSV_HEADERS.size(); i++) {
139 1 1. uploadStaffCsv : negated conditional → KILLED
            if (!STAFF_CSV_HEADERS.get(i).equalsIgnoreCase(headerValues.get(i))) {
140
                throw new IllegalArgumentException("Unrecognized CSV header format");
141
            }
142
        }
143
144
        int created = 0;
145
        List<String> skippedEmails = new ArrayList<>();
146
147 1 1. uploadStaffCsv : negated conditional → KILLED
        while (rows.hasNext()) {
148
            CSVRecord row = rows.next();
149 1 1. uploadStaffCsv : negated conditional → KILLED
            if (row.size() != STAFF_CSV_HEADERS.size()) {
150
                throw new IllegalArgumentException(
151
                        "Row %d does not have the expected number of columns".formatted(row.getRecordNumber()));
152
            }
153
154
            Staff staff = Staff.builder()
155
                    .lastName(row.get(0).trim())
156
                    .firstMiddleName(row.get(1).trim())
157
                    .email(row.get(2).trim())
158
                    .courseId(courseId)
159
                    .build();
160
161 1 1. uploadStaffCsv : negated conditional → KILLED
            if (staffRepository.findByCourseIdAndEmail(courseId, staff.getEmail()).iterator().hasNext()) {
162
                skippedEmails.add(staff.getEmail());
163
                continue;
164
            }
165
166
            staffRepository.save(staff);
167 1 1. uploadStaffCsv : Changed increment from 1 to -1 → KILLED
            created++;
168
        }
169
170 1 1. uploadStaffCsv : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::uploadStaffCsv → KILLED
        return CsvUploadResult.builder()
171
                .created(created)
172
                .skippedEmails(skippedEmails)
173
                .build();
174
    }
175
}

Mutations

40

1.1
Location : allStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_get_all_staff()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StaffController::allStaff → KILLED

48

1.1
Location : staffForCourse
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_get_staff_for_a_course()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StaffController::staffForCourse → KILLED

56

1.1
Location : getStaffById
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_get_staff_by_id()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::getStaffById → KILLED

57

1.1
Location : lambda$getStaffById$0
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_cannot_get_staff_when_it_does_not_exist()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$getStaffById$0 → KILLED

65

1.1
Location : postStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_post_new_staff()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::postStaff → KILLED

74

1.1
Location : lambda$updateStaff$1
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_cannot_edit_staff_that_does_not_exist()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$updateStaff$1 → KILLED

76

1.1
Location : updateStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_edit_an_existing_staff_member()]
removed call to edu/ucsb/cs156/happiercows/entities/Staff::setLastName → KILLED

77

1.1
Location : updateStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_edit_an_existing_staff_member()]
removed call to edu/ucsb/cs156/happiercows/entities/Staff::setFirstMiddleName → KILLED

78

1.1
Location : updateStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_edit_an_existing_staff_member()]
removed call to edu/ucsb/cs156/happiercows/entities/Staff::setEmail → KILLED

79

1.1
Location : updateStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_edit_an_existing_staff_member()]
removed call to edu/ucsb/cs156/happiercows/entities/Staff::setCourseId → KILLED

83

1.1
Location : updateStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_edit_an_existing_staff_member()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::updateStaff → KILLED

92

1.1
Location : lambda$deleteStaff$2
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_tries_to_delete_non_existant_staff_and_gets_right_error_message()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$deleteStaff$2 → KILLED

94

1.1
Location : deleteStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_delete_a_staff_member()]
removed call to edu/ucsb/cs156/happiercows/repositories/StaffRepository::delete → KILLED

95

1.1
Location : deleteStaff
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_delete_a_staff_member()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::deleteStaff → KILLED

124

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:empty_csv_file_is_rejected()]
negated conditional → KILLED

135

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:duplicate_emails_are_skipped_not_saved()]
negated conditional → KILLED

138

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:unrecognized_header_format_is_rejected()]
negated conditional → KILLED

2.2
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:malformed_row_with_too_few_columns_is_rejected()]
changed conditional boundary → KILLED

139

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:unrecognized_header_format_is_rejected()]
negated conditional → KILLED

147

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:malformed_row_with_too_few_columns_is_rejected()]
negated conditional → KILLED

149

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:malformed_row_with_too_few_columns_is_rejected()]
negated conditional → KILLED

161

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:duplicate_emails_are_skipped_not_saved()]
negated conditional → KILLED

167

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:admin_can_upload_staff_csv()]
Changed increment from 1 to -1 → KILLED

170

1.1
Location : uploadStaffCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StaffControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StaffControllerTests]/[method:duplicate_emails_are_skipped_not_saved()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::uploadStaffCsv → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0