CoursesController.java

1
package edu.ucsb.cs156.frontiers.controllers;
2
3
import com.fasterxml.jackson.core.JsonProcessingException;
4
import edu.ucsb.cs156.frontiers.annotations.AllowApiKeyAccess;
5
import edu.ucsb.cs156.frontiers.entities.ApiCourseKey;
6
import edu.ucsb.cs156.frontiers.entities.Course;
7
import edu.ucsb.cs156.frontiers.entities.CourseStaff;
8
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
9
import edu.ucsb.cs156.frontiers.entities.User;
10
import edu.ucsb.cs156.frontiers.enums.OrgStatus;
11
import edu.ucsb.cs156.frontiers.enums.School;
12
import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException;
13
import edu.ucsb.cs156.frontiers.errors.InvalidInstallationTypeException;
14
import edu.ucsb.cs156.frontiers.models.CourseWarning;
15
import edu.ucsb.cs156.frontiers.models.CurrentUser;
16
import edu.ucsb.cs156.frontiers.repositories.AdminRepository;
17
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
18
import edu.ucsb.cs156.frontiers.repositories.CourseStaffRepository;
19
import edu.ucsb.cs156.frontiers.repositories.InstructorRepository;
20
import edu.ucsb.cs156.frontiers.repositories.JobsRepository;
21
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
22
import edu.ucsb.cs156.frontiers.repositories.UserRepository;
23
import edu.ucsb.cs156.frontiers.services.ApiCourseKeyService;
24
import edu.ucsb.cs156.frontiers.services.OrganizationLinkerService;
25
import io.swagger.v3.oas.annotations.Operation;
26
import io.swagger.v3.oas.annotations.Parameter;
27
import io.swagger.v3.oas.annotations.tags.Tag;
28
import java.security.NoSuchAlgorithmException;
29
import java.security.spec.InvalidKeySpecException;
30
import java.time.ZonedDateTime;
31
import java.util.ArrayList;
32
import java.util.List;
33
import java.util.Map;
34
import java.util.Objects;
35
import java.util.Optional;
36
import java.util.stream.Collectors;
37
import java.util.stream.StreamSupport;
38
import lombok.extern.slf4j.Slf4j;
39
import org.springframework.beans.factory.annotation.Autowired;
40
import org.springframework.http.HttpHeaders;
41
import org.springframework.http.HttpStatus;
42
import org.springframework.http.ResponseEntity;
43
import org.springframework.security.access.prepost.PreAuthorize;
44
import org.springframework.transaction.annotation.Transactional;
45
import org.springframework.web.bind.annotation.*;
46
47
@Tag(name = "Course")
48
@RequestMapping("/api/courses")
49
@RestController
50
@Slf4j
51
public class CoursesController extends ApiController {
52
53
  @Autowired private CourseRepository courseRepository;
54
55
  @Autowired private UserRepository userRepository;
56
57
  @Autowired private RosterStudentRepository rosterStudentRepository;
58
59
  @Autowired private CourseStaffRepository courseStaffRepository;
60
61
  @Autowired private InstructorRepository instructorRepository;
62
63
  @Autowired private AdminRepository adminRepository;
64
65
  @Autowired private OrganizationLinkerService linkerService;
66
67
  @Autowired private ApiCourseKeyService apiCourseKeyService;
68
  @Autowired private JobsRepository jobsRepository;
69
70
  /**
71
   * This method creates a new Course.
72
   *
73
   * @param courseName the name of the course
74
   * @param term the term of the course
75
   * @param school the school of the course
76
   * @param canvasApiToken the Canvas API token (optional)
77
   * @param canvasCourseId the Canvas course ID (optional)
78
   */
79
  @Operation(summary = "Create a new course")
80
  @PreAuthorize("hasRole('ROLE_ADMIN') || hasRole('ROLE_INSTRUCTOR')")
81
  @PostMapping("/post")
82
  public InstructorCourseView postCourse(
83
      @Parameter(name = "courseName") @RequestParam String courseName,
84
      @Parameter(name = "term") @RequestParam String term,
85
      @Parameter(name = "school") @RequestParam School school,
86
      @Parameter(name = "canvasApiToken") @RequestParam(required = false) String canvasApiToken,
87
      @Parameter(name = "canvasCourseId") @RequestParam(required = false) String canvasCourseId) {
88
    // get current date right now and set status to pending
89
    CurrentUser currentUser = getCurrentUser();
90
    Course course =
91
        Course.builder()
92
            .courseName(courseName)
93
            .term(term)
94
            .school(school)
95
            .instructorEmail(currentUser.getUser().getEmail().strip())
96
            .canvasApiToken(canvasApiToken)
97
            .canvasCourseId(canvasCourseId)
98
            .build();
99
    Course savedCourse = courseRepository.save(course);
100
101 1 1. postCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::postCourse → KILLED
    return new InstructorCourseView(savedCourse);
102
  }
103
104
  /** Projection of Course entity with fields that are relevant for instructors and admins */
105
  public static record InstructorCourseView(
106
      Long id,
107
      String installationId,
108
      String orgName,
109
      String courseName,
110
      String term,
111
      School school,
112
      String instructorEmail,
113
      boolean hideBasePermissionWarning,
114
      int numStudents,
115
      int numStaff) {
116
117
    // Creates view from Course entity
118
    public InstructorCourseView(Course c) {
119
      this(
120
          c.getId(),
121
          c.getInstallationId(),
122
          c.getOrgName(),
123
          c.getCourseName(),
124
          c.getTerm(),
125
          c.getSchool(),
126
          c.getInstructorEmail(),
127
          c.getHideBasePermissionWarning(),
128 1 1. <init> : negated conditional → KILLED
          c.getRosterStudents() != null ? c.getRosterStudents().size() : 0,
129 1 1. <init> : negated conditional → KILLED
          c.getCourseStaff() != null ? c.getCourseStaff().size() : 0);
130
    }
131
  }
132
133
  public static record ApiCourseKeyView(
134
      Long id,
135
      String keySuffix,
136
      Long createdById,
137
      ZonedDateTime createdAt,
138
      ZonedDateTime expiresAt,
139
      boolean revoked,
140
      long usageCount,
141
      ZonedDateTime lastUsedAt) {
142
    public ApiCourseKeyView(ApiCourseKey key) {
143
      this(
144
          key.getId(),
145
          key.getKeySuffix(),
146
          key.getCreatedBy().getId(),
147
          key.getCreatedAt(),
148
          key.getExpiresAt(),
149
          key.getRevoked(),
150
          key.getUsageCount(),
151
          key.getLastUsedAt());
152
    }
153
  }
154
155
  public static record GeneratedApiCourseKeyView(String apiKey, ApiCourseKeyView keyMetadata) {}
156
157
  /**
158
   * This method returns a list of courses.
159
   *
160
   * @return a list of all courses for an instructor.
161
   */
162
  @Operation(summary = "List all courses for an instructor")
163
  @PreAuthorize("hasRole('ROLE_INSTRUCTOR')")
164
  @GetMapping("/allForInstructors")
165
  public Iterable<InstructorCourseView> allForInstructors() {
166
    CurrentUser currentUser = getCurrentUser();
167
    String instructorEmail = currentUser.getUser().getEmail();
168
    List<Course> courses = courseRepository.findByInstructorEmail(instructorEmail);
169
170
    List<InstructorCourseView> courseViews =
171
        courses.stream().map(InstructorCourseView::new).collect(Collectors.toList());
172 1 1. allForInstructors : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::allForInstructors → KILLED
    return courseViews;
173
  }
174
175
  /**
176
   * This method returns a list of courses.
177
   *
178
   * @return a list of all courses for an admin.
179
   */
180
  @Operation(summary = "List all courses for an admin")
181
  @PreAuthorize("hasRole('ROLE_ADMIN')")
182
  @GetMapping("/allForAdmins")
183
  public Iterable<InstructorCourseView> allForAdmins() {
184
    List<Course> courses = courseRepository.findAll();
185
186
    List<InstructorCourseView> courseViews =
187
        courses.stream().map(InstructorCourseView::new).collect(Collectors.toList());
188 1 1. allForAdmins : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::allForAdmins → KILLED
    return courseViews;
189
  }
190
191
  /**
192
   * This method returns single course by its id
193
   *
194
   * @return a course
195
   */
196
  @Operation(summary = "Get course by id")
197
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #id)")
198
  @GetMapping("/{id}")
199
  public InstructorCourseView getCourseById(@Parameter(name = "id") @PathVariable Long id) {
200
    Course course =
201
        courseRepository
202
            .findById(id)
203 1 1. lambda$getCourseById$0 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCourseById$0 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, id));
204
    // Convert to InstructorCourseView
205
    InstructorCourseView courseView = new InstructorCourseView(course);
206 1 1. getCourseById : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCourseById → KILLED
    return courseView;
207
  }
208
209
  /**
210
   * This method returns the Canvas course ID and partially obscured Canvas token for a course by
211
   * its id. If the token is less than or equal to 3 characters long, it is returned in full.
212
   * Otherwise, all but the last three characters are replaced with asterisks. This is okay because
213
   * such short tokens are not generated by Canvas.
214
   *
215
   * @param courseId the id of the course
216
   * @return a map with courseId, canvasCourseId, and obscured canvasApiToken
217
   */
218
  @Operation(summary = "Get course Canvas course ID and Canvas token (partially obscured)")
219
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
220
  @GetMapping("getCanvasInfo")
221
  public Map<String, String> getCourseCanvasInfo(
222
      @Parameter(name = "courseId") @RequestParam Long courseId) {
223
    Course course =
224
        courseRepository
225
            .findById(courseId)
226 1 1. lambda$getCourseCanvasInfo$1 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCourseCanvasInfo$1 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
227
228
    String obscuredToken = null;
229
230 1 1. getCourseCanvasInfo : negated conditional → KILLED
    if (course.getCanvasApiToken() != null) {
231
      String token = course.getCanvasApiToken();
232 2 1. getCourseCanvasInfo : changed conditional boundary → KILLED
2. getCourseCanvasInfo : negated conditional → KILLED
      if (token.length() < 4) {
233
        obscuredToken = token;
234
      } else {
235 1 1. getCourseCanvasInfo : Replaced integer subtraction with addition → KILLED
        String lastThree = token.substring(token.length() - 3);
236 1 1. getCourseCanvasInfo : Replaced integer subtraction with addition → KILLED
        obscuredToken = "*".repeat(token.length() - 3) + lastThree;
237
      }
238
    }
239 1 1. getCourseCanvasInfo : replaced return value with Collections.emptyMap for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCourseCanvasInfo → KILLED
    return Map.of(
240
        "courseId", course.getId().toString(),
241 1 1. getCourseCanvasInfo : negated conditional → KILLED
        "canvasCourseId", course.getCanvasCourseId() != null ? course.getCanvasCourseId() : "",
242 1 1. getCourseCanvasInfo : negated conditional → KILLED
        "canvasApiToken", obscuredToken != null ? obscuredToken : "");
243
  }
244
245
  /**
246
   * This is the outgoing method, redirecting from Frontiers to GitHub to allow a Course to be
247
   * linked to a GitHub Organization. It redirects from Frontiers to the GitHub app installation
248
   * process, and will return with the {@link #addInstallation(Optional, String, String, Long)
249
   * addInstallation()} endpoint
250
   *
251
   * @param courseId id of the course to be linked to
252
   * @return dynamically loaded url to install Frontiers to a Github Organization, with the courseId
253
   *     marked as the state parameter, which GitHub will return.
254
   */
255
  @Operation(summary = "Authorize Frontiers to a Github Course")
256
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
257
  @GetMapping("/redirect")
258
  public ResponseEntity<Void> linkCourse(@Parameter Long courseId)
259
      throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
260
    String newUrl = linkerService.getRedirectUrl();
261
    newUrl += "/installations/new?state=" + courseId;
262
    // found this convenient solution here:
263
    // https://stackoverflow.com/questions/29085295/spring-mvc-restcontroller-and-redirect
264 1 1. linkCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::linkCourse → KILLED
    return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
265
        .header(HttpHeaders.LOCATION, newUrl)
266
        .build();
267
  }
268
269
  /**
270
   * @param installation_id id of the incoming GitHub Organization installation
271
   * @param setup_action whether the permissions are installed or updated. Required RequestParam but
272
   *     not used by the method.
273
   * @param code token to be exchanged with GitHub to ensure the request is legitimate and not
274
   *     spoofed.
275
   * @param state id of the Course to be linked with the GitHub installation.
276
   * @return ResponseEntity, returning /success if the course was successfully linked or /noperms if
277
   *     the user does not have the permission to install the application on GitHub. Alternately
278
   *     returns 403 Forbidden if the user is not the creator.
279
   */
280
  @Operation(summary = "Link a Course to a Github Organization by installing Github App")
281
  @PreAuthorize("hasRole('ROLE_ADMIN') || hasRole('ROLE_INSTRUCTOR')")
282
  @GetMapping("link")
283
  public ResponseEntity<Void> addInstallation(
284
      @Parameter(name = "installationId") @RequestParam Optional<String> installation_id,
285
      @Parameter(name = "setupAction") @RequestParam String setup_action,
286
      @Parameter(name = "code") @RequestParam String code,
287
      @Parameter(name = "state") @RequestParam Long state)
288
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
289 1 1. addInstallation : negated conditional → KILLED
    if (installation_id.isEmpty()) {
290 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
      return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
291
          .header(HttpHeaders.LOCATION, "/courses/nopermissions")
292
          .build();
293
    } else {
294
      Course course =
295
          courseRepository
296
              .findById(state)
297 1 1. lambda$addInstallation$2 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$addInstallation$2 → KILLED
              .orElseThrow(() -> new EntityNotFoundException(Course.class, state));
298 1 1. addInstallation : negated conditional → KILLED
      if (!isCurrentUserAdmin()
299 1 1. addInstallation : negated conditional → KILLED
          && !course.getInstructorEmail().equals(getCurrentUser().getUser().getEmail())) {
300 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
301
      } else {
302
        String orgName = linkerService.getOrgName(installation_id.get());
303 1 1. addInstallation : removed call to edu/ucsb/cs156/frontiers/entities/Course::setInstallationId → KILLED
        course.setInstallationId(installation_id.get());
304 1 1. addInstallation : removed call to edu/ucsb/cs156/frontiers/entities/Course::setOrgName → KILLED
        course.setOrgName(orgName);
305
        course
306
            .getRosterStudents()
307 1 1. addInstallation : removed call to java/util/List::forEach → KILLED
            .forEach(
308
                rs -> {
309 1 1. lambda$addInstallation$3 : removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED
                  rs.setOrgStatus(OrgStatus.JOINCOURSE);
310
                });
311
        course
312
            .getCourseStaff()
313 1 1. addInstallation : removed call to java/util/List::forEach → KILLED
            .forEach(
314
                cs -> {
315 1 1. lambda$addInstallation$4 : removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED
                  cs.setOrgStatus(OrgStatus.JOINCOURSE);
316
                });
317
        courseRepository.save(course);
318 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
        return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
319
            .header(HttpHeaders.LOCATION, "/login/success")
320
            .build();
321
      }
322
    }
323
  }
324
325
  /**
326
   * This method handles the InvalidInstallationTypeException.
327
   *
328
   * @param e the exception
329
   * @return a map with the type and message of the exception
330
   */
331
  @ExceptionHandler({InvalidInstallationTypeException.class})
332
  @ResponseStatus(HttpStatus.BAD_REQUEST)
333
  public Object handleInvalidInstallationType(Throwable e) {
334 1 1. handleInvalidInstallationType : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::handleInvalidInstallationType → KILLED
    return Map.of(
335
        "type", e.getClass().getSimpleName(),
336
        "message", e.getMessage());
337
  }
338
339
  public record RosterStudentCoursesDTO(
340
      Long id,
341
      String installationId,
342
      String orgName,
343
      String courseName,
344
      String term,
345
      String school,
346
      OrgStatus studentStatus,
347
      Long rosterStudentId) {}
348
349
  /**
350
   * This method returns a list of courses that the current user is enrolled.
351
   *
352
   * @return a list of courses in the DTO form along with the student status in the organization.
353
   */
354
  @Operation(summary = "List all courses for the current student, including their org status")
355
  @PreAuthorize("hasRole('ROLE_USER')")
356
  @GetMapping("/list")
357
  public List<RosterStudentCoursesDTO> listCoursesForCurrentUser() {
358
    String email = getCurrentUser().getUser().getEmail();
359
    Iterable<RosterStudent> rosterStudentsIterable = rosterStudentRepository.findAllByEmail(email);
360
    List<RosterStudent> rosterStudents = new ArrayList<>();
361 1 1. listCoursesForCurrentUser : removed call to java/lang/Iterable::forEach → KILLED
    rosterStudentsIterable.forEach(rosterStudents::add);
362 1 1. listCoursesForCurrentUser : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::listCoursesForCurrentUser → KILLED
    return rosterStudents.stream()
363
        .map(
364
            rs -> {
365
              Course course = rs.getCourse();
366
              RosterStudentCoursesDTO rsDto =
367
                  new RosterStudentCoursesDTO(
368
                      course.getId(),
369
                      course.getInstallationId(),
370
                      course.getOrgName(),
371
                      course.getCourseName(),
372
                      course.getTerm(),
373
                      course.getSchool().getDisplayName(),
374
                      rs.getOrgStatus(),
375
                      rs.getId());
376 1 1. lambda$listCoursesForCurrentUser$5 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$listCoursesForCurrentUser$5 → KILLED
              return rsDto;
377
            })
378
        .collect(Collectors.toList());
379
  }
380
381
  public record StaffCoursesDTO(
382
      Long id,
383
      String installationId,
384
      String orgName,
385
      String courseName,
386
      String term,
387
      School school,
388
      OrgStatus studentStatus,
389
      Long staffId) {}
390
391
  public enum EmailTypes {
392
    STUDENTS,
393
    STAFF,
394
    ALL
395
  }
396
397
  public enum EmailFormats {
398
    COMMA_SEPARATED,
399
    ONE_PER_LINE
400
  }
401
402
  /**
403
   * student see what courses they appear as staff in
404
   *
405
   * @param studentId the id of the student making request
406
   * @return a list of all courses student is staff in
407
   */
408
  @Operation(summary = "Student see what courses they appear as staff in")
409
  @PreAuthorize("hasRole('ROLE_USER')")
410
  @GetMapping("/staffCourses")
411
  public List<StaffCoursesDTO> staffCourses() {
412
    CurrentUser currentUser = getCurrentUser();
413
    User user = currentUser.getUser();
414
415
    String email = user.getEmail();
416
417
    List<CourseStaff> staffMembers = courseStaffRepository.findAllByEmail(email);
418 1 1. staffCourses : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::staffCourses → KILLED
    return staffMembers.stream()
419
        .map(
420
            s -> {
421
              Course course = s.getCourse();
422
              StaffCoursesDTO sDto =
423
                  new StaffCoursesDTO(
424
                      course.getId(),
425
                      course.getInstallationId(),
426
                      course.getOrgName(),
427
                      course.getCourseName(),
428
                      course.getTerm(),
429
                      course.getSchool(),
430
                      s.getOrgStatus(),
431
                      s.getId());
432 1 1. lambda$staffCourses$6 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$staffCourses$6 → KILLED
              return sDto;
433
            })
434
        .collect(Collectors.toList());
435
  }
436
437
  @Operation(summary = "Update instructor email for a course (admin only)")
438
  @PreAuthorize("hasRole('ROLE_ADMIN')")
439
  @PutMapping("/updateInstructor")
440
  public InstructorCourseView updateInstructorEmail(
441
      @Parameter(name = "courseId") @RequestParam Long courseId,
442
      @Parameter(name = "instructorEmail") @RequestParam String instructorEmail) {
443
444
    instructorEmail = instructorEmail.strip();
445
446
    Course course =
447
        courseRepository
448
            .findById(courseId)
449 1 1. lambda$updateInstructorEmail$7 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$updateInstructorEmail$7 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
450
451
    // Validate that the email exists in either instructor or admin table
452
    boolean isInstructor = instructorRepository.existsByEmail(instructorEmail);
453
    boolean isAdmin = adminRepository.existsByEmail(instructorEmail);
454
455 2 1. updateInstructorEmail : negated conditional → KILLED
2. updateInstructorEmail : negated conditional → KILLED
    if (!isInstructor && !isAdmin) {
456
      throw new IllegalArgumentException("Email must belong to either an instructor or admin");
457
    }
458
459 1 1. updateInstructorEmail : removed call to edu/ucsb/cs156/frontiers/entities/Course::setInstructorEmail → KILLED
    course.setInstructorEmail(instructorEmail);
460
    Course savedCourse = courseRepository.save(course);
461
462 1 1. updateInstructorEmail : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::updateInstructorEmail → KILLED
    return new InstructorCourseView(savedCourse);
463
  }
464
465
  @Operation(summary = "Get course emails")
466
  @AllowApiKeyAccess
467
  @GetMapping("/emails")
468
  public String getCourseEmails(
469
      @Parameter(name = "courseId") @RequestParam Long courseId,
470
      @Parameter(name = "type") @RequestParam(defaultValue = "STUDENTS") EmailTypes type,
471
      @Parameter(name = "team") @RequestParam(required = false) String team,
472
      @Parameter(name = "format") @RequestParam(defaultValue = "ONE_PER_LINE")
473
          EmailFormats format) {
474
475
    List<String> staffEmails =
476
        StreamSupport.stream(courseStaffRepository.findByCourseId(courseId).spliterator(), false)
477
            .map(CourseStaff::getEmail)
478
            .filter(Objects::nonNull)
479
            .sorted()
480
            .collect(Collectors.toList());
481
482
    List<String> studentEmails =
483
        StreamSupport.stream(rosterStudentRepository.findByCourseId(courseId).spliterator(), false)
484 4 1. lambda$getCourseEmails$8 : negated conditional → KILLED
2. lambda$getCourseEmails$8 : replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCourseEmails$8 → KILLED
3. lambda$getCourseEmails$8 : negated conditional → KILLED
4. lambda$getCourseEmails$8 : negated conditional → KILLED
            .filter(student -> team == null || team.isBlank() || student.getTeams().contains(team))
485
            .map(RosterStudent::getEmail)
486
            .filter(Objects::nonNull)
487
            .sorted()
488
            .collect(Collectors.toList());
489
490
    List<String> emails = studentEmails;
491 1 1. getCourseEmails : negated conditional → KILLED
    if (type == EmailTypes.STAFF) {
492
      emails = staffEmails;
493 1 1. getCourseEmails : negated conditional → KILLED
    } else if (type == EmailTypes.ALL) {
494
      emails = new ArrayList<>(staffEmails);
495
      emails.addAll(studentEmails);
496
    }
497
498 1 1. getCourseEmails : negated conditional → KILLED
    String separator = format == EmailFormats.COMMA_SEPARATED ? "," : "\r\n";
499 1 1. getCourseEmails : replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCourseEmails → KILLED
    return String.join(separator, emails);
500
  }
501
502
  @Operation(summary = "Create an API key for a course")
503
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
504
  @PostMapping("/apiKeys")
505
  public GeneratedApiCourseKeyView createApiKey(
506
      @Parameter(name = "courseId") @RequestParam Long courseId,
507
      @Parameter(name = "expirationChoice") @RequestParam(defaultValue = "DAYS_90")
508
          ApiCourseKeyService.ExpirationChoice expirationChoice) {
509
    User creator = getCurrentUser().getUser();
510
    ApiCourseKeyService.GeneratedApiCourseKey generated =
511
        apiCourseKeyService.createKey(courseId, creator, expirationChoice);
512 1 1. createApiKey : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::createApiKey → KILLED
    return new GeneratedApiCourseKeyView(
513
        generated.rawKey(), new ApiCourseKeyView(generated.apiCourseKey()));
514
  }
515
516
  @Operation(summary = "List active API keys for a course")
517
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
518
  @GetMapping("/apiKeys")
519
  public List<ApiCourseKeyView> listApiKeys(
520
      @Parameter(name = "courseId") @RequestParam Long courseId) {
521 1 1. listApiKeys : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::listApiKeys → KILLED
    return apiCourseKeyService.listActiveKeys(courseId).stream()
522
        .map(ApiCourseKeyView::new)
523
        .toList();
524
  }
525
526
  @Operation(summary = "Revoke an API key for a course")
527
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
528
  @DeleteMapping("/apiKeys")
529
  public Object revokeApiKey(
530
      @Parameter(name = "courseId") @RequestParam Long courseId,
531
      @Parameter(name = "apiKeyId") @RequestParam Long apiKeyId) {
532
    ApiCourseKey revokedKey = apiCourseKeyService.revokeKey(courseId, apiKeyId);
533 1 1. revokeApiKey : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::revokeApiKey → KILLED
    return Map.of("message", "API key revoked", "apiKeyId", revokedKey.getId());
534
  }
535
536
  @Operation(summary = "Delete a course")
537
  @PreAuthorize("hasRole('ROLE_ADMIN')")
538
  @DeleteMapping("")
539
  @Transactional
540
  public Object deleteCourse(@RequestParam Long courseId)
541
      throws NoSuchAlgorithmException, InvalidKeySpecException {
542
    Course course =
543
        courseRepository
544
            .findById(courseId)
545 1 1. lambda$deleteCourse$9 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$deleteCourse$9 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
546
547
    // Check if course has roster students or staff
548 2 1. deleteCourse : negated conditional → KILLED
2. deleteCourse : negated conditional → KILLED
    if (!course.getRosterStudents().isEmpty() || !course.getCourseStaff().isEmpty()) {
549
      throw new IllegalArgumentException("Cannot delete course with students or staff");
550
    }
551
552 1 1. deleteCourse : removed call to edu/ucsb/cs156/frontiers/services/OrganizationLinkerService::unenrollOrganization → KILLED
    linkerService.unenrollOrganization(course);
553 1 1. deleteCourse : removed call to edu/ucsb/cs156/frontiers/repositories/JobsRepository::deleteByCourse_Id → KILLED
    jobsRepository.deleteByCourse_Id(courseId);
554 1 1. deleteCourse : removed call to edu/ucsb/cs156/frontiers/repositories/CourseRepository::delete → KILLED
    courseRepository.delete(course);
555 1 1. deleteCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::deleteCourse → KILLED
    return genericMessage("Course with id %s deleted".formatted(course.getId()));
556
  }
557
558
  /**
559
   * This method updates an existing course.
560
   *
561
   * @param courseId the id of the course to update
562
   * @param courseName the new name of the course
563
   * @param term the new term of the course
564
   * @param school the new school of the course
565
   * @return the updated course
566
   */
567
  @Operation(summary = "Update an existing course")
568
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
569
  @PutMapping("")
570
  public InstructorCourseView updateCourse(
571
      @Parameter(name = "courseId") @RequestParam Long courseId,
572
      @Parameter(name = "courseName") @RequestParam String courseName,
573
      @Parameter(name = "term") @RequestParam String term,
574
      @Parameter(name = "school") @RequestParam School school) {
575
    Course course =
576
        courseRepository
577
            .findById(courseId)
578 1 1. lambda$updateCourse$10 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$updateCourse$10 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
579
580 1 1. updateCourse : removed call to edu/ucsb/cs156/frontiers/entities/Course::setCourseName → KILLED
    course.setCourseName(courseName);
581 1 1. updateCourse : removed call to edu/ucsb/cs156/frontiers/entities/Course::setTerm → KILLED
    course.setTerm(term);
582 1 1. updateCourse : removed call to edu/ucsb/cs156/frontiers/entities/Course::setSchool → KILLED
    course.setSchool(school);
583
584
    Course savedCourse = courseRepository.save(course);
585
586 1 1. updateCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::updateCourse → KILLED
    return new InstructorCourseView(savedCourse);
587
  }
588
589
  /**
590
   * This method updates an existing course.
591
   *
592
   * @param courseId the id of the course to update
593
   * @param courseName the new name of the course
594
   * @param term the new term of the course
595
   * @param school the new school of the course
596
   * @param canvasApiToken the new Canvas API token for the course
597
   * @param canvasCourseId the new Canvas course ID
598
   * @return the updated course
599
   */
600
  @Operation(summary = "Update an existing course with Canvas token and course ID")
601
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
602
  @PutMapping("/updateCourseCanvasToken")
603
  public InstructorCourseView updateCourseWithCanvasToken(
604
      @Parameter(name = "courseId") @RequestParam Long courseId,
605
      @Parameter(name = "canvasApiToken") @RequestParam(required = false) String canvasApiToken,
606
      @Parameter(name = "canvasCourseId") @RequestParam(required = false) String canvasCourseId) {
607
    Course course =
608
        courseRepository
609
            .findById(courseId)
610 1 1. lambda$updateCourseWithCanvasToken$11 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$updateCourseWithCanvasToken$11 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
611
612 1 1. updateCourseWithCanvasToken : negated conditional → KILLED
    if (canvasApiToken != null
613 1 1. updateCourseWithCanvasToken : negated conditional → KILLED
        && !canvasApiToken.isEmpty()
614 1 1. updateCourseWithCanvasToken : negated conditional → KILLED
        && !canvasApiToken.equals(course.getCanvasApiToken())) {
615 1 1. updateCourseWithCanvasToken : removed call to edu/ucsb/cs156/frontiers/entities/Course::setCanvasApiToken → KILLED
      course.setCanvasApiToken(canvasApiToken);
616
    }
617
618 1 1. updateCourseWithCanvasToken : negated conditional → KILLED
    if (canvasCourseId != null
619 1 1. updateCourseWithCanvasToken : negated conditional → KILLED
        && !canvasCourseId.isEmpty()
620 1 1. updateCourseWithCanvasToken : negated conditional → KILLED
        && !canvasCourseId.equals(course.getCanvasCourseId())) {
621 1 1. updateCourseWithCanvasToken : removed call to edu/ucsb/cs156/frontiers/entities/Course::setCanvasCourseId → KILLED
      course.setCanvasCourseId(canvasCourseId);
622
    }
623
624
    Course savedCourse = courseRepository.save(course);
625
626 1 1. updateCourseWithCanvasToken : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::updateCourseWithCanvasToken → KILLED
    return new InstructorCourseView(savedCourse);
627
  }
628
629
  @Operation(summary = "Get course warnings")
630
  @GetMapping("/warnings/{courseId}")
631
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
632
  public CourseWarning warnings(@PathVariable @Parameter Long courseId) throws Exception {
633
    Course course =
634
        courseRepository
635
            .findById(courseId)
636 1 1. lambda$warnings$12 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$warnings$12 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
637 1 1. warnings : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::warnings → KILLED
    return linkerService.checkCourseWarnings(course);
638
  }
639
640
  @Operation(summary = "Hide base permission warning for a course")
641
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
642
  @PostMapping("/warnings/hideBasePermissionWarning/{courseId}")
643
  public Object hideBasePermissionWarning(@PathVariable @Parameter Long courseId) {
644
    Course course =
645
        courseRepository
646
            .findById(courseId)
647 1 1. lambda$hideBasePermissionWarning$13 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$hideBasePermissionWarning$13 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
648
649 1 1. hideBasePermissionWarning : removed call to edu/ucsb/cs156/frontiers/entities/Course::setHideBasePermissionWarning → KILLED
    course.setHideBasePermissionWarning(true);
650
    courseRepository.save(course);
651
652 1 1. hideBasePermissionWarning : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::hideBasePermissionWarning → KILLED
    return genericMessage(
653
        "hideBasePermissionWarning set to true for course with id %s".formatted(courseId));
654
  }
655
}

Mutations

101

1.1
Location : postCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testPostCourse_byInstructor()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::postCourse → KILLED

128

1.1
Location : <init>
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testInstructorCourseView_withNullRosterStudents()]
negated conditional → KILLED

129

1.1
Location : <init>
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testInstructorCourseView_withNullCourseStaff()]
negated conditional → KILLED

172

1.1
Location : allForInstructors
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testAllCourses_ROLE_INSTRUCTOR()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::allForInstructors → KILLED

188

1.1
Location : allForAdmins
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testAllCourses_ROLE_ADMIN()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::allForAdmins → KILLED

203

1.1
Location : lambda$getCourseById$0
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testGetCourseById_courseDoesNotExist()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCourseById$0 → KILLED

206

1.1
Location : getCourseById
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testGetCourseById()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCourseById → KILLED

226

1.1
Location : lambda$getCourseCanvasInfo$1
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testGetCanvasInfo_courseDoesNotExist()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCourseCanvasInfo$1 → KILLED

230

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
negated conditional → KILLED

232

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_obscuresCorrectlyForFourCharacters()]
changed conditional boundary → KILLED

2.2
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
negated conditional → KILLED

235

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
Replaced integer subtraction with addition → KILLED

236

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
Replaced integer subtraction with addition → KILLED

239

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
replaced return value with Collections.emptyMap for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCourseCanvasInfo → KILLED

241

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
negated conditional → KILLED

242

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCanvasInfo_returnsCorrectOutput()]
negated conditional → KILLED

264

1.1
Location : linkCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testRedirect()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::linkCourse → KILLED

289

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testNoPerms()]
negated conditional → KILLED

290

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testNoPerms()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED

297

1.1
Location : lambda$addInstallation$2
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testCourseLinkNotFound()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$addInstallation$2 → KILLED

298

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testNotCreator()]
negated conditional → KILLED

299

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testNotCreator()]
negated conditional → KILLED

300

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testNotCreator()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED

303

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testCourseLinkSuccessWhenAdminNotCreator()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setInstallationId → KILLED

304

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testCourseLinkSuccessWhenAdminNotCreator()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setOrgName → KILLED

307

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testLinkCourseSuccessfully()]
removed call to java/util/List::forEach → KILLED

309

1.1
Location : lambda$addInstallation$3
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testLinkCourseSuccessfully()]
removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED

313

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testLinkCourseSuccessfully()]
removed call to java/util/List::forEach → KILLED

315

1.1
Location : lambda$addInstallation$4
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testLinkCourseSuccessfully()]
removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED

318

1.1
Location : addInstallation
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testCourseLinkSuccessWhenAdminNotCreator()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED

334

1.1
Location : handleInvalidInstallationType
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testNotOrganization()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::handleInvalidInstallationType → KILLED

361

1.1
Location : listCoursesForCurrentUser
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testListCoursesForCurrentUser()]
removed call to java/lang/Iterable::forEach → KILLED

362

1.1
Location : listCoursesForCurrentUser
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testListCoursesForCurrentUser()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::listCoursesForCurrentUser → KILLED

376

1.1
Location : lambda$listCoursesForCurrentUser$5
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testListCoursesForCurrentUser()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$listCoursesForCurrentUser$5 → KILLED

418

1.1
Location : staffCourses
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testStudenIsStaffInCourse()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::staffCourses → KILLED

432

1.1
Location : lambda$staffCourses$6
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testStudenIsStaffInCourse()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$staffCourses$6 → KILLED

449

1.1
Location : lambda$updateInstructorEmail$7
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testUpdateInstructorEmail_courseDoesNotExist()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$updateInstructorEmail$7 → KILLED

455

1.1
Location : updateInstructorEmail
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testUpdateInstructorEmail_emailNotFound()]
negated conditional → KILLED

2.2
Location : updateInstructorEmail
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testUpdateInstructorEmail_emailNotFound()]
negated conditional → KILLED

459

1.1
Location : updateInstructorEmail
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testUpdateInstructorEmail_byAdmin_email_is_admin()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setInstructorEmail → KILLED

462

1.1
Location : updateInstructorEmail
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:testUpdateInstructorEmail_byAdmin_email_is_admin()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::updateInstructorEmail → KILLED

484

1.1
Location : lambda$getCourseEmails$8
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_blank_team_is_unfiltered()]
negated conditional → KILLED

2.2
Location : lambda$getCourseEmails$8
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_filtered_by_team()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCourseEmails$8 → KILLED

3.3
Location : lambda$getCourseEmails$8
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_filtered_by_team()]
negated conditional → KILLED

4.4
Location : lambda$getCourseEmails$8
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_defaults_to_students_one_per_line()]
negated conditional → KILLED

491

1.1
Location : getCourseEmails
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_blank_team_is_unfiltered()]
negated conditional → KILLED

493

1.1
Location : getCourseEmails
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_explicit_type_comma_separated()]
negated conditional → KILLED

498

1.1
Location : getCourseEmails
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_blank_team_is_unfiltered()]
negated conditional → KILLED

499

1.1
Location : getCourseEmails
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:getCourseEmails_students_blank_team_is_unfiltered()]
replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCourseEmails → KILLED

512

1.1
Location : createApiKey
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:createApiKey_defaults_to_90_days()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::createApiKey → KILLED

521

1.1
Location : listApiKeys
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:listApiKeys_returns_active_keys()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::listApiKeys → KILLED

533

1.1
Location : revokeApiKey
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:revokeApiKey_returns_confirmation()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::revokeApiKey → KILLED

545

1.1
Location : lambda$deleteCourse$9
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_not_found_returns_not_found()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$deleteCourse$9 → KILLED

548

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_success_returns_ok()]
negated conditional → KILLED

2.2
Location : deleteCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_success_returns_ok()]
negated conditional → KILLED

552

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_success_returns_ok()]
removed call to edu/ucsb/cs156/frontiers/services/OrganizationLinkerService::unenrollOrganization → KILLED

553

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_success_returns_ok()]
removed call to edu/ucsb/cs156/frontiers/repositories/JobsRepository::deleteByCourse_Id → KILLED

554

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_success_returns_ok()]
removed call to edu/ucsb/cs156/frontiers/repositories/CourseRepository::delete → KILLED

555

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:delete_success_returns_ok()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::deleteCourse → KILLED

578

1.1
Location : lambda$updateCourse$10
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:update_course_not_found_returns_not_found()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$updateCourse$10 → KILLED

580

1.1
Location : updateCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:update_course_success_returns_ok()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setCourseName → KILLED

581

1.1
Location : updateCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:update_course_success_returns_ok()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setTerm → KILLED

582

1.1
Location : updateCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:update_course_success_returns_ok()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setSchool → KILLED

586

1.1
Location : updateCourse
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:updateCourse_success_admin()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::updateCourse → KILLED

610

1.1
Location : lambda$updateCourseWithCanvasToken$11
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:updateCourseCanvasToken_not_found_returns_not_found()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$updateCourseWithCanvasToken$11 → KILLED

612

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:updateCourseCanvasToken_null_params_no_change()]
negated conditional → KILLED

613

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
negated conditional → KILLED

614

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
negated conditional → KILLED

615

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setCanvasApiToken → KILLED

618

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:updateCourseCanvasToken_null_params_no_change()]
negated conditional → KILLED

619

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
negated conditional → KILLED

620

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
negated conditional → KILLED

621

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setCanvasCourseId → KILLED

626

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:updateCourseCanvasToken_success_admin()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::updateCourseWithCanvasToken → KILLED

636

1.1
Location : lambda$warnings$12
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:test_warnings_not_found()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$warnings$12 → KILLED

637

1.1
Location : warnings
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:calls_org_service_for_warnings()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::warnings → KILLED

647

1.1
Location : lambda$hideBasePermissionWarning$13
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:hideBasePermissionWarning_notFound()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$hideBasePermissionWarning$13 → KILLED

649

1.1
Location : hideBasePermissionWarning
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:hideBasePermissionWarning_setsFieldTrue()]
removed call to edu/ucsb/cs156/frontiers/entities/Course::setHideBasePermissionWarning → KILLED

652

1.1
Location : hideBasePermissionWarning
Killed by : edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.CoursesControllerTests]/[method:hideBasePermissionWarning_setsFieldTrue()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::hideBasePermissionWarning → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0