mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
## Summary - Add comprehensive E2E test coverage for RUM sourcemap functionality (26 total tests) - API tests: 20 pytest tests (upload/list/delete/resolve operations + RBAC) - UI tests: 6 Playwright tests for error tracking UI - Helper scripts for RUM data ingestion ## Implementation Details ### API Tests (pytest) - **test_sourcemap_api.py**: 15 tests covering: - Sourcemap upload (multipart/form-data) - List sourcemaps - Delete sourcemaps - Stacktrace resolution with sourcemaps - **test_sourcemap_rbac.py**: 5 tests for role-based access control - **ingest_rum_errors.py**: Helper to inject synthetic RUM error data for UI tests - **ingest_rum_performance.py**: Helper to enable RUM feature in UI ### UI Tests (Playwright) - **sourcemap-ui.spec.js**: 6 tests validating: - Error tracking page loads - Query execution - Error list display - Error detail navigation - Metadata display - Stack trace display - **rumPage.js**: Updated page object with API response interception - Workaround for Vue 3 @click handlers not responding to Playwright clicks - Navigate via URL using error IDs extracted from API responses - Tests run in **parallel mode** with proper assertions (no graceful skips) ## Test Execution ```bash # API tests (pytest) cd tests/api-testing pytest tests/test_sourcemap_api.py -v # 15 tests pytest tests/test_sourcemap_rbac.py -v # 5 tests # UI tests (Playwright) cd tests/ui-testing npx playwright test playwright-tests/RUM/sourcemap-ui.spec.js --workers=3 # 6 tests ``` ## Technical Notes - Vue 3 production builds don't respond to Playwright's synthetic click events on @click handlers - Solution: Intercept API responses to extract error IDs, then navigate directly via URL - All tests use hard assertions - tests fail if data doesn't exist (no graceful skips) --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Shrinath Rao <undefined@users.noreply.github.com>
350 lines
11 KiB
Python
350 lines
11 KiB
Python
"""
|
|
Sourcemap RBAC E2E Tests
|
|
|
|
Tests for Role-Based Access Control on sourcemap APIs.
|
|
|
|
Test Coverage:
|
|
- Viewer role permissions (read-only: can list, cannot upload/delete)
|
|
- Editor role permissions (write access: can upload, list, delete)
|
|
- Admin role permissions (full access: can upload, list, delete)
|
|
|
|
Expected RBAC Behavior (Role-Based):
|
|
- Viewer: Read-only access (list, translate stacktrace)
|
|
- Editor: Full write access (upload, list, delete)
|
|
- Admin/Root: Full access (upload, list, delete)
|
|
|
|
Prerequisites:
|
|
- OpenObserve ENTERPRISE build running on ZO_BASE_URL
|
|
- Root user credentials for user creation
|
|
- OpenFGA enabled for RBAC enforcement
|
|
|
|
Note:
|
|
- Tests marked with @pytest.mark.skip for OSS CI (enterprise-only feature)
|
|
- Tests must run serially due to shared module-scoped fixture
|
|
- Uses session-based authentication (matches enterprise RBAC test pattern)
|
|
"""
|
|
|
|
import pytest
|
|
import logging
|
|
import requests
|
|
|
|
# Mark all tests in this module to run serially and skip in OSS (enterprise-only feature)
|
|
pytestmark = [
|
|
pytest.mark.order(2),
|
|
pytest.mark.skip(reason="Sourcemaps is an enterprise feature and cannot be tested in CI with non-enterprise build")
|
|
]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Test user credentials (created during test run)
|
|
test_users = {}
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def setup_rbac_users(create_session, base_url, org_id):
|
|
"""
|
|
Create test users with different roles for RBAC testing.
|
|
|
|
Creates:
|
|
- viewer@sourcemap-test.local (role: viewer)
|
|
- editor@sourcemap-test.local (role: editor)
|
|
- admin@sourcemap-test.local (role: admin)
|
|
"""
|
|
global test_users
|
|
|
|
session = create_session
|
|
url = base_url
|
|
|
|
logger.info("========== SETUP: Creating RBAC test users ==========")
|
|
|
|
roles = ['viewer', 'editor', 'admin']
|
|
|
|
for role in roles:
|
|
email = f"{role}@sourcemap-test.local"
|
|
password = "TestPass123!"
|
|
|
|
logger.info(f"Creating user: {email} with role: {role}")
|
|
|
|
try:
|
|
response = session.post(
|
|
f"{url}api/{org_id}/users",
|
|
json={
|
|
'email': email,
|
|
'password': password,
|
|
'first_name': 'Test',
|
|
'last_name': role.capitalize(),
|
|
'role': role
|
|
}
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
logger.info(f"✅ User created: {email}")
|
|
else:
|
|
logger.warning(f"⚠️ User creation returned {response.status_code}: {response.text}")
|
|
|
|
# Create authenticated session via login endpoint
|
|
user_session = create_user_session(email, password, url)
|
|
if not user_session:
|
|
logger.error(f"Failed to create session for {email}")
|
|
raise Exception(f"Login failed for {email}")
|
|
|
|
# Store user info
|
|
test_users[role] = {
|
|
'email': email,
|
|
'password': password,
|
|
'role': role,
|
|
'session': user_session
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to create user {email}: {e}")
|
|
raise
|
|
|
|
logger.info(f"========== SETUP COMPLETE: Created {len(test_users)} test users ==========")
|
|
|
|
yield test_users
|
|
|
|
# Cleanup
|
|
logger.info("========== TEARDOWN: Deleting RBAC test users ==========")
|
|
|
|
for role, user_info in test_users.items():
|
|
try:
|
|
response = session.delete(
|
|
f"{url}api/{org_id}/users/{user_info['email']}"
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
logger.info(f"✅ User deleted: {user_info['email']}")
|
|
else:
|
|
logger.warning(f"⚠️ User deletion returned {response.status_code}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete user {user_info['email']}: {e}")
|
|
|
|
logger.info("========== TEARDOWN COMPLETE ==========")
|
|
|
|
|
|
def create_user_session(email, password, base_url):
|
|
"""
|
|
Create authenticated session via /auth/login endpoint.
|
|
|
|
This matches the enterprise RBAC test pattern, ensuring proper
|
|
OpenFGA permission setup through the login flow.
|
|
"""
|
|
user_session = requests.Session()
|
|
login_url = f"{base_url}auth/login"
|
|
login_data = {"name": email, "password": password}
|
|
|
|
try:
|
|
login_resp = user_session.post(login_url, json=login_data, timeout=30)
|
|
|
|
if login_resp.status_code != 200:
|
|
logger.error(f"Login failed for {email}: {login_resp.status_code} - {login_resp.text}")
|
|
return None
|
|
|
|
logger.info(f"Successfully logged in as {email}")
|
|
return user_session # Returns session with cookies from login
|
|
|
|
except requests.RequestException as e:
|
|
logger.error(f"Network error during login for {email}: {str(e)}")
|
|
return None
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def test_sourcemaps_zip():
|
|
"""
|
|
Path to static sourcemaps ZIP fixture.
|
|
Uses pre-built sourcemaps from fixtures directory.
|
|
"""
|
|
from pathlib import Path
|
|
zip_path = Path(__file__).parent.parent / "fixtures" / "sourcemaps" / "sourcemaps.zip"
|
|
|
|
if not zip_path.exists():
|
|
pytest.skip(f"Sourcemaps ZIP not found at {zip_path}. Static fixture missing.")
|
|
|
|
return str(zip_path)
|
|
|
|
|
|
# ==============================================================================
|
|
# P1 - VIEWER PERMISSIONS
|
|
# ==============================================================================
|
|
|
|
def test_p1_viewer_cannot_upload_sourcemaps(base_url, org_id, test_sourcemaps_zip):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Viewer role CANNOT upload sourcemaps (read-only access).
|
|
|
|
Expected: 401 or 403 (Unauthorized/Forbidden)
|
|
"""
|
|
assert 'viewer' in test_users, "Viewer user was not created - RBAC setup failed"
|
|
|
|
viewer_session = test_users['viewer']['session']
|
|
|
|
with open(test_sourcemaps_zip, 'rb') as f:
|
|
files = {'file': ('sourcemaps.zip', f, 'application/zip')}
|
|
data = {
|
|
'service': 'o2-sourcemap-rbac-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-rbac'
|
|
}
|
|
|
|
response = viewer_session.post(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
assert response.status_code in [401, 403], \
|
|
f"Viewer should NOT be able to upload sourcemaps. Got {response.status_code}: {response.text}"
|
|
logger.info("✅ Viewer correctly denied upload access")
|
|
|
|
|
|
def test_p1_viewer_can_list_sourcemaps(base_url, org_id):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Viewer role CAN list sourcemaps (read-only access).
|
|
|
|
Expected: 200 OK with list of sourcemaps
|
|
"""
|
|
assert 'viewer' in test_users, "Viewer user was not created - RBAC setup failed"
|
|
|
|
viewer_session = test_users['viewer']['session']
|
|
|
|
response = viewer_session.get(
|
|
f"{base_url}api/{org_id}/sourcemaps"
|
|
)
|
|
|
|
assert response.status_code == 200, \
|
|
f"Viewer should be able to list sourcemaps. Got {response.status_code}: {response.text}"
|
|
|
|
maps = response.json()
|
|
assert isinstance(maps, list), "Response should be a list"
|
|
logger.info(f"✅ Viewer correctly allowed list access ({len(maps)} sourcemaps)")
|
|
|
|
|
|
def test_p1_viewer_cannot_delete_sourcemaps(base_url, org_id):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Viewer role CANNOT delete sourcemaps (read-only access).
|
|
|
|
Expected: 401 or 403 (Unauthorized/Forbidden)
|
|
"""
|
|
assert 'viewer' in test_users, "Viewer user was not created - RBAC setup failed"
|
|
|
|
viewer_session = test_users['viewer']['session']
|
|
|
|
# Use isolated service/version to avoid affecting shared test data
|
|
params = {
|
|
'service': 'o2-sourcemap-rbac-viewer-delete-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-rbac-delete'
|
|
}
|
|
|
|
response = viewer_session.delete(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params=params
|
|
)
|
|
|
|
assert response.status_code in [401, 403], \
|
|
f"Viewer should NOT be able to delete sourcemaps. Got {response.status_code}: {response.text}"
|
|
logger.info("✅ Viewer correctly denied delete access")
|
|
|
|
|
|
# ==============================================================================
|
|
# P1 - EDITOR PERMISSIONS
|
|
# ==============================================================================
|
|
|
|
def test_p1_editor_can_upload_sourcemaps(base_url, org_id, test_sourcemaps_zip):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Editor role CAN upload sourcemaps (write access).
|
|
|
|
Expected: 200 or 201 (Success/Created)
|
|
"""
|
|
assert 'editor' in test_users, "Editor user was not created - RBAC setup failed"
|
|
|
|
editor_session = test_users['editor']['session']
|
|
|
|
with open(test_sourcemaps_zip, 'rb') as f:
|
|
files = {'file': ('sourcemaps.zip', f, 'application/zip')}
|
|
data = {
|
|
'service': 'o2-sourcemap-editor-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-editor'
|
|
}
|
|
|
|
response = editor_session.post(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
assert response.status_code in [200, 201], \
|
|
f"Editor should be able to upload sourcemaps. Got {response.status_code}: {response.text}"
|
|
logger.info("✅ Editor correctly allowed upload access")
|
|
|
|
# Cleanup: Delete what we just uploaded (editors have full write access)
|
|
delete_response = editor_session.delete(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params={'service': data['service'], 'env': data['env'], 'version': data['version']}
|
|
)
|
|
|
|
if delete_response.status_code == 200:
|
|
logger.info("✅ Editor cleanup successful")
|
|
else:
|
|
logger.warning(f"⚠️ Editor cleanup returned {delete_response.status_code}: {delete_response.text}")
|
|
|
|
|
|
# ==============================================================================
|
|
# P1 - ADMIN PERMISSIONS
|
|
# ==============================================================================
|
|
|
|
def test_p1_admin_can_delete_sourcemaps(create_session, base_url, org_id, test_sourcemaps_zip):
|
|
"""
|
|
P1 - FUNCTIONAL TEST
|
|
Admin role can delete sourcemaps.
|
|
"""
|
|
assert 'admin' in test_users, "Admin user was not created - RBAC setup failed"
|
|
|
|
# First, upload sourcemaps as root
|
|
root_session = create_session
|
|
|
|
with open(test_sourcemaps_zip, 'rb') as f:
|
|
files = {'file': ('sourcemaps.zip', f, 'application/zip')}
|
|
data = {
|
|
'service': 'o2-sourcemap-admin-test',
|
|
'env': 'testing',
|
|
'version': '1.0.0-admin'
|
|
}
|
|
|
|
upload_response = root_session.post(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
assert upload_response.status_code in [200, 201], \
|
|
f"Root upload failed: {upload_response.status_code}"
|
|
|
|
# Try to delete as admin
|
|
admin_session = test_users['admin']['session']
|
|
|
|
delete_response = admin_session.delete(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params={'service': data['service'], 'env': data['env'], 'version': data['version']}
|
|
)
|
|
|
|
assert delete_response.status_code == 200, \
|
|
f"Admin delete failed: {delete_response.status_code} - {delete_response.text}"
|
|
|
|
# Verify deletion
|
|
list_response = root_session.get(
|
|
f"{base_url}api/{org_id}/sourcemaps",
|
|
params={'service': data['service'], 'env': data['env'], 'version': data['version']}
|
|
)
|
|
|
|
maps = list_response.json()
|
|
assert len(maps) == 0, f"Expected empty list after delete, got {len(maps)}"
|
|
|
|
logger.info("✅ Admin can delete sourcemaps")
|