test(functions): un-skip JS-in-all-orgs tests + add per-event runtime-error coverage (#13341)

Replaces the `skip` band-aids that PR #13156 ("workflows v1") left on
the **Functions** tests with real, running coverage of the current
behaviour — on the correct edition — plus new tests for behaviour #13156
introduced but never covered. Test files only, no feature-code churn.
Clean branch off `main`.

## Background — what #13156 actually changed

#13156 bundled a Functions backend change unrelated to Workflows, and
the old tests still asserted the *pre*-#13156 rules, so they were
silenced with `pytest.skip(...)` / `test.skip(...)`. Two rules changed:

1. **JS is no longer `_meta`-only at the backend.** `save_function` /
`test_run_function` have **no org gate on any edition** — JS
(`transType:1`) is accepted in every org, including `default`, on **both
OSS and Enterprise**. The `_meta`-only rule now lives **only in the
frontend** (`AddFunction.vue` gates the JS radio on `isEnterprise ||
isCloud || org === "_meta"`, baked at build time by
`VITE_OPENOBSERVE_ENTERPRISE`).
2. **The compile-check reports only *syntax* errors, not *runtime*
errors.** A syntax error still returns **400**; code that compiles but
throws at runtime now returns **200** with the error surfaced
**per-event** in `results[].message` — for **both VRL and JS**.

Because the API tests hit the backend directly, there is **no edition
difference to assert for them** — the earlier "OSS asserts 400 / ENT
asserts 200" framing was incorrect. Only the **e2e** (frontend JS-radio)
is genuinely edition-dependent.

## API tests (pytest · `tests/api-testing`)

**`test_functions.py`**
- Keep the compile-error → 400 test
(`test_test_endpoint_with_invalid_vrl_returns_400`) — it was never
actually failing; `test`/`=====` are genuine VRL *compile* errors
(`undefined variable` / syntax) and still 400.
- Add `test_test_endpoint_with_js_runtime_error_returns_200` and
`test_test_endpoint_with_vrl_runtime_error_returns_200` — runtime errors
now return 200 with a per-event `results[].message`.

**`test_functions_meta_org_restriction.py`**
- Flip the 3 previously-skipped "blocked in default" tests → assert
**200** (JS allowed in every org): `..._blocked` → `..._allowed`.
- Repurpose `TestErrorMessages` (its old restriction-message assertions
no longer exist) into per-event runtime-error coverage: JS
`ReferenceError` content, VRL + JS surfacing consistency,
partial-failure (only the failing event carries a message), and a
syntax-error → 400 regression guard.
- Update the class/module docstrings to the real contract.

**`test_js_enrichment.py`**
- Flip `test_js_function_in_default_org_returns_400` →
`test_js_function_in_default_org_allowed` (200, transform runs).

## e2e (Playwright ·
`tests/ui-testing/Functions/js-transform-type.spec.js`)

The JS-radio rule *is* edition-dependent. Instead of a skip (or an
env-var hack), the two tests now detect edition at runtime via
`EditionFeaturesPage.detectEdition()` — which reads the header edition
button, driven by the **same** build flag that gates the JS radio, so
it's the exact source of truth and needs **no CI change**:

- **OSS build** → assert JS radio **hidden** in a non-meta org.
- **Enterprise / cloud build** → assert JS radio **visible** in every
org.

The edition-agnostic steps that were trapped inside the skipped tests
now **always run**: VRL-function create in a non-meta org (a P0 smoke
path), and JS-visible in `_meta`. Added `P1: VRL runtime error surfaces
in error-details` (mirrors the existing JS `ReferenceError` case; covers
the VRL half of the per-event-error change).

## Skips removed (before → after)

| Old (skipped) | New (runs & passes) |
|---|---|
| `test_create_js_function_in_default_org_blocked` |
`..._default_org_allowed` |
| `test_test_js_function_in_default_org_blocked` |
`..._default_org_allowed` |
| `test_update_vrl_to_js_in_default_org_blocked` |
`..._default_org_allowed` |
| `test_error_message_content` / `_consistency` | per-event
runtime-error tests |
| `test_js_function_in_default_org_returns_400` (enrichment) |
`..._default_org_allowed` |
| e2e `P0: Non-meta org - JS hidden, VRL works` | `P0: Non-meta org - JS
radio edition-gated, VRL works` |
| e2e `P1: Radio visibility across orgs` (skipped) | same name,
edition-aware, no `test.skip` |

`grep -rn "JS fns are now allowed" tests/` and `grep -rn "JS is allowed
in all orgs on enterprise" tests/` → **both empty**. (The 7 remaining
pytest skips are the unrelated `JS_HELPER_UNAVAILABLE` enrichment-helper
cases — a separate pre-existing limitation, not part of this change.)

## Verification (local, against current `main` source)

Built the current-`main` OSS binary and ran everything live:

- **API:** `32 passed`, `0` target skips (7 unrelated
`JS_HELPER_UNAVAILABLE` skips remain).
- **e2e OSS build:** `7/7` — `detectEdition=opensource`, JS hidden in
non-meta.
- **e2e Enterprise-frontend build:** `3/3` edition tests —
`detectEdition=enterprise`, JS visible everywhere, VRL runtime error
renders in error-details.

All backend behaviour was confirmed by probing the live `/functions` +
`/functions/test` endpoints, not assumed.

## What CI proves, and where

- **This PR (openobserve):** `api-testing.yml` runs the API tests;
`playwright.yml` (`Functions` shard) runs the e2e on the **OSS** edition
(JS-hidden branch).
- **Enterprise edition:** dispatch `o2-enterprise` → `playwright.yml`
(workflow_dispatch) with `o2_opensource_repo =
test/functions-skipped-tests-13318` to run the same specs on the
**enterprise** build (JS-visible branch). Pairs with the ENT Functions
work in #13318 / o2-enterprise#2217.


🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
This commit is contained in:
Vamsi Karthik R
2026-07-27 01:45:28 +00:00
committed by GitHub
co-authored by Claude Opus 4.8 Hengfei Yang
parent 30c80cd2ce
commit 442ca09688
4 changed files with 317 additions and 160 deletions
@@ -157,7 +157,7 @@ def test_test_endpoint_with_valid_vrl_returns_transformed_events(client: OpenObs
("vrl_function", "description"),
[
(".test=2jehwkhhe\n.", "VRL with syntax error"),
("test", "plain text (not a VRL function)"),
("test", "plain text (undefined VRL variable)"),
("=====", "VRL with only special chars"),
],
ids=["syntax-error", "plain-text", "special-chars-only"],
@@ -165,13 +165,67 @@ def test_test_endpoint_with_valid_vrl_returns_transformed_events(client: OpenObs
def test_test_endpoint_with_invalid_vrl_returns_400(
client: OpenObserveClient, vrl_function: str, description: str
):
"""Various invalid VRL function bodies passed to /functions/test all return 400.
"""COMPILE-invalid VRL bodies passed to /functions/test return 400.
Consolidates 3 near-identical legacy tests (testinvalidfunction,
onlytextfunction, testonlyspecialcharfunction) — each was 25-30 LOC
of copy-paste differing only in one field.
Note (PR #13156): the compile-check now reports only COMPILE errors, not
runtime errors. All three bodies below are genuine VRL *compile* errors
when trans_type=0 (`test` and `=====` are "call to undefined variable" /
syntax errors, not merely runtime failures), so they still 400. Runtime
failures that compile cleanly now return 200 with a per-event message —
see test_test_endpoint_with_*_runtime_error_returns_200 below.
"""
payload = {"function": vrl_function, "events": SAMPLE_EVENTS, "trans_type":0 }
payload = {"function": vrl_function, "events": SAMPLE_EVENTS, "trans_type": 0}
resp = client.post("functions/test", json=payload)
assert resp.status_code == 400, \
f"{description}: expected 400, got {resp.status_code}: {resp.text}"
def test_test_endpoint_with_js_runtime_error_returns_200(client: OpenObserveClient):
"""JS that compiles but throws at RUNTIME returns 200, with the error
surfaced per-event in results[].message (PR #13156 behaviour change).
`undefinedVar` is a valid JS identifier (syntactically fine), so the
compile-check passes; the ReferenceError only happens when the code runs
against each event. Previously this 200-path dropped results[].message;
#13156 wires the runtime error through per-event.
"""
payload = {
"function": "row.field = undefinedVar;",
"events": SAMPLE_EVENTS,
"trans_type": 1,
}
resp = client.post("functions/test", json=payload)
assert resp.status_code == 200, \
f"JS runtime error should be 200, got {resp.status_code}: {resp.text}"
body = resp.json()
assert "results" in body, f"response missing 'results': {body}"
assert len(body["results"]) == len(SAMPLE_EVENTS), \
f"expected one result per event, got: {body['results']}"
assert all("ReferenceError" in r.get("message", "") for r in body["results"]), \
f"expected a per-event ReferenceError message, got: {body['results']}"
def test_test_endpoint_with_vrl_runtime_error_returns_200(client: OpenObserveClient):
"""VRL that compiles but aborts at RUNTIME returns 200, with the error
surfaced per-event in results[].message (PR #13156 behaviour change).
`to_int!(.s)` compiles fine; it only aborts when `.s` can't be coerced to
an int at runtime. Before #13156 VRL runtime errors were tooltip-only —
now they render per-event, mirroring the JS path.
"""
payload = {
"function": ".x = to_int!(.s)",
"events": [{"s": "notanumber"}, {"s": "alsobad"}],
"trans_type": 0,
}
resp = client.post("functions/test", json=payload)
assert resp.status_code == 200, \
f"VRL runtime error should be 200, got {resp.status_code}: {resp.text}"
body = resp.json()
assert "results" in body, f"response missing 'results': {body}"
assert all("vrl runtime error" in r.get("message", "") for r in body["results"]), \
f"expected a per-event VRL runtime error message, got: {body['results']}"
@@ -1,9 +1,19 @@
import pytest
"""
API tests for JavaScript function _meta organization restriction.
API tests for JavaScript function organization behaviour.
This module tests that JavaScript functions (transType=1) are only allowed
in the _meta organization, while VRL functions (transType=0) work in all orgs.
Backend contract (validated on OSS + ENT builds, current main):
The functions *backend* imposes NO org restriction on JavaScript functions —
JS (transType=1) is accepted in every org, including `default`, on BOTH OSS
and Enterprise builds. The `_meta`-only rule that used to exist now lives ONLY
in the frontend (AddFunction.vue gates the JS radio on
`isEnterprise || isCloud || org === "_meta"`, baked at build time). Since API
tests hit the backend directly, there is no edition difference to assert here.
Historical note (PR #13156): earlier the backend returned 400 with
"JavaScript functions are only allowed in the '_meta' organization" for JS in
non-_meta orgs. That restriction was removed. These tests assert the current
"JS allowed everywhere" contract; the class name is kept for git continuity.
Note: The functions API uses camelCase for field names (transType) due to
serde(rename_all = "camelCase") on the Transform struct.
@@ -12,7 +22,7 @@ The test endpoint uses snake_case (trans_type) for TestVRLRequest struct.
class TestMetaOrgJavaScriptRestriction:
"""Test JavaScript functions restricted to _meta org only.
"""JavaScript functions are accepted in all orgs at the backend/API layer.
IMPORTANT - API Field Name Format:
- Create/Update endpoints (POST/PUT /api/{org}/functions): use `transType` (camelCase)
@@ -42,34 +52,36 @@ class TestMetaOrgJavaScriptRestriction:
# Cleanup
session.delete(f"{base_url}api/{org_id}/functions/test_js_meta")
def test_create_js_function_in_default_org_blocked(self, create_session, base_url):
"""JavaScript function creation should fail in default org."""
def test_create_js_function_in_default_org_allowed(self, create_session, base_url):
"""JavaScript function creation succeeds in default org (JS no longer _meta-only).
pytest.skip("JS fns are now allowed in non _meta orgs")
return
Previously this returned 400 with a restriction message. The backend
restriction was removed (PR #13156), so JS create in a non-_meta org
now returns 200 on every edition. (Positive side of "JS allowed
everywhere" — the negative case no longer exists at the API layer.)
"""
session = create_session
org_id = "default"
payload = {
"name": "test_js_default",
"name": "test_js_default_allowed",
"function": "row.processed = true;",
"params": "row",
"transType": 1, # JavaScript - should be blocked (camelCase for Transform struct)
"transType": 1, # JavaScript (camelCase for Transform struct)
}
# Cleanup any leftover from previous runs
session.delete(f"{base_url}api/{org_id}/functions/test_js_default_allowed")
resp = session.post(f"{base_url}api/{org_id}/functions", json=payload)
assert resp.status_code == 400, (
f"Expected 400 for JS function in default org, "
f"but got {resp.status_code}: {resp.content}"
assert resp.status_code == 200, (
f"Expected 200 for JS function in default org (JS is no longer "
f"_meta-only), but got {resp.status_code}: {resp.content}"
)
# Verify error message
response_text = resp.text
assert "JavaScript functions are only allowed in the '_meta' organization" in response_text, (
f"Expected restriction error message, but got: {response_text}"
)
# Cleanup
session.delete(f"{base_url}api/{org_id}/functions/test_js_default_allowed")
def test_test_js_function_in_meta_org_success(self, create_session, base_url):
"""Testing JavaScript function should succeed in _meta org."""
@@ -89,46 +101,44 @@ class TestMetaOrgJavaScriptRestriction:
f"but got {resp.status_code}: {resp.content}"
)
def test_test_js_function_in_default_org_blocked(self, create_session, base_url):
"""Testing JavaScript function should fail in default org."""
pytest.skip("JS fns are now allowed in non _meta orgs")
return
def test_test_js_function_in_default_org_allowed(self, create_session, base_url):
"""Testing a JavaScript function succeeds in default org (JS no longer _meta-only).
/functions/test with a valid JS transform in a non-_meta org now
returns 200 and the transformed event, on every edition (PR #13156).
"""
session = create_session
org_id = "default"
payload = {
"function": "row.count = (row.count || 0) + 1;",
"events": [{"name": "test", "count": 5}],
"trans_type": 1, # JavaScript - should be blocked
"trans_type": 1, # JavaScript
}
resp = session.post(f"{base_url}api/{org_id}/functions/test", json=payload)
assert resp.status_code == 400, (
f"Expected 400 for JS function test in default org, "
f"but got {resp.status_code}: {resp.content}"
assert resp.status_code == 200, (
f"Expected 200 for JS function test in default org (JS is no longer "
f"_meta-only), but got {resp.status_code}: {resp.content}"
)
# Verify error message
response_text = resp.text
assert "JavaScript functions are only allowed in the '_meta' organization" in response_text
# The transform runs: count 5 -> 6
body = resp.json()
assert body["results"][0]["event"].get("count") == 6, (
f"expected JS transform to run (count 5 -> 6), got: {body['results']}"
)
def test_update_vrl_to_js_in_default_org_blocked(self, create_session, base_url):
def test_update_vrl_to_js_in_default_org_allowed(self, create_session, base_url):
"""
Test that updating a VRL function to JavaScript in default org is BLOCKED.
Updating a VRL function to JavaScript in default org succeeds (JS no longer _meta-only).
This ensures users cannot bypass the JS restriction by:
1. Creating a VRL function in default org
2. Updating it to JavaScript
The backend enforces JS restriction on both create AND update operations.
Previously the backend rejected VRL->JS updates outside _meta with a
400 restriction message. That restriction was removed (PR #13156), so:
1. Create a VRL function in default org
2. Update it to JavaScript
both now return 200 on every edition.
"""
pytest.skip("JS fns are now allowed in non _meta orgs")
return
session = create_session
org_id = "default"
@@ -146,7 +156,7 @@ class TestMetaOrgJavaScriptRestriction:
create_resp = session.post(f"{base_url}api/{org_id}/functions", json=vrl_payload)
assert create_resp.status_code == 200, f"Failed to create VRL function: {create_resp.content}"
# Try to update to JavaScript - should be BLOCKED
# Update to JavaScript - now allowed
js_payload = {
"name": "test_update_to_js",
"function": "row.processed = true;",
@@ -159,16 +169,9 @@ class TestMetaOrgJavaScriptRestriction:
json=js_payload
)
# Should return 400 - JS restriction enforced on updates
assert update_resp.status_code == 400, (
f"Expected 400 for VRL->JS update in default org, "
f"but got {update_resp.status_code}: {update_resp.content}"
)
# Verify error message
response_text = update_resp.text
assert "JavaScript functions are only allowed in the '_meta' organization" in response_text, (
f"Expected restriction error message, but got: {response_text}"
assert update_resp.status_code == 200, (
f"Expected 200 for VRL->JS update in default org (JS is no longer "
f"_meta-only), but got {update_resp.status_code}: {update_resp.content}"
)
# Cleanup
@@ -320,67 +323,133 @@ class TestTransTypeAutoDetection:
class TestErrorMessages:
"""Test error message quality and consistency."""
"""Runtime-error surfacing on /functions/test (PR #13156 behaviour change).
def test_error_message_content(self, create_session, base_url):
"""Error message should be clear and actionable."""
pytest.skip("JS fns are now allowed in non _meta orgs")
return
The old "JS is only allowed in _meta" 400 restriction is gone (see
TestMetaOrgJavaScriptRestriction). What matters now for error messaging is
the per-event surfacing of RUNTIME errors: code that compiles cleanly but
throws when run against an event returns 200, with the error attached to
that event in results[].message (for both VRL and JS). These tests pin that
contract. (Absorbs the former restriction-message tests, which asserted
behaviour that no longer exists.)
"""
def test_js_runtime_error_message_content(self, create_session, base_url):
"""A JS runtime error is surfaced per-event with a clear ReferenceError."""
session = create_session
org_id = "default"
payload = {
"name": "test_error_msg",
"function": "row.test = true;",
"params": "row",
"transType": 1, # JavaScript - blocked (camelCase for Transform struct)
}
resp = session.post(f"{base_url}api/{org_id}/functions", json=payload)
assert resp.status_code == 400
response_text = resp.text
# Check error message contains key information
assert "JavaScript" in response_text, "Error should mention JavaScript"
assert "_meta" in response_text, "Error should mention _meta org"
assert "organization" in response_text, "Error should mention organization"
assert "VRL" in response_text, "Error should suggest VRL alternative"
def test_error_message_consistency(self, create_session, base_url):
"""Error message should be consistent across save and test endpoints."""
pytest.skip("JS fns are now allowed in non _meta orgs")
return
session = create_session
org_id = "default"
# Test save endpoint (camelCase for Transform struct)
save_payload = {
"name": "test_consistency",
"function": "row.test = true;",
"params": "row",
"transType": 1,
}
save_resp = session.post(f"{base_url}api/{org_id}/functions", json=save_payload)
save_error = save_resp.text
# Test test endpoint (snake_case for TestVRLRequest struct)
test_payload = {
"function": "row.test = true;",
"function": "row.field = undefinedVar;", # valid syntax, throws at runtime
"events": [{"name": "test"}],
"trans_type": 1,
}
test_resp = session.post(f"{base_url}api/{org_id}/functions/test", json=test_payload)
test_error = test_resp.text
resp = session.post(f"{base_url}api/{org_id}/functions/test", json=payload)
# Both should have same core error message
expected_message = "JavaScript functions are only allowed in the '_meta' organization"
assert expected_message in save_error, "Save endpoint should have restriction message"
assert expected_message in test_error, "Test endpoint should have restriction message"
assert resp.status_code == 200, (
f"JS runtime error should be 200 (per-event surfacing), "
f"but got {resp.status_code}: {resp.content}"
)
results = resp.json()["results"]
assert results, f"expected per-event results, got: {results}"
msg = results[0].get("message", "")
assert "ReferenceError" in msg, f"expected ReferenceError in message, got: {msg!r}"
assert "undefinedVar" in msg, f"expected the offending name in message, got: {msg!r}"
def test_runtime_error_surfaced_for_both_vrl_and_js(self, create_session, base_url):
"""Runtime errors surface per-event for BOTH languages (200 + results[].message).
Consistency check: whether the transform is VRL or JS, a runtime
failure that compiles cleanly returns 200 and carries the error in
results[].message rather than a top-level 400.
"""
session = create_session
org_id = "default"
# JS runtime error
js_resp = session.post(
f"{base_url}api/{org_id}/functions/test",
json={
"function": "row.field = undefinedVar;",
"events": [{"name": "test"}],
"trans_type": 1,
},
)
assert js_resp.status_code == 200, f"JS: {js_resp.status_code} {js_resp.content}"
js_msg = js_resp.json()["results"][0].get("message", "")
assert js_msg, f"JS runtime error should carry a per-event message, got: {js_msg!r}"
# VRL runtime error (to_int! aborts when the value can't be coerced)
vrl_resp = session.post(
f"{base_url}api/{org_id}/functions/test",
json={
"function": ".x = to_int!(.s)",
"events": [{"s": "notanumber"}],
"trans_type": 0,
},
)
assert vrl_resp.status_code == 200, f"VRL: {vrl_resp.status_code} {vrl_resp.content}"
vrl_msg = vrl_resp.json()["results"][0].get("message", "")
assert "vrl runtime error" in vrl_msg, (
f"VRL runtime error should carry a per-event message, got: {vrl_msg!r}"
)
def test_partial_failure_flags_only_failing_events(self, create_session, base_url):
"""Per-event semantics: in a mixed batch, only the failing event carries a message.
The JS function throws only when row.bad is truthy, so event 1 fails
(ReferenceError) and event 2 transforms cleanly. Response is 200 with
one message-bearing result and one clean result.
"""
session = create_session
org_id = "default"
payload = {
"function": "if (row.bad) { row.x = undefinedVar; } else { row.x = 1; }",
"events": [{"bad": True}, {"bad": False}],
"trans_type": 1,
}
resp = session.post(f"{base_url}api/{org_id}/functions/test", json=payload)
assert resp.status_code == 200, f"{resp.status_code}: {resp.content}"
results = resp.json()["results"]
assert len(results) == 2, f"expected one result per event, got: {results}"
assert "ReferenceError" in results[0].get("message", ""), (
f"failing event should carry a runtime error, got: {results[0]}"
)
assert not results[1].get("message"), (
f"clean event should have no error message, got: {results[1]}"
)
assert results[1]["event"].get("x") == 1, (
f"clean event should be transformed (x=1), got: {results[1]}"
)
def test_syntax_error_still_returns_400(self, create_session, base_url):
"""Regression guard: a genuine SYNTAX error still 400s (not the 200 runtime path).
Pins the line between "syntax error -> 400" and "runtime error -> 200"
so a future change can't blur it. Covers both languages.
"""
session = create_session
org_id = "default"
# JS syntax error
js_resp = session.post(
f"{base_url}api/{org_id}/functions/test",
json={"function": "function broken {", "events": [{"a": 1}], "trans_type": 1},
)
assert js_resp.status_code == 400, (
f"JS syntax error should be 400, got {js_resp.status_code}: {js_resp.content}"
)
# VRL syntax error
vrl_resp = session.post(
f"{base_url}api/{org_id}/functions/test",
json={"function": "=====", "events": [{"a": 1}], "trans_type": 0},
)
assert vrl_resp.status_code == 400, (
f"VRL syntax error should be 400, got {vrl_resp.status_code}: {vrl_resp.content}"
)
class TestEdgeCases:
@@ -234,27 +234,24 @@ def test_js_enrichment_multiple_lookups(client: OpenObserveClient):
# ----- documented org-restriction (always runs) -----
def test_js_function_in_default_org_returns_400(client: OpenObserveClient):
"""OO restricts JS functions to the `_meta` org. Documents this constraint.
def test_js_function_in_default_org_allowed(client: OpenObserveClient):
"""JS functions run in the `default` (non-_meta) org. Documents the current contract.
The original suite missed this — it POSTed JS to `default` and silently
failed. This test makes the constraint a first-class contract: if/when
the backend opens JS to other orgs, this test starts failing and you
know to update.
The backend used to restrict JS to `_meta` (400 elsewhere). That
restriction was removed (PR #13156): JS is now accepted in every org on
every edition. This test pins the positive contract — a valid JS transform
in `default` returns 200 and transforms the event. If the backend ever
re-introduces an org restriction, this starts failing and you know to update.
"""
pytest.skip("JS fns are now allowed in non _meta orgs")
return
payload = {
"function": "row.x = 'js works here';",
"events": [{"id": 1}],
"trans_type": 1,
}
resp = client.post("functions/test", json=payload, org=DEFAULT_ORG)
assert resp.status_code == 400, \
f"JS in non-_meta org should be 400, got {resp.status_code}: {resp.text}"
assert resp.status_code == 200, \
f"JS in non-_meta org should now be 200, got {resp.status_code}: {resp.text}"
body = resp.json()
assert "JavaScript" in body.get("message", "") or "_meta" in body.get("message", ""), \
f"error message should mention JS restriction, got: {body}"
assert body["results"][0]["event"].get("x") == "js works here", \
f"JS transform should have run, got: {body['results']}"
@@ -218,7 +218,7 @@ test.describe('JavaScript Transform Type', { tag: ['@jsTransformType', '@functio
// - test_create_js_function_in_meta_org_success
});
test.describe('Non-Meta Organization - JavaScript Blocked', () => {
test.describe('Non-Meta Organization - JavaScript edition-gated', () => {
test.beforeEach(async ({ page }) => {
// Switch to a non-meta organization (self-hosted: 'default', cloud: user's own org)
const nonMetaOrg = getNonMetaOrg();
@@ -235,33 +235,36 @@ test.describe('JavaScript Transform Type', { tag: ['@jsTransformType', '@functio
// NOTE: Consolidated from meta-org "JS radio NOT visible" + "VRL functions work" tests
// Both validations preserved: JS hidden check + VRL creation
test('P0: Non-meta org - JS hidden, VRL works', { tag: ['@smoke', '@P0'] }, async ({ page }) => {
// SKIPPED — asserts the OSS-only rule, but runs on the enterprise build.
// JS is no longer _meta-only: AddFunction.vue offers it whenever
// `isEnterprise || isCloud || org === "_meta"`, so on ENT/cloud the JS radio
// is visible in EVERY org and step 1 below fails. The rule still holds on OSS.
// Re-enabling needs an edition signal, which is awkward: Vite bakes
// VITE_OPENOBSERVE_ENTERPRISE into the bundle at build time and /config
// exposes no edition key, so it can't be detected at runtime.
// TODO(QA): move to the OSS suite, or branch on an edition env var.
// NOTE: step 2 (VRL function creation in a non-meta org) is edition-agnostic
// and is lost while this is skipped.
test.skip(true, 'JS is allowed in all orgs on enterprise/cloud builds; OSS-only assertion');
test('P0: Non-meta org - JS radio edition-gated, VRL works', { tag: ['@smoke', '@P0'] }, async ({ page }) => {
// JS radio visibility in a non-meta org is EDITION-dependent (see file header):
// OSS -> JS is _meta-only, so the radio is HIDDEN here.
// ENT/cloud-> JS is offered in every org, so the radio is VISIBLE here.
// We detect the edition at runtime from the header edition button label
// (data-test="upgrade-to-enterprise-btn"), which is driven by the SAME
// build-time flags (config.isEnterprise/isCloud via @/aws-exports) that gate
// the JS radio — so it's the exact source of truth, no CI env var needed.
// Step 2 (VRL create in a non-meta org) is edition-agnostic and always runs.
const nonMetaOrg = getNonMetaOrg();
testLogger.info(`Test: Non-meta org (${nonMetaOrg}) - JS hidden, VRL works`);
const edition = await pm.editionFeaturesPage.detectEdition();
testLogger.info(`Test: Non-meta org (${nonMetaOrg}) - edition=${edition}`);
// Step 1: Verify JS radio is hidden, VRL is visible
// Step 1: VRL is always visible; JS radio visibility depends on edition.
await pm.functionsPage.clickAddFunctionButton(nonMetaOrg);
await pm.functionsPage.expectVrlRadioVisible();
testLogger.info('VRL radio button visible');
const isJsRadioVisible = await pm.functionsPage.isJsRadioVisible();
expect(isJsRadioVisible).toBe(false);
testLogger.info(`JavaScript radio button correctly hidden in ${nonMetaOrg} org`);
if (edition === 'opensource') {
const isJsRadioVisible = await pm.functionsPage.isJsRadioVisible();
expect(isJsRadioVisible).toBe(false);
testLogger.info(`OSS: JavaScript radio correctly hidden in ${nonMetaOrg} org`);
} else {
// enterprise / cloud
await pm.functionsPage.expectJsRadioVisible();
testLogger.info(`${edition}: JavaScript radio correctly visible in ${nonMetaOrg} org`);
}
await pm.functionsPage.clickCancelButton();
// Step 2: Create VRL function to verify it works
// Step 2 (edition-agnostic): Create a VRL function to verify it works.
const functionName = `nonmeta_vrl_fn_${Date.now()}`;
const vrlCode = '.processed = true\n.org = "non_meta"';
@@ -283,26 +286,62 @@ test.describe('JavaScript Transform Type', { tag: ['@jsTransformType', '@functio
await pm.functionsPage.deleteFunctionByName(functionName, nonMetaOrg);
});
// NOTE: API tests for JavaScript function blocking in default org have been migrated to:
// tests/api-testing/tests/test_functions_meta_org_restriction.py
// - test_create_js_function_in_default_org_blocked
// - test_test_js_function_in_default_org_blocked
// Edition-agnostic: a VRL runtime error surfaces in the error-details section
// (mirrors the JS ReferenceError case in the _meta describe). VRL works in every
// org on every edition, so this runs unconditionally. Covers the VRL half of the
// "runtime errors now render per-event" behaviour change (PR #13156).
test('P1: VRL runtime error surfaces in error-details', { tag: ['@P1', '@errorHandling'] }, async ({ page }) => {
const nonMetaOrg = getNonMetaOrg();
testLogger.info(`Test: VRL runtime error in error-details (${nonMetaOrg} org)`);
// to_int!(.s) compiles fine but aborts at runtime when .s can't be coerced.
const functionName = `nonmeta_vrl_err_${Date.now()}`;
const vrlCode = '.x = to_int!(.s)';
await pm.functionsPage.clickAddFunctionButton(nonMetaOrg);
await pm.functionsPage.fillFunctionName(functionName);
// VRL is the default type
await pm.functionsPage.enterFunctionCode(vrlCode);
// Run against an event that triggers the runtime abort.
await pm.functionsPage.testFunctionExecution('[{"s":"notanumber"}]');
// The per-event VRL runtime error renders in the error-details section
// (200 response with results[].message, not a 400).
await pm.functionsPage.expectFunctionErrorContains('runtime error');
testLogger.info('VRL runtime error rendered in error-details section');
await pm.functionsPage.clickCancelButton();
});
// NOTE: API tests for JavaScript function behaviour in default org live in:
// tests/api-testing/tests/pipelines/test_functions_meta_org_restriction.py
// - test_create_js_function_in_default_org_allowed
// - test_test_js_function_in_default_org_allowed
});
test.describe('Organization Switching & VRL Control', () => {
// NOTE: Consolidated from meta-org "Org switching" + "VRL always visible" tests
// Both validations preserved: JS visibility changes with org + VRL always visible
test('P1: Radio visibility across orgs', { tag: ['@P1', '@orgSwitching', '@control'] }, async ({ page }) => {
// SKIPPED — same reason as 'P0: Non-meta org - JS hidden, VRL works' above:
// steps 1 and 3 expect the JS radio hidden in a non-meta org, which is only
// true on OSS. TODO(QA): move to the OSS suite, or branch on an edition env var.
// NOTE: step 2 (JS visible in _meta) is edition-agnostic and is lost meanwhile.
test.skip(true, 'JS is allowed in all orgs on enterprise/cloud builds; OSS-only assertion');
// JS radio visibility in a NON-meta org is edition-gated (OSS hides it,
// ENT/cloud shows it); in the _meta org JS is ALWAYS visible on every
// edition. VRL is always visible everywhere. We detect the edition from
// the header edition button (same build-flag source as the JS gate).
const nonMetaOrg = getNonMetaOrg();
testLogger.info(`Test: Radio visibility across orgs (non-meta: ${nonMetaOrg})`);
const edition = await pm.editionFeaturesPage.detectEdition();
testLogger.info(`Test: Radio visibility across orgs (non-meta: ${nonMetaOrg}, edition=${edition})`);
// Step 1: Non-meta org - JS hidden, VRL visible
const expectJsRadioForNonMeta = async (label) => {
if (edition === 'opensource') {
await pm.functionsPage.expectJsRadioHidden();
testLogger.info(`OSS: JS hidden in ${nonMetaOrg} org (${label})`);
} else {
await pm.functionsPage.expectJsRadioVisible();
testLogger.info(`${edition}: JS visible in ${nonMetaOrg} org (${label})`);
}
};
// Step 1: Non-meta org - VRL visible, JS edition-gated
await page.goto(`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${nonMetaOrg}`);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(2000);
@@ -310,11 +349,10 @@ test.describe('JavaScript Transform Type', { tag: ['@jsTransformType', '@functio
await pm.functionsPage.clickAddFunctionButton(nonMetaOrg);
await pm.functionsPage.expectVrlRadioVisible();
testLogger.info(`VRL visible in ${nonMetaOrg} org`);
await pm.functionsPage.expectJsRadioHidden();
testLogger.info(`JS hidden in ${nonMetaOrg} org`);
await expectJsRadioForNonMeta('step 1');
await pm.functionsPage.clickCancelButton();
// Step 2: Switch to _meta org - JS visible, VRL visible
// Step 2 (edition-agnostic): Switch to _meta org - JS visible, VRL visible
await page.goto(`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=_meta`);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(2000);
@@ -323,10 +361,10 @@ test.describe('JavaScript Transform Type', { tag: ['@jsTransformType', '@functio
await pm.functionsPage.expectVrlRadioVisible();
testLogger.info('VRL visible in _meta org');
await pm.functionsPage.expectJsRadioVisible();
testLogger.info('JS visible in _meta org');
testLogger.info('JS visible in _meta org (always, every edition)');
await pm.functionsPage.clickCancelButton();
// Step 3: Switch back to non-meta - JS hidden again, VRL still visible
// Step 3: Switch back to non-meta - JS gated again, VRL still visible
await page.goto(`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${nonMetaOrg}`);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(2000);
@@ -334,8 +372,7 @@ test.describe('JavaScript Transform Type', { tag: ['@jsTransformType', '@functio
await pm.functionsPage.clickAddFunctionButton(nonMetaOrg);
await pm.functionsPage.expectVrlRadioVisible();
testLogger.info('VRL still visible after switching back');
await pm.functionsPage.expectJsRadioHidden();
testLogger.info(`JS hidden after switching back to ${nonMetaOrg}`);
await expectJsRadioForNonMeta('step 3');
await pm.functionsPage.clickCancelButton();
});
});