Files
openobserve/tests/api-testing
Vamsi Karthik R 99b1a23773 test(slo): E2E + API suite for SLOs (#13836)
Adds automated test coverage for **SLOs** (#13547, #13577, #13579,
#13704, #13761, #13784), which shipped with none. **149 tests** — 77
Playwright + 72 pytest — all three SLI types, both query languages,
happy / negative / edge paths. Clean branch off `main`.

**UI (Playwright · `tests/ui-testing`)**
- `pages/sloPages/` — 4 page objects (verified `data-test` selectors,
hydration + Monaco-sync waits, OSelect commit-retry, virtualized-list
filtering).
- `playwright-tests/SLO/slo-crud.spec.js` (24) — create
count/time-slice/grouped through the form, detail, edit, SLI-type
change, tags, pause, delete, filters; negative: dismissed delete, empty
search, cancel, unmeasured SLI is absent not zero.
- `playwright-tests/SLO/slo-form-validation.spec.js` (20) — server
errors surfacing in the form, conditional fields per SLI type, count +
time-slice previews, **PromQL count and time-slice created end to end**;
negative: no stream, invalid aggregate, over-long name, target
boundaries.
- `playwright-tests/SLO/slo-alert-sli.spec.js` (8) — **the `alert` SLI
type**: source picker, create, config carries only the alert id, detail
names the source; negative: ineligible source offered-but-disabled,
grouped alert SLI, slice finer than source cadence.
- `playwright-tests/SLO/slo-timeslice.spec.js` (10) — **#13761 P0
regression** (measured SLI, not the preview), PromQL, previews, gaps,
language-flip clearing.
- `playwright-tests/SLO/slo-alerts.spec.js` (15) — burn-rate +
error-budget kinds, presets, edit, freeze door, burndown; negative: no
destination, cancel, blank/invalid name.
- `playwright-tests/utils/slo-seed.js` — backdated seeding, metrics
seeding, eligible-source-alert fixture, ground-truth query, fail-fast
env guard, transient-store retry, full fixture teardown (alerts →
streams → destinations → templates).

**API (pytest · `tests/api-testing`)**
- `tests/slos/test_slo_validation.py` (72) — full REST surface
(create/get/update/enable/delete/groups/slo-eligible), validation on
**both create and update** (name, target, window, slice, config shapes,
comparators, float-threshold round-trip), boundaries (target
0.001/99.999, name 256 vs 257, threshold 0/negative/1e12/NaN, unicode
names), alert-source eligibility (silence-gated, too-infrequent, cron),
PromQL config shapes, auth 401/404.

**CI**
- Two shards: `SLO` (`workers: 3`) and `SLO-Measurement` (`workers: 1`).
Split because the backfill job runs with
`ZO_SCHEDULER_SLO_BACKFILL_CONCURRENCY=1`, so measurement specs sharing
an instance starve each other's waits — `fullyParallel` races spec
*files*, which `mode:'serial'` cannot prevent. The 3-worker pin is
SQLite: at 5, concurrent SLO/alert/destination writes trip `database is
locked` on the single-node meta store.
- New optional per-shard `ingest_allowed_upto` (`240`) alongside the
existing `quick_mode_enabled`: the workflow default of 5 **hours**
silently drops the 8 days of history an SLO window needs, while still
answering `200`. Other shards keep the default. Same precedent as
`api-testing.yml`'s `48`.

**Feature code**: 9 additive `data-test`/`dataTest` attributes in
`SloDetail.vue`, `SloList.vue`, `SloAlertCondition.vue` — no logic
changes, `vue-tsc` clean. The stat tiles, tabs and burn-window inputs
had no stable selectors; `OStatStrip`/`OTab` already forward the
attribute.

**Behaviour**: self-cleaning (`e2e_slo_*`, worker-scoped since
`afterAll` runs per worker).

**Verified in CI** on this branch: `e2e / SLO` **52 passed** (1.8m,
workers pinned to 3), `e2e / SLO-Measurement` **25 passed** (11.3m,
workers pinned to 1) — the measurement shard passing confirms the
per-shard `ingest_allowed_upto` took effect, since those tests are
impossible under the 5-hour default. pytest **72 passed** locally; in
CI's 599-test run one setup delete hit a transient 500 (see findings)
and now retries.

**Findings for the feature owner** (reported, not fixed here):
- A blank "good when" shows `Request failed with status code 422` —
`pruned()` drops the empty key, so the API's real 400 message never
reaches `save()`.
- A rejected alert name shows no banner: `SloAlertForm.submit()` returns
on `nameError` before setting `saveError`.
- `avg(latency_ms); DROP TABLE users--` is accepted at create time
(parsed only at measurement).
- The name-length message says "less than 256 characters" but 256 is
accepted — 257 is the first rejection.
- Duplicate alert names on one SLO are accepted (duplicate SLO names are
a 409), and the panel lists them by name, so they are indistinguishable.
- SLO names accept characters that break the list's own
`slos-slolist-edit-${name}` selectors; alert names correctly reject
them.
- All chart/preview root `data-test` ids are unreachable — a parent's
fallthrough attribute overrides the child root's.
- Both edit forms hydrate asynchronously, so a blank form can flash
before the stored values appear.
- `DELETE /slos/{id}` on a freshly-created SLO returned **500** once
under CI load (not reproducible locally in 12 rapid iterations).
Contention is arguably a retryable 503, and the body carried no
actionable message.
- **A scheduled alert whose source stream is deleted is retried
forever** (`Stream "…" not found in schema`), and the SLO backfill lane
shares the scheduler's worker budget — so accumulated dead alerts
*starve SLO measurement*. Observed: 65 of them held coverage at 1 of 7
chunks indefinitely; deleting them restored 0.143 → 1.0 in 240s.
Harmless in CI (fresh instance per shard) but it degrades any long-lived
environment. The suite now cleans up after itself; the retry-forever
behaviour itself may deserve a look.

**Paired ENT PR: openobserve/o2-enterprise#2434.** ENT's matrix is this
repo's manifest plus an overlay, merged by this repo's
`build-ci-matrix.js`, so both SLO shards reach ENT automatically —
verified: 31 merged shards, both SLO entries carrying
`ingest_allowed_upto=240` and their worker pins, no other shard
affected. What ENT was missing is *consuming* those two fields, which
#2434 adds. Merge the two together: this one alone leaves ENT booting
the SLO shards on the 5-hour ingest window, and #2434 alone is inert.

*(An earlier review reported ENT as using an inline matrix with no SLO
shard — that turned out not to be the case.)*
2026-08-17 18:50:29 +00:00
..
2026-01-14 16:54:35 +08:00

OpenObserve API Tests

End-to-end pytest suite against the OpenObserve REST API. Runs in CI on every PR (.github/workflows/api-testing.yml); also runnable locally.

Quick start

# one-time setup
brew install rye           # or: curl -sSf https://rye.astral.sh/get | bash
rye sync                   # install Python + deps from requirements.lock
cargo build                # build the OO debug binary (~10 min first time)

# every run
ZO_ROOT_USER_EMAIL=root@example.com \
ZO_ROOT_USER_PASSWORD='Complexpass#123' \
ZO_BASE_URL=http://localhost:5080/ \
WS_ZO_BASE_URL=ws://localhost:5080/ \
target/debug/openobserve &
rye run pytest             # full suite
rye run pytest -m framework        # just the new-framework self-tests
rye run pytest tests/test_kvstore.py -v   # one file

Layout

tests/api-testing/
├── pyproject.toml          # pytest + ruff config
├── conftest.py             # legacy session-wide fixtures (do not extend)
├── support/                # NEW FRAMEWORK — write new tests against this
│   ├── client.py           # OpenObserveClient: HTTP entry point
│   ├── endpoints/          # one wrapper class per resource group
│   │   ├── search.py       # client.search.sql(...), .hits(...), .count(...)
│   │   ├── streams.py      # client.streams.list(), .delete(...), .ingest_json(...)
│   │   ├── users.py
│   │   ├── dashboards.py
│   │   ├── alerts.py
│   │   ├── folders.py
│   │   └── kvstore.py
│   ├── factories.py        # payload builders (user_payload, dashboard_payload, …)
│   ├── wait.py             # wait_until(predicate, timeout, ...) — REPLACES time.sleep
│   └── fixtures.py         # client, temp_stream_name, temp_user_email, …
├── tests/                  # test files
│   ├── conftest.py
│   ├── test_framework_selfcheck.py   # 14 tests that prove support/ works
│   └── test_*.py           # 40+ legacy test files (in revamp progress)
├── helpers/                # non-test shared code
│   ├── sourcemap_helpers.py
│   └── workflow/           # workflow chain helpers (with embedded pages/)
├── fixtures/               # static test data
│   └── rum/                # RUM ingest scripts
└── README.md               # this file

Writing a new test — the rules

Use the new framework. Don't copy-paste from old test_*.py files; they are scheduled for rewrite (see revamp plan).

# tests/<area>/test_my_feature.py
from support.client import OpenObserveClient
from support.factories import user_payload
from support.fixtures import client, temp_user_email   # noqa: F401
from support.wait import wait_until


def test_create_user_returns_complete_record(client: OpenObserveClient, temp_user_email: str):
    """Create a user — verify the response has the email AND role we sent."""
    payload = user_payload(email=temp_user_email, role="admin")
    resp = client.users.create(payload)

    assert resp.status_code == 200, resp.text
    body = resp.json()
    # VALIDATE THE BODY, not just status_code
    assert body.get("data", {}).get("email") == temp_user_email
    assert body.get("data", {}).get("role") == "admin"
    # cleanup happens automatically via temp_user_email fixture

Mandatory patterns

✅ Do ❌ Don't
from support.fixtures import client session = create_session; url = base_url; org_id = "default" (307 occurrences in legacy code)
client.streams.list() session.get(f"{url}api/{org_id}/streams")
body = resp.json(); assert body["foo"] == ... assert resp.status_code == 200 and stop
wait_until(lambda: client.search.count(sql) >= N, timeout=30) time.sleep(5)
unique_email("user") / temp_user_email fixture hardcoded pytests@gmail.com
logger.debug("got %s", resp.status_code) print(resp.content)
start, end = time_window(minutes=15) hardcoded start_time = 1700629279639000

Anti-patterns that fail review

  • assert resp.status_code in [200, 400, 404, 500] — pick one. If the API behaves wrongly, file a bug; don't paper over it.
  • pytest.skip(...) when the test discovers the bug it's meant to catch — use @pytest.mark.xfail(strict=True).
  • Commented-out test functions — delete them.
  • pages.* direct imports — that subtree is legacy. New tests use support.client.

Lint

rye run ruff check support/ tests/test_framework_selfcheck.py  # strict mode
rye run ruff check --fix support/                              # autofix

Existing legacy tests have a per-file ruff-ignore (so print etc. don't fail the whole suite); new tests under support/ and any new well-formed test file should pass strict ruff.

Test markers

Marker Meaning
framework Self-tests for support/ framework. Run with -m framework.
order(N) Run order pin (legacy — avoid in new tests).

Troubleshooting

  • KeyError: 'ZO_BASE_URL' on collection — conftest.py reads env vars at import. Export them before running pytest (even --collect-only).
  • test_workflow.py fails with SMTP error — ZO_SMTP_ENABLED=true not set. CI does this; locally you'd need to wire an SMTP env.
  • Test_organisations.py warning about yield — test_create_organization uses pre-pytest-4 yield-style and is silently ignored. Scheduled for rewrite in Phase 4.

Revamp in progress

This suite is being incrementally reworked. See: