Skip to content

Commit 44a00c2

Browse files
authored
Add samples/usecase/usecase1 UI smoke test and align doc/sample artifacts (#185)
1 parent d7bd51f commit 44a00c2

6 files changed

Lines changed: 373 additions & 6 deletions

File tree

.github/workflows/build.yml

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
matrix:
9494
java: [ '17', '26' ]
9595
context_path: [ "", "/myidm" ]
96-
samples: [ "", "samples/getting-started", "samples/workflow" ]
96+
samples: [ "", "samples/getting-started", "samples/workflow", "samples/usecase/usecase1" ]
9797
include:
9898
- context_path: ""
9999
context_label: default
@@ -105,6 +105,8 @@ jobs:
105105
samples_label: getting-started
106106
- samples: "samples/workflow"
107107
samples_label: workflow
108+
- samples: "samples/usecase/usecase1"
109+
samples_label: usecase1
108110
steps:
109111
- uses: actions/checkout@v6
110112
- name: Set up Java ${{ matrix.java }}
@@ -125,6 +127,38 @@ jobs:
125127
path: ~/.cache/ms-playwright
126128
key: ${{ runner.os }}-playwright-browsers
127129
restore-keys: ${{ runner.os }}-playwright-
130+
- name: Start OpenDJ for usecase samples
131+
if: startsWith(matrix.samples, 'samples/usecase')
132+
run: |
133+
# samples/usecase/usecase1..7 all assume an LDAP server bound to
134+
# cn=Directory Manager / password on port 1389 with the contents
135+
# of samples/usecase/data/hr_data.ldif imported. Provision it via
136+
# the openidentityplatform/opendj image so the smoke test does not
137+
# require a manually-installed directory server.
138+
mkdir -p /tmp/opendj-bootstrap
139+
# The openidm zip assembly does not currently package
140+
# samples/usecase/data/, so source the LDIF directly from the
141+
# checkout instead of the unpacked deployment.
142+
cp openidm-zip/src/main/resources/samples/usecase/data/hr_data.ldif /tmp/opendj-bootstrap/
143+
docker run -d --name opendj \
144+
-p 1389:1389 -p 1636:1636 -p 4444:4444 \
145+
-e BASE_DN=dc=example,dc=com \
146+
-e ROOT_USER_DN="cn=Directory Manager" \
147+
-e ROOT_PASSWORD=password \
148+
-e ADD_BASE_ENTRY= \
149+
-v /tmp/opendj-bootstrap:/opt/opendj/bootstrap/data \
150+
openidentityplatform/opendj:latest
151+
# Wait for OpenDJ to report readiness in its container logs.
152+
for i in $(seq 1 60); do
153+
if docker logs opendj 2>&1 | grep -q "OpenDJ is started"; then
154+
echo "OpenDJ is started"
155+
break
156+
fi
157+
sleep 2
158+
done
159+
docker logs opendj 2>&1 | grep -q "OpenDJ is started" || {
160+
echo "OpenDJ did not start"; docker logs opendj; exit 1;
161+
}
128162
- name: Start OpenIDM (context_path='${{ matrix.context_path }}', samples='${{ matrix.samples }}')
129163
run: |
130164
OPTS=""
@@ -138,7 +172,28 @@ jobs:
138172
OPENIDM_OPTS="$OPTS" openidm/startup.sh $ARGS &
139173
timeout 3m bash -c 'until grep -q "OpenIDM ready" openidm/logs/openidm0.log.0 ; do sleep 5; done' || cat openidm/logs/openidm0.log.0
140174
grep -q "OpenIDM ready" openidm/logs/openidm0.log.0
141-
! grep -E "ERROR|SEVERE|Exception|Throwable" openidm/logs/openidm0.log.0
175+
# Allow-list of documented, expected log-noise per sample. The
176+
# usecase1 walk-through explicitly relies on three iterative recon
177+
# passes: each early pass legitimately fails for source rows whose
178+
# 'manager' attribute references a managed/user that has not yet
179+
# been created, producing the SynchronizationException /
180+
# BadRequestException / NotFoundException stack traces logged
181+
# below. They are part of the documented behaviour and must not
182+
# turn the smoke-test job red. All other ERROR / SEVERE /
183+
# Exception / Throwable lines remain failing.
184+
ALLOW=""
185+
case "${{ matrix.samples }}" in
186+
samples/usecase/usecase1)
187+
ALLOW="The referenced relationship 'managed/user/[^']+', does not exist|Object [^ ]+ not found in managed/user|org\\.forgerock\\.openidm\\.sync\\.SynchronizationException: The referenced relationship"
188+
;;
189+
esac
190+
if [ -n "$ALLOW" ]; then
191+
! grep -E "ERROR|SEVERE|Exception|Throwable" openidm/logs/openidm0.log.0 \
192+
| grep -vE "$ALLOW" \
193+
| grep -E "ERROR|SEVERE|Exception|Throwable" -q
194+
else
195+
! grep -E "ERROR|SEVERE|Exception|Throwable" openidm/logs/openidm0.log.0
196+
fi
142197
- name: UI Smoke Tests (Playwright)
143198
run: |
144199
cd e2e
@@ -160,6 +215,9 @@ jobs:
160215
openidm/logs/**
161216
e2e/playwright-report/**
162217
e2e/test-results/**
218+
- name: Print OpenDJ logs
219+
if: ${{ always() && startsWith(matrix.samples, 'samples/usecase') }}
220+
run: docker logs opendj || true
163221
- name: Print openidm logs
164222
if: ${{ always() }}
165223
shell: bash
@@ -174,9 +232,29 @@ jobs:
174232
exit 0
175233
fi
176234
echo "----- Checking logs for errors/exceptions -----"
235+
# Per-sample allow-list of expected, documented log-noise. See the
236+
# rationale in the "Start OpenIDM" step above. Any pattern listed
237+
# here is filtered out before deciding whether errors remain.
238+
ALLOW=""
239+
case "${{ matrix.samples }}" in
240+
samples/usecase/usecase1)
241+
ALLOW="The referenced relationship 'managed/user/[^']+', does not exist|Object [^ ]+ not found in managed/user|org\\.forgerock\\.openidm\\.sync\\.SynchronizationException: The referenced relationship"
242+
;;
243+
esac
177244
status=0
178245
while IFS= read -r f; do
179-
if grep -E -n "ERROR|SEVERE|Exception|Throwable" "$f" > /tmp/log_errors.$$ 2>/dev/null; then
246+
if [ -n "$ALLOW" ]; then
247+
grep -E -n "ERROR|SEVERE|Exception|Throwable" "$f" 2>/dev/null \
248+
| grep -vE "$ALLOW" > /tmp/log_errors.$$ || true
249+
[ -s /tmp/log_errors.$$ ] && hit=0 || hit=1
250+
else
251+
if grep -E -n "ERROR|SEVERE|Exception|Throwable" "$f" > /tmp/log_errors.$$ 2>/dev/null; then
252+
hit=0
253+
else
254+
hit=1
255+
fi
256+
fi
257+
if [ "$hit" -eq 0 ]; then
180258
echo "Found errors/exceptions in $f:"
181259
cat /tmp/log_errors.$$
182260
status=1

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ mvn install -f OpenIDM
4141
## How-to run after build
4242
```bash
4343
unzip OpenIDM/openidm-zip/target/openidm-*.zip
44-
./opendm/startup.sh
44+
./openidm/startup.sh
4545
```
4646
Wait for the message **OpenIDM ready** and go:
4747

e2e/helpers.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,57 @@ export async function assertNoErrors(page) {
9393
* verifies that the details panel mentions "success".
9494
*/
9595
export async function runReconcileNow(page, mappingName, expectedSuccessCount) {
96+
// Capture the most-recent recon id for this mapping BEFORE clicking, so
97+
// we can verify that a genuinely new recon ran. Polling the syncLabel for
98+
// "completed" alone is unreliable - that text persists from any prior
99+
// recon and the auto-retry assertion matches it immediately, masking
100+
// cases where the click did not actually start a new run.
101+
const reconHeaders = {
102+
"X-OpenIDM-Username": ADMIN_USER,
103+
"X-OpenIDM-Password": ADMIN_PASS,
104+
"Accept": "application/json",
105+
};
106+
async function latestReconId() {
107+
const url = `${BASE_URL}${CONTEXT_PATH}/audit/recon?_queryFilter=` +
108+
encodeURIComponent(`mapping eq "${mappingName}" and entryType eq "summary"`) +
109+
`&_sortKeys=-timestamp&_pageSize=1&_fields=_id,timestamp`;
110+
const res = await page.request.get(url, { headers: reconHeaders });
111+
// Fail fast on a clearly mis-routed request (most often a hard-coded
112+
// /openidm/ prefix vs. a custom OPENIDM_CONTEXT_PATH) so the helper
113+
// does not silently spin for 180s before reporting "no new audit
114+
// summary".
115+
if (res.status() === 404) {
116+
throw new Error(
117+
`audit/recon endpoint returned 404 for ${url} - ` +
118+
`check OPENIDM_CONTEXT_PATH (current: "${CONTEXT_PATH}")`
119+
);
120+
}
121+
if (!res.ok()) return null;
122+
const body = await res.json();
123+
return body.result && body.result[0] ? body.result[0]._id : null;
124+
}
125+
const beforeId = await latestReconId();
126+
96127
await page.goto(`${BASE_URL}/admin/#properties/${mappingName}/`);
97128
await expect(page.locator("h1")).toContainText(mappingName, { timeout: 30000 });
98129
await page.locator("#propertiesTab").waitFor({ state: "visible", timeout: 30000 });
99130
await page.locator("#syncNowButton").waitFor({ state: "visible", timeout: 30000 });
100131
await page.evaluate(() => window.scrollTo(0, 0));
101132
await page.locator("#syncNowButton").click();
102133

134+
// Wait for a NEW recon summary record to appear (i.e. distinct from the
135+
// one observed prior to the click). This is the authoritative signal
136+
// that the click actually triggered a fresh reconciliation and that it
137+
// has finished writing its audit summary.
138+
let afterId = null;
139+
for (let i = 0; i < 180; i++) {
140+
afterId = await latestReconId();
141+
if (afterId && afterId !== beforeId) break;
142+
await new Promise(r => setTimeout(r, 1000));
143+
}
144+
expect(afterId, `recon for mapping ${mappingName} did not produce a new audit summary`)
145+
.not.toBe(beforeId);
146+
103147
// syncLabel switches to the "Last reconciled" / "Completed" translation when
104148
// the recon ends successfully (see MappingBaseView.setReconEnded).
105149
await expect(page.locator("#syncLabel"))

0 commit comments

Comments
 (0)