mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 23:57:58 +09:00
213 lines
7.9 KiB
Python
213 lines
7.9 KiB
Python
"""Tests for the single Setup Health nag + snooze/dismiss (plan 22 Phase 6):
|
|
the weekly digest fires only while non-snoozed critical items exist and only
|
|
when the critical set changed; snoozed items still render but are muted, don't
|
|
count as open, and don't trigger the nag."""
|
|
import pytest
|
|
|
|
from app.services.setup_health_service import (
|
|
NAG_JOB_KIND,
|
|
NAG_MARKER_KEY,
|
|
NAG_SCHEDULE_NAME,
|
|
PANEL_SNOOZE_KEY,
|
|
SetupHealthService,
|
|
)
|
|
|
|
|
|
def set_setting(key, value, vtype='string'):
|
|
from app import db
|
|
from app.models.system_settings import SystemSettings
|
|
SystemSettings.set(key=key, value=value, value_type=vtype)
|
|
db.session.commit()
|
|
|
|
|
|
def force_critical():
|
|
"""A settings state that yields a critical (fail) panel item:
|
|
per-site DNS mode + a base but no provider/IP → setup.dns_provider fail."""
|
|
set_setting('sites_base_domain', 'apps.example.com')
|
|
set_setting('sites_dns_mode', 'per-site')
|
|
|
|
|
|
@pytest.fixture
|
|
def admin(app):
|
|
from werkzeug.security import generate_password_hash
|
|
from app import db
|
|
from app.models import User
|
|
u = User(email='nag@t.local', username='nagadmin',
|
|
password_hash=generate_password_hash('x'),
|
|
role=User.ROLE_ADMIN, is_active=True)
|
|
db.session.add(u)
|
|
db.session.commit()
|
|
return u
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Snooze mutes an item (still renders, not counted, no nag)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_panel_snooze_mutes_item_and_drops_open_count(app):
|
|
force_critical()
|
|
before = SetupHealthService.evaluate(scope='panel')
|
|
dns = {c['key']: c for c in before['items']}['setup.dns_provider']
|
|
assert dns['status'] == 'fail'
|
|
assert before['summary']['critical_open'] >= 1
|
|
|
|
SetupHealthService.snooze('setup.dns_provider', days=14)
|
|
after = SetupHealthService.evaluate(scope='panel')
|
|
dns = {c['key']: c for c in after['items']}['setup.dns_provider']
|
|
# Still rendered, but muted …
|
|
assert dns['snoozed'] is True
|
|
assert dns['snoozed_until']
|
|
# … and no longer counted as open.
|
|
assert after['summary']['critical_open'] == before['summary']['critical_open'] - 1
|
|
assert after['summary']['snoozed'] >= 1
|
|
|
|
|
|
def test_unsnooze_restores_open_count(app):
|
|
force_critical()
|
|
SetupHealthService.snooze('setup.dns_provider', days=14)
|
|
SetupHealthService.unsnooze('setup.dns_provider')
|
|
after = SetupHealthService.evaluate(scope='panel')
|
|
dns = {c['key']: c for c in after['items']}['setup.dns_provider']
|
|
assert not dns.get('snoozed')
|
|
|
|
|
|
def test_expired_snooze_is_ignored(app):
|
|
force_critical()
|
|
# Write an already-expired snooze directly.
|
|
import json
|
|
set_setting(PANEL_SNOOZE_KEY, json.dumps({'setup.dns_provider': '2000-01-01T00:00:00'}))
|
|
after = SetupHealthService.evaluate(scope='panel')
|
|
dns = {c['key']: c for c in after['items']}['setup.dns_provider']
|
|
assert not dns.get('snoozed') # expired → active again
|
|
|
|
|
|
def test_user_snooze_lives_on_the_user_row(app, admin):
|
|
SetupHealthService.snooze('setup.account_security', days=7, user=admin)
|
|
assert admin.setup_snoozes # persisted on the row, not the settings map
|
|
res = SetupHealthService.evaluate(scope='user', user=admin)
|
|
item = {c['key']: c for c in res['items']}['setup.account_security']
|
|
assert item['snoozed'] is True
|
|
|
|
|
|
def test_snooze_unknown_item_errors(app):
|
|
assert 'error' in SetupHealthService.snooze('setup.nonexistent')
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# The nag job — critical-only, fingerprint-throttled, snooze-aware
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def _capture_notify(monkeypatch):
|
|
import app.plugins_sdk as sdk
|
|
sent = []
|
|
monkeypatch.setattr(sdk.notify, 'send',
|
|
lambda event, to, data=None, **kw: sent.append((event, to, data)))
|
|
return sent
|
|
|
|
|
|
def test_nag_fires_once_then_throttles(app, monkeypatch):
|
|
force_critical()
|
|
sent = _capture_notify(monkeypatch)
|
|
|
|
first = SetupHealthService.run_nag_job(object())
|
|
assert first['notified'] is True
|
|
assert len(sent) == 1
|
|
event, to, data = sent[0]
|
|
assert event == 'setup.incomplete'
|
|
assert to == 'admins'
|
|
assert data['count'] >= 1
|
|
|
|
# Second run, same critical set → throttled (no re-nag).
|
|
second = SetupHealthService.run_nag_job(object())
|
|
assert second['notified'] is False
|
|
assert second.get('reason') == 'unchanged'
|
|
assert len(sent) == 1
|
|
|
|
|
|
def test_nag_does_not_fire_without_criticals(app, monkeypatch):
|
|
# No base/per-site config → no critical items.
|
|
sent = _capture_notify(monkeypatch)
|
|
res = SetupHealthService.run_nag_job(object())
|
|
assert res['notified'] is False
|
|
assert res['critical_open'] == 0
|
|
assert sent == []
|
|
|
|
|
|
def test_snoozed_critical_does_not_trigger_nag(app, monkeypatch):
|
|
force_critical()
|
|
SetupHealthService.snooze('setup.dns_provider', days=30)
|
|
# public_ip may also be critical in per-site mode; snooze it too.
|
|
SetupHealthService.snooze('setup.public_ip', days=30)
|
|
sent = _capture_notify(monkeypatch)
|
|
res = SetupHealthService.run_nag_job(object())
|
|
assert res['notified'] is False
|
|
assert res['critical_open'] == 0
|
|
assert sent == []
|
|
|
|
|
|
def test_nag_marker_resets_when_criticals_clear(app, monkeypatch):
|
|
from app.services.settings_service import SettingsService
|
|
force_critical()
|
|
_capture_notify(monkeypatch)
|
|
SetupHealthService.run_nag_job(object())
|
|
assert SettingsService.get(NAG_MARKER_KEY) # marker set
|
|
|
|
# Resolve the criticals (wildcard mode, no per-site requirement).
|
|
set_setting('sites_dns_mode', 'wildcard')
|
|
SetupHealthService.run_nag_job(object())
|
|
assert (SettingsService.get(NAG_MARKER_KEY) or '') == '' # marker cleared
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Schedule + API
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_nag_schedule_is_seeded(app):
|
|
from app.jobs.builtin_handlers import seed_builtin_schedules
|
|
from app.jobs.models import ScheduledJob
|
|
seed_builtin_schedules()
|
|
row = ScheduledJob.query.filter_by(name=NAG_SCHEDULE_NAME).first()
|
|
assert row is not None
|
|
assert row.kind == NAG_JOB_KIND
|
|
assert row.interval_seconds == 604800
|
|
|
|
|
|
def test_setup_incomplete_in_catalog():
|
|
from app.notifications import catalog
|
|
entry = catalog.get('setup.incomplete')
|
|
assert entry is not None
|
|
assert entry['category'] == 'system'
|
|
|
|
|
|
def test_snooze_api_panel_requires_admin(app, client):
|
|
from app import db
|
|
from app.models import User
|
|
from flask_jwt_extended import create_access_token
|
|
from werkzeug.security import generate_password_hash
|
|
dev = User(email='d@t.local', username='devuser',
|
|
password_hash=generate_password_hash('x'),
|
|
role='developer', is_active=True)
|
|
db.session.add(dev)
|
|
db.session.commit()
|
|
headers = {'Authorization': f'Bearer {create_access_token(identity=dev.id)}'}
|
|
res = client.post('/api/v1/setup-health/snooze',
|
|
json={'key': 'setup.dns_provider'}, headers=headers)
|
|
assert res.status_code == 403 # panel item, non-admin
|
|
|
|
|
|
def test_snooze_api_user_item_allows_self(app, client):
|
|
from app import db
|
|
from app.models import User
|
|
from flask_jwt_extended import create_access_token
|
|
from werkzeug.security import generate_password_hash
|
|
dev = User(email='d2@t.local', username='devuser2',
|
|
password_hash=generate_password_hash('x'),
|
|
role='developer', is_active=True)
|
|
db.session.add(dev)
|
|
db.session.commit()
|
|
headers = {'Authorization': f'Bearer {create_access_token(identity=dev.id)}'}
|
|
res = client.post('/api/v1/setup-health/snooze',
|
|
json={'key': 'setup.account_security', 'days': 7}, headers=headers)
|
|
assert res.status_code == 200
|
|
assert res.get_json()['scope'] == 'user'
|