Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/main/java/com/sivalabs/ft/features/domain/FeatureService.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,17 @@ public String createFeature(CreateFeatureCommand cmd) {
public void updateFeature(UpdateFeatureCommand cmd) {
Feature feature = featureRepository.findByCode(cmd.code()).orElseThrow();

String oldTitle = feature.getTitle();
String oldDescription = feature.getDescription();
String oldAssignedTo = feature.getAssignedTo();
String oldStatus = feature.getStatus() != null ? feature.getStatus().name() : null;
String oldReleaseCode =
feature.getRelease() != null ? feature.getRelease().getCode() : null;
var oldPlannedCompletionAt = feature.getPlannedCompletionAt();
var oldActualCompletionAt = feature.getActualCompletionAt();
var oldFeaturePlanningStatus = feature.getFeaturePlanningStatus();
var oldFeatureOwner = feature.getFeatureOwner();
var oldBlockageReason = feature.getBlockageReason();

feature.setTitle(cmd.title());
feature.setDescription(cmd.description());
Expand All @@ -143,6 +150,59 @@ public void updateFeature(UpdateFeatureCommand cmd) {
feature.setUpdatedAt(Instant.now());
featureRepository.save(feature);

if (!Objects.equals(oldTitle, cmd.title())) {
planningHistoryService.recordFeatureFieldChange(
feature, "title", oldTitle, cmd.title(), ChangeType.UPDATED, cmd.updatedBy());
}
if (!Objects.equals(oldDescription, cmd.description())) {
planningHistoryService.recordFeatureFieldChange(
feature, "description", oldDescription, cmd.description(), ChangeType.UPDATED, cmd.updatedBy());
}
if (!Objects.equals(oldPlannedCompletionAt, cmd.plannedCompletionAt())) {
planningHistoryService.recordFeatureFieldChange(
feature,
"plannedCompletionAt",
oldPlannedCompletionAt != null ? oldPlannedCompletionAt.toString() : null,
cmd.plannedCompletionAt() != null
? cmd.plannedCompletionAt().toString()
: null,
ChangeType.UPDATED,
cmd.updatedBy());
}
if (!Objects.equals(oldActualCompletionAt, cmd.actualCompletionAt())) {
planningHistoryService.recordFeatureFieldChange(
feature,
"actualCompletionAt",
oldActualCompletionAt != null ? oldActualCompletionAt.toString() : null,
cmd.actualCompletionAt() != null ? cmd.actualCompletionAt().toString() : null,
ChangeType.UPDATED,
cmd.updatedBy());
}
if (!Objects.equals(oldFeaturePlanningStatus, cmd.featurePlanningStatus())) {
planningHistoryService.recordFeatureFieldChange(
feature,
"featurePlanningStatus",
oldFeaturePlanningStatus != null ? oldFeaturePlanningStatus.name() : null,
cmd.featurePlanningStatus() != null
? cmd.featurePlanningStatus().name()
: null,
ChangeType.UPDATED,
cmd.updatedBy());
}
if (!Objects.equals(oldFeatureOwner, cmd.featureOwner())) {
planningHistoryService.recordFeatureFieldChange(
feature, "featureOwner", oldFeatureOwner, cmd.featureOwner(), ChangeType.UPDATED, cmd.updatedBy());
}
if (!Objects.equals(oldBlockageReason, cmd.blockageReason())) {
planningHistoryService.recordFeatureFieldChange(
feature,
"blockageReason",
oldBlockageReason,
cmd.blockageReason(),
ChangeType.UPDATED,
cmd.updatedBy());
}

Comment thread
codecharlan marked this conversation as resolved.
String newStatus = cmd.status() != null ? cmd.status().name() : null;
String newReleaseCode = cmd.releaseCode();
if (!Objects.equals(oldStatus, newStatus)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,78 @@

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sivalabs.ft.features.AbstractIT;
import com.sivalabs.ft.features.WithMockOAuth2User;
import com.sivalabs.ft.features.domain.dtos.FeatureDto;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

class PlanningHistoryControllerTests extends AbstractIT {
@Autowired
private ObjectMapper objectMapper;

record EntityInfo(String code, Long id) {}

// ==================== Helper Methods ====================

private EntityInfo createFeatureForHistoryTest(String title) throws Exception {
var payload = String.format(
"""
{
"productCode": "intellij",
"releaseCode": "IDEA-2023.3.8",
"title": "%s",
"description": "Test feature for planning history",
"assignedTo": "developer"
}
""",
title);

var result = mvc.post()
.uri("/api/features")
.contentType(MediaType.APPLICATION_JSON)
.content(payload)
.exchange();

assertThat(result).hasStatus(HttpStatus.CREATED);

String location = result.getMvcResult().getResponse().getHeader("Location");
String code = location.substring(location.lastIndexOf("/") + 1);

// Fetch the feature to get its ID
var getResult = mvc.get().uri("/api/features/{code}", code).exchange();
assertThat(getResult).hasStatusOk();
String responseBody = getResult.getMvcResult().getResponse().getContentAsString();
FeatureDto featureDto = objectMapper.readValue(responseBody, FeatureDto.class);

return new EntityInfo(code, featureDto.id());
}

private void updateFeatureForHistoryTest(String featureCode, String newTitle, String newDescription) {
var payload = String.format(
"""
{
"title": "%s",
"description": "%s",
"assignedTo": "developer",
"status": "IN_PROGRESS"
}
""",
newTitle, newDescription);

var result = mvc.put()
.uri("/api/features/{code}", featureCode)
.contentType(MediaType.APPLICATION_JSON)
.content(payload)
.exchange();

assertThat(result).hasStatusOk();
}

@Test
void shouldGetAllPlanningHistory() {
Expand Down Expand Up @@ -230,4 +295,133 @@ void shouldTrackFeatureDeletion() {
.extracting(Number::intValue)
.satisfies(n -> assertThat(n).isGreaterThanOrEqualTo(1));
}

@Test
@WithMockOAuth2User(username = "testuser")
void shouldTrackAllUpdatedFields() throws Exception {
EntityInfo feature = createFeatureForHistoryTest("All Fields Update Test");

var payload =
"""
{
"title": "New Title",
"description": "New Description",
"assignedTo": "new-developer",
"status": "IN_PROGRESS",
"releaseCode": "IDEA-2024.2.3",
"plannedCompletionAt": "2026-12-31T23:59:59Z",
"actualCompletionAt": "2026-12-25T10:00:00Z",
"featurePlanningStatus": "DONE",
"featureOwner": "New Owner",
"blockageReason": "None anymore"
}
""";

var updateResult = mvc.put()
.uri("/api/features/{code}", feature.code())
.contentType(MediaType.APPLICATION_JSON)
.content(payload)
.exchange();
assertThat(updateResult).hasStatusOk();

var historyResult =
mvc.get().uri("/api/features/{code}/history", feature.code()).exchange();
assertThat(historyResult).hasStatusOk();

String responseBody = historyResult.getMvcResult().getResponse().getContentAsString();
Map<String, Object> response = objectMapper.readValue(responseBody, Map.class);
List<Map<String, Object>> content = (List<Map<String, Object>>) response.get("content");

// 1 (CREATED) + 1 (MOVED) + 1 (STATUS_CHANGED) + 1 (ASSIGNED) + 7 (UPDATED) = 11
assertThat(response.get("totalElements")).isEqualTo(11);

// Verify UPDATED fields
assertFieldChange(content, "UPDATED", "title", "New Title");
assertFieldChange(content, "UPDATED", "description", "New Description");
assertFieldChange(content, "UPDATED", "plannedCompletionAt", "2026-12-31T23:59:59Z");
assertFieldChange(content, "UPDATED", "actualCompletionAt", "2026-12-25T10:00:00Z");
assertFieldChange(content, "UPDATED", "featurePlanningStatus", "DONE");
assertFieldChange(content, "UPDATED", "featureOwner", "New Owner");
assertFieldChange(content, "UPDATED", "blockageReason", "None anymore");

// Verify specific transition types
assertFieldChange(content, "STATUS_CHANGED", "status", "IN_PROGRESS");
assertFieldChange(content, "ASSIGNED", "assignedTo", "new-developer");
assertFieldChange(content, "MOVED", "releaseCode", "IDEA-2024.2.3");
}

private void assertFieldChange(
List<Map<String, Object>> content, String changeType, String fieldName, String newValue) {
content.stream()
.filter(h -> changeType.equals(h.get("changeType")) && fieldName.equals(h.get("fieldName")))
.findFirst()
.ifPresentOrElse(h -> assertThat(h.get("newValue")).isEqualTo(newValue), () -> {
throw new AssertionError(String.format("History entry not found for %s:%s", changeType, fieldName));
});
}
Comment thread
codecharlan marked this conversation as resolved.

@Test
@WithMockOAuth2User(username = "testuser")
void shouldGetFeatureHistoryByCode() throws Exception {
// Create and update a feature to generate history
EntityInfo feature = createFeatureForHistoryTest("Feature for History Test");
updateFeatureForHistoryTest(feature.code(), "Updated Title", "Updated description for history test");

var result =
mvc.get().uri("/api/features/{code}/history", feature.code()).exchange();

assertThat(result).hasStatusOk();

String responseBody = result.getMvcResult().getResponse().getContentAsString();
Map<String, Object> response = objectMapper.readValue(responseBody, Map.class);
List<Map<String, Object>> content = (List<Map<String, Object>>) response.get("content");

// Expected: CREATED + MOVED (release cleared) + STATUS_CHANGED + UPDATED (title) + UPDATED (description) = 5
assertThat(response.get("totalElements")).isEqualTo(5);

// Verify CREATED record exists
assertThat(content.stream().anyMatch(h -> "CREATED".equals(h.get("changeType"))))
.isTrue();

// Verify UPDATED title record exists
assertFieldChange(content, "UPDATED", "title", "Updated Title");

// Verify UPDATED description record exists
assertFieldChange(content, "UPDATED", "description", "Updated description for history test");
}

@Test
@WithMockOAuth2User(username = "testuser")
void shouldNotRecordHistoryWhenNoFieldsChanged() throws Exception {
EntityInfo feature = createFeatureForHistoryTest("No Change Test");

// Update with same values
var payload =
"""
{
"title": "No Change Test",
"description": "Test feature for planning history",
"assignedTo": "developer",
"status": "NEW",
"releaseCode": "IDEA-2023.3.8"
}
""";
var updateResult = mvc.put()
.uri("/api/features/{code}", feature.code())
.contentType(MediaType.APPLICATION_JSON)
.content(payload)
.exchange();
assertThat(updateResult).hasStatusOk();

var result =
mvc.get().uri("/api/features/{code}/history", feature.code()).exchange();
assertThat(result).hasStatusOk();

String responseBody = result.getMvcResult().getResponse().getContentAsString();
Map<String, Object> response = objectMapper.readValue(responseBody, Map.class);
List<Map<String, Object>> content = (List<Map<String, Object>>) response.get("content");

// Only CREATED should be there (size 1)
assertThat(content).hasSize(1);
}
}
Loading