Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,10 +124,14 @@ void createCustomCheckRemovesTheCheckWhenItsDmnCannotBeStored() throws Exception
.thenReturn("<dmn:definitions/>");
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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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> T captureExpectedErrorLog(String expectedMessage, Throwable expectedCause, Supplier<T> action) {
Logger resourceLogger = Logger.getLogger(EligibilityCheckResource.class.getName());
Filter previousFilter = resourceLogger.getFilter();
List<LogRecord> 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);
}
Expand Down
42 changes: 41 additions & 1 deletion builder-frontend/src/api/check.test.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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({
Expand All @@ -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,
Expand All @@ -47,23 +63,47 @@ 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 }),
);

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),
);
});
});
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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<void>((resolve, reject) => {
Expand All @@ -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 () => {
Expand Down
Loading