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.stream.Collectors;
8
import java.util.ArrayList;
9
import java.util.HashMap;
10
import java.util.LinkedHashMap;
11
import java.util.List;
12
import java.util.Set;
13
14
import org.springframework.beans.factory.annotation.Autowired;
15
import org.springframework.data.repository.query.Param;
16
import org.springframework.http.HttpHeaders;
17
import org.springframework.http.HttpStatus;
18
import org.springframework.http.ResponseEntity;
19
import org.springframework.security.access.prepost.PreAuthorize;
20
import org.springframework.security.core.context.SecurityContextHolder;
21
import org.springframework.security.oauth2.core.user.OAuth2User;
22
import org.springframework.security.core.Authentication;
23
import org.springframework.web.bind.annotation.ExceptionHandler;
24
import org.springframework.web.bind.annotation.GetMapping;
25
import org.springframework.web.bind.annotation.PostMapping;
26
import org.springframework.web.bind.annotation.RequestMapping;
27
import org.springframework.web.bind.annotation.RequestParam;
28
import org.springframework.web.bind.annotation.ResponseStatus;
29
import org.springframework.web.bind.annotation.RestController;
30
31
import com.fasterxml.jackson.core.JsonProcessingException;
32
33
import edu.ucsb.cs156.frontiers.entities.Course;
34
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
35
import edu.ucsb.cs156.frontiers.entities.User;
36
import edu.ucsb.cs156.frontiers.enums.OrgStatus;
37
import edu.ucsb.cs156.frontiers.enums.RosterStatus;
38
import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException;
39
import edu.ucsb.cs156.frontiers.errors.InvalidInstallationTypeException;
40
import edu.ucsb.cs156.frontiers.models.CurrentUser;
41
import edu.ucsb.cs156.frontiers.models.RosterStudentDTO;
42
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
43
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
44
import edu.ucsb.cs156.frontiers.repositories.UserRepository;
45
import edu.ucsb.cs156.frontiers.services.OrganizationLinkerService;
46
import io.swagger.v3.oas.annotations.Operation;
47
import io.swagger.v3.oas.annotations.Parameter;
48
import io.swagger.v3.oas.annotations.tags.Tag;
49
import lombok.extern.slf4j.Slf4j;
50
51
@Tag(name = "Course")
52
@RequestMapping("/api/courses")
53
@RestController
54
@Slf4j
55
public class CoursesController extends ApiController {
56
57
    @Autowired
58
    private CourseRepository courseRepository;
59
60
    @Autowired
61
    private UserRepository userRepository;
62
63
    @Autowired
64
    private RosterStudentRepository rosterStudentRepository;
65
66
    @Autowired
67
    private OrganizationLinkerService linkerService;
68
69
    /**
70
     * This method creates a new Course.
71
     * 
72
     * @param orgName    the name of the organization
73
     * @param courseName the name of the course
74
     * @param term       the term of the course
75
     * @param school     the school of the course
76
     * @return the created course
77
     */
78
79
    @Operation(summary = "Create a new course")
80
    @PreAuthorize("hasRole('ROLE_ADMIN')")
81
    @PostMapping("/post")
82
    public Course postCourse(
83
            @Parameter(name = "orgName") @RequestParam String orgName,
84
            @Parameter(name = "courseName") @RequestParam String courseName,
85
            @Parameter(name = "term") @RequestParam String term,
86
            @Parameter(name = "school") @RequestParam String school) {
87
        // get current date right now and set status to pending
88
        CurrentUser currentUser = getCurrentUser();
89
        Course course = Course.builder()
90
                .orgName(orgName)
91
                .courseName(courseName)
92
                .term(term)
93
                .school(school)
94
                .creator(currentUser.getUser())
95
                .build();
96
        Course savedCourse = courseRepository.save(course);
97
98 1 1. postCourse : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::postCourse → KILLED
        return savedCourse;
99
    }
100
101
    /**
102
     * This method returns a list of courses.
103
     * 
104
     * @return a list of all courses.
105
     */
106
    @Operation(summary = "List all courses")
107
    @PreAuthorize("hasRole('ROLE_ADMIN')")
108
    @GetMapping("/all")
109
    public Iterable<Course> allCourses() {
110
        Iterable<Course> courses = courseRepository.findAll();
111 1 1. allCourses : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::allCourses → KILLED
        return courses;
112
    }
113
114
    /**
115
     * <p>
116
     * This is the outgoing method, redirecting from Frontiers to GitHub to allow a
117
     * Course to be linked to a GitHub Organization.
118
     * It redirects from Frontiers to the GitHub app installation process, and will
119
     * return with the {@link #addInstallation(Optional, String, String, Long)
120
     * addInstallation()} endpoint
121
     * </p>
122
     * 
123
     * @param courseId id of the course to be linked to
124
     * @return dynamically loaded url to install Frontiers to a Github Organization,
125
     *         with the courseId marked as the state parameter, which GitHub will
126
     *         return.
127
     *
128
     */
129
    @Operation(summary = "Authorize Frontiers to a Github Course")
130
    @PreAuthorize("hasRole('ROLE_PROFESSOR')")
131
    @GetMapping("/redirect")
132
    public ResponseEntity<Void> linkCourse(@Parameter Long courseId)
133
            throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
134
        String newUrl = linkerService.getRedirectUrl();
135
        newUrl += "/installations/new?state=" + courseId;
136
        // found this convenient solution here:
137
        // https://stackoverflow.com/questions/29085295/spring-mvc-restcontroller-and-redirect
138 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();
139
    }
140
141
    /**
142
     *
143
     * @param installation_id id of the incoming GitHub Organization installation
144
     * @param setup_action    whether the permissions are installed or updated.
145
     *                        Required RequestParam but not used by the method.
146
     * @param code            token to be exchanged with GitHub to ensure the
147
     *                        request is legitimate and not spoofed.
148
     * @param state           id of the Course to be linked with the GitHub
149
     *                        installation.
150
     * @return ResponseEntity, returning /success if the course was successfully
151
     *         linked or /noperms if the user does not have the permission to
152
     *         install the application on GitHub. Alternately returns 403 Forbidden
153
     *         if the user is not the creator.
154
     */
155
    @Operation(summary = "Link a Course to a Github Course")
156
    @PreAuthorize("hasRole('ROLE_PROFESSOR')")
157
    @GetMapping("link")
158
    public ResponseEntity<Void> addInstallation(
159
            @Parameter(name = "installationId") @RequestParam Optional<String> installation_id,
160
            @Parameter(name = "setupAction") @RequestParam String setup_action,
161
            @Parameter(name = "code") @RequestParam String code,
162
            @Parameter(name = "state") @RequestParam Long state)
163
            throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
164 1 1. addInstallation : negated conditional → KILLED
        if (installation_id.isEmpty()) {
165 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
            return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
166
                    .header(HttpHeaders.LOCATION, "/courses/nopermissions").build();
167
        } else {
168
            Course course = courseRepository.findById(state)
169 1 1. lambda$addInstallation$0 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$addInstallation$0 → KILLED
                    .orElseThrow(() -> new EntityNotFoundException(Course.class, state));
170 1 1. addInstallation : negated conditional → KILLED
            if (!(course.getCreator().getId() == getCurrentUser().getUser().getId())) {
171 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
172
            } else {
173
                String orgName = linkerService.getOrgName(installation_id.get());
174 1 1. addInstallation : removed call to edu/ucsb/cs156/frontiers/entities/Course::setInstallationId → KILLED
                course.setInstallationId(installation_id.get());
175 1 1. addInstallation : removed call to edu/ucsb/cs156/frontiers/entities/Course::setOrgName → KILLED
                course.setOrgName(orgName);
176
                courseRepository.save(course);
177 1 1. addInstallation : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::addInstallation → KILLED
                return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
178
                        .header(HttpHeaders.LOCATION, "/admin/courses?success=True&course=" + state).build();
179
            }
180
        }
181
    }
182
183
    /**
184
     * This method handles the InvalidInstallationTypeException.
185
     * 
186
     * @param e the exception
187
     * @return a map with the type and message of the exception
188
     */
189
    @ExceptionHandler({ InvalidInstallationTypeException.class })
190
    @ResponseStatus(HttpStatus.BAD_REQUEST)
191
    public Object handleInvalidInstallationType(Throwable e) {
192 1 1. handleInvalidInstallationType : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/CoursesController::handleInvalidInstallationType → KILLED
        return Map.of(
193
                "type", e.getClass().getSimpleName(),
194
                "message", e.getMessage());
195
    }
196
197
    /**
198
     * This method returns a list of courses that the current user is enrolled.
199
     * 
200
     * @return a list of courses in the DTO form along with the student status in the organization.
201
     */
202
    @Operation(summary = "List all courses for the current student, including their org status")
203
    @PreAuthorize("hasRole('ROLE_USER')")
204
    @GetMapping("/list")
205
    public List<Map<String, Object>> listCoursesForCurrentUser() {
206
        String email = getCurrentUser().getUser().getEmail();
207
        Iterable<RosterStudent> rosterStudentsIterable = rosterStudentRepository.findAllByEmail(email);
208
        List<RosterStudent> rosterStudents = new ArrayList<>();
209 1 1. listCoursesForCurrentUser : removed call to java/lang/Iterable::forEach → KILLED
        rosterStudentsIterable.forEach(rosterStudents::add);
210 1 1. listCoursesForCurrentUser : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/CoursesController::listCoursesForCurrentUser → KILLED
        return rosterStudents.stream()
211
                .map(rs -> {
212
                    Course course = rs.getCourse();
213
                    RosterStudentDTO rsDto = RosterStudentDTO.from(rs);
214
                    Map<String, Object> response = new LinkedHashMap<>();
215
                    response.put("id", course.getId());
216
                    response.put("installationId", course.getInstallationId());
217
                    response.put("orgName", course.getOrgName());
218
                    response.put("courseName", course.getCourseName());
219
                    response.put("term", course.getTerm());
220
                    response.put("school", course.getSchool());
221
                    response.put("studentStatus", rsDto.getOrgStatus());
222 1 1. lambda$listCoursesForCurrentUser$1 : replaced return value with Collections.emptyMap for edu/ucsb/cs156/frontiers/controllers/CoursesController::lambda$listCoursesForCurrentUser$1 → KILLED
                    return response;
223
                })
224
                .collect(Collectors.toList());
225
    }
226
227
}

Mutations

98

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

111

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

138

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

164

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

165

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

169

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

170

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

171

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

174

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

175

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

177

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

192

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

209

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

210

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

222

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

Active mutators

Tests examined


Report generated by PIT 1.17.0