WebhookController.java

1
package edu.ucsb.cs156.frontiers.controllers;
2
3
import com.fasterxml.jackson.core.JsonProcessingException;
4
import com.fasterxml.jackson.databind.JsonNode;
5
import edu.ucsb.cs156.frontiers.entities.Course;
6
import edu.ucsb.cs156.frontiers.entities.CourseStaff;
7
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
8
import edu.ucsb.cs156.frontiers.enums.OrgStatus;
9
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
10
import edu.ucsb.cs156.frontiers.repositories.CourseStaffRepository;
11
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
12
import edu.ucsb.cs156.frontiers.utilities.WebhookSecurityUtils;
13
import io.swagger.v3.oas.annotations.tags.Tag;
14
import java.security.InvalidKeyException;
15
import java.security.NoSuchAlgorithmException;
16
import java.util.Optional;
17
import lombok.extern.slf4j.Slf4j;
18
import org.springframework.beans.factory.annotation.Value;
19
import org.springframework.http.ResponseEntity;
20
import org.springframework.web.bind.annotation.PostMapping;
21
import org.springframework.web.bind.annotation.RequestBody;
22
import org.springframework.web.bind.annotation.RequestHeader;
23
import org.springframework.web.bind.annotation.RequestMapping;
24
import org.springframework.web.bind.annotation.RestController;
25
26
@Tag(name = "Webhooks Controller")
27
@RestController
28
@RequestMapping("/api/webhooks")
29
@Slf4j
30
public class WebhookController {
31
32
  private final CourseRepository courseRepository;
33
  private final RosterStudentRepository rosterStudentRepository;
34
  private final CourseStaffRepository courseStaffRepository;
35
36
  @Value("${app.webhook.secret}")
37
  private String webhookSecret;
38
39
  public WebhookController(
40
      CourseRepository courseRepository,
41
      RosterStudentRepository rosterStudentRepository,
42
      CourseStaffRepository courseStaffRepository) {
43
    this.courseRepository = courseRepository;
44
    this.rosterStudentRepository = rosterStudentRepository;
45
    this.courseStaffRepository = courseStaffRepository;
46
  }
47
48
  /**
49
   * Accepts webhooks from GitHub, currently to update the membership status of a RosterStudent.
50
   *
51
   * @param jsonBody body of the webhook. The description of the currently used webhook is available
52
   *     in docs/webhooks.md
53
   * @param signature the GitHub webhook signature header for security validation
54
   * @return either the word success so GitHub will not flag the webhook as a failure, or the
55
   *     updated RosterStudent
56
   */
57
  @PostMapping("/github")
58
  public ResponseEntity<String> createGitHubWebhook(
59
      @RequestBody String requestBody,
60
      @RequestHeader(value = "X-Hub-Signature-256", required = false) String signature)
61
      throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeyException {
62
63
    // Validate webhook signature
64 1 1. createGitHubWebhook : negated conditional → KILLED
    if (!WebhookSecurityUtils.validateGitHubSignature(requestBody, signature, webhookSecret)) {
65
      log.error("Webhook signature validation failed");
66 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
      return ResponseEntity.status(401).body("Unauthorized: Invalid signature");
67
    }
68
69
    // Parse JSON after signature validation
70
    JsonNode jsonBody;
71
    try {
72
      jsonBody = new com.fasterxml.jackson.databind.ObjectMapper().readTree(requestBody);
73
    } catch (JsonProcessingException e) {
74
      log.error("Failed to parse webhook JSON body", e);
75 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
      return ResponseEntity.badRequest().body("Invalid JSON");
76
    }
77
78
    log.info("Received GitHub webhook: {}", jsonBody.toString());
79
80 1 1. createGitHubWebhook : negated conditional → KILLED
    if (!jsonBody.has("action")) {
81 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
      return ResponseEntity.ok().body("success");
82
    }
83
84
    String action = jsonBody.get("action").asText();
85
    log.info("Webhook action: {}", action);
86
87
    // Early return if not an action we care about
88 2 1. createGitHubWebhook : negated conditional → KILLED
2. createGitHubWebhook : negated conditional → KILLED
    if (!action.equals("member_added") && !action.equals("member_invited")) {
89 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
      return ResponseEntity.ok().body("success");
90
    }
91
92
    // Extract GitHub login based on payload structure
93
    String githubLogin = null;
94
    String installationId = null;
95
    OrgStatus role = null;
96
97
    // For member_added events, the structure is different
98 1 1. createGitHubWebhook : negated conditional → KILLED
    if (action.equals("member_added")) {
99 1 1. createGitHubWebhook : negated conditional → KILLED
      if (!jsonBody.has("membership")
100 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.get("membership").has("user")
101 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.get("membership").has("role")
102 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.get("membership").get("user").has("login")
103 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.has("installation")
104 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.get("installation").has("id")) {
105 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
        return ResponseEntity.ok().body("success");
106
      }
107
108
      githubLogin = jsonBody.get("membership").get("user").get("login").asText();
109
      installationId = jsonBody.get("installation").get("id").asText();
110
      String textRole = jsonBody.get("membership").get("role").asText();
111 1 1. createGitHubWebhook : negated conditional → KILLED
      if (textRole.equals("admin")) {
112
        role = OrgStatus.OWNER;
113
      } else {
114
        role = OrgStatus.MEMBER;
115
      }
116
    }
117
    // For member_invited events, use the original structure
118
    else { // must be "member_invited" based on earlier check
119 1 1. createGitHubWebhook : negated conditional → KILLED
      if (!jsonBody.has("user")
120 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.get("user").has("login")
121 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.has("installation")
122 1 1. createGitHubWebhook : negated conditional → KILLED
          || !jsonBody.get("installation").has("id")) {
123 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
        return ResponseEntity.ok().body("success");
124
      }
125
126
      githubLogin = jsonBody.get("user").get("login").asText();
127
      installationId = jsonBody.get("installation").get("id").asText();
128
    }
129
130
    log.info("GitHub login: {}, Installation ID: {}", githubLogin, installationId);
131
132
    Optional<Course> course = courseRepository.findByInstallationId(installationId);
133
    log.info("Course found: {}", course.isPresent());
134
135 1 1. createGitHubWebhook : negated conditional → KILLED
    if (!course.isPresent()) {
136
      log.warn("No course found with installation ID: {}", installationId);
137 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
      return ResponseEntity.ok().body("success");
138
    }
139
140
    Optional<RosterStudent> student =
141
        rosterStudentRepository.findByCourseAndGithubLogin(course.get(), githubLogin);
142
    Optional<CourseStaff> staff =
143
        courseStaffRepository.findByCourseAndGithubLogin(course.get(), githubLogin);
144
    log.info("Student found: {}", student.isPresent());
145
    log.info("Staff found: {}", staff.isPresent());
146
147 2 1. createGitHubWebhook : negated conditional → KILLED
2. createGitHubWebhook : negated conditional → KILLED
    if (!student.isPresent() && !staff.isPresent()) {
148
      log.warn(
149
          "No student or staff found with GitHub login: {} in course: {}",
150
          githubLogin,
151
          course.get().getCourseName());
152 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
      return ResponseEntity.ok().body("success");
153
    }
154
    StringBuilder response = new StringBuilder();
155 1 1. createGitHubWebhook : negated conditional → KILLED
    if (student.isPresent()) {
156
      RosterStudent updatedStudent = student.get();
157
      log.info("Current student org status: {}", updatedStudent.getOrgStatus());
158
159
      // Update status based on action
160 1 1. createGitHubWebhook : negated conditional → KILLED
      if (action.equals("member_added")) {
161 1 1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED
        updatedStudent.setOrgStatus(role);
162
        log.info("Setting status to {}", role.toString());
163
      } else { // must be "member_invited" based on earlier check
164 1 1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED
        updatedStudent.setOrgStatus(OrgStatus.INVITED);
165
        log.info("Setting status to INVITED");
166
      }
167
168
      rosterStudentRepository.save(updatedStudent);
169
      log.info("Student saved with new org status: {}", updatedStudent.getOrgStatus());
170
      response.append(updatedStudent);
171
    }
172 1 1. createGitHubWebhook : negated conditional → KILLED
    if (staff.isPresent()) {
173
      CourseStaff updatedStaff = staff.get();
174
      log.info("Current course staff member org status: {}", updatedStaff.getOrgStatus());
175
176
      // Update status based on action
177 1 1. createGitHubWebhook : negated conditional → KILLED
      if (action.equals("member_added")) {
178 1 1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED
        updatedStaff.setOrgStatus(role);
179
        log.info("Setting status to {}", role.toString());
180
      } else { // must be "member_invited" based on earlier check
181 1 1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED
        updatedStaff.setOrgStatus(OrgStatus.INVITED);
182
        log.info("Setting status to INVITED");
183
      }
184
185
      courseStaffRepository.save(updatedStaff);
186
      log.info("Course staff member saved with new org status: {}", updatedStaff.getOrgStatus());
187
      response.append(updatedStaff);
188
    }
189 1 1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED
    return ResponseEntity.ok().body(response.toString());
190
  }
191
}

Mutations

64

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

66

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:webhookWithoutSignature_returnsUnauthorized()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

75

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:unsuccessfulWebhook_badJSON()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

80

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

81

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:no_action()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

88

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

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

89

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:testUnrecognizedAction_withValidFields()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

98

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

99

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

100

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

101

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

102

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

103

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

104

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

105

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:memberAdded_missingMembershipField()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

111

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

119

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

120

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

121

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

122

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

123

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:memberInvited_missingLoginField()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

135

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

137

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:noCourse()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

147

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

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

152

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:noStudent()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

155

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

160

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

161

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:successfulWebhook_admin()]
removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED

164

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:successfulWebhook_memberInvited()]
removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED

172

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

177

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

178

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:successfulWebhook_member_course_staff()]
removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED

181

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:courseStaffInvited()]
removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED

189

1.1
Location : createGitHubWebhook
Killed by : edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.WebhookControllerTests]/[method:successfulWebhook_memberInvited()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0