Files
openobserve/tests/ui-testing/playwright-tests/utils/o2-api.js
T
Vamsi Karthik R c3056cc720 test(alerts): E2E suite for email destinations and distribution lists (#13876)
Automated coverage for **email alert destinations and distribution
lists**, and unblocks the email tests that have been skipped since they
were written. **32 cases** — 27 active, 5 skipped regressions tracking
open defects (openobserve/o2-enterprise#2471).

**The unblocking.** The two skipped tests said *"the test environment
does not have email infrastructure set up"*. That over-stated it:
creating, editing, validating and deleting an email destination needs
SMTP switched **on**, and never reads a mailbox. Only delivery
assertions do. The suite is tiered on that:

| Tier | Needs | Cases |
|---|---|---|
| A | SMTP enabled | 24 |
| B | a readable sink | 2 |
| C | an alias that fans out | 1 |

**Tests**
- `Alerts/dl-email-destinations.spec.js` (32) — the DL half (alias
accepted, addressed, fanned out) and the recipient-list half, where our
code actually runs: splitting/trimming/lowercasing, the org-membership
gate, edit round-trips, add/remove/delete, both form paradigms (prebuilt
free-text vs the custom path's org-user picker), template auto-attach,
duplicate names, cancel, type-switching, keyboard, dark mode at 420px.
- `utils/mail-sink.js` — pluggable sink: Mailpit, a Mailinator inbox, or
a clean skip with a stated reason.
- `utils/o2-api.js` — storage is asserted from the API, never the form
field: an input can echo what you typed while the stored value differs.
- `scripts/dl-relay.js` — SMTP relay that expands one alias into
per-member deliveries, so fan-out runs with no external provider. Same
pattern as the in-test HTTP receiver in
`alerts-content-templates.spec.js`.
- `alertDestinationsPage.js` — +17 methods; the spec carries zero raw
locators.

**Replaces, without losing coverage.** The two skipped tests in
`alerts-destinations-prebuilt.spec.js` are removed for a pointer mapping
their intent to its new home: create (`D-01`), edit-add (`D-07`),
edit-remove (`ML-08`), delete (`UI-12`), format validation (`UI-02`).
Three of those did not exist before. That file now has no skips.

**CI.** `ZO_SMTP_ENABLED: true` was already set in `playwright.yml` but
had no host or port, so it fell back to `localhost:25` where nothing
listens — destinations could be **saved** while every **send** failed
silently. The workflow now starts Mailpit and a DL relay as static
binaries before the app (no Docker-in-Docker on these runners) and
points `ZO_SMTP_*` at them.

All three tiers run with no secrets and no egress. Verified in the CI
topology: **27 passed, 5 skipped**, `D-02` confirmed reaching all three
member mailboxes.

**Behaviour.** Self-cleaning via the `auto_dest_dl_*` prefix
`cleanup.spec.js` already sweeps, plus an API `afterAll`. `mode:
'parallel'` — each test owns its fixtures.

**Findings** are filed in openobserve/o2-enterprise#2471, not fixed
here. Two P1s: the *"Show matching rows"* toggle is ignored so log rows
are always embedded, and a user removed from the org keeps receiving
alerts while the destination can no longer be saved. The regression
tests for each are in this PR, skipped until the fix lands.

**Paired ENT PR: openobserve/o2-enterprise#2472.** ENT's workflow sets
`ZO_SMTP_ENABLED: false`, so without it the suite skips all 27 cases
there. The shard matrix needs no ENT change — ENT builds it from this
repo's `ci_matrix.json` plus its overlay, so the new spec is picked up
automatically. Merge the two together: this one alone leaves enterprise
with no email coverage.
2026-08-20 04:19:42 +00:00

67 lines
3.2 KiB
JavaScript

/**
* Thin authenticated REST client for the destination/template/user endpoints.
* Used by the DL suite to verify what the UI actually persisted — the form can
* echo your input while the stored value differs (trimming, lowercasing,
* empty entries), so every storage assertion reads the API, not the field.
*/
const BASE = process.env.ZO_BASE_URL || 'http://localhost:5080';
const ORG = process.env.ORGNAME || 'default';
const USER = process.env.ZO_ROOT_USER_EMAIL || 'root@example.com';
// The repo's documented default for local runs — the same value playwright.yml
// commits publicly. Not a secret; CI and any real environment override it.
const PASS = process.env.ZO_ROOT_USER_PASSWORD || 'Complexpass#123';
const AUTH = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64');
async function req(method, path, body) {
const res = await fetch(`${BASE}/api/${ORG}${path}`, {
method,
headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch (_) { /* non-JSON body */ }
return { status: res.status, body: json, text };
}
const getDestination = (name) => req('GET', `/alerts/destinations/${name}`);
const listDestinations = () => req('GET', '/alerts/destinations');
const createDestination = (payload) => req('POST', '/alerts/destinations', payload);
const deleteDestination = (name) => req('DELETE', `/alerts/destinations/${name}`);
const testDestination = (payload) => req('POST', '/alerts/destinations/test', payload);
const listTemplates = () => req('GET', '/alerts/templates');
/**
* A destination recipient must be a user of the org — see the membership gate in
* alerts/destinations.rs. A distribution-list alias is not a person, so the DL
* fan-out case has to enrol the alias before it can be addressed at all.
*
* role MUST be 'admin': UserRoleRequest::from() (common/src/meta/user.rs) matches
* the requested role string against get_roles() and, on no match, treats it as a
* CUSTOM role — which core/src/users.rs::post_user rejects outright with 400 when
* OpenFGA/RBAC is off (the OSS CI topology this suite actually runs in restricts
* the standard-role set to Admin only). A lower role 400s the create outright,
* which is worse for the "stray privileged account" concern this once tried to
* avoid — cleanup being flag-gated (see dlUserCreated below) is the real mitigation.
*/
const createOrgUser = (email) => req('POST', '/users', {
email, first_name: 'dl', last_name: 'alias',
password: PASS,
role: 'admin',
});
const deleteOrgUser = (email) => req('DELETE', `/users/${encodeURIComponent(email)}`);
const getTemplate = (name) => req('GET', `/alerts/templates/${name}`);
/** Stored recipients for a destination, or null when it does not exist. */
async function storedRecipients(name) {
const r = await getDestination(name);
if (r.status !== 200 || !r.body) return null;
return r.body.emails;
}
module.exports = {
BASE, ORG, req, getDestination, listDestinations, createDestination,
deleteDestination, testDestination, listTemplates, getTemplate, storedRecipients,
createOrgUser, deleteOrgUser,
};