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
84 changes: 81 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
matrix:
java: [ '17', '26' ]
context_path: [ "", "/myidm" ]
samples: [ "", "samples/getting-started", "samples/workflow" ]
samples: [ "", "samples/getting-started", "samples/workflow", "samples/usecase/usecase1" ]
include:
- context_path: ""
context_label: default
Expand All @@ -105,6 +105,8 @@ jobs:
samples_label: getting-started
- samples: "samples/workflow"
samples_label: workflow
- samples: "samples/usecase/usecase1"
samples_label: usecase1
steps:
- uses: actions/checkout@v6
- name: Set up Java ${{ matrix.java }}
Expand All @@ -125,6 +127,38 @@ jobs:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-browsers
restore-keys: ${{ runner.os }}-playwright-
- name: Start OpenDJ for usecase samples
if: startsWith(matrix.samples, 'samples/usecase')
run: |
# samples/usecase/usecase1..7 all assume an LDAP server bound to
# cn=Directory Manager / password on port 1389 with the contents
# of samples/usecase/data/hr_data.ldif imported. Provision it via
# the openidentityplatform/opendj image so the smoke test does not
# require a manually-installed directory server.
mkdir -p /tmp/opendj-bootstrap
# The openidm zip assembly does not currently package
# samples/usecase/data/, so source the LDIF directly from the
# checkout instead of the unpacked deployment.
cp openidm-zip/src/main/resources/samples/usecase/data/hr_data.ldif /tmp/opendj-bootstrap/
docker run -d --name opendj \
-p 1389:1389 -p 1636:1636 -p 4444:4444 \
-e BASE_DN=dc=example,dc=com \
-e ROOT_USER_DN="cn=Directory Manager" \
-e ROOT_PASSWORD=password \
-e ADD_BASE_ENTRY= \
-v /tmp/opendj-bootstrap:/opt/opendj/bootstrap/data \
openidentityplatform/opendj:latest
# Wait for OpenDJ to report readiness in its container logs.
for i in $(seq 1 60); do
if docker logs opendj 2>&1 | grep -q "OpenDJ is started"; then
echo "OpenDJ is started"
break
fi
sleep 2
done
docker logs opendj 2>&1 | grep -q "OpenDJ is started" || {
echo "OpenDJ did not start"; docker logs opendj; exit 1;
}
- name: Start OpenIDM (context_path='${{ matrix.context_path }}', samples='${{ matrix.samples }}')
run: |
OPTS=""
Expand All @@ -138,7 +172,28 @@ jobs:
OPENIDM_OPTS="$OPTS" openidm/startup.sh $ARGS &
timeout 3m bash -c 'until grep -q "OpenIDM ready" openidm/logs/openidm0.log.0 ; do sleep 5; done' || cat openidm/logs/openidm0.log.0
grep -q "OpenIDM ready" openidm/logs/openidm0.log.0
! grep -E "ERROR|SEVERE|Exception|Throwable" openidm/logs/openidm0.log.0
# Allow-list of documented, expected log-noise per sample. The
# usecase1 walk-through explicitly relies on three iterative recon
# passes: each early pass legitimately fails for source rows whose
# 'manager' attribute references a managed/user that has not yet
# been created, producing the SynchronizationException /
# BadRequestException / NotFoundException stack traces logged
# below. They are part of the documented behaviour and must not
# turn the smoke-test job red. All other ERROR / SEVERE /
# Exception / Throwable lines remain failing.
ALLOW=""
case "${{ matrix.samples }}" in
samples/usecase/usecase1)
ALLOW="The referenced relationship 'managed/user/[^']+', does not exist|Object [^ ]+ not found in managed/user|org\\.forgerock\\.openidm\\.sync\\.SynchronizationException: The referenced relationship"
;;
esac
if [ -n "$ALLOW" ]; then
! grep -E "ERROR|SEVERE|Exception|Throwable" openidm/logs/openidm0.log.0 \
| grep -vE "$ALLOW" \
| grep -E "ERROR|SEVERE|Exception|Throwable" -q
else
! grep -E "ERROR|SEVERE|Exception|Throwable" openidm/logs/openidm0.log.0
fi
- name: UI Smoke Tests (Playwright)
run: |
cd e2e
Expand All @@ -160,6 +215,9 @@ jobs:
openidm/logs/**
e2e/playwright-report/**
e2e/test-results/**
- name: Print OpenDJ logs
if: ${{ always() && startsWith(matrix.samples, 'samples/usecase') }}
run: docker logs opendj || true
- name: Print openidm logs
if: ${{ always() }}
shell: bash
Expand All @@ -174,9 +232,29 @@ jobs:
exit 0
fi
echo "----- Checking logs for errors/exceptions -----"
# Per-sample allow-list of expected, documented log-noise. See the
# rationale in the "Start OpenIDM" step above. Any pattern listed
# here is filtered out before deciding whether errors remain.
ALLOW=""
case "${{ matrix.samples }}" in
samples/usecase/usecase1)
ALLOW="The referenced relationship 'managed/user/[^']+', does not exist|Object [^ ]+ not found in managed/user|org\\.forgerock\\.openidm\\.sync\\.SynchronizationException: The referenced relationship"
;;
esac
status=0
while IFS= read -r f; do
if grep -E -n "ERROR|SEVERE|Exception|Throwable" "$f" > /tmp/log_errors.$$ 2>/dev/null; then
if [ -n "$ALLOW" ]; then
grep -E -n "ERROR|SEVERE|Exception|Throwable" "$f" 2>/dev/null \
| grep -vE "$ALLOW" > /tmp/log_errors.$$ || true
[ -s /tmp/log_errors.$$ ] && hit=0 || hit=1
else
if grep -E -n "ERROR|SEVERE|Exception|Throwable" "$f" > /tmp/log_errors.$$ 2>/dev/null; then
hit=0
else
hit=1
fi
fi
if [ "$hit" -eq 0 ]; then
echo "Found errors/exceptions in $f:"
cat /tmp/log_errors.$$
status=1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ mvn install -f OpenIDM
## How-to run after build
```bash
unzip OpenIDM/openidm-zip/target/openidm-*.zip
./opendm/startup.sh
./openidm/startup.sh
```
Wait for the message **OpenIDM ready** and go:

Expand Down
44 changes: 44 additions & 0 deletions e2e/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,57 @@ export async function assertNoErrors(page) {
* verifies that the details panel mentions "success".
*/
export async function runReconcileNow(page, mappingName, expectedSuccessCount) {
// Capture the most-recent recon id for this mapping BEFORE clicking, so
// we can verify that a genuinely new recon ran. Polling the syncLabel for
// "completed" alone is unreliable - that text persists from any prior
// recon and the auto-retry assertion matches it immediately, masking
// cases where the click did not actually start a new run.
const reconHeaders = {
"X-OpenIDM-Username": ADMIN_USER,
"X-OpenIDM-Password": ADMIN_PASS,
"Accept": "application/json",
};
async function latestReconId() {
const url = `${BASE_URL}${CONTEXT_PATH}/audit/recon?_queryFilter=` +
encodeURIComponent(`mapping eq "${mappingName}" and entryType eq "summary"`) +
`&_sortKeys=-timestamp&_pageSize=1&_fields=_id,timestamp`;
const res = await page.request.get(url, { headers: reconHeaders });
// Fail fast on a clearly mis-routed request (most often a hard-coded
// /openidm/ prefix vs. a custom OPENIDM_CONTEXT_PATH) so the helper
// does not silently spin for 180s before reporting "no new audit
// summary".
if (res.status() === 404) {
throw new Error(
`audit/recon endpoint returned 404 for ${url} - ` +
`check OPENIDM_CONTEXT_PATH (current: "${CONTEXT_PATH}")`
);
}
if (!res.ok()) return null;
const body = await res.json();
return body.result && body.result[0] ? body.result[0]._id : null;
}
const beforeId = await latestReconId();

await page.goto(`${BASE_URL}/admin/#properties/${mappingName}/`);
await expect(page.locator("h1")).toContainText(mappingName, { timeout: 30000 });
await page.locator("#propertiesTab").waitFor({ state: "visible", timeout: 30000 });
await page.locator("#syncNowButton").waitFor({ state: "visible", timeout: 30000 });
await page.evaluate(() => window.scrollTo(0, 0));
await page.locator("#syncNowButton").click();

// Wait for a NEW recon summary record to appear (i.e. distinct from the
// one observed prior to the click). This is the authoritative signal
// that the click actually triggered a fresh reconciliation and that it
// has finished writing its audit summary.
let afterId = null;
for (let i = 0; i < 180; i++) {
afterId = await latestReconId();
if (afterId && afterId !== beforeId) break;
await new Promise(r => setTimeout(r, 1000));
}
expect(afterId, `recon for mapping ${mappingName} did not produce a new audit summary`)
.not.toBe(beforeId);

// syncLabel switches to the "Last reconciled" / "Completed" translation when
// the recon ends successfully (see MappingBaseView.setReconEnded).
await expect(page.locator("#syncLabel"))
Expand Down
Loading
Loading