CanvasService.java

1
package edu.ucsb.cs156.frontiers.services;
2
3
import com.fasterxml.jackson.databind.DeserializationFeature;
4
import com.fasterxml.jackson.databind.JsonNode;
5
import com.fasterxml.jackson.databind.ObjectMapper;
6
import edu.ucsb.cs156.frontiers.entities.Course;
7
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
8
import edu.ucsb.cs156.frontiers.models.CanvasGroup;
9
import edu.ucsb.cs156.frontiers.models.CanvasGroupSet;
10
import edu.ucsb.cs156.frontiers.models.CanvasStudent;
11
import edu.ucsb.cs156.frontiers.utilities.CanonicalFormConverter;
12
import edu.ucsb.cs156.frontiers.validators.HasLinkedCanvasCourse;
13
import java.util.ArrayList;
14
import java.util.List;
15
import org.springframework.graphql.client.HttpSyncGraphQlClient;
16
import org.springframework.stereotype.Service;
17
import org.springframework.validation.annotation.Validated;
18
import org.springframework.web.client.RestClient;
19
20
/**
21
 * Service for interacting with the Canvas API.
22
 *
23
 * <p>Note that the Canvas API uses a GraphQL endpoint, which allows for more flexible queries
24
 * compared to traditional REST APIs.
25
 *
26
 * <p>For more information on the Canvas API, visit the official documentation at <a
27
 * href="https://canvas.instructure.com/doc/api/">...</a>.
28
 *
29
 * <p>You can typically interact with Canvas API GraphQL endpoints interactively by appending
30
 * /graphiql to the URL of the Canvas instance.
31
 *
32
 * <p>For example, for UCSB Canvas, use: <a href="https://ucsb.instructure.com/graphiql">...</a>
33
 */
34
@Service
35
@Validated
36
public class CanvasService {
37
38
  private HttpSyncGraphQlClient graphQlClient;
39
  private ObjectMapper mapper;
40
  private CanvasApiTokenSecurityService canvasApiTokenSecurityService;
41
42
  public CanvasService(
43
      ObjectMapper mapper,
44
      RestClient.Builder builder,
45
      CanvasApiTokenSecurityService canvasApiTokenSecurityService) {
46
    this.graphQlClient = HttpSyncGraphQlClient.builder(builder.build()).build();
47
    this.mapper = mapper;
48
    this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
49
    this.canvasApiTokenSecurityService = canvasApiTokenSecurityService;
50
  }
51
52
  public List<CanvasGroupSet> getCanvasGroupSets(@HasLinkedCanvasCourse Course course) {
53
    // language=GraphQL
54
    String query =
55
        """
56
        query GetGroupSets($courseId: ID!) {
57
          course(id: $courseId) {
58
            groupSets {
59
              _id
60
              name
61
              id
62
            }
63
          }
64
        }
65
        """;
66
67
    HttpSyncGraphQlClient authedClient =
68
        graphQlClient
69
            .mutate()
70
            .header(
71
                "Authorization",
72
                "Bearer " + canvasApiTokenSecurityService.decrypt(course.getCanvasApiToken()))
73
            .url(course.getSchool().getCanvasImplementation())
74
            .build();
75
76
    List<CanvasGroupSet> groupSets =
77
        authedClient
78
            .document(query)
79
            .variable("courseId", course.getCanvasCourseId())
80
            .retrieveSync("course.groupSets")
81
            .toEntityList(CanvasGroupSet.class);
82 1 1. getCanvasGroupSets : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroupSets → KILLED
    return groupSets;
83
  }
84
85
  /**
86
   * Fetches the roster of students from Canvas for the given course.
87
   *
88
   * @param course the Course entity containing canvasApiToken and canvasCourseId
89
   * @return list of RosterStudent objects from Canvas
90
   */
91
  public List<RosterStudent> getCanvasRoster(@HasLinkedCanvasCourse Course course) {
92
93
    // language=GraphQL
94
    String query =
95
        """
96
              query GetRoster($courseId: ID!) {
97
              course(id: $courseId) {
98
                usersConnection(filter: {enrollmentTypes: StudentEnrollment}) {
99
                  edges {
100
                    node {
101
                      firstName
102
                      lastName
103
                      sisId
104
                      email
105
                      integrationId
106
                    }
107
                  }
108
                }
109
              }
110
            }
111
            """;
112
113
    HttpSyncGraphQlClient authedClient =
114
        graphQlClient
115
            .mutate()
116
            .header(
117
                "Authorization",
118
                "Bearer " + canvasApiTokenSecurityService.decrypt(course.getCanvasApiToken()))
119
            .url(course.getSchool().getCanvasImplementation())
120
            .build();
121
122
    List<CanvasStudent> students =
123
        authedClient
124
            .document(query)
125
            .variable("courseId", course.getCanvasCourseId())
126
            .retrieveSync("course.usersConnection.edges")
127
            .toEntityList(JsonNode.class)
128
            .stream()
129 1 1. lambda$getCanvasRoster$0 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$0 → KILLED
            .map(node -> mapper.convertValue(node.get("node"), CanvasStudent.class))
130
            .toList();
131
132 1 1. getCanvasRoster : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasRoster → KILLED
    return students.stream()
133
        .map(
134
            student ->
135 1 1. lambda$getCanvasRoster$1 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$1 → KILLED
                RosterStudent.builder()
136
                    .firstName(student.getFirstName())
137
                    .lastName(student.getLastName())
138
                    .studentId(student.getStudentId())
139
                    .email(student.getEmail())
140
                    .build())
141
        .toList();
142
  }
143
144
  public List<CanvasGroup> getCanvasGroups(
145
      @HasLinkedCanvasCourse Course course, String groupSetId) {
146
    // language=GraphQL
147
    String query =
148
        """
149
            query GetTeams($groupId: ID!) {
150
              node(id: $groupId) {
151
                ... on GroupSet {
152
                  id
153
                  name
154
                  groups {
155
                    name
156
                    _id
157
                    membersConnection {
158
                      edges {
159
                        node {
160
                          user {
161
                            email
162
                          }
163
                        }
164
                      }
165
                    }
166
                  }
167
                }
168
              }
169
            }
170
            """;
171
172
    HttpSyncGraphQlClient authedClient =
173
        graphQlClient
174
            .mutate()
175
            .header(
176
                "Authorization",
177
                "Bearer " + canvasApiTokenSecurityService.decrypt(course.getCanvasApiToken()))
178
            .url(course.getSchool().getCanvasImplementation())
179
            .build();
180
181
    List<JsonNode> groups =
182
        authedClient
183
            .document(query)
184
            .variable("groupId", groupSetId)
185
            .retrieveSync("node.groups")
186
            .toEntityList(JsonNode.class);
187
188
    List<CanvasGroup> parsedGroups =
189
        groups.stream()
190
            .map(
191
                group -> {
192
                  CanvasGroup canvasGroup =
193
                      CanvasGroup.builder()
194
                          .name(group.get("name").asText())
195
                          .id(group.get("_id").asInt())
196
                          .members(new ArrayList<>())
197
                          .build();
198
                  group
199
                      .get("membersConnection")
200
                      .get("edges")
201 1 1. lambda$getCanvasGroups$3 : removed call to com/fasterxml/jackson/databind/JsonNode::forEach → KILLED
                      .forEach(
202
                          edge -> {
203
                            canvasGroup
204
                                .getMembers()
205
                                .add(
206
                                    CanonicalFormConverter.convertToValidEmail(
207
                                        edge.path("node").path("user").get("email").asText()));
208
                          });
209 1 1. lambda$getCanvasGroups$3 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasGroups$3 → KILLED
                  return canvasGroup;
210
                })
211
            .toList();
212
213 1 1. getCanvasGroups : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroups → KILLED
    return parsedGroups;
214
  }
215
}

Mutations

82

1.1
Location : getCanvasGroupSets
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasGroupSets_returnsGroupSets()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroupSets → KILLED

129

1.1
Location : lambda$getCanvasRoster$0
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasRoster_usesIntegrationIdWhenPresent()]
replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$0 → KILLED

132

1.1
Location : getCanvasRoster
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasRoster_usesIntegrationIdWhenPresent()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasRoster → KILLED

135

1.1
Location : lambda$getCanvasRoster$1
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasRoster_usesIntegrationIdWhenPresent()]
replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$1 → KILLED

201

1.1
Location : lambda$getCanvasGroups$3
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasGroups_returnsGroups()]
removed call to com/fasterxml/jackson/databind/JsonNode::forEach → KILLED

209

1.1
Location : lambda$getCanvasGroups$3
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasGroups_handlesGroupWithNoMembers()]
replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasGroups$3 → KILLED

213

1.1
Location : getCanvasGroups
Killed by : edu.ucsb.cs156.frontiers.services.CanvasServiceTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.frontiers.services.CanvasServiceTests]/[method:testGetCanvasGroups_handlesGroupWithNoMembers()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroups → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0