diff --git a/builder-api/src/test/java/org/acme/controller/EligibilityCheckResourceTest.java b/builder-api/src/test/java/org/acme/controller/EligibilityCheckResourceTest.java
index 8adc1d6a..bd5748b5 100644
--- a/builder-api/src/test/java/org/acme/controller/EligibilityCheckResourceTest.java
+++ b/builder-api/src/test/java/org/acme/controller/EligibilityCheckResourceTest.java
@@ -15,8 +15,14 @@
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.logging.Filter;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -118,10 +124,14 @@ void createCustomCheckRemovesTheCheckWhenItsDmnCannotBeStored() throws Exception
.thenReturn("");
when(repository.saveNewWorkingCustomCheck(any(EligibilityCheck.class))).thenReturn(checkId);
when(storageService.getCheckDmnModelPath(checkId)).thenReturn("check/" + checkId + ".dmn");
- doThrow(new RuntimeException("storage unavailable"))
+ RuntimeException storageFailure = new RuntimeException("storage unavailable");
+ doThrow(storageFailure)
.when(storageService).writeStringToStorage(anyString(), anyString(), anyString());
- Response response = resource.createCustomCheck(identity, request);
+ Response response = captureExpectedErrorLog(
+ "Could not save the DMN model of check " + checkId + ", removing the check",
+ storageFailure,
+ () -> resource.createCustomCheck(identity, request));
assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus());
verify(repository).deleteWorkingCustomCheck(checkId);
@@ -204,13 +214,17 @@ void createCustomCheckReportsArchivedStateAfterAWriteCollision() throws Exceptio
void createCustomCheckStillConflictsWhenTheCollidingCheckCannotBeRead() throws Exception {
CreateCheckRequest request = createCheckRequest();
String checkId = "W-owner-1-income-incomeCheck";
+ IllegalArgumentException readFailure = new IllegalArgumentException("unmappable document");
when(repository.getWorkingCustomCheckMetadata(USER_ID, checkId))
.thenReturn(Optional.empty())
- .thenThrow(new IllegalArgumentException("unmappable document"));
+ .thenThrow(readFailure);
when(repository.saveNewWorkingCustomCheck(any()))
.thenThrow(new DocumentAlreadyExistsException(checkId, new RuntimeException()));
- Response response = resource.createCustomCheck(identity, request);
+ Response response = captureExpectedErrorLog(
+ "Could not read the check " + checkId + " that collided with the new check",
+ readFailure,
+ () -> resource.createCustomCheck(identity, request));
assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus());
assertEquals(
@@ -414,15 +428,46 @@ void firstPublishOfACorruptWorkingVersionStartsAtTheInitialVersion() throws Exce
@Test
void publishFailsWhenPublishedVersionsCannotBeRead() throws Exception {
workingCheck.setVersion("2.0.0");
- when(repository.getPublishedCheckVersions(workingCheck)).thenThrow(new RuntimeException("firestore unavailable"));
+ RuntimeException readFailure = new RuntimeException("firestore unavailable");
+ when(repository.getPublishedCheckVersions(workingCheck)).thenThrow(readFailure);
- Response response = resource.publishCustomCheck(identity, CHECK_ID);
+ Response response = captureExpectedErrorLog(
+ "Could not read published versions of check " + CHECK_ID,
+ readFailure,
+ () -> resource.publishCustomCheck(identity, CHECK_ID));
assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus());
verify(repository, never()).saveNewPublishedCustomCheck(any());
verify(repository, never()).updateWorkingCustomCheck(any());
}
+ private T captureExpectedErrorLog(String expectedMessage, Throwable expectedCause, Supplier action) {
+ Logger resourceLogger = Logger.getLogger(EligibilityCheckResource.class.getName());
+ Filter previousFilter = resourceLogger.getFilter();
+ List capturedLogs = new ArrayList<>();
+ resourceLogger.setFilter(record -> {
+ if (expectedMessage.equals(record.getMessage()) && record.getThrown() == expectedCause) {
+ capturedLogs.add(record);
+ return false;
+ }
+ return previousFilter == null || previousFilter.isLoggable(record);
+ });
+
+ T result;
+ try {
+ result = action.get();
+ } finally {
+ resourceLogger.setFilter(previousFilter);
+ }
+
+ assertEquals(1, capturedLogs.size(), "expected exactly one matching log event");
+ LogRecord capturedLog = capturedLogs.get(0);
+ assertEquals(expectedMessage, capturedLog.getMessage());
+ assertEquals(Level.SEVERE.intValue(), capturedLog.getLevel().intValue());
+ assertSame(expectedCause, capturedLog.getThrown());
+ return result;
+ }
+
private EligibilityCheck publishedVersion(String version) {
return publishedVersion(version, PUBLISHED_PREFIX + "-" + version);
}
diff --git a/builder-frontend/src/api/check.test.ts b/builder-frontend/src/api/check.test.ts
index 9d92bc26..5136219b 100644
--- a/builder-frontend/src/api/check.test.ts
+++ b/builder-frontend/src/api/check.test.ts
@@ -1,4 +1,4 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/api/auth", () => ({
authGet: vi.fn(),
@@ -19,8 +19,12 @@ const request = {
describe("addCheck", () => {
beforeEach(() => vi.clearAllMocks());
+ afterEach(() => vi.restoreAllMocks());
it("reports the API error message when check creation fails", async () => {
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
vi.mocked(authPost).mockResolvedValue(
new Response(
JSON.stringify({
@@ -34,9 +38,21 @@ describe("addCheck", () => {
await expect(addCheck(request)).rejects.toThrow(
'You already have a check named "incomeCheck" in module "income".',
);
+ expect(consoleError).toHaveBeenCalledOnce();
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error creating new check:",
+ expect.objectContaining({
+ message:
+ 'You already have a check named "incomeCheck" in module "income".',
+ status: 409,
+ }),
+ );
});
it("carries the response status so callers can tell failures apart", async () => {
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
vi.mocked(authPost).mockResolvedValue(
new Response(JSON.stringify({ error: "Could not save Check" }), {
status: 500,
@@ -47,9 +63,17 @@ describe("addCheck", () => {
await expect(addCheck(request)).rejects.toMatchObject({
status: 500,
});
+ expect(consoleError).toHaveBeenCalledOnce();
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error creating new check:",
+ expect.objectContaining({ message: "Could not save Check", status: 500 }),
+ );
});
it("falls back to the status when the error response is not JSON", async () => {
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
vi.mocked(authPost).mockResolvedValue(
new Response("unavailable", { status: 503 }),
);
@@ -57,13 +81,29 @@ describe("addCheck", () => {
await expect(addCheck(request)).rejects.toThrow(
"Post failed with status: 503",
);
+ expect(consoleError).toHaveBeenCalledOnce();
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error creating new check:",
+ expect.objectContaining({
+ message: "Post failed with status: 503",
+ status: 503,
+ }),
+ );
});
it("rejects with an ApiError", async () => {
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
vi.mocked(authPost).mockResolvedValue(
new Response("unavailable", { status: 503 }),
);
await expect(addCheck(request)).rejects.toBeInstanceOf(ApiError);
+ expect(consoleError).toHaveBeenCalledOnce();
+ expect(consoleError).toHaveBeenCalledWith(
+ "Error creating new check:",
+ expect.any(ApiError),
+ );
});
});
diff --git a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.test.ts b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.test.ts
index 1bb90312..0fd13079 100644
--- a/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.test.ts
+++ b/builder-frontend/src/components/homeScreen/eligibilityCheckList/eligibilityCheckResource.test.ts
@@ -1,5 +1,5 @@
import { createRoot } from "solid-js";
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/api/check", () => ({
addCheck: vi.fn(),
@@ -18,9 +18,13 @@ import type { EligibilityCheck } from "@/types";
describe("eligibilityCheckResource", () => {
beforeEach(() => vi.clearAllMocks());
+ afterEach(() => vi.restoreAllMocks());
it("propagates create failures to the modal", async () => {
const failure = new Error("That check name is already in use.");
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => {});
vi.mocked(addCheck).mockRejectedValue(failure);
await new Promise((resolve, reject) => {
@@ -47,6 +51,11 @@ describe("eligibilityCheckResource", () => {
});
});
});
+ expect(consoleError).toHaveBeenCalledOnce();
+ expect(consoleError).toHaveBeenCalledWith(
+ "Failed to add new check",
+ failure,
+ );
});
it("splits one response into the active and archived lists", async () => {