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 TokenEncryptionService tokenEncryptionService;
41
42
  private static final String CANVAS_GRAPHQL_URL = "https://ucsb.instructure.com/api/graphql";
43
44
  public CanvasService(
45
      ObjectMapper mapper,
46
      RestClient.Builder builder,
47
      TokenEncryptionService tokenEncryptionService) {
48
    this.graphQlClient =
49
        HttpSyncGraphQlClient.builder(builder.baseUrl(CANVAS_GRAPHQL_URL).build()).build();
50
    this.mapper = mapper;
51
    this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
52
    this.tokenEncryptionService = tokenEncryptionService;
53
  }
54
55
  public List<CanvasGroupSet> getCanvasGroupSets(@HasLinkedCanvasCourse Course course) {
56
    // language=GraphQL
57
    String query =
58
        """
59
        query GetGroupSets($courseId: ID!) {
60
          course(id: $courseId) {
61
            groupSets {
62
              _id
63
              name
64
              id
65
            }
66
          }
67
        }
68
        """;
69
70
    HttpSyncGraphQlClient authedClient =
71
        graphQlClient
72
            .mutate()
73
            .header(
74
                "Authorization",
75
                "Bearer " + tokenEncryptionService.decryptToken(course.getCanvasApiToken()))
76
            .build();
77
78
    List<CanvasGroupSet> groupSets =
79
        authedClient
80
            .document(query)
81
            .variable("courseId", course.getCanvasCourseId())
82
            .retrieveSync("course.groupSets")
83
            .toEntityList(CanvasGroupSet.class);
84 1 1. getCanvasGroupSets : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroupSets → KILLED
    return groupSets;
85
  }
86
87
  /**
88
   * Fetches the roster of students from Canvas for the given course.
89
   *
90
   * @param course the Course entity containing canvasApiToken and canvasCourseId
91
   * @return list of RosterStudent objects from Canvas
92
   */
93
  public List<RosterStudent> getCanvasRoster(@HasLinkedCanvasCourse Course course) {
94
95
    // language=GraphQL
96
    String query =
97
        """
98
              query GetRoster($courseId: ID!) {
99
              course(id: $courseId) {
100
                usersConnection(filter: {enrollmentTypes: StudentEnrollment}) {
101
                  edges {
102
                    node {
103
                      firstName
104
                      lastName
105
                      sisId
106
                      email
107
                      integrationId
108
                    }
109
                  }
110
                }
111
              }
112
            }
113
            """;
114
115
    HttpSyncGraphQlClient authedClient =
116
        graphQlClient
117
            .mutate()
118
            .header(
119
                "Authorization",
120
                "Bearer " + tokenEncryptionService.decryptToken(course.getCanvasApiToken()))
121
            .build();
122
123
    List<CanvasStudent> students =
124
        authedClient
125
            .document(query)
126
            .variable("courseId", course.getCanvasCourseId())
127
            .retrieveSync("course.usersConnection.edges")
128
            .toEntityList(JsonNode.class)
129
            .stream()
130 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))
131
            .toList();
132
133 1 1. getCanvasRoster : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasRoster → KILLED
    return students.stream()
134
        .map(
135
            student ->
136 1 1. lambda$getCanvasRoster$1 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$1 → KILLED
                RosterStudent.builder()
137
                    .firstName(student.getFirstName())
138
                    .lastName(student.getLastName())
139
                    .studentId(student.getStudentId())
140
                    .email(student.getEmail())
141
                    .build())
142
        .toList();
143
  }
144
145
  public List<CanvasGroup> getCanvasGroups(
146
      @HasLinkedCanvasCourse Course course, String groupSetId) {
147
    // language=GraphQL
148
    String query =
149
        """
150
            query GetTeams($groupId: ID!) {
151
              node(id: $groupId) {
152
                ... on GroupSet {
153
                  id
154
                  name
155
                  groups {
156
                    name
157
                    _id
158
                    membersConnection {
159
                      edges {
160
                        node {
161
                          user {
162
                            email
163
                          }
164
                        }
165
                      }
166
                    }
167
                  }
168
                }
169
              }
170
            }
171
            """;
172
173
    HttpSyncGraphQlClient authedClient =
174
        graphQlClient
175
            .mutate()
176
            .header(
177
                "Authorization",
178
                "Bearer " + tokenEncryptionService.decryptToken(course.getCanvasApiToken()))
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

84

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

130

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

133

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

136

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