CoursesController.java

1
package edu.ucsb.cs156.frontiers.controllers;
2
3
import java.security.NoSuchAlgorithmException;
4
import java.security.spec.InvalidKeySpecException;
5
import java.util.Map;
6
import java.util.Optional;
7
import java.util.List;
8
import java.util.stream.Collectors;
9
import java.util.stream.StreamSupport;
10
import java.util.stream.Collectors;
11
12
import org.springframework.beans.factory.annotation.Autowired;
13
import org.springframework.http.HttpHeaders;
14
import org.springframework.http.HttpStatus;
15
import org.springframework.http.ResponseEntity;
16
import org.springframework.security.access.prepost.PreAuthorize;
17
import org.springframework.web.bind.annotation.ExceptionHandler;
18
import org.springframework.web.bind.annotation.GetMapping;
19
import org.springframework.web.bind.annotation.PostMapping;
20
import org.springframework.web.bind.annotation.RequestMapping;
21
import org.springframework.web.bind.annotation.RequestParam;
22
import org.springframework.web.bind.annotation.ResponseStatus;
23
import org.springframework.web.bind.annotation.RestController;
24
25
import com.fasterxml.jackson.core.JsonProcessingException;
26
27
import edu.ucsb.cs156.frontiers.entities.Course;
28
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
29
import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException;
30
import edu.ucsb.cs156.frontiers.errors.InvalidInstallationTypeException;
31
import edu.ucsb.cs156.frontiers.models.CurrentUser;
32
import edu.ucsb.cs156.frontiers.models.RosterStudentDTO;
33
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
34
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
35
import edu.ucsb.cs156.frontiers.services.OrganizationLinkerService;
36
import io.swagger.v3.oas.annotations.Operation;
37
import io.swagger.v3.oas.annotations.Parameter;
38
import io.swagger.v3.oas.annotations.tags.Tag;
39
import lombok.extern.slf4j.Slf4j;
40
41
import java.util.ArrayList;
42
import java.util.List;
43
import java.util.stream.Collectors;
44
45
import edu.ucsb.cs156.frontiers.enums.OrgStatus;
46
47
@Tag(name = "Course")
48
@RequestMapping("/api/courses")
49
@RestController
50
@Slf4j
51
public class CoursesController extends ApiController {
52
    
53
    @Autowired
54
    private CourseRepository courseRepository;
55
56
    @Autowired private OrganizationLinkerService linkerService;
57
58
    @Autowired
59
    private RosterStudentRepository rosterStudentRepository;
60
     /**
61
     * This method creates a new Course.
62
     * 
63
    * @param orgName the name of the organization
64
    * @param courseName the name of the course
65
    * @param term the term of the course
66
    * @param school the school of the course
67
    * @return the created course
68
     */
69
70
    @Operation(summary = "Create a new course")
71
    @PreAuthorize("hasRole('ROLE_ADMIN')")
72
    @PostMapping("/post")
73
    public Course postCourse(
74
            @Parameter(name = "orgName") @RequestParam String orgName,
75
            @Parameter(name = "courseName") @RequestParam String courseName,
76
            @Parameter(name = "term") @RequestParam String term,
77
            @Parameter(name = "school") @RequestParam String school
78
           )
79
            {
80
        //get current date right now and set status to pending
81
        CurrentUser currentUser = getCurrentUser();
82
        Course course = Course.builder()
83
                .orgName(orgName)
84
                .courseName(courseName)
85
                .term(term)
86
                .school(school)
87
                .creator(currentUser.getUser())
88
                .build();
89
        Course savedCourse = courseRepository.save(course);
90
91 1 1. postCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::postCourse → KILLED
        return savedCourse;
92
    }
93
94
      /**
95
     * This method returns a list of courses.
96
     * @return a list of all courses.
97
     */
98
    @Operation(summary = "List all courses")
99
    @PreAuthorize("hasRole('ROLE_ADMIN')")
100
    @GetMapping("/all")
101
    public Iterable<Course> allCourses(
102
    ) {
103
        Iterable<Course> courses = courseRepository.findAll();
104 1 1. allCourses : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::allCourses → KILLED
        return courses;
105
    }
106
107
108
    /**
109
     * <p>This is the outgoing method, redirecting from Frontiers to GitHub to allow a Course to be linked to a GitHub Organization.
110
     * It redirects from Frontiers to the GitHub app installation process, and will return with the {@link #addInstallation(Optional, String, String, Long) addInstallation()} endpoint
111
     * </p>
112
     * @param courseId id of the course to be linked to
113
     * @return dynamically loaded url to install Frontiers to a Github Organization, with the courseId marked as the state parameter, which GitHub will return.
114
     *
115
     */
116
    @Operation(summary = "Authorize Frontiers to a Github Course")
117
    @PreAuthorize("hasRole('ROLE_PROFESSOR')")
118
    @GetMapping("/redirect")
119
    public ResponseEntity<Void> linkCourse(@Parameter Long courseId) throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
120
        String newUrl = linkerService.getRedirectUrl();
121
        newUrl += "/installations/new?state="+courseId;
122
        //found this convenient solution here: https://stackoverflow.com/questions/29085295/spring-mvc-restcontroller-and-redirect
123 1 1. linkCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::linkCourse → KILLED
        return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, newUrl).build();
124
    }
125
126
127
    /**
128
     *
129
     * @param installation_id id of the incoming GitHub Organization installation
130
     * @param setup_action whether the permissions are installed or updated. Required RequestParam but not used by the method.
131
     * @param code token to be exchanged with GitHub to ensure the request is legitimate and not spoofed.
132
     * @param state id of the Course to be linked with the GitHub installation.
133
     * @return ResponseEntity, returning /success if the course was successfully linked or /noperms if the user does not have the permission to install the application on GitHub. Alternately returns 403 Forbidden if the user is not the creator.
134
     */
135
    @Operation(summary = "Link a Course to a Github Course")
136
    @PreAuthorize("hasRole('ROLE_PROFESSOR')")
137
    @GetMapping("link")
138
    public ResponseEntity<Void> addInstallation(@Parameter(name = "installationId") @RequestParam Optional<String> installation_id,
139
                                                @Parameter(name = "setupAction") @RequestParam String setup_action,
140
                                                @Parameter(name = "code") @RequestParam String code,
141
                                                @Parameter(name = "state") @RequestParam Long state) throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
142 1 1. addInstallation : negated conditional → KILLED
        if(installation_id.isEmpty()) {
143 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
            return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, "/courses/nopermissions").build();
144
        }else {
145 1 1. lambda$addInstallation$0 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$addInstallation$0 → KILLED
            Course course = courseRepository.findById(state).orElseThrow(() -> new EntityNotFoundException(Course.class, state));
146 1 1. addInstallation : negated conditional → KILLED
            if(!(course.getCreator().getId() ==getCurrentUser().getUser().getId())) {
147 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
148
            }else{
149
                String orgName = linkerService.getOrgName(installation_id.get());
150 1 1. addInstallation : removed call to edu/ucsb/cs156/frontiers/entities/Course::setInstallationId → KILLED
                course.setInstallationId(installation_id.get());
151 1 1. addInstallation : removed call to edu/ucsb/cs156/frontiers/entities/Course::setOrgName → KILLED
                course.setOrgName(orgName);
152
                courseRepository.save(course);
153 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
                return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, "/admin/courses?success=True&course=" + state).build();
154
            }
155
        }
156
    }
157
158
    /**
159
     * This method returns a list of students in the roster for a given course,
160
     * with each student represented as a RosterStudentDTO including GitHub org status.
161
     * 
162
     * @param courseId the ID of the course
163
     * @return a list of RosterStudentDTOs for the given course
164
     */
165
    @Operation(summary = "Get list of students in a course roster, including orgStatus")
166
    @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_PROFESSOR')")
167
    @GetMapping("/roster")
168
    public List<RosterStudentDTO> getRosterForCourse(
169
            @Parameter(name = "courseId") @RequestParam Long courseId
170
    ) {
171
        Iterable<RosterStudent> studentsIterable = rosterStudentRepository.findByCourseId(courseId);
172
        List<RosterStudent> students = StreamSupport.stream(studentsIterable.spliterator(), false).collect(Collectors.toList());
173 1 1. getRosterForCourse : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::getRosterForCourse → KILLED
        return students.stream()
174
                .map(RosterStudentDTO::from)
175
                .collect(Collectors.toList());
176
    }
177
178
    /**
179
     * This method handles the InvalidInstallationTypeException.
180
     * @param e the exception
181
     * @return a map with the type and message of the exception
182
     */
183
    @ExceptionHandler({ InvalidInstallationTypeException.class })
184
    @ResponseStatus(HttpStatus.BAD_REQUEST)
185
    public Object handleInvalidInstallationType(Throwable e) {
186 1 1. handleInvalidInstallationType : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::handleInvalidInstallationType → KILLED
        return Map.of(
187
                "type", e.getClass().getSimpleName(),
188
                "message", e.getMessage()
189
        );
190
    }
191
192
193
194
    /**
195
     * This method looks up a current user and gets their email.
196
     * With that email, it that email on every course roster,
197
     * and when it appears, notes/stores course id
198
     * We then return all courses for those course ids, with relevant
199
     * fields for the student.
200
     * 
201
     * Relevant fields are: id, installationId, orgName, courseName, term, school
202
     * For each course, return the status that the student is in
203
     */
204
    
205
    @Operation(summary = "Get all courses for a student")
206
    @PreAuthorize("hasRole('ROLE_USER')")
207
    @GetMapping("/student")
208
    public ResponseEntity<List<StudentCourseView>> getCoursesForStudent() {
209
        String email = getCurrentUser().getUser().getEmail();
210
211
        List<StudentCourseView> results = courseRepository
212
                .findAllByRosterStudents_Email(email)
213
.stream()
214 1 1. lambda$getCoursesForStudent$1 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$getCoursesForStudent$1 → KILLED
            .map(c -> new StudentCourseView(c, email))
215
                .toList();
216
217 1 1. getCoursesForStudent : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::getCoursesForStudent → KILLED
        return ResponseEntity.ok(results);
218
    }
219
220
    
221
    // Lightweight projection of Course entity with only student-relevant fields
222
    public static record StudentCourseView(
223
            Long id,
224
            String installationId,
225
            String orgName,
226
            String courseName,
227
            String term,
228
            String school,
229
            String status) {
230
231
        
232
        // Creates view from Course entity and student email
233
        public StudentCourseView(Course c, String email) {
234
            this(
235
                c.getId(),
236
                c.getInstallationId(),
237
                c.getOrgName(),
238
                c.getCourseName(),
239
                c.getTerm(),
240
                c.getSchool(),
241
                mapStatus(
242
                    c.getRosterStudents()
243
                     .stream()
244 2 1. lambda$new$0 : replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::lambda$new$0 → KILLED
2. lambda$new$0 : replaced boolean return with false for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::lambda$new$0 → KILLED
                     .filter(s -> s.getEmail().equals(email))
245
                     .findFirst()
246
                     .orElse(null)));
247
        }
248
249
        
250
        // Maps OrgStatus enum to human-readable status message
251
        private static String mapStatus(RosterStudent rs) {
252 2 1. mapStatus : replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::mapStatus → KILLED
2. mapStatus : negated conditional → KILLED
            if (rs == null) return "Not yet requested an invitation";
253
254 1 1. mapStatus : replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::mapStatus → KILLED
            return switch (rs.getOrgStatus()) {
255
                case NONE -> "Not yet requested an invitation";
256
                case INVITED -> "Has requested an invitation but isn't yet a member";
257
                case MEMBER -> "Is a member of the org";
258
                case OWNER -> "Is an admin in the org";
259
                case EXPIRED -> "Invitation has expired";
260
            };
261
        }
262
    }
263
264
}

Mutations

91

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

104

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

123

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

142

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

143

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

145

1.1
Location : lambda$addInstallation$0
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$0 → KILLED

146

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

147

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

150

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 edu/ucsb/cs156/frontiers/entities/Course::setInstallationId → KILLED

151

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 edu/ucsb/cs156/frontiers/entities/Course::setOrgName → KILLED

153

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

173

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

186

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

214

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

217

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

244

1.1
Location : lambda$new$0
Killed by : edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests]/[method:status_whenNoRosterEntry()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::lambda$new$0 → KILLED

2.2
Location : lambda$new$0
Killed by : edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests]/[method:status_for_EXPIRED()]
replaced boolean return with false for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::lambda$new$0 → KILLED

252

1.1
Location : mapStatus
Killed by : edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests]/[method:status_whenNoRosterEntry()]
replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::mapStatus → KILLED

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

254

1.1
Location : mapStatus
Killed by : edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.StudentCourseViewTests]/[method:status_for_EXPIRED()]
replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/CoursesController$StudentCourseView::mapStatus → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0