GithubGraphQLController.java

1
package edu.ucsb.cs156.frontiers.controllers;
2
3
import com.opencsv.bean.StatefulBeanToCsv;
4
import com.opencsv.exceptions.CsvFieldAssignmentException;
5
import edu.ucsb.cs156.frontiers.entities.Branch;
6
import edu.ucsb.cs156.frontiers.entities.BranchId;
7
import edu.ucsb.cs156.frontiers.entities.Course;
8
import edu.ucsb.cs156.frontiers.entities.Job;
9
import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException;
10
import edu.ucsb.cs156.frontiers.jobs.LoadCommitHistoryJob;
11
import edu.ucsb.cs156.frontiers.models.CommitDto;
12
import edu.ucsb.cs156.frontiers.repositories.BranchRepository;
13
import edu.ucsb.cs156.frontiers.repositories.CommitRepository;
14
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
15
import edu.ucsb.cs156.frontiers.services.GithubGraphQLService;
16
import edu.ucsb.cs156.frontiers.services.GithubGraphQLService.ValidationStatus;
17
import edu.ucsb.cs156.frontiers.services.jobs.JobService;
18
import edu.ucsb.cs156.frontiers.utilities.StatefulBeanToCsvBuilderFactory;
19
import io.swagger.v3.oas.annotations.Operation;
20
import io.swagger.v3.oas.annotations.Parameter;
21
import io.swagger.v3.oas.annotations.tags.Tag;
22
import jakarta.validation.Valid;
23
import java.io.OutputStreamWriter;
24
import java.time.Instant;
25
import java.util.List;
26
import java.util.Map;
27
import java.util.Map.Entry;
28
import lombok.extern.slf4j.Slf4j;
29
import org.springframework.beans.factory.annotation.Autowired;
30
import org.springframework.http.HttpHeaders;
31
import org.springframework.http.HttpStatus;
32
import org.springframework.http.MediaType;
33
import org.springframework.http.ResponseEntity;
34
import org.springframework.security.access.prepost.PreAuthorize;
35
import org.springframework.web.bind.annotation.GetMapping;
36
import org.springframework.web.bind.annotation.PostMapping;
37
import org.springframework.web.bind.annotation.RequestBody;
38
import org.springframework.web.bind.annotation.RequestMapping;
39
import org.springframework.web.bind.annotation.RequestParam;
40
import org.springframework.web.bind.annotation.RestController;
41
import org.springframework.web.server.ResponseStatusException;
42
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
43
44
@Tag(name = "GithubGraphQL")
45
@RequestMapping("/api/github/graphql/")
46
@RestController
47
@Slf4j
48
public class GithubGraphQLController extends ApiController {
49
50
  private final GithubGraphQLService githubGraphQLService;
51
  private final CourseRepository courseRepository;
52
  private final JobService jobService;
53
  private final CommitRepository commitRepository;
54
  private final StatefulBeanToCsvBuilderFactory statefulBeanToCsvBuilderFactory;
55
  private final BranchRepository branchRepository;
56
57
  public GithubGraphQLController(
58
      @Autowired GithubGraphQLService gitHubGraphQLService,
59
      @Autowired CourseRepository courseRepository,
60
      JobService jobService,
61
      CommitRepository commitRepository,
62
      StatefulBeanToCsvBuilderFactory statefulBeanToCsvBuilderFactory,
63
      BranchRepository branchRepository) {
64
    this.githubGraphQLService = gitHubGraphQLService;
65
    this.courseRepository = courseRepository;
66
    this.jobService = jobService;
67
    this.statefulBeanToCsvBuilderFactory = statefulBeanToCsvBuilderFactory;
68
    this.commitRepository = commitRepository;
69
    this.branchRepository = branchRepository;
70
  }
71
72
  /**
73
   * Return default branch name for a given repository.
74
   *
75
   * @param courseId the id of the course whose installation is being used for credentails
76
   * @param owner the owner of the repository
77
   * @param repo the name of the repository
78
   * @return the default branch name
79
   */
80
  @Operation(summary = "Get default branch name")
81
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
82
  @GetMapping("defaultBranchName")
83
  public String getDefaultBranchName(
84
      @Parameter Long courseId, @Parameter String owner, @Parameter String repo) throws Exception {
85
    log.info(
86
        "getDefaultBranchName called with courseId: {}, owner: {}, repo: {}",
87
        courseId,
88
        owner,
89
        repo);
90
    Course course =
91
        courseRepository
92
            .findById(courseId)
93 1 1. lambda$getDefaultBranchName$0 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getDefaultBranchName$0 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
94
95
    log.info("Found course: {}", course);
96
97
    log.info("Current user is authorized to access course: {}", course.getId());
98
99
    String result = this.githubGraphQLService.getDefaultBranchName(course, owner, repo);
100
101
    log.info("Result from getDefaultBranchName: {}", result);
102
103 1 1. getDefaultBranchName : replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::getDefaultBranchName → KILLED
    return result;
104
  }
105
106
  /**
107
   * Return default branch name for a given repository.
108
   *
109
   * @param courseId the id of the course whose installation is being used for credentails
110
   * @param owner the owner of the repository
111
   * @param repo the name of the repository
112
   * @return the default branch name
113
   */
114
  @Operation(summary = "Get commits")
115
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
116
  @GetMapping("commits")
117
  public String getCommits(
118
      @Parameter Long courseId,
119
      @Parameter String owner,
120
      @Parameter String repo,
121
      @Parameter String branch,
122
      @Parameter Integer first,
123
      @RequestParam(name = "after", required = false) @Parameter String after)
124
      throws Exception {
125
    log.info(
126
        "getCommits called with courseId: {}, owner: {}, repo: {}, branch: {}, first: {}, after: {} ",
127
        courseId,
128
        owner,
129
        repo,
130
        branch,
131
        first,
132
        after);
133
    Course course =
134
        courseRepository
135
            .findById(courseId)
136 1 1. lambda$getCommits$1 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getCommits$1 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
137
138
    log.info("Found course: {}", course);
139
140
    log.info("Current user is authorized to access course: {}", course.getId());
141
142
    String result = this.githubGraphQLService.getCommits(course, owner, repo, branch, first, after);
143
144
    log.info("Result from getCommits: {}", result);
145
146 1 1. getCommits : replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::getCommits → KILLED
    return result;
147
  }
148
149
  /**
150
   * Returns a job to load the commit data of a number of branches
151
   *
152
   * @param courseId the id of the course whose installation is being used for credentials
153
   * @param branches the list of branches to load
154
   * @return the job identifier
155
   */
156
  @Operation(summary = "Get commits", description = "Loads commit history for the given branches")
157
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
158
  @PostMapping("history")
159
  public Job loadCommitHistory(
160
      @Parameter Long courseId, @Valid @RequestBody List<BranchId> branches) throws Exception {
161
    log.debug(
162
        "Commit History loader called with courseId {} for the following branches: {}",
163
        courseId,
164
        branches);
165
    Course course =
166
        courseRepository
167
            .findById(courseId)
168 1 1. lambda$loadCommitHistory$2 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$loadCommitHistory$2 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
169
170 1 1. loadCommitHistory : negated conditional → KILLED
    if (branches.isEmpty()) {
171
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No branches specified");
172
    }
173
174
    Map<BranchId, ValidationStatus> validBranches =
175
        githubGraphQLService.assertBranchesExist(course, branches);
176
177
    List<Entry<BranchId, ValidationStatus>> invalidBranches =
178
        validBranches.entrySet().stream()
179 2 1. lambda$loadCommitHistory$3 : replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$loadCommitHistory$3 → KILLED
2. lambda$loadCommitHistory$3 : negated conditional → KILLED
            .filter(entry -> entry.getValue() != ValidationStatus.EXISTS)
180
            .toList();
181
182 1 1. loadCommitHistory : negated conditional → KILLED
    if (!invalidBranches.isEmpty()) {
183
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid branches specified");
184
    }
185
186
    LoadCommitHistoryJob job =
187
        LoadCommitHistoryJob.builder()
188
            .course(course)
189
            .branches(branches)
190
            .githubService(githubGraphQLService)
191
            .build();
192 1 1. loadCommitHistory : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::loadCommitHistory → KILLED
    return jobService.runAsJob(job);
193
  }
194
195
  @Operation(
196
      summary = "Get commits as a CSV",
197
      description = "Returns preloaded commit history for the given branches as a CSV")
198
  @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
199
  @PostMapping(value = "csv", produces = "text/csv")
200
  public ResponseEntity<StreamingResponseBody> getCommitsCsv(
201
      @Parameter Long courseId,
202
      @Parameter Instant start,
203
      @Parameter Instant end,
204
      @Parameter Boolean skipMergeCommits,
205
      @Valid @RequestBody List<BranchId> branches)
206
      throws Exception {
207
208
    List<Branch> selectedBranches = branchRepository.findByIdIn(branches);
209
210 1 1. getCommitsCsv : negated conditional → KILLED
    if (branches.isEmpty()) {
211
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No branches specified");
212
    }
213
214 1 1. getCommitsCsv : negated conditional → KILLED
    if (start.isAfter(end)) {
215
      throw new ResponseStatusException(
216
          HttpStatus.BAD_REQUEST, "Start time must be before end time.");
217
    }
218
219 1 1. getCommitsCsv : negated conditional → KILLED
    if (selectedBranches.size() != branches.size()) {
220
      throw new ResponseStatusException(
221
          HttpStatus.BAD_REQUEST,
222
          "One or more branches not found; Please load commit history for those branches first.");
223
    }
224
225 3 1. getCommitsCsv : negated conditional → KILLED
2. lambda$getCommitsCsv$4 : replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getCommitsCsv$4 → KILLED
3. lambda$getCommitsCsv$4 : replaced boolean return with false for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getCommitsCsv$4 → KILLED
    if (selectedBranches.stream().anyMatch(branch -> branch.getRetrievedTime().isBefore(end))) {
226
      throw new ResponseStatusException(
227
          HttpStatus.BAD_REQUEST,
228
          "One or more branches have not been updated since the requested end time; Please load commit history for those branches first.");
229
    }
230
231
    StreamingResponseBody stream =
232
        (outputStream) -> {
233
          try (var writer = new OutputStreamWriter(outputStream)) {
234
            StatefulBeanToCsv<CommitDto> csvWriter = statefulBeanToCsvBuilderFactory.build(writer);
235
            List<CommitDto> commits;
236 1 1. lambda$getCommitsCsv$5 : negated conditional → KILLED
            if (skipMergeCommits) {
237
              commits =
238
                  commitRepository.findByBranchIdInAndCommitTimeBetweenAndIsMergeCommitEquals(
239
                      branches, start, end, false);
240
            } else {
241
              commits = commitRepository.findByBranchIdInAndCommitTimeBetween(branches, start, end);
242
            }
243
            try {
244 1 1. lambda$getCommitsCsv$5 : removed call to com/opencsv/bean/StatefulBeanToCsv::write → KILLED
              csvWriter.write(commits);
245
            } catch (CsvFieldAssignmentException ignored) {
246 1 1. lambda$getCommitsCsv$5 : removed call to java/io/OutputStreamWriter::write → KILLED
              writer.write("Error writing CSV file");
247
            }
248
          }
249
        };
250
251 1 1. getCommitsCsv : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::getCommitsCsv → KILLED
    return ResponseEntity.ok()
252
        .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
253
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=commit_history.csv")
254
        .header(HttpHeaders.CONTENT_TYPE, "text/csv; charset=UTF-8")
255
        .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
256
        .body(stream);
257
  }
258
}

Mutations

93

1.1
Location : lambda$getDefaultBranchName$0
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:test_getDefaultMainBranch_courseNotFound()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getDefaultBranchName$0 → KILLED

103

1.1
Location : getDefaultBranchName
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:test_getDefaultMainBranch_happyPath_Admin()]
replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::getDefaultBranchName → KILLED

136

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

146

1.1
Location : getCommits
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:test_getCommits_happyPath_Admin()]
replaced return value with "" for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::getCommits → KILLED

168

1.1
Location : lambda$loadCommitHistory$2
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:course_not_found()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$loadCommitHistory$2 → KILLED

170

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

179

1.1
Location : lambda$loadCommitHistory$3
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:successful_launch_load_history_job()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$loadCommitHistory$3 → KILLED

2.2
Location : lambda$loadCommitHistory$3
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:branches_invalid()]
negated conditional → KILLED

182

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

192

1.1
Location : loadCommitHistory
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:successful_launch_load_history_job()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::loadCommitHistory → KILLED

210

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

214

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

219

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

225

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

2.2
Location : lambda$getCommitsCsv$4
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:writes_commit_correctly()]
replaced boolean return with true for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getCommitsCsv$4 → KILLED

3.3
Location : lambda$getCommitsCsv$4
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:branch_not_updated()]
replaced boolean return with false for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::lambda$getCommitsCsv$4 → KILLED

236

1.1
Location : lambda$getCommitsCsv$5
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:writes_commit_correctly()]
negated conditional → KILLED

244

1.1
Location : lambda$getCommitsCsv$5
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:writes_commit_correctly()]
removed call to com/opencsv/bean/StatefulBeanToCsv::write → KILLED

246

1.1
Location : lambda$getCommitsCsv$5
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:streaming_throws_error()]
removed call to java/io/OutputStreamWriter::write → KILLED

251

1.1
Location : getCommitsCsv
Killed by : edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.controllers.GithubGraphQLControllerTests]/[method:writes_commit_correctly()]
replaced return value with null for edu/ucsb/cs156/frontiers/controllers/GithubGraphQLController::getCommitsCsv → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0