RepositoryService.java

1
package edu.ucsb.cs156.frontiers.services;
2
3
import com.fasterxml.jackson.core.JsonProcessingException;
4
import com.fasterxml.jackson.databind.ObjectMapper;
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.entities.Team;
9
import edu.ucsb.cs156.frontiers.enums.RepositoryPermissions;
10
import edu.ucsb.cs156.frontiers.repositories.TeamRepository;
11
import java.security.NoSuchAlgorithmException;
12
import java.security.spec.InvalidKeySpecException;
13
import java.util.HashMap;
14
import java.util.Map;
15
import lombok.extern.slf4j.Slf4j;
16
import org.springframework.boot.web.client.RestTemplateBuilder;
17
import org.springframework.http.*;
18
import org.springframework.stereotype.Service;
19
import org.springframework.web.client.HttpClientErrorException;
20
import org.springframework.web.client.RestTemplate;
21
22
@Service
23
@Slf4j
24
public class RepositoryService {
25
  private final JwtService jwtService;
26
  private final GithubTeamService githubTeamService;
27
  private final TeamRepository teamRepository;
28
  private final RestTemplate restTemplate;
29
  private final ObjectMapper mapper;
30
31
  /**
32
   * Creates a GitHub repository for a user (student or staff), given only their GitHub login.
33
   *
34
   * <p>This helper method contains the shared logic used by both {@link
35
   * #createStudentRepository(Course, RosterStudent, String, Boolean, RepositoryPermissions)} and
36
   * {@link #createStaffRepository(Course, CourseStaff, String, Boolean, RepositoryPermissions)}.
37
   *
38
   * <ul>
39
   *   <li>Checks whether the repository already exists.
40
   *   <li>If not, creates a new repository under the course's organization.
41
   *   <li>Adds the user as a collaborator with the given permission level.
42
   * </ul>
43
   *
44
   * @param course the course whose organization the repo belongs to
45
   * @param githubLogin GitHub username of the student or staff member
46
   * @param repoPrefix prefix for the repository name (repoPrefix-githubLogin)
47
   * @param isPrivate whether the created repository should be private
48
   * @param permissions collaborator permissions to grant the user
49
   * @throws NoSuchAlgorithmException if signing fails
50
   * @throws InvalidKeySpecException if signing fails
51
   * @throws JsonProcessingException if JSON serialization fails
52
   */
53
  private void createRepositoryForStudentOrStaff(
54
      Course course,
55
      String githubLogin,
56
      String repoPrefix,
57
      Boolean isPrivate,
58
      RepositoryPermissions permissions)
59
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
60
61
    String newRepoName = repoPrefix + "-" + githubLogin;
62
    String token = jwtService.getInstallationToken(course);
63
64
    String existenceEndpoint =
65
        "https://api.github.com/repos/" + course.getOrgName() + "/" + newRepoName;
66
    String createEndpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/repos";
67
    String provisionEndpoint =
68
        "https://api.github.com/repos/"
69
            + course.getOrgName()
70
            + "/"
71
            + newRepoName
72
            + "/collaborators/"
73
            + githubLogin;
74
75
    HttpHeaders existenceHeaders = new HttpHeaders();
76 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Authorization", "Bearer " + token);
77 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Accept", "application/vnd.github+json");
78 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("X-GitHub-Api-Version", "2022-11-28");
79
80
    HttpEntity<String> existenceEntity = new HttpEntity<>(existenceHeaders);
81
82
    try {
83
      restTemplate.exchange(existenceEndpoint, HttpMethod.GET, existenceEntity, String.class);
84
    } catch (HttpClientErrorException e) {
85 1 1. createRepositoryForStudentOrStaff : negated conditional → KILLED
      if (e.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
86
        HttpHeaders createHeaders = new HttpHeaders();
87 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Authorization", "Bearer " + token);
88 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Accept", "application/vnd.github+json");
89 1 1. createRepositoryForStudentOrStaff : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("X-GitHub-Api-Version", "2022-11-28");
90
91
        Map<String, Object> body = new HashMap<>();
92
        body.put("name", newRepoName);
93
        body.put("private", isPrivate);
94
        String bodyAsJson = mapper.writeValueAsString(body);
95
96
        HttpEntity<String> createEntity = new HttpEntity<>(bodyAsJson, createHeaders);
97
98
        restTemplate.exchange(createEndpoint, HttpMethod.POST, createEntity, String.class);
99
      } else {
100
        log.warn(
101
            "Unexpected response code {} when checking for existence of repository {}",
102
            e.getStatusCode(),
103
            newRepoName);
104
        return;
105
      }
106
    }
107
108
    try {
109
      Map<String, Object> provisionBody = new HashMap<>();
110
      provisionBody.put("permission", permissions.getApiName());
111
      String provisionAsJson = mapper.writeValueAsString(provisionBody);
112
113
      HttpEntity<String> provisionEntity = new HttpEntity<>(provisionAsJson, existenceHeaders);
114
      restTemplate.exchange(provisionEndpoint, HttpMethod.PUT, provisionEntity, String.class);
115
    } catch (HttpClientErrorException ignored) {
116
      // silently ignore if provisioning fails (same as before)
117
    }
118
  }
119
120
  public RepositoryService(
121
      JwtService jwtService,
122
      GithubTeamService githubTeamService,
123
      TeamRepository teamRepository,
124
      RestTemplateBuilder restTemplateBuilder,
125
      ObjectMapper mapper) {
126
    this.jwtService = jwtService;
127
    this.githubTeamService = githubTeamService;
128
    this.teamRepository = teamRepository;
129
    this.restTemplate = restTemplateBuilder.build();
130
    this.mapper = mapper;
131
  }
132
133
  /**
134
   * Creates a single student repository if it doesn't already exist, and provisions access to the
135
   * repository by that student
136
   *
137
   * @param course The Course in question
138
   * @param student RosterStudent of the student the repository should be created for
139
   * @param repoPrefix Name of the project or assignment. Used to title the repository, in the
140
   *     format repoPrefix-githubLogin
141
   * @param isPrivate Whether the repository is private or not
142
   */
143
  public void createStudentRepository(
144
      Course course,
145
      RosterStudent student,
146
      String repoPrefix,
147
      Boolean isPrivate,
148
      RepositoryPermissions permissions)
149
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
150 1 1. createStudentRepository : removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED
    createRepositoryForStudentOrStaff(
151
        course, student.getGithubLogin(), repoPrefix, isPrivate, permissions);
152
  }
153
154
  /**
155
   * Creates a single staff repository if it doesn't already exist, and provisions access to the
156
   * repository by that staff member
157
   *
158
   * @param course The Course in question
159
   * @param staff CourseStaff of the staff the repository should be created for
160
   * @param repoPrefix Name of the project or assignment. Used to title the repository, in the
161
   *     format repoPrefix-githubLogin
162
   * @param isPrivate Whether the repository is private or not
163
   */
164
  public void createStaffRepository(
165
      Course course,
166
      CourseStaff staff,
167
      String repoPrefix,
168
      Boolean isPrivate,
169
      RepositoryPermissions permissions)
170
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
171
172 1 1. createStaffRepository : removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED
    createRepositoryForStudentOrStaff(
173
        course, staff.getGithubLogin(), repoPrefix, isPrivate, permissions);
174
  }
175
176
  /**
177
   * Creates a GitHub repository for a team (student or staff), given only their team name.
178
   *
179
   * <ul>
180
   *   <li>Checks whether the repository already exists.
181
   *   <li>If not, creates a new repository under the course's organization.
182
   *   <li>Adds all team members as collaborators with the given permission level.
183
   * </ul>
184
   *
185
   * @param course the course whose organization the repo belongs to
186
   * @param team the team for which the repo is being created
187
   * @param repoPrefix prefix for the repository name (repoPrefix-teamSlug)
188
   * @param isPrivate whether the created repository should be private
189
   * @param permissions collaborator permissions to grant the user
190
   * @param orgId GitHub organization ID used for team-based repo provisioning
191
   * @throws NoSuchAlgorithmException if signing fails
192
   * @throws InvalidKeySpecException if signing fails
193
   * @throws JsonProcessingException if JSON serialization fails
194
   */
195
  public void createTeamRepository(
196
      Course course,
197
      Team team,
198
      String repoPrefix,
199
      Boolean isPrivate,
200
      RepositoryPermissions permissions,
201
      Integer orgId)
202
      throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
203
    String teamSlug = getOrFetchTeamSlug(course, team, orgId);
204
    String newRepoName = repoPrefix + "-" + teamSlug;
205
    String token = jwtService.getInstallationToken(course);
206
207
    String existenceEndpoint =
208
        "https://api.github.com/repos/" + course.getOrgName() + "/" + newRepoName;
209
    String createEndpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/repos";
210
    String provisionEndpoint =
211
        "https://api.github.com/organizations/"
212
            + orgId
213
            + "/team/"
214
            + team.getGithubTeamId()
215
            + "/repos/"
216
            + course.getOrgName()
217
            + "/"
218
            + newRepoName;
219
220
    HttpHeaders existenceHeaders = new HttpHeaders();
221 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Authorization", "Bearer " + token);
222 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("Accept", "application/vnd.github+json");
223 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
    existenceHeaders.add("X-GitHub-Api-Version", "2022-11-28");
224
225
    HttpEntity<String> existenceEntity = new HttpEntity<>(existenceHeaders);
226
227
    try {
228
      restTemplate.exchange(existenceEndpoint, HttpMethod.GET, existenceEntity, String.class);
229
    } catch (HttpClientErrorException e) {
230 1 1. createTeamRepository : negated conditional → KILLED
      if (e.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
231
        HttpHeaders createHeaders = new HttpHeaders();
232 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Authorization", "Bearer " + token);
233 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("Accept", "application/vnd.github+json");
234 1 1. createTeamRepository : removed call to org/springframework/http/HttpHeaders::add → KILLED
        createHeaders.add("X-GitHub-Api-Version", "2022-11-28");
235
236
        Map<String, Object> body = new HashMap<>();
237
        body.put("name", newRepoName);
238
        body.put("private", isPrivate);
239
        String bodyAsJson = mapper.writeValueAsString(body);
240
241
        HttpEntity<String> createEntity = new HttpEntity<>(bodyAsJson, createHeaders);
242
243
        restTemplate.exchange(createEndpoint, HttpMethod.POST, createEntity, String.class);
244
      } else {
245
        log.warn(
246
            "Unexpected response code {} when checking for existence of repository {}",
247
            e.getStatusCode(),
248
            newRepoName);
249
        return;
250
      }
251
    }
252
    try {
253
      Map<String, Object> provisionBody = new HashMap<>();
254
      provisionBody.put("permission", permissions.getApiName());
255
      String provisionAsJson = mapper.writeValueAsString(provisionBody);
256
257
      HttpEntity<String> provisionEntity = new HttpEntity<>(provisionAsJson, existenceHeaders);
258
      restTemplate.exchange(provisionEndpoint, HttpMethod.PUT, provisionEntity, String.class);
259
    } catch (HttpClientErrorException ignored) {
260
      // silently ignore if provisioning fails (same as before)
261
    }
262
  }
263
264
  private String getOrFetchTeamSlug(Course course, Team team, Integer orgId)
265
      throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
266 2 1. getOrFetchTeamSlug : negated conditional → KILLED
2. getOrFetchTeamSlug : negated conditional → KILLED
    if (team.getGithubTeamSlug() != null && !team.getGithubTeamSlug().isBlank()) {
267 1 1. getOrFetchTeamSlug : replaced return value with "" for edu/ucsb/cs156/frontiers/services/RepositoryService::getOrFetchTeamSlug → KILLED
      return team.getGithubTeamSlug();
268
    }
269
270 1 1. getOrFetchTeamSlug : negated conditional → KILLED
    if (team.getGithubTeamId() == null) {
271
      throw new IllegalStateException(
272
          "Cannot create team repository without a GitHub team ID for team '"
273
              + team.getName()
274
              + "'");
275
    }
276
277
    GithubTeamService.GithubTeamInfo teamInfo =
278
        githubTeamService.getTeamInfoById(orgId, team.getGithubTeamId(), course);
279
280 3 1. getOrFetchTeamSlug : negated conditional → KILLED
2. getOrFetchTeamSlug : negated conditional → KILLED
3. getOrFetchTeamSlug : negated conditional → KILLED
    if (teamInfo == null || teamInfo.slug() == null || teamInfo.slug().isBlank()) {
281
      throw new IllegalStateException(
282
          "Cannot determine GitHub team slug for team '" + team.getName() + "'");
283
    }
284
285 1 1. getOrFetchTeamSlug : removed call to edu/ucsb/cs156/frontiers/entities/Team::setGithubTeamSlug → KILLED
    team.setGithubTeamSlug(teamInfo.slug());
286
    teamRepository.save(team);
287 1 1. getOrFetchTeamSlug : replaced return value with "" for edu/ucsb/cs156/frontiers/services/RepositoryService::getOrFetchTeamSlug → KILLED
    return teamInfo.slug();
288
  }
289
}

Mutations

76

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:warn_on_not_no_content()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

77

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:warn_on_not_no_content()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

78

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:warn_on_not_no_content()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

85

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_not_not_found()]
negated conditional → KILLED

87

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

88

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

89

1.1
Location : createRepositoryForStudentOrStaff
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

150

1.1
Location : createStudentRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:warn_on_not_no_content()]
removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED

172

1.1
Location : createStaffRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_staff_repo_public()]
removed call to edu/ucsb/cs156/frontiers/services/RepositoryService::createRepositoryForStudentOrStaff → KILLED

221

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

222

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

223

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

230

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
negated conditional → KILLED

232

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_team_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

233

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_team_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

234

1.1
Location : createTeamRepository
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:successfully_creates_team_repo_public()]
removed call to org/springframework/http/HttpHeaders::add → KILLED

266

1.1
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_throws_when_slug_missing_and_team_id_missing()]
negated conditional → KILLED

2.2
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
negated conditional → KILLED

267

1.1
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:exits_if_team_repo_not_not_found()]
replaced return value with "" for edu/ucsb/cs156/frontiers/services/RepositoryService::getOrFetchTeamSlug → KILLED

270

1.1
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_throws_when_slug_missing_and_team_id_missing()]
negated conditional → KILLED

280

1.1
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_throws_when_fetched_team_slug_is_blank()]
negated conditional → KILLED

2.2
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_throws_when_fetched_team_slug_is_null()]
negated conditional → KILLED

3.3
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_throws_when_fetched_team_info_is_null()]
negated conditional → KILLED

285

1.1
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_fetches_and_persists_slug_when_missing()]
removed call to edu/ucsb/cs156/frontiers/entities/Team::setGithubTeamSlug → KILLED

287

1.1
Location : getOrFetchTeamSlug
Killed by : edu.ucsb.cs156.frontiers.services.RepositoryServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.RepositoryServiceTests]/[method:team_repo_fetches_and_persists_slug_when_missing()]
replaced return value with "" for edu/ucsb/cs156/frontiers/services/RepositoryService::getOrFetchTeamSlug → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0