UserStateController.java

1
package edu.ucsb.cs.scaffold.controller;
2
3
import com.fasterxml.jackson.annotation.JsonProperty;
4
import com.fasterxml.jackson.core.JsonProcessingException;
5
import com.fasterxml.jackson.core.type.TypeReference;
6
import com.fasterxml.jackson.databind.JsonNode;
7
import com.fasterxml.jackson.databind.ObjectMapper;
8
import edu.ucsb.cs.scaffold.model.UserState;
9
import edu.ucsb.cs.scaffold.repository.UserStateRepository;
10
import io.swagger.v3.oas.annotations.Operation;
11
import io.swagger.v3.oas.annotations.Parameter;
12
import io.swagger.v3.oas.annotations.tags.Tag;
13
import java.util.List;
14
import lombok.RequiredArgsConstructor;
15
import org.springframework.dao.DataIntegrityViolationException;
16
import org.springframework.http.ResponseEntity;
17
import org.springframework.web.bind.annotation.*;
18
19
@Tag(name = "User State")
20
@RestController
21
@RequiredArgsConstructor
22
public class UserStateController {
23
24
  private final UserStateRepository userStateRepository;
25
  private final ObjectMapper objectMapper;
26
27
  @Operation(summary = "Get saved user state by user ID and course ID")
28
  @GetMapping("/api/user-state")
29
  public ResponseEntity<UserStateResponse> getUserState(
30
      @Parameter(description = "User ID (from users table)") @RequestParam Long userid,
31
      @Parameter(description = "Course ID (from course table)") @RequestParam Long courseId) {
32
    // A user with no saved state yet gets empty defaults rather than a 404, so the
33
    // frontend's useBackend query treats "brand new user" as data, not an error.
34 1 1. getUserState : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::getUserState → KILLED
    return ResponseEntity.ok(
35
        userStateRepository
36
            .findByUseridAndCourseId(userid, courseId)
37
            .map(
38
                state ->
39 1 1. lambda$getUserState$0 : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::lambda$getUserState$0 → KILLED
                    new UserStateResponse(
40
                        parseStringList(state.getStarredIds()),
41
                        parseJsonNode(state.getDetailCards()),
42
                        parseStringList(state.getMasteredSubconcepts()),
43
                        parseJsonNode(state.getTopLevelPositions())))
44
            .orElseGet(
45
                () ->
46 1 1. lambda$getUserState$1 : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::lambda$getUserState$1 → KILLED
                    new UserStateResponse(
47
                        List.of(),
48
                        objectMapper.createArrayNode(),
49
                        List.of(),
50
                        objectMapper.createObjectNode())));
51
  }
52
53
  @Operation(summary = "Upsert saved user state by user ID and course ID")
54
  @PostMapping("/api/user-state")
55
  public ResponseEntity<Void> upsertUserState(@RequestBody UserStateRequest body) {
56 2 1. upsertUserState : negated conditional → KILLED
2. upsertUserState : negated conditional → KILLED
    if (body.userid() == null || body.courseId() == null) {
57 1 1. upsertUserState : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::upsertUserState → KILLED
      return ResponseEntity.badRequest().build();
58
    }
59
60
    try {
61 1 1. upsertUserState : removed call to edu/ucsb/cs/scaffold/controller/UserStateController::upsert → KILLED
      upsert(body);
62
    } catch (DataIntegrityViolationException e) {
63
      // Two near-simultaneous saves for a brand new (userid, courseId) pair (e.g. React
64
      // StrictMode double-invoking a state updater in dev) can both find no existing row and
65
      // both attempt an insert; the second violates the unique constraint. Retry once as an
66
      // update against the row the other request just inserted.
67 1 1. upsertUserState : removed call to edu/ucsb/cs/scaffold/controller/UserStateController::upsert → KILLED
      upsert(body);
68
    }
69
70 1 1. upsertUserState : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::upsertUserState → KILLED
    return ResponseEntity.noContent().build();
71
  }
72
73
  private void upsert(UserStateRequest body) {
74
    UserState state =
75
        userStateRepository
76
            .findByUseridAndCourseId(body.userid(), body.courseId())
77
            .orElseGet(UserState::new);
78 1 1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setUserid → KILLED
    state.setUserid(body.userid());
79 1 1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setCourseId → KILLED
    state.setCourseId(body.courseId());
80 2 1. upsert : negated conditional → KILLED
2. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setStarredIds → KILLED
    state.setStarredIds(writeJson(body.starredIds() == null ? List.of() : body.starredIds()));
81 1 1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setDetailCards → KILLED
    state.setDetailCards(
82
        writeJson(
83 1 1. upsert : negated conditional → KILLED
            body.detailCards() == null ? objectMapper.createArrayNode() : body.detailCards()));
84 1 1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setMasteredSubconcepts → KILLED
    state.setMasteredSubconcepts(
85 1 1. upsert : negated conditional → KILLED
        writeJson(body.masteredSubconcepts() == null ? List.of() : body.masteredSubconcepts()));
86 1 1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setTopLevelPositions → KILLED
    state.setTopLevelPositions(
87
        writeJson(
88 1 1. upsert : negated conditional → KILLED
            body.topLevelPositions() == null
89
                ? objectMapper.createObjectNode()
90
                : body.topLevelPositions()));
91
    userStateRepository.save(state);
92
  }
93
94
  private List<String> parseStringList(String json) {
95
    try {
96 1 1. parseStringList : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/UserStateController::parseStringList → KILLED
      return objectMapper.readValue(json, new TypeReference<>() {});
97
    } catch (JsonProcessingException e) {
98
      throw new IllegalStateException("Unable to parse stored string list JSON", e);
99
    }
100
  }
101
102
  private JsonNode parseJsonNode(String json) {
103
    try {
104 1 1. parseJsonNode : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::parseJsonNode → KILLED
      return objectMapper.readTree(json);
105
    } catch (JsonProcessingException e) {
106
      throw new IllegalStateException("Unable to parse stored JSON payload", e);
107
    }
108
  }
109
110
  private String writeJson(Object value) {
111
    try {
112 1 1. writeJson : replaced return value with "" for edu/ucsb/cs/scaffold/controller/UserStateController::writeJson → KILLED
      return objectMapper.writeValueAsString(value);
113
    } catch (JsonProcessingException e) {
114
      throw new IllegalArgumentException("Unable to serialize JSON payload", e);
115
    }
116
  }
117
118
  public record UserStateRequest(
119
      Long userid,
120
      @JsonProperty("courseId") Long courseId,
121
      @JsonProperty("starred_ids") List<String> starredIds,
122
      @JsonProperty("detail_cards") JsonNode detailCards,
123
      @JsonProperty("mastered_subconcepts") List<String> masteredSubconcepts,
124
      @JsonProperty("top_level_positions") JsonNode topLevelPositions) {}
125
126
  public record UserStateResponse(
127
      @JsonProperty("starred_ids") List<String> starredIds,
128
      @JsonProperty("detail_cards") JsonNode detailCards,
129
      @JsonProperty("mastered_subconcepts") List<String> masteredSubconcepts,
130
      @JsonProperty("top_level_positions") JsonNode topLevelPositions) {}
131
}

Mutations

34

1.1
Location : getUserState
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:missingUserStateReturnsEmptyDefaults()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::getUserState → KILLED

39

1.1
Location : lambda$getUserState$0
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::lambda$getUserState$0 → KILLED

46

1.1
Location : lambda$getUserState$1
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:missingUserStateReturnsEmptyDefaults()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::lambda$getUserState$1 → KILLED

56

1.1
Location : upsertUserState
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:writeJsonThrowsIllegalArgumentOnJsonProcessingException()]
negated conditional → KILLED

2.2
Location : upsertUserState
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:writeJsonThrowsIllegalArgumentOnJsonProcessingException()]
negated conditional → KILLED

57

1.1
Location : upsertUserState
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:upsertUserStateWithNullCourseIdReturnsBadRequest()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::upsertUserState → KILLED

61

1.1
Location : upsertUserState
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:writeJsonThrowsIllegalArgumentOnJsonProcessingException()]
removed call to edu/ucsb/cs/scaffold/controller/UserStateController::upsert → KILLED

67

1.1
Location : upsertUserState
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:upsertUserStateRetriesOnceAfterAConcurrentDuplicateInsert()]
removed call to edu/ucsb/cs/scaffold/controller/UserStateController::upsert → KILLED

70

1.1
Location : upsertUserState
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:upsertUserStateWritesAProvidedTopLevelPositionsPayload()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::upsertUserState → KILLED

78

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
removed call to edu/ucsb/cs/scaffold/model/UserState::setUserid → KILLED

79

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
removed call to edu/ucsb/cs/scaffold/model/UserState::setCourseId → KILLED

80

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
negated conditional → KILLED

2.2
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
removed call to edu/ucsb/cs/scaffold/model/UserState::setStarredIds → KILLED

81

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
removed call to edu/ucsb/cs/scaffold/model/UserState::setDetailCards → KILLED

83

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
negated conditional → KILLED

84

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
removed call to edu/ucsb/cs/scaffold/model/UserState::setMasteredSubconcepts → KILLED

85

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
negated conditional → KILLED

86

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:upsertUserStateWritesAProvidedTopLevelPositionsPayload()]
removed call to edu/ucsb/cs/scaffold/model/UserState::setTopLevelPositions → KILLED

88

1.1
Location : upsert
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:upsertUserStateWritesAProvidedTopLevelPositionsPayload()]
negated conditional → KILLED

96

1.1
Location : parseStringList
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/UserStateController::parseStringList → KILLED

104

1.1
Location : parseJsonNode
Killed by : edu.ucsb.cs.scaffold.ScaffoldApplicationTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.ScaffoldApplicationTests]/[method:userStateCanBeUpsertedAndFetchedByUserIdAndCourseId()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::parseJsonNode → KILLED

112

1.1
Location : writeJson
Killed by : edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.UserStateControllerExceptionTests]/[method:upsertUserStateWritesAProvidedTopLevelPositionsPayload()]
replaced return value with "" for edu/ucsb/cs/scaffold/controller/UserStateController::writeJson → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0