Skip to content
Merged
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
@@ -1,10 +1,19 @@
package uk.co.compendiumdev.thingifier.api.http.requests;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import uk.co.compendiumdev.thingifier.Thingifier;
import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiRequestHook;
import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest;
import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse;
import uk.co.compendiumdev.thingifier.api.http.ThingifierHttpApi;
Expand Down Expand Up @@ -141,6 +150,73 @@ public void aPostRequestWillCreateNewSessionWithDatabase() {
.getDefinitionWithSingularOrPluralNamed("thing")));
}

@Test
public void generatedHttpApiRequestsAreSerializedOnTheThingifierModel() throws Exception {
// TODO: currently we synchronise at HttpAPI boundary to prevent GET happening before POST
// work is complete - this should only really be necessary for inmemory persistence - need
// to evaluate and push this lower when trying to scale because this might slow and impact
// the disk and database based persistence backed APIs
Thingifier thingifier = getTestThingifier();
CountDownLatch firstRequestEnteredHook = new CountDownLatch(1);
CountDownLatch releaseFirstRequest = new CountDownLatch(1);
CountDownLatch secondRequestEnteredHook = new CountDownLatch(1);
AtomicInteger hookCallCount = new AtomicInteger();
AtomicBoolean hooksRanInsideThingifierLock = new AtomicBoolean(true);

HttpApiRequestHook blockingHook =
(request, config) -> {
if (!Thread.holdsLock(thingifier)) {
hooksRanInsideThingifierLock.set(false);
}

int callNumber = hookCallCount.incrementAndGet();
if (callNumber == 1) {
firstRequestEnteredHook.countDown();
waitForLatch(releaseFirstRequest, "Timed out waiting to release POST");
} else if (callNumber == 2) {
secondRequestEnteredHook.countDown();
}

return null;
};
ThingifierHttpApi api = new ThingifierHttpApi(thingifier, List.of(blockingHook), null);
ExecutorService executor = Executors.newFixedThreadPool(2);

try {
Future<HttpApiResponse> postResponse =
executor.submit(
() -> api.post(jsonRequest("/things", "{\"title\":\"thing1\"}")));

Assertions.assertTrue(
firstRequestEnteredHook.await(1, TimeUnit.SECONDS),
"POST did not enter the HTTP API hook");

Future<HttpApiResponse> getResponse =
executor.submit(() -> api.get(new HttpApiRequest("/things")));

Assertions.assertFalse(
secondRequestEnteredHook.await(200, TimeUnit.MILLISECONDS),
"GET entered generated API processing before POST completed");
Comment on lines +197 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Coordinate the GET worker before asserting it is blocked

If the executor's second worker is not scheduled during this 200 ms window, the latch remains closed even after synchronization is removed; the test then releases the POST, the delayed GET runs, and every later assertion can pass. Ensure the GET worker has actually attempted the API call—for example, by capturing it in the expected blocked state—before treating failure to enter the hook as evidence of serialization.

AGENTS.md reference: AGENTS.md:L3-L4

Useful? React with 👍 / 👎.


releaseFirstRequest.countDown();

Assertions.assertEquals(201, postResponse.get(1, TimeUnit.SECONDS).getStatusCode());
HttpApiResponse actualGetResponse = getResponse.get(1, TimeUnit.SECONDS);

Assertions.assertTrue(
secondRequestEnteredHook.await(1, TimeUnit.SECONDS),
"GET did not enter generated API processing after POST completed");
Assertions.assertEquals(200, actualGetResponse.getStatusCode());
Assertions.assertTrue(actualGetResponse.getBody().contains("thing1"));
Assertions.assertTrue(
hooksRanInsideThingifierLock.get(),
"Generated API hook ran outside the Thingifier model lock");
Comment on lines +211 to +213

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Separate monitor ownership from request serialization

This assertion adds a distinct implementation-level rule to a test that already verifies request ordering and POST-to-GET data visibility. Serialization could remain correct after moving the lock lower, as the TODO proposes, or after replacing the intrinsic monitor, yet this assertion would fail; conversely, a failure does not identify which behavior regressed. Keep the domain-level serialization scenario focused and test exact monitor ownership separately only if it is an intentional contract.

AGENTS.md reference: AGENTS.md:L6-L8

Useful? React with 👍 / 👎.

} finally {
releaseFirstRequest.countDown();
executor.shutdownNow();
}
}

@Test
public void strictPostRejectsNumericValueForStringField() {
Thingifier thingifier = strictTypedThingifier();
Expand Down Expand Up @@ -400,4 +476,15 @@ private HttpApiRequest patchRequest(
.setHeaders(Map.of("content-type", contentType))
.setBody(body);
}

private void waitForLatch(final CountDownLatch latch, final String timeoutMessage) {
try {
if (!latch.await(2, TimeUnit.SECONDS)) {
throw new AssertionError(timeoutMessage);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError(timeoutMessage, e);
}
}
}
Loading