Skip to content

fix(#247): export DC collection endpoints as Atom feeds instead of a bare List - #250

Merged
dfcoffin merged 3 commits into
mainfrom
fix/247-atom-feeds
Nov 14, 2026
Merged

dfcoffin merged 3 commits into
mainfrom
fix/247-atom-feeds

Conversation

@dfcoffin

@dfcoffin dfcoffin commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #247. Every Data Custodian collection ("feed") endpoint returned 500. The controllers handed JAXB a bare List<XxxDto>, which has no root element (ImmutableCollections$ListN … not known to this context). Even an empty list failed. The endpoints now return ESPI Atom feeds.

Feed assembly: openespi-common

  • BaseExportService.buildFeed/exportFeed: one shared implementation, in common so E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 can reuse it.
    • Feed: urn:uuid id (UUID v5 of the feed URL), title, published/updated, self link (espi-feed/<Resource>).
    • Entry: urn:uuid:<entity id>, title (description, else the resource name), the entity's timestamps, self (espi-entry/<Resource>) and up (espi-feed/<Resource>) links, and the DTO as content.
    • Stored links are used only when they are absolute http(s) URLs; otherwise they are derived from the request URL, so they include the host and /DataCustodian.
  • newEntry(): usage exporters produce UsageAtomEntryDto; CustomerExportService and CustomerAccountExportService produce CustomerAtomEntryDto, the type in their JAXB context.
  • exportDto now rejects a bare Collection with a message naming exportFeed, instead of an obscure JAXB error.
  • AtomFeedDto bugs found by the new tests:
    • feed published/updated were written as empty elements (no OffsetDateTimeAdapter);
    • every entry carried xsi:type="atom:UsageAtomEntry" (@XmlElement on the abstract type). It is now @XmlElementRef.
  • LinkDto: a comment pins the field order rel, href, type, because JAXB uses it as the attribute order, which the CMD certification test requires. A test guards it. I verified the test fails when the fields are reordered: Lombok's @AllArgsConstructor then also swaps the values.
  • IdentifiedObject.ensureSelfLink/ensureUpLink passed (rel, href) to the LinkType(href, rel) constructor. Every entity given an id stored href="self" / href="up". Rows already saved in persistent dev databases keep those values; the feed code ignores non-absolute links, so feeds are correct either way.

Data Custodian

  • All 13 list call sites in 10 controllers use exportFeed with the request URL. ApplicationInformationService.export(List, …) now takes the feed URL.
  • AuthorizationController: both methods were stubs returning null (200 with an empty body). They now return an Atom feed, and an Atom entry or 404. Token values, authorization codes and state are @XmlTransient in AuthorizationDto, so they are never in the XML.
  • Diagnostics, EspiXmlCaptureFilter (local/dev profiles only): a 4xx/5xx response or an exception on /espi/1_1/resource/** writes <METHOD>_<path>.error.txt (status, request, exception chain, body) next to the captured XML in dev-xml-capture/run-<timestamp>/; the exception is still rethrown. A relative capture directory now resolves to the repository root, so captures no longer land in the module folder when the DC is started from openespi-datacustodian/.

Row-level scope for the admin resources

ApplicationInformation and Authorization accept DataCustodian_Admin_Access or ThirdParty_Admin_Access. Now that their feeds return data, each caller is scoped by one rule (AdminCaller):

  • DataCustodian_Admin_Access sees every record;
  • ThirdParty_Admin_Access sees only its own application's records, matched on the token's client_id (new AuthorizationRepository.findAllByApplicationClientId);
  • a Third Party admin token without client_id sees nothing (fails closed);
  • another application's record returns 404.

AdminRowScopeTest covers the feed and the entry for each kind of caller. I verified it fails when the check is bypassed.

Not changed here (follow-up)

Each resource is written directly under <atom:entry> (<espi:UsagePoint>…) rather than inside <atom:content>, as in the REQ.21 sample feeds. That comes from UsageAtomEntryDto/CustomerAtomEntryDto and affects single entries as well as feeds. Changing it changes the wire format the Third Party parses (#197), so it should be its own change.

Test plan

  • AtomFeedExportTest (common, real exporters, 5 tests): usage feed with two entries, empty feed, customer feed (cust namespace, no espi), link attribute order rel, href, type, collection rejection
  • FeedEndpointsIntegrationTest (DC, real exporters, a UsagePoint saved in H2): feed with an entry and correct ids/links/type values; empty Authorization feed; Authorization 404
  • EspiXmlCaptureFilterTest: +3 (exception chain captured and rethrown, 4xx captured, repo-root resolution)
  • AdminRowScopeTest (8): row-level scope for ApplicationInformation and Authorization, feed and entry
  • DC mvn verify: 195 pass. Common: 784 run; the only failures locally are 20 MySQL/PostgreSQL Testcontainers classes that need Docker (verified: all Could not find a valid Docker environment), plus one timing flake in CustomerAccountRepositoryTest (shouldUpdateTimestampsOnModification, two equal microsecond timestamps), which passed 3/3 on re-run. CI runs the Docker suites.
  • Live (local profile, DataCustodian_Admin_Access token): all 10 feed endpoints return 200 with a valid Atom feed (they were 500); an unknown Authorization id gives 404 and an .error.txt in the repo-root capture folder.

🤖 Generated with Claude Code

…bare List

Every DC feed endpoint returned 500: the controllers passed a List<XxxDto> to
BaseExportService.exportDto, and JAXB cannot marshal a collection root
("ImmutableCollections$ListN ... not known to this context"). Even an empty
List.of() failed.

Feed assembly (openespi-common)
- BaseExportService.buildFeed/exportFeed: Atom feed with a UUID v5 id (from
  the feed URL), title, published/updated, self link (espi-feed/<Resource>),
  and one entry per resource: urn:uuid:<id>, title (description, else the
  resource name), the entity's timestamps, self (espi-entry/) and up
  (espi-feed/) links, and the mapped DTO as content. Stored links are used
  only when they are absolute http(s) URLs; otherwise they are derived from
  the feed URL, so they carry the real host and /DataCustodian context.
- newEntry(): usage exporters build UsageAtomEntryDto; the customer exporters
  override it to build CustomerAtomEntryDto (the type in their JAXBContext).
- exportDto rejects a bare Collection with a message naming exportFeed.
- AtomFeedDto: published/updated get OffsetDateTimeAdapter (they were
  written as empty elements); entries use @XmlElementRef so entries carry no
  xsi:type="atom:UsageAtomEntry".
- LinkDto: comment pinning the rel, href, type field order, which JAXB uses
  as the attribute order the CMD certification test requires.
- IdentifiedObject.ensureSelfLink/ensureUpLink passed (rel, href) to the
  LinkType(href, rel) constructor, storing href="self" and href="up" on every
  entity given an id.

Data Custodian
- All 13 list call sites in 10 controllers use exportFeed with the request
  URL; ApplicationInformationService.export(List) takes the feed URL.
- AuthorizationController: both stubs (which returned null, i.e. 200 with an
  empty body) are implemented: an Atom feed, and an Atom entry or 404.
- EspiXmlCaptureFilter (local/dev profiles): a 4xx/5xx response or an
  exception writes <METHOD>_<path>.error.txt (status, request, exception
  chain, body) beside the captured XML; the exception is still rethrown. A
  relative capture dir resolves to the repository root.

Tests
- AtomFeedExportTest (real exporters): usage and customer feeds, empty feed,
  link attribute order rel/href/type, collection rejection.
- FeedEndpointsIntegrationTest (DC, real exporters, H2 data): UsagePoint
  feed with an entry, empty Authorization feed, Authorization 404.
- EspiXmlCaptureFilterTest: exception and 4xx capture, repo-root resolution.

Closes #247

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dfcoffin and others added 2 commits November 14, 2026 15:07
ApplicationInformation and Authorization are readable with either
DataCustodian_Admin_Access or ThirdParty_Admin_Access. Now that their feeds
return data, the records a caller sees are scoped:

- DataCustodian_Admin_Access: every record.
- ThirdParty_Admin_Access: only records of its own application, matched on
  the token's client_id (Authorization via a new
  AuthorizationRepository.findAllByApplicationClientId query).
- A ThirdParty admin token without client_id: nothing (fails closed).
- A record of another application: 404, so its existence is not disclosed.

The rule lives in one place (AdminCaller) for both controllers.
AdminRowScopeTest covers feed and entry for each kind of caller, and fails if
the check is bypassed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Third Party: UsagePointRESTRepositoryImpl built a JAXBContext from
  AtomFeedDto alone. With AtomFeedDto.entries now an @XmlElementRef to the
  abstract AtomEntryDto, JAXB needs a concrete entry type in the same context
  ("Invalid @XmlElementRef ... not known to this context"), so the TP
  application context failed to start. Register UsageAtomEntryDto. That is
  also what the TP needs to read usage feeds at all (#197: abstract entry
  type).
- EspiXmlCaptureFilterTest.relativeDirResolvesToRepoRoot used AssertJ
  Path.endsWith, which canonicalises the path and fails when the directory
  does not exist, as on a fresh CI checkout; use endsWithRaw.
- EspiXmlCaptureFilter: explicit containment check before writing a capture
  file (normalised path must sit directly in the run directory), plus a
  traversal test. fileName() already stripped path separators; this makes
  the guarantee explicit for code scanning.

Verified locally with no dev-xml-capture directory present: DC 196, TP 56
(2 skipped, pre-existing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DC feed (collection) endpoints return 500: a bare List is passed to JAXB instead of an Atom feed

2 participants