Add GOPACSHandler day-ahead UFTP flow tests#42
Conversation
…he example message format
There was a problem hiding this comment.
Pull request overview
Adds isolated Spock-based test coverage for the GOPACS UFTP day-ahead message flow on the EMS/AGR side by driving real signed UFTP v3.0.0 XML payloads through GOPACSHandler.processRawMessage, and introduces a test-support constructor to wire collaborators without container/OAuth/JAX-RS setup.
Changes:
- Add
GOPACSHandlerTestcovering FlexRequest → (FlexRequestResponse + FlexOffer), FlexOfferResponse handling, FlexOrder flows, and out-of-scope congestion point dropping. - Add a protected test-support constructor on
GOPACSHandlerto allow exercising message handling without container wiring and remote configuration.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
ems/src/test/groovy/org/openremote/extension/ems/manager/gopacs/GOPACSHandlerTest.groovy |
New Spock spec with signed-message fixtures and assertions for day-ahead GOPACS/UFTP flows. |
ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java |
Adds a test-support constructor to instantiate the handler with injected services/executor/private key and without deployment/remote config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this.client = null; | ||
| this.gopacsAddressBookResource = null; | ||
| this.gopacsAuthResource = null; | ||
| this.gopacsServerResource = null; | ||
|
|
There was a problem hiding this comment.
This path already degrades gracefully — no NPE is thrown.
-
fetchBearerToken()wraps thegopacsAuthResourcecall in try/catch (Exception e). A nullgopacsAuthResourceraises an NPE, which is caught and the method returns"". -
getParticipantInformation()checks the blank token before touching the address book:
String authorization = fetchBearerToken();
if (authorization.isBlank()) {
LOG.warning("Skipping participant lookup for " + domain + ": no OAuth2 bearer token available");
return Optional.empty();
}
try (Response response = gopacsAddressBookResource.fetchParticipantByDomain(authorization, domain)) {So even with participants unseeded: caught NPE → blank token → Optional.empty(), and gopacsAddressBookResource is never dereferenced. Additionally, the covered flows pre-seed participants, so the participants.containsKey(domain) cache returns first and fetchBearerToken() is not called at all.
Leaving the constructor as-is; no-op resource implementations would be unused plumbing.
|
Will these tests also find any regressions that might occur due to the changes in #43? |
|
@wborn Not really, and that's intentional. These tests stub the OpenRemote collaborators ( |
richturner
left a comment
There was a problem hiding this comment.
I have concerns that this doesn't verify any of the HTTP request/response chain, it should at least follow the same structure as other HTTP tests including the ENTSOE one in this repo; I asked Codex for thoughts on this also:
Findings
- Medium: inbound HTTP endpoint is not covered. The tests call handler.processRawMessage(...) directly in GOPACSHandlerTest.groovy, while production traffic enters via POST /gopacs/message through GOPACSHandler.java and GOPACSServerResource.java. This leaves the deployed path, accepted media types, JAX-RS delegation, and WebApplicationException status mapping in GOPACSHandler.java untested. A path/annotation regression could pass the current suite while real GOPACS POSTs fail.
- Medium: outbound HTTP delivery is bypassed. The production sender goes through uftpSendMessageService.attemptToSendMessage(...) in GOPACSHandler.java, but the test subclass overrides notifyNewOutgoingMessage and only records payload objects in GOPACSHandlerTest.groovy. That means no test asserts that signed XML is actually posted to the broker endpoint, that the recipient role/domain is resolved correctly, or that the Authorization header is attached.
- Medium: OAuth and address-book HTTP calls are not exercised. The test constructor sets gopacsAuthResource, gopacsAddressBookResource, and client to null in GOPACSHandler.java, and the test pre-seeds participants in GOPACSHandlerTest.groovy. So fetchBearerToken() and getParticipantInformation() in GOPACSHandler.java / GOPACSHandler.java are unprotected: form params, bearer parsing, 404 handling, lookup failures, participant caching, and synthesized broker URL are all outside this PR’s tests.
Suggested Improvement
Add a focused HTTP-level test class alongside the current message-flow unit test. The current tests are useful for UFTP semantics; the missing layer is the actual HTTP contract.
A pragmatic route is to use the existing RESTEasy filter style already used in EntsoeProtocolTest.groovy, or add a small stub server dependency such as WireMock/MockWebServer.
Minimum scenarios I’d add:
- POST /gopacs/message with signed XML and application/xml returns success and triggers the same flow as the direct processRawMessage test.
- Malformed XML or invalid signature returns 400, covering the UftpConnectorException branch.
- A valid out-of-scope message returns success but causes no asset mutation and no outbound HTTP POST.
- Outbound FlexRequestResponse/FlexOffer/FlexOrderResponse produces actual broker POSTs to /shapeshifter/api/v3/message with signed XML and Authorization: Bearer ....
- OAuth/address-book failure cases: token 500, invalid token JSON, participant 404, and successful lookup caching.
|
Yes I agree it would be nicer if there are more integration like tests using real container services instead of mocks that mimic the current behavior. |
The GOPACS broker delivery goes through java.net.http.HttpClient (via shapeshifter's UftpSendMessageService), which a JAX-RS ClientRequestFilter cannot intercept. WireMock (standalone, shaded to avoid clashing with the manager's web stack) provides a local HTTP server to stub and verify the broker, OAuth2 token and address-book endpoints on the wire -- the same approach shapeshifter uses to test UftpSendMessageService.
Complements the in-process GOPACSHandlerTest (which calls processRawMessage directly and records outgoing payload objects) by exercising the real transport chain end to end, addressing the HTTP coverage gap raised in review: - Inbound: real POST to the deployed /gopacs/message JAX-RS resource, asserting accepted media type, JAX-RS delegation and the WebApplicationException status mapping (success vs 400). - Outbound: real broker delivery via UftpSendMessageService (over java.net.http.HttpClient), plus the OAuth2 token and address-book lookups via the handler's RESTEasy client. All three are pointed at one WireMock server, so the signed XML, synthesised broker endpoint and 'Authorization: Bearer' header are verified on the wire. Covers: in-scope FlexRequest -> FlexRequestResponse + FlexOffer broker delivery; FlexOrder -> FlexOrderResponse; out-of-scope drop (accepted, no delivery); malformed XML and signature-mismatch -> 400; OAuth2 500 / invalid token JSON and address-book 404 -> 400; and participant-lookup caching. Like the other manager-container integration tests in this repo (e.g. EntsoeProtocolTest) it requires a running OpenRemote dev stack and is skipped on CI via @IgnoreIf(GITHUB_ACTIONS).
|
Added It POSTs real signed messages to the deployed Note: the broker send uses |
Summary
Adds test coverage for the
GOPACSHandlerday-ahead capacity-steering message flow on the AGR (EMS) side, closing the gap left by the UFTP V2→V3 participant migration (#40). Theemsmodule previously had no tests for the GOPACS handler.The spec drives real signed UFTP messages through the handler's actual entry point (
processRawMessage) using libsodium crypto — no database, no container, no network. Inbound payloads are embedded as XML fixtures matching the real wire format (DSOnilsgrid.net→ AGRopenremote.io, v3.0.0, attribute-style). A small test-support constructor onGOPACSHandlerskips remote config (OAuth client / private-key file) and JAX-RS deployment; aRecordingGOPACSHandlersubclass captures outbound messages (both the library-generated responses and the handler-builtFlexOffer); scheduled work runs inline for deterministic assertions.Covered flows:
powerMaximumFlexRequest= importMax,powerMinimumFlexRequest= exportMax); replies withFlexRequestResponse(Accepted) thenFlexOffer. The emittedFlexRequestResponseis asserted to match the reference message shape (result, referenced request id, swapped domains, version, conversation id).currentPowerFlexRequest, plus the offtakepowerLimitMaximumProfileFlexOrder/ feed-inpowerLimitMinimumProfileFlexOrderprofile); replies withFlexOrderResponse(Accepted).CongestionPointdiffers from the contracted EAN is dropped: no asset mutation, no reply (the V3 re-scoping).