CoursesController.java

1
package edu.ucsb.cs.scaffold.controller;
2
3
import com.fasterxml.jackson.databind.JsonNode;
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import edu.ucsb.cs.scaffold.entity.Course;
6
import edu.ucsb.cs.scaffold.entity.CourseStaff;
7
import edu.ucsb.cs.scaffold.entity.PatCredential;
8
import edu.ucsb.cs.scaffold.entity.PlInstance;
9
import edu.ucsb.cs.scaffold.entity.PlRepo;
10
import edu.ucsb.cs.scaffold.entity.RosterStudent;
11
import edu.ucsb.cs.scaffold.entity.User;
12
import edu.ucsb.cs.scaffold.enums.PatPlatform;
13
import edu.ucsb.cs.scaffold.enums.School;
14
import edu.ucsb.cs.scaffold.errors.EntityNotFoundException;
15
import edu.ucsb.cs.scaffold.errors.ForbiddenException;
16
import edu.ucsb.cs.scaffold.jobs.SyncCourseWithPlRepoJobFactory;
17
import edu.ucsb.cs.scaffold.model.CurrentUser;
18
import edu.ucsb.cs.scaffold.repository.AdminRepository;
19
import edu.ucsb.cs.scaffold.repository.CourseRepository;
20
import edu.ucsb.cs.scaffold.repository.CourseStaffRepository;
21
import edu.ucsb.cs.scaffold.repository.InstructorRepository;
22
import edu.ucsb.cs.scaffold.repository.PatCredentialRepository;
23
import edu.ucsb.cs.scaffold.repository.PlInstanceRepository;
24
import edu.ucsb.cs.scaffold.repository.PlRepoRepository;
25
import edu.ucsb.cs.scaffold.repository.RosterStudentRepository;
26
import edu.ucsb.cs.scaffold.repository.UserRepository;
27
import edu.ucsb.cs.scaffold.services.GithubService;
28
import edu.ucsb.cs.scaffold.services.PatEncryptionService;
29
import edu.ucsb.cs.scaffold.services.PrairieLearnService;
30
import edu.ucsb.cs156.jobs.repositories.JobsRepository;
31
import edu.ucsb.cs156.jobs.services.JobService;
32
import io.swagger.v3.oas.annotations.Operation;
33
import io.swagger.v3.oas.annotations.Parameter;
34
import io.swagger.v3.oas.annotations.tags.Tag;
35
import java.security.NoSuchAlgorithmException;
36
import java.security.spec.InvalidKeySpecException;
37
import java.util.ArrayList;
38
import java.util.HashSet;
39
import java.util.List;
40
import java.util.Map;
41
import java.util.Objects;
42
import java.util.Set;
43
import java.util.stream.Collectors;
44
import java.util.stream.StreamSupport;
45
import lombok.extern.slf4j.Slf4j;
46
import org.springframework.beans.factory.annotation.Autowired;
47
import org.springframework.security.access.prepost.PreAuthorize;
48
import org.springframework.transaction.annotation.Transactional;
49
import org.springframework.web.bind.annotation.DeleteMapping;
50
import org.springframework.web.bind.annotation.GetMapping;
51
import org.springframework.web.bind.annotation.PathVariable;
52
import org.springframework.web.bind.annotation.PostMapping;
53
import org.springframework.web.bind.annotation.PutMapping;
54
import org.springframework.web.bind.annotation.RequestMapping;
55
import org.springframework.web.bind.annotation.RequestParam;
56
import org.springframework.web.bind.annotation.RestController;
57
import org.springframework.web.client.HttpClientErrorException;
58
59
@Tag(name = "Course")
60
@RequestMapping("/api/courses")
61
@RestController
62
@Slf4j
63
public class CoursesController extends ApiController {
64
65
  @Autowired private CourseRepository courseRepository;
66
67
  @Autowired private UserRepository userRepository;
68
69
  @Autowired private RosterStudentRepository rosterStudentRepository;
70
71
  @Autowired private CourseStaffRepository courseStaffRepository;
72
73
  @Autowired private InstructorRepository instructorRepository;
74
75
  @Autowired private AdminRepository adminRepository;
76
77
  @Autowired private JobsRepository jobsRepository;
78
79
  @Autowired private PatCredentialRepository patCredentialRepository;
80
81
  @Autowired private PatEncryptionService patEncryptionService;
82
83
  @Autowired private PlRepoRepository plRepoRepository;
84
85
  @Autowired private GithubService githubService;
86
87
  @Autowired private PlInstanceRepository plInstanceRepository;
88
89
  @Autowired private PrairieLearnService prairieLearnService;
90
91
  @Autowired private SyncCourseWithPlRepoJobFactory syncCourseWithPlRepoJobFactory;
92
93
  @Autowired private JobService jobService;
94
95
  /**
96
   * This method creates a new Course.
97
   *
98
   * @param courseName the name of the course
99
   * @param term the term of the course
100
   * @param school the school of the course
101
   * @param canvasApiToken the Canvas API token (optional)
102
   * @param canvasCourseId the Canvas course ID (optional)
103
   */
104
  @Operation(summary = "Create a new course")
105
  @PreAuthorize("hasRole('ROLE_ADMIN') || hasRole('ROLE_INSTRUCTOR')")
106
  @PostMapping("/post")
107
  public InstructorCourseView postCourse(
108
      @Parameter(name = "courseName") @RequestParam String courseName,
109
      @Parameter(name = "term") @RequestParam String term,
110
      @Parameter(name = "school") @RequestParam School school,
111
      @Parameter(name = "canvasApiToken") @RequestParam(required = false) String canvasApiToken,
112
      @Parameter(name = "canvasCourseId") @RequestParam(required = false) String canvasCourseId) {
113
    // get current date right now and set status to pending
114
    edu.ucsb.cs.scaffold.model.CurrentUser currentUser = getCurrentUser();
115
    Course course =
116
        Course.builder()
117
            .courseName(courseName)
118
            .term(term)
119
            .school(school)
120
            .instructorEmail(currentUser.getUser().getEmail().strip())
121
            .canvasApiToken(canvasApiToken)
122
            .canvasCourseId(canvasCourseId)
123
            .build();
124
    Course savedCourse = courseRepository.save(course);
125
126 1 1. postCourse : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::postCourse → KILLED
    return new InstructorCourseView(savedCourse);
127
  }
128
129
  /** Projection of Course entity with fields that are relevant for instructors and admins */
130
  public static record InstructorCourseView(
131
      Long id,
132
      String courseName,
133
      String term,
134
      School school,
135
      String instructorEmail,
136
      int numStudents,
137
      int numStaff,
138
      Long plRepoId,
139
      Long plInstanceId,
140
      // Human-readable details of the PL associations; resolved only by the
141
      // single-course endpoints (getCourseById and the two update endpoints),
142
      // null in course lists.
143
      String plRepoName,
144
      String plInstanceShortName,
145
      Long plInstanceNumericId) {
146
147
    // Creates view from Course entity
148
    public InstructorCourseView(Course c) {
149
      this(
150
          c.getId(),
151
          c.getCourseName(),
152
          c.getTerm(),
153
          c.getSchool(),
154
          c.getInstructorEmail(),
155 1 1. <init> : negated conditional → KILLED
          c.getRosterStudents() != null ? c.getRosterStudents().size() : 0,
156 1 1. <init> : negated conditional → KILLED
          c.getCourseStaff() != null ? c.getCourseStaff().size() : 0,
157
          c.getPlRepoId(),
158
          c.getPlInstanceId(),
159
          null,
160
          null,
161
          null);
162
    }
163
  }
164
165
  /**
166
   * Builds an InstructorCourseView with the PL association details (repo name, instance short name,
167
   * instance numeric id) resolved from their tables — used by the single-course endpoints so the
168
   * frontend can show what is currently associated. List endpoints use the plain constructor and
169
   * leave these null.
170
   */
171
  private InstructorCourseView viewWithPlDetails(Course c) {
172
    String plRepoName =
173 1 1. viewWithPlDetails : negated conditional → KILLED
        c.getPlRepoId() == null
174
            ? null
175
            : plRepoRepository.findById(c.getPlRepoId()).map(PlRepo::getRepoName).orElse(null);
176
    PlInstance plInstance =
177 1 1. viewWithPlDetails : negated conditional → KILLED
        c.getPlInstanceId() == null
178
            ? null
179
            : plInstanceRepository.findById(c.getPlInstanceId()).orElse(null);
180 1 1. viewWithPlDetails : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::viewWithPlDetails → KILLED
    return new InstructorCourseView(
181
        c.getId(),
182
        c.getCourseName(),
183
        c.getTerm(),
184
        c.getSchool(),
185
        c.getInstructorEmail(),
186 1 1. viewWithPlDetails : negated conditional → KILLED
        c.getRosterStudents() != null ? c.getRosterStudents().size() : 0,
187 1 1. viewWithPlDetails : negated conditional → KILLED
        c.getCourseStaff() != null ? c.getCourseStaff().size() : 0,
188
        c.getPlRepoId(),
189
        c.getPlInstanceId(),
190
        plRepoName,
191 1 1. viewWithPlDetails : negated conditional → KILLED
        plInstance == null ? null : plInstance.getShortName(),
192 1 1. viewWithPlDetails : negated conditional → KILLED
        plInstance == null ? null : plInstance.getNumericId());
193
  }
194
195
  /**
196
   * This method returns a list of courses.
197
   *
198
   * @return a list of all courses for an instructor.
199
   */
200
  @Operation(summary = "List all courses for an instructor")
201
  @PreAuthorize("hasRole('ROLE_INSTRUCTOR')")
202
  @GetMapping("/list/instructors")
203
  public Iterable<InstructorCourseView> allForInstructors() {
204
    CurrentUser currentUser = getCurrentUser();
205
    String instructorEmail = currentUser.getUser().getEmail();
206
    List<Course> courses = courseRepository.findByInstructorEmail(instructorEmail);
207
208
    List<InstructorCourseView> courseViews =
209
        courses.stream().map(InstructorCourseView::new).collect(Collectors.toList());
210 1 1. allForInstructors : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::allForInstructors → KILLED
    return courseViews;
211
  }
212
213
  /**
214
   * This method returns a list of courses.
215
   *
216
   * @return a list of all courses for an admin.
217
   */
218
  @Operation(summary = "List all courses for an admin")
219
  @PreAuthorize("hasRole('ROLE_ADMIN')")
220
  @GetMapping("/list/admins")
221
  public Iterable<InstructorCourseView> allForAdmins() {
222
    List<Course> courses = courseRepository.findAll();
223
224
    List<InstructorCourseView> courseViews =
225
        courses.stream().map(InstructorCourseView::new).collect(Collectors.toList());
226 1 1. allForAdmins : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::allForAdmins → KILLED
    return courseViews;
227
  }
228
229
  /**
230
   * This method returns single course by its id
231
   *
232
   * @return a course
233
   */
234
  @Operation(summary = "Get course by id")
235
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #id)")
236
  @GetMapping("/{id}")
237
  public InstructorCourseView getCourseById(@Parameter(name = "id") @PathVariable Long id) {
238
    Course course =
239
        courseRepository
240
            .findById(id)
241 1 1. lambda$getCourseById$0 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseById$0 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, id));
242 1 1. getCourseById : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseById → KILLED
    return viewWithPlDetails(course);
243
  }
244
245
  /**
246
   * This method returns the Canvas course ID and partially obscured Canvas token for a course by
247
   * its id. If the token is less than or equal to 3 characters long, it is returned in full.
248
   * Otherwise, all but the last three characters are replaced with asterisks. This is okay because
249
   * such short tokens are not generated by Canvas.
250
   *
251
   * @param courseId the id of the course
252
   * @return a map with courseId, canvasCourseId, and obscured canvasApiToken
253
   */
254
  @Operation(summary = "Get course Canvas course ID and Canvas token (partially obscured)")
255
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
256
  @GetMapping("getCanvasInfo")
257
  public Map<String, String> getCourseCanvasInfo(
258
      @Parameter(name = "courseId") @RequestParam Long courseId) {
259
    Course course =
260
        courseRepository
261
            .findById(courseId)
262 1 1. lambda$getCourseCanvasInfo$1 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseCanvasInfo$1 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
263
264
    String obscuredToken = null;
265
266 1 1. getCourseCanvasInfo : negated conditional → KILLED
    if (course.getCanvasApiToken() != null) {
267
      String token = course.getCanvasApiToken();
268 2 1. getCourseCanvasInfo : negated conditional → KILLED
2. getCourseCanvasInfo : changed conditional boundary → KILLED
      if (token.length() < 4) {
269
        obscuredToken = token;
270
      } else {
271 1 1. getCourseCanvasInfo : Replaced integer subtraction with addition → KILLED
        String lastThree = token.substring(token.length() - 3);
272 1 1. getCourseCanvasInfo : Replaced integer subtraction with addition → KILLED
        obscuredToken = "*".repeat(token.length() - 3) + lastThree;
273
      }
274
    }
275 1 1. getCourseCanvasInfo : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseCanvasInfo → KILLED
    return Map.of(
276
        "courseId", course.getId().toString(),
277 1 1. getCourseCanvasInfo : negated conditional → KILLED
        "canvasCourseId", course.getCanvasCourseId() != null ? course.getCanvasCourseId() : "",
278 1 1. getCourseCanvasInfo : negated conditional → KILLED
        "canvasApiToken", obscuredToken != null ? obscuredToken : "");
279
  }
280
281
  public record RosterStudentCoursesDTO(
282
      Long id, String courseName, String term, String school, Long rosterStudentId) {}
283
284
  /**
285
   * This method returns a list of courses that the current user is enrolled.
286
   *
287
   * @return a list of courses in the DTO form along with the student status in the organization.
288
   */
289
  @Operation(summary = "List all courses for the current student, including their org status")
290
  @PreAuthorize("hasRole('ROLE_USER')")
291
  @GetMapping("/list/students")
292
  public List<RosterStudentCoursesDTO> listCoursesForCurrentUser() {
293
    String email = getCurrentUser().getUser().getEmail();
294
    Iterable<RosterStudent> rosterStudentsIterable = rosterStudentRepository.findAllByEmail(email);
295
    List<RosterStudent> rosterStudents = new ArrayList<>();
296 1 1. listCoursesForCurrentUser : removed call to java/lang/Iterable::forEach → KILLED
    rosterStudentsIterable.forEach(rosterStudents::add);
297 1 1. listCoursesForCurrentUser : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::listCoursesForCurrentUser → KILLED
    return rosterStudents.stream()
298
        .map(
299
            rs -> {
300
              Course course = rs.getCourse();
301
              RosterStudentCoursesDTO rsDto =
302
                  new RosterStudentCoursesDTO(
303
                      course.getId(),
304
                      course.getCourseName(),
305
                      course.getTerm(),
306
                      course.getSchool().getDisplayName(),
307
                      rs.getId());
308 1 1. lambda$listCoursesForCurrentUser$2 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUser$2 → KILLED
              return rsDto;
309
            })
310
        .collect(Collectors.toList());
311
  }
312
313
  public record StaffCoursesDTO(
314
      Long id, String courseName, String term, School school, Long staffId) {}
315
316
  public enum EmailTypes {
317
    STUDENTS,
318
    STAFF,
319
    ALL
320
  }
321
322
  public enum EmailFormats {
323
    COMMA_SEPARATED,
324
    ONE_PER_LINE
325
  }
326
327
  /**
328
   * student see what courses they appear as staff in
329
   *
330
   * @param studentId the id of the student making request
331
   * @return a list of all courses student is staff in
332
   */
333
  @Operation(summary = "Student see what courses they appear as staff in")
334
  @PreAuthorize("hasRole('ROLE_USER')")
335
  @GetMapping("/list/staff")
336
  public List<StaffCoursesDTO> staffCourses() {
337
    CurrentUser currentUser = getCurrentUser();
338
    User user = currentUser.getUser();
339
340
    String email = user.getEmail();
341
342
    List<CourseStaff> staffMembers = courseStaffRepository.findAllByEmail(email);
343 1 1. staffCourses : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::staffCourses → KILLED
    return staffMembers.stream()
344
        .map(
345
            s -> {
346
              Course course = s.getCourse();
347
              StaffCoursesDTO sDto =
348
                  new StaffCoursesDTO(
349
                      course.getId(),
350
                      course.getCourseName(),
351
                      course.getTerm(),
352
                      course.getSchool(),
353
                      s.getId());
354 1 1. lambda$staffCourses$3 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$staffCourses$3 → KILLED
              return sDto;
355
            })
356
        .collect(Collectors.toList());
357
  }
358
359
  /** DTO representing a course along with the ways in which the current user has access to it. */
360
  public record CourseListDTO(
361
      Long id,
362
      String courseName,
363
      String term,
364
      School school,
365
      String instructorEmail,
366
      boolean studentAccess,
367
      boolean staffAccess,
368
      boolean instructorAccess,
369
      boolean adminAccess) {}
370
371
  /**
372
   * This method returns a unified list of courses that the current user has access to, whether as a
373
   * student, staff member, instructor, or admin.
374
   *
375
   * @return a list of courses along with the access flags for the current user.
376
   */
377
  @Operation(summary = "List all courses the current user has access to")
378
  @PreAuthorize("hasRole('ROLE_USER')")
379
  @GetMapping("/list")
380
  public List<CourseListDTO> listCoursesForCurrentUserUnified() {
381
    String email = getCurrentUser().getUser().getEmail();
382
383
    boolean isAdmin = adminRepository.existsByEmail(email);
384
385
    Set<Long> studentCourseIds =
386
        rosterStudentRepository.findAllByEmail(email).stream()
387 1 1. lambda$listCoursesForCurrentUserUnified$4 : replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$4 → KILLED
            .map(rs -> rs.getCourse().getId())
388
            .collect(Collectors.toSet());
389
390
    Set<Long> staffCourseIds =
391
        courseStaffRepository.findAllByEmail(email).stream()
392 1 1. lambda$listCoursesForCurrentUserUnified$5 : replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$5 → KILLED
            .map(cs -> cs.getCourse().getId())
393
            .collect(Collectors.toSet());
394
395
    Set<Long> instructorCourseIds =
396
        courseRepository.findByInstructorEmail(email).stream()
397
            .map(Course::getId)
398
            .collect(Collectors.toSet());
399
400
    List<Course> courses;
401 1 1. listCoursesForCurrentUserUnified : negated conditional → KILLED
    if (isAdmin) {
402
      courses = courseRepository.findAll();
403
    } else {
404
      Set<Long> accessibleCourseIds = new HashSet<>();
405
      accessibleCourseIds.addAll(studentCourseIds);
406
      accessibleCourseIds.addAll(staffCourseIds);
407
      accessibleCourseIds.addAll(instructorCourseIds);
408
      courses = courseRepository.findAllById(accessibleCourseIds);
409
    }
410
411 1 1. listCoursesForCurrentUserUnified : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::listCoursesForCurrentUserUnified → KILLED
    return courses.stream()
412
        .map(
413
            c ->
414 1 1. lambda$listCoursesForCurrentUserUnified$6 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$6 → KILLED
                new CourseListDTO(
415
                    c.getId(),
416
                    c.getCourseName(),
417
                    c.getTerm(),
418
                    c.getSchool(),
419
                    c.getInstructorEmail(),
420
                    studentCourseIds.contains(c.getId()),
421
                    staffCourseIds.contains(c.getId()),
422
                    instructorCourseIds.contains(c.getId()),
423
                    isAdmin))
424
        .collect(Collectors.toList());
425
  }
426
427
  /**
428
   * This method returns the unified access info for a single course, if the current user has access
429
   * to it. If the user does not have access, or the course does not exist, a 404 is returned.
430
   *
431
   * @param courseId the id of the course
432
   * @return the course along with the access flags for the current user.
433
   */
434
  @Operation(summary = "Get unified course access info for a single course")
435
  @PreAuthorize("hasRole('ROLE_USER')")
436
  @GetMapping("/list/{courseId}")
437
  public CourseListDTO getCourseAccessInfo(
438
      @Parameter(name = "courseId") @PathVariable Long courseId) {
439
    String email = getCurrentUser().getUser().getEmail();
440
441
    Course course =
442
        courseRepository
443
            .findById(courseId)
444 1 1. lambda$getCourseAccessInfo$7 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$7 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
445
446
    boolean isAdmin = adminRepository.existsByEmail(email);
447
448
    boolean studentAccess =
449
        rosterStudentRepository.findAllByEmail(email).stream()
450 2 1. lambda$getCourseAccessInfo$8 : replaced boolean return with false for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$8 → KILLED
2. lambda$getCourseAccessInfo$8 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$8 → KILLED
            .anyMatch(rs -> rs.getCourse().getId().equals(courseId));
451
452
    boolean staffAccess =
453
        courseStaffRepository.findAllByEmail(email).stream()
454 2 1. lambda$getCourseAccessInfo$9 : replaced boolean return with false for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$9 → KILLED
2. lambda$getCourseAccessInfo$9 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$9 → KILLED
            .anyMatch(cs -> cs.getCourse().getId().equals(courseId));
455
456
    boolean instructorAccess = email.equals(course.getInstructorEmail());
457
458 4 1. getCourseAccessInfo : negated conditional → KILLED
2. getCourseAccessInfo : negated conditional → KILLED
3. getCourseAccessInfo : negated conditional → KILLED
4. getCourseAccessInfo : negated conditional → KILLED
    if (!isAdmin && !studentAccess && !staffAccess && !instructorAccess) {
459
      throw new EntityNotFoundException(Course.class, courseId);
460
    }
461
462 1 1. getCourseAccessInfo : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseAccessInfo → KILLED
    return new CourseListDTO(
463
        course.getId(),
464
        course.getCourseName(),
465
        course.getTerm(),
466
        course.getSchool(),
467
        course.getInstructorEmail(),
468
        studentAccess,
469
        staffAccess,
470
        instructorAccess,
471
        isAdmin);
472
  }
473
474
  @Operation(summary = "Update instructor email for a course (admin only)")
475
  @PreAuthorize("hasRole('ROLE_ADMIN')")
476
  @PutMapping("/updateInstructor")
477
  public InstructorCourseView updateInstructorEmail(
478
      @Parameter(name = "courseId") @RequestParam Long courseId,
479
      @Parameter(name = "instructorEmail") @RequestParam String instructorEmail) {
480
481
    instructorEmail = instructorEmail.strip();
482
483
    Course course =
484
        courseRepository
485
            .findById(courseId)
486 1 1. lambda$updateInstructorEmail$10 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateInstructorEmail$10 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
487
488
    // Validate that the email exists in either instructor or admin table
489
    boolean isInstructor = instructorRepository.existsByEmail(instructorEmail);
490
    boolean isAdmin = adminRepository.existsByEmail(instructorEmail);
491
492 2 1. updateInstructorEmail : negated conditional → KILLED
2. updateInstructorEmail : negated conditional → KILLED
    if (!isInstructor && !isAdmin) {
493
      throw new IllegalArgumentException("Email must belong to either an instructor or admin");
494
    }
495
496 1 1. updateInstructorEmail : removed call to edu/ucsb/cs/scaffold/entity/Course::setInstructorEmail → KILLED
    course.setInstructorEmail(instructorEmail);
497
    Course savedCourse = courseRepository.save(course);
498
499 1 1. updateInstructorEmail : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateInstructorEmail → KILLED
    return new InstructorCourseView(savedCourse);
500
  }
501
502
  @Operation(summary = "Get course emails")
503
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
504
  @GetMapping("/emails")
505
  public String getCourseEmails(
506
      @Parameter(name = "courseId") @RequestParam Long courseId,
507
      @Parameter(name = "type") @RequestParam(defaultValue = "STUDENTS") EmailTypes type,
508
      @Parameter(name = "format") @RequestParam(defaultValue = "ONE_PER_LINE")
509
          EmailFormats format) {
510
511
    List<String> staffEmails =
512
        StreamSupport.stream(courseStaffRepository.findByCourseId(courseId).spliterator(), false)
513
            .map(CourseStaff::getEmail)
514
            .filter(Objects::nonNull)
515
            .sorted()
516
            .collect(Collectors.toList());
517
518
    List<String> studentEmails =
519
        StreamSupport.stream(rosterStudentRepository.findByCourseId(courseId).spliterator(), false)
520
            .map(RosterStudent::getEmail)
521
            .filter(Objects::nonNull)
522
            .sorted()
523
            .collect(Collectors.toList());
524
525
    List<String> emails = studentEmails;
526 1 1. getCourseEmails : negated conditional → KILLED
    if (type == EmailTypes.STAFF) {
527
      emails = staffEmails;
528 1 1. getCourseEmails : negated conditional → KILLED
    } else if (type == EmailTypes.ALL) {
529
      emails = new ArrayList<>(staffEmails);
530
      emails.addAll(studentEmails);
531
    }
532
533 1 1. getCourseEmails : negated conditional → KILLED
    String separator = format == EmailFormats.COMMA_SEPARATED ? "," : "\r\n";
534 1 1. getCourseEmails : replaced return value with "" for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseEmails → KILLED
    return String.join(separator, emails);
535
  }
536
537
  @Operation(summary = "Delete a course")
538
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
539
  @DeleteMapping("")
540
  @Transactional
541
  public Object deleteCourse(@RequestParam Long courseId)
542
      throws NoSuchAlgorithmException, InvalidKeySpecException {
543
    Course course =
544
        courseRepository
545
            .findById(courseId)
546 1 1. lambda$deleteCourse$11 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$deleteCourse$11 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
547
548
    // Check if course has roster students or staff
549 2 1. deleteCourse : negated conditional → KILLED
2. deleteCourse : negated conditional → KILLED
    if (!course.getRosterStudents().isEmpty() || !course.getCourseStaff().isEmpty()) {
550
      throw new IllegalArgumentException("Cannot delete course with students or staff");
551
    }
552
553 1 1. deleteCourse : removed call to edu/ucsb/cs156/jobs/repositories/JobsRepository::deleteByScopeTypeAndScopeId → KILLED
    jobsRepository.deleteByScopeTypeAndScopeId("course", courseId);
554 1 1. deleteCourse : removed call to edu/ucsb/cs/scaffold/repository/CourseRepository::delete → KILLED
    courseRepository.delete(course);
555 1 1. deleteCourse : replaced return value with null for edu/ucsb/cs/scaffold/controller/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$12 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateCourse$12 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
579
580 1 1. updateCourse : removed call to edu/ucsb/cs/scaffold/entity/Course::setCourseName → KILLED
    course.setCourseName(courseName);
581 1 1. updateCourse : removed call to edu/ucsb/cs/scaffold/entity/Course::setTerm → KILLED
    course.setTerm(term);
582 1 1. updateCourse : removed call to edu/ucsb/cs/scaffold/entity/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/cs/scaffold/controller/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$13 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateCourseWithCanvasToken$13 → 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/cs/scaffold/entity/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/cs/scaffold/entity/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/cs/scaffold/controller/CoursesController::updateCourseWithCanvasToken → KILLED
    return new InstructorCourseView(savedCourse);
627
  }
628
629
  /**
630
   * Associates a GitHub repo (PlRepo) with a course, after verifying that the current user's stored
631
   * GitHub PAT has read/write access to the repo. The check is a single GET /repos/{owner}/{repo}
632
   * call: a successful response proves read access and its permissions block reports push (write)
633
   * access, so nothing is written to the repo.
634
   *
635
   * @param courseId the id of the course
636
   * @param repoName the repo in owner/repo form (i.e. the part after https://github.com/)
637
   * @return the updated course
638
   */
639
  @Operation(summary = "Associate a GitHub repo (PlRepo) with a course")
640
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
641
  @PutMapping("/updateGithubRepo")
642
  public InstructorCourseView updateGithubRepo(
643
      @Parameter(name = "courseId") @RequestParam Long courseId,
644
      @Parameter(name = "repoName", description = "GitHub repo in owner/repo form") @RequestParam
645
          String repoName) {
646
    Course course =
647
        courseRepository
648
            .findById(courseId)
649 1 1. lambda$updateGithubRepo$14 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$14 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
650
651
    long userId = getCurrentUser().getUser().getId();
652
    PatCredential credential =
653
        patCredentialRepository
654
            .findByUserIdAndPlatform(userId, PatPlatform.GITHUB)
655 1 1. lambda$updateGithubRepo$15 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$15 → KILLED
            .orElseThrow(() -> new ForbiddenException("must set up Github PAT first"));
656
    String token =
657
        patEncryptionService.decrypt(credential.getCiphertext(), credential.getKeyVersion());
658
659
    String trimmedRepoName = repoName.strip();
660
    boolean canWrite;
661
    try {
662
      canWrite = githubService.hasWriteAccess(trimmedRepoName, token);
663
    } catch (HttpClientErrorException e) {
664
      throw new ForbiddenException("No access to repo via Github PAT token");
665
    }
666 1 1. updateGithubRepo : negated conditional → KILLED
    if (!canWrite) {
667
      throw new ForbiddenException("Read/write access to repo via Github PAT is required");
668
    }
669
670
    PlRepo plRepo =
671
        plRepoRepository
672
            .findByRepoName(trimmedRepoName)
673
            .orElseGet(
674 1 1. lambda$updateGithubRepo$16 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$16 → KILLED
                () -> plRepoRepository.save(PlRepo.builder().repoName(trimmedRepoName).build()));
675 1 1. updateGithubRepo : removed call to edu/ucsb/cs/scaffold/entity/Course::setPlRepoId → KILLED
    course.setPlRepoId(plRepo.getId());
676 1 1. updateGithubRepo : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateGithubRepo → KILLED
    return viewWithPlDetails(courseRepository.save(course));
677
  }
678
679
  /**
680
   * Associates a PrairieLearn course instance (PlInstance) with a course. The numeric instance id
681
   * is verified in two steps: it is fetched from the PrairieLearn API using the caller's
682
   * PrairieLearn PAT, and the course's GitHub repo must contain a matching
683
   * courseInstances/{shortName}/infoCourseInstance.json whose longName agrees. Only the
684
   * PrairieLearn API can supply the numeric id, and only the repo check proves the instance belongs
685
   * to this course's repo.
686
   *
687
   * @param courseId the id of the course
688
   * @param instanceId PrairieLearn's numeric course instance id
689
   * @return the updated course
690
   */
691
  @Operation(summary = "Associate a PrairieLearn course instance (PlInstance) with a course")
692
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
693
  @PutMapping("/updatePLInstance")
694
  public InstructorCourseView updatePLInstance(
695
      @Parameter(name = "courseId") @RequestParam Long courseId,
696
      @Parameter(name = "instanceId", description = "numeric PrairieLearn course instance id")
697
          @RequestParam
698
          Long instanceId) {
699
    Course course =
700
        courseRepository
701
            .findById(courseId)
702 1 1. lambda$updatePLInstance$17 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$17 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
703
704
    long userId = getCurrentUser().getUser().getId();
705
    PatCredential githubCredential =
706
        patCredentialRepository
707
            .findByUserIdAndPlatform(userId, PatPlatform.GITHUB)
708 1 1. lambda$updatePLInstance$18 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$18 → KILLED
            .orElseThrow(() -> new ForbiddenException("must set up Github PAT first"));
709
    PatCredential plCredential =
710
        patCredentialRepository
711
            .findByUserIdAndPlatform(userId, PatPlatform.PRAIRIELEARN)
712 1 1. lambda$updatePLInstance$19 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$19 → KILLED
            .orElseThrow(() -> new ForbiddenException("must set up PrairieLearn PAT first"));
713 1 1. updatePLInstance : negated conditional → KILLED
    if (course.getPlRepoId() == null) {
714
      throw new ForbiddenException("must associate course with PlRepo first");
715
    }
716
    PlRepo plRepo =
717
        plRepoRepository
718
            .findById(course.getPlRepoId())
719 1 1. lambda$updatePLInstance$20 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$20 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(PlRepo.class, course.getPlRepoId()));
720
721
    String plToken =
722
        patEncryptionService.decrypt(plCredential.getCiphertext(), plCredential.getKeyVersion());
723
    PrairieLearnService.CourseInstanceInfo info;
724
    try {
725
      info = prairieLearnService.getCourseInstance(instanceId, plToken);
726
    } catch (HttpClientErrorException e) {
727
      throw new ForbiddenException("course instance id not found");
728
    }
729 1 1. updatePLInstance : negated conditional → KILLED
    if (info == null) {
730
      throw new ForbiddenException("course instance id not found");
731
    }
732
733
    String githubToken =
734
        patEncryptionService.decrypt(
735
            githubCredential.getCiphertext(), githubCredential.getKeyVersion());
736 1 1. updatePLInstance : negated conditional → KILLED
    if (!repoConfirmsInstance(plRepo, info, githubToken)) {
737
      throw new ForbiddenException("course instance id not found");
738
    }
739
740
    PlInstance plInstance =
741
        plInstanceRepository
742
            .findByPlRepoIdAndShortName(plRepo.getId(), info.shortName())
743
            .orElseGet(
744
                () ->
745 1 1. lambda$updatePLInstance$21 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$21 → KILLED
                    PlInstance.builder()
746
                        .plRepoId(plRepo.getId())
747
                        .shortName(info.shortName())
748
                        .build());
749 1 1. updatePLInstance : removed call to edu/ucsb/cs/scaffold/entity/PlInstance::setLongName → KILLED
    plInstance.setLongName(info.longName());
750 1 1. updatePLInstance : removed call to edu/ucsb/cs/scaffold/entity/PlInstance::setNumericId → KILLED
    plInstance.setNumericId(info.courseInstanceId());
751
    PlInstance savedInstance = plInstanceRepository.save(plInstance);
752
753 1 1. updatePLInstance : removed call to edu/ucsb/cs/scaffold/entity/Course::setPlInstanceId → KILLED
    course.setPlInstanceId(savedInstance.getId());
754
    Course savedCourse = courseRepository.save(course);
755
756
    // A successful association immediately kicks off a sync of the course's questions and
757
    // assessments (issue #69), so the instructor doesn't have to launch it by hand.
758
    jobService.runAsJob(syncCourseWithPlRepoJobFactory.create(userId, savedCourse));
759
760 1 1. updatePLInstance : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updatePLInstance → KILLED
    return viewWithPlDetails(savedCourse);
761
  }
762
763
  /**
764
   * True when the course's repo has courseInstances/{shortName}/infoCourseInstance.json and its
765
   * longName matches the one PrairieLearn reported for the instance.
766
   */
767
  private boolean repoConfirmsInstance(
768
      PlRepo plRepo, PrairieLearnService.CourseInstanceInfo info, String githubToken) {
769
    String infoJson;
770
    try {
771
      infoJson =
772
          githubService.getFileContent(
773
              plRepo.getRepoName(),
774
              "courseInstances/" + info.shortName() + "/infoCourseInstance.json",
775
              githubToken);
776
    } catch (HttpClientErrorException e) {
777 1 1. repoConfirmsInstance : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED
      return false;
778
    }
779
    try {
780
      JsonNode longName = new ObjectMapper().readTree(infoJson).path("longName");
781 3 1. repoConfirmsInstance : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED
2. repoConfirmsInstance : negated conditional → KILLED
3. repoConfirmsInstance : negated conditional → KILLED
      return !longName.isMissingNode() && longName.asText().equals(info.longName());
782
    } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
783 1 1. repoConfirmsInstance : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED
      return false;
784
    }
785
  }
786
}

Mutations

126

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

155

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

156

1.1
Location : <init>
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testInstructorCourseView_withBothCollectionsNull()]
negated conditional → KILLED

173

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

177

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

180

1.1
Location : viewWithPlDetails
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:getCourseById_leaves_details_null_when_the_associated_rows_are_missing()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::viewWithPlDetails → KILLED

186

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

187

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

191

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

192

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

210

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

226

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

241

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

242

1.1
Location : getCourseById
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:getCourseById_leaves_details_null_when_the_associated_rows_are_missing()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseById → KILLED

262

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

266

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

268

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

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

271

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

272

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

275

1.1
Location : getCourseCanvasInfo
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:getCanvasInfo_obscuresCorrectlyForLessThanThreeCharacters()]
replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseCanvasInfo → KILLED

277

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

278

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

296

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

297

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

308

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

343

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

354

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

387

1.1
Location : lambda$listCoursesForCurrentUserUnified$4
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testListCoursesForCurrentUserUnified_nonAdmin()]
replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$4 → KILLED

392

1.1
Location : lambda$listCoursesForCurrentUserUnified$5
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testListCoursesForCurrentUserUnified_nonAdmin()]
replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$5 → KILLED

401

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

411

1.1
Location : listCoursesForCurrentUserUnified
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testListCoursesForCurrentUserUnified_admin()]
replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::listCoursesForCurrentUserUnified → KILLED

414

1.1
Location : lambda$listCoursesForCurrentUserUnified$6
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testListCoursesForCurrentUserUnified_admin()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$6 → KILLED

444

1.1
Location : lambda$getCourseAccessInfo$7
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_courseDoesNotExist()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$7 → KILLED

450

1.1
Location : lambda$getCourseAccessInfo$8
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_withStudentAccess()]
replaced boolean return with false for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$8 → KILLED

2.2
Location : lambda$getCourseAccessInfo$8
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_noAccess()]
replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$8 → KILLED

454

1.1
Location : lambda$getCourseAccessInfo$9
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_withStaffAccess()]
replaced boolean return with false for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$9 → KILLED

2.2
Location : lambda$getCourseAccessInfo$9
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_noAccess()]
replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$9 → KILLED

458

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

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

3.3
Location : getCourseAccessInfo
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_withStudentAccess()]
negated conditional → KILLED

4.4
Location : getCourseAccessInfo
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_noAccess()]
negated conditional → KILLED

462

1.1
Location : getCourseAccessInfo
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testGetCourseAccessInfo_withAdminAccessOnly()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseAccessInfo → KILLED

486

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

492

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

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

496

1.1
Location : updateInstructorEmail
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testUpdateInstructorEmail_byAdmin_email_is_admin()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setInstructorEmail → KILLED

499

1.1
Location : updateInstructorEmail
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:testUpdateInstructorEmail_byAdmin_email_is_instructor()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateInstructorEmail → KILLED

526

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

528

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

533

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

534

1.1
Location : getCourseEmails
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:getCourseEmails_staff_only_sorted_lexicographically()]
replaced return value with "" for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseEmails → KILLED

546

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

549

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

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

553

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:delete_success_returns_ok()]
removed call to edu/ucsb/cs156/jobs/repositories/JobsRepository::deleteByScopeTypeAndScopeId → KILLED

554

1.1
Location : deleteCourse
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:delete_success_returns_ok()]
removed call to edu/ucsb/cs/scaffold/repository/CourseRepository::delete → KILLED

555

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

578

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

580

1.1
Location : updateCourse
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateCourse_success_admin()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setCourseName → KILLED

581

1.1
Location : updateCourse
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateCourse_success_admin()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setTerm → KILLED

582

1.1
Location : updateCourse
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateCourse_success_admin()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setSchool → KILLED

586

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

610

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

612

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

613

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

614

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

615

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setCanvasApiToken → KILLED

618

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

619

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

620

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

621

1.1
Location : updateCourseWithCanvasToken
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:admin_can_updateCourseCanvasToken_created_by_someone_else()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setCanvasCourseId → KILLED

626

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

649

1.1
Location : lambda$updateGithubRepo$14
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateGithubRepo_returns_404_when_course_not_found()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$14 → KILLED

655

1.1
Location : lambda$updateGithubRepo$15
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateGithubRepo_returns_403_when_user_has_no_github_pat()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$15 → KILLED

666

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

674

1.1
Location : lambda$updateGithubRepo$16
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateGithubRepo_creates_the_pl_repo_when_it_is_not_yet_in_the_table()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$16 → KILLED

675

1.1
Location : updateGithubRepo
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateGithubRepo_records_an_existing_pl_repo_id_on_the_course()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setPlRepoId → KILLED

676

1.1
Location : updateGithubRepo
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updateGithubRepo_records_an_existing_pl_repo_id_on_the_course()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateGithubRepo → KILLED

702

1.1
Location : lambda$updatePLInstance$17
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_404_when_course_not_found()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$17 → KILLED

708

1.1
Location : lambda$updatePLInstance$18
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_403_when_user_has_no_github_pat()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$18 → KILLED

712

1.1
Location : lambda$updatePLInstance$19
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_403_when_user_has_no_prairielearn_pat()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$19 → KILLED

713

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

719

1.1
Location : lambda$updatePLInstance$20
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_404_when_the_pl_repo_row_is_missing()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$20 → KILLED

729

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

736

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

745

1.1
Location : lambda$updatePLInstance$21
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_creates_the_pl_instance_when_it_does_not_exist()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$21 → KILLED

749

1.1
Location : updatePLInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_updates_the_numeric_id_when_the_pl_instance_exists()]
removed call to edu/ucsb/cs/scaffold/entity/PlInstance::setLongName → KILLED

750

1.1
Location : updatePLInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_updates_the_numeric_id_when_the_pl_instance_exists()]
removed call to edu/ucsb/cs/scaffold/entity/PlInstance::setNumericId → KILLED

753

1.1
Location : updatePLInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_updates_the_numeric_id_when_the_pl_instance_exists()]
removed call to edu/ucsb/cs/scaffold/entity/Course::setPlInstanceId → KILLED

760

1.1
Location : updatePLInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_updates_the_numeric_id_when_the_pl_instance_exists()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updatePLInstance → KILLED

777

1.1
Location : repoConfirmsInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_403_when_the_repo_has_no_matching_instance_directory()]
replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED

781

1.1
Location : repoConfirmsInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_403_when_the_long_name_is_missing_from_the_repo_json()]
replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED

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

3.3
Location : repoConfirmsInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_403_when_the_long_names_do_not_match()]
negated conditional → KILLED

783

1.1
Location : repoConfirmsInstance
Killed by : edu.ucsb.cs.scaffold.controller.CoursesControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.CoursesControllerTests]/[method:updatePLInstance_returns_403_when_the_repo_json_is_unparseable()]
replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0