refactor to remove config_files in favor of db
This commit is contained in:
@@ -13,7 +13,7 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from web.app import create_app
|
||||
from web.models import Base, Scan
|
||||
from web.models import Base, Scan, ScanConfig
|
||||
from web.utils.settings import PasswordManager, SettingsManager
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def sample_scan_report():
|
||||
'title': 'Test Scan',
|
||||
'scan_time': '2025-11-14T10:30:00Z',
|
||||
'scan_duration': 125.5,
|
||||
'config_file': '/app/configs/test.yaml',
|
||||
'config_id': 1,
|
||||
'sites': [
|
||||
{
|
||||
'name': 'Test Site',
|
||||
@@ -199,6 +199,53 @@ def sample_invalid_config_file(tmp_path):
|
||||
return str(config_file)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_db_config(db):
|
||||
"""
|
||||
Create a sample database config for testing.
|
||||
|
||||
Args:
|
||||
db: Database session fixture
|
||||
|
||||
Returns:
|
||||
ScanConfig model instance with ID
|
||||
"""
|
||||
import json
|
||||
|
||||
config_data = {
|
||||
'title': 'Test Scan',
|
||||
'sites': [
|
||||
{
|
||||
'name': 'Test Site',
|
||||
'ips': [
|
||||
{
|
||||
'address': '192.168.1.10',
|
||||
'expected': {
|
||||
'ping': True,
|
||||
'tcp_ports': [22, 80, 443],
|
||||
'udp_ports': [53],
|
||||
'services': ['ssh', 'http', 'https']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
scan_config = ScanConfig(
|
||||
title='Test Scan',
|
||||
config_data=json.dumps(config_data),
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(scan_config)
|
||||
db.commit()
|
||||
db.refresh(scan_config)
|
||||
|
||||
return scan_config
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def app():
|
||||
"""
|
||||
@@ -269,7 +316,7 @@ def sample_scan(db):
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='completed',
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
title='Test Scan',
|
||||
duration=125.5,
|
||||
triggered_by='test',
|
||||
|
||||
@@ -23,12 +23,12 @@ class TestBackgroundJobs:
|
||||
assert app.scheduler.scheduler is not None
|
||||
assert app.scheduler.scheduler.running
|
||||
|
||||
def test_queue_scan_job(self, app, db, sample_config_file):
|
||||
def test_queue_scan_job(self, app, db, sample_db_config):
|
||||
"""Test queuing a scan for background execution."""
|
||||
# Create a scan via service
|
||||
scan_service = ScanService(db)
|
||||
scan_id = scan_service.trigger_scan(
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
triggered_by='test',
|
||||
scheduler=app.scheduler
|
||||
)
|
||||
@@ -43,12 +43,12 @@ class TestBackgroundJobs:
|
||||
assert job is not None
|
||||
assert job.id == f'scan_{scan_id}'
|
||||
|
||||
def test_trigger_scan_without_scheduler(self, db, sample_config_file):
|
||||
def test_trigger_scan_without_scheduler(self, db, sample_db_config):
|
||||
"""Test triggering scan without scheduler logs warning."""
|
||||
# Create scan without scheduler
|
||||
scan_service = ScanService(db)
|
||||
scan_id = scan_service.trigger_scan(
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
triggered_by='test',
|
||||
scheduler=None # No scheduler
|
||||
)
|
||||
@@ -58,13 +58,13 @@ class TestBackgroundJobs:
|
||||
assert scan is not None
|
||||
assert scan.status == 'running'
|
||||
|
||||
def test_scheduler_service_queue_scan(self, app, db, sample_config_file):
|
||||
def test_scheduler_service_queue_scan(self, app, db, sample_db_config):
|
||||
"""Test SchedulerService.queue_scan directly."""
|
||||
# Create scan record first
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='running',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title='Test Scan',
|
||||
triggered_by='test'
|
||||
)
|
||||
@@ -72,27 +72,27 @@ class TestBackgroundJobs:
|
||||
db.commit()
|
||||
|
||||
# Queue the scan
|
||||
job_id = app.scheduler.queue_scan(scan.id, sample_config_file)
|
||||
job_id = app.scheduler.queue_scan(scan.id, sample_db_config)
|
||||
|
||||
# Verify job was queued
|
||||
assert job_id == f'scan_{scan.id}'
|
||||
job = app.scheduler.scheduler.get_job(job_id)
|
||||
assert job is not None
|
||||
|
||||
def test_scheduler_list_jobs(self, app, db, sample_config_file):
|
||||
def test_scheduler_list_jobs(self, app, db, sample_db_config):
|
||||
"""Test listing scheduled jobs."""
|
||||
# Queue a few scans
|
||||
for i in range(3):
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='running',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title=f'Test Scan {i}',
|
||||
triggered_by='test'
|
||||
)
|
||||
db.add(scan)
|
||||
db.commit()
|
||||
app.scheduler.queue_scan(scan.id, sample_config_file)
|
||||
app.scheduler.queue_scan(scan.id, sample_db_config)
|
||||
|
||||
# List jobs
|
||||
jobs = app.scheduler.list_jobs()
|
||||
@@ -106,20 +106,20 @@ class TestBackgroundJobs:
|
||||
assert 'name' in job
|
||||
assert 'trigger' in job
|
||||
|
||||
def test_scheduler_get_job_status(self, app, db, sample_config_file):
|
||||
def test_scheduler_get_job_status(self, app, db, sample_db_config):
|
||||
"""Test getting status of a specific job."""
|
||||
# Create and queue a scan
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='running',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title='Test Scan',
|
||||
triggered_by='test'
|
||||
)
|
||||
db.add(scan)
|
||||
db.commit()
|
||||
|
||||
job_id = app.scheduler.queue_scan(scan.id, sample_config_file)
|
||||
job_id = app.scheduler.queue_scan(scan.id, sample_db_config)
|
||||
|
||||
# Get job status
|
||||
status = app.scheduler.get_job_status(job_id)
|
||||
@@ -133,13 +133,13 @@ class TestBackgroundJobs:
|
||||
status = app.scheduler.get_job_status('nonexistent_job_id')
|
||||
assert status is None
|
||||
|
||||
def test_scan_timing_fields(self, db, sample_config_file):
|
||||
def test_scan_timing_fields(self, db, sample_db_config):
|
||||
"""Test that scan timing fields are properly set."""
|
||||
# Create scan with started_at
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='running',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title='Test Scan',
|
||||
triggered_by='test',
|
||||
started_at=datetime.utcnow()
|
||||
@@ -161,13 +161,13 @@ class TestBackgroundJobs:
|
||||
assert scan.completed_at is not None
|
||||
assert (scan.completed_at - scan.started_at).total_seconds() >= 0
|
||||
|
||||
def test_scan_error_handling(self, db, sample_config_file):
|
||||
def test_scan_error_handling(self, db, sample_db_config):
|
||||
"""Test that error messages are stored correctly."""
|
||||
# Create failed scan
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='failed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title='Failed Scan',
|
||||
triggered_by='test',
|
||||
started_at=datetime.utcnow(),
|
||||
@@ -188,7 +188,7 @@ class TestBackgroundJobs:
|
||||
assert status['error_message'] == 'Test error message'
|
||||
|
||||
@pytest.mark.skip(reason="Requires actual scanner execution - slow test")
|
||||
def test_background_scan_execution(self, app, db, sample_config_file):
|
||||
def test_background_scan_execution(self, app, db, sample_db_config):
|
||||
"""
|
||||
Integration test for actual background scan execution.
|
||||
|
||||
@@ -200,7 +200,7 @@ class TestBackgroundJobs:
|
||||
# Trigger scan
|
||||
scan_service = ScanService(db)
|
||||
scan_id = scan_service.trigger_scan(
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
triggered_by='test',
|
||||
scheduler=app.scheduler
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ class TestScanAPIEndpoints:
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='completed',
|
||||
config_file=f'/app/configs/test{i}.yaml',
|
||||
config_id=sample_db_config.id,
|
||||
title=f'Test Scan {i}',
|
||||
triggered_by='test'
|
||||
)
|
||||
@@ -81,7 +81,7 @@ class TestScanAPIEndpoints:
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status=status,
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
title=f'{status.capitalize()} Scan',
|
||||
triggered_by='test'
|
||||
)
|
||||
@@ -123,10 +123,10 @@ class TestScanAPIEndpoints:
|
||||
assert 'error' in data
|
||||
assert data['error'] == 'Not found'
|
||||
|
||||
def test_trigger_scan_success(self, client, db, sample_config_file):
|
||||
def test_trigger_scan_success(self, client, db, sample_db_config):
|
||||
"""Test triggering a new scan."""
|
||||
response = client.post('/api/scans',
|
||||
json={'config_file': str(sample_config_file)},
|
||||
json={'config_file': str(sample_db_config)},
|
||||
content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 201
|
||||
@@ -222,7 +222,7 @@ class TestScanAPIEndpoints:
|
||||
assert 'error' in data
|
||||
assert 'message' in data
|
||||
|
||||
def test_scan_workflow_integration(self, client, db, sample_config_file):
|
||||
def test_scan_workflow_integration(self, client, db, sample_db_config):
|
||||
"""
|
||||
Test complete scan workflow: trigger → status → retrieve → delete.
|
||||
|
||||
@@ -231,7 +231,7 @@ class TestScanAPIEndpoints:
|
||||
"""
|
||||
# Step 1: Trigger scan
|
||||
response = client.post('/api/scans',
|
||||
json={'config_file': str(sample_config_file)},
|
||||
json={'config_file': str(sample_db_config)},
|
||||
content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
@@ -17,10 +17,10 @@ class TestScanComparison:
|
||||
"""Tests for scan comparison methods."""
|
||||
|
||||
@pytest.fixture
|
||||
def scan1_data(self, test_db, sample_config_file):
|
||||
def scan1_data(self, test_db, sample_db_config):
|
||||
"""Create first scan with test data."""
|
||||
service = ScanService(test_db)
|
||||
scan_id = service.trigger_scan(sample_config_file, triggered_by='manual')
|
||||
scan_id = service.trigger_scan(sample_db_config, triggered_by='manual')
|
||||
|
||||
# Get scan and add some test data
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
@@ -77,10 +77,10 @@ class TestScanComparison:
|
||||
return scan_id
|
||||
|
||||
@pytest.fixture
|
||||
def scan2_data(self, test_db, sample_config_file):
|
||||
def scan2_data(self, test_db, sample_db_config):
|
||||
"""Create second scan with modified test data."""
|
||||
service = ScanService(test_db)
|
||||
scan_id = service.trigger_scan(sample_config_file, triggered_by='manual')
|
||||
scan_id = service.trigger_scan(sample_db_config, triggered_by='manual')
|
||||
|
||||
# Get scan and add some test data
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
|
||||
@@ -13,49 +13,42 @@ from web.services.scan_service import ScanService
|
||||
class TestScanServiceTrigger:
|
||||
"""Tests for triggering scans."""
|
||||
|
||||
def test_trigger_scan_valid_config(self, test_db, sample_config_file):
|
||||
"""Test triggering a scan with valid config file."""
|
||||
service = ScanService(test_db)
|
||||
def test_trigger_scan_valid_config(self, db, sample_db_config):
|
||||
"""Test triggering a scan with valid config."""
|
||||
service = ScanService(db)
|
||||
|
||||
scan_id = service.trigger_scan(sample_config_file, triggered_by='manual')
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id, triggered_by='manual')
|
||||
|
||||
# Verify scan created
|
||||
assert scan_id is not None
|
||||
assert isinstance(scan_id, int)
|
||||
|
||||
# Verify scan in database
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
scan = db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
assert scan is not None
|
||||
assert scan.status == 'running'
|
||||
assert scan.title == 'Test Scan'
|
||||
assert scan.triggered_by == 'manual'
|
||||
assert scan.config_file == sample_config_file
|
||||
assert scan.config_id == sample_db_config.id
|
||||
|
||||
def test_trigger_scan_invalid_config(self, test_db, sample_invalid_config_file):
|
||||
"""Test triggering a scan with invalid config file."""
|
||||
service = ScanService(test_db)
|
||||
def test_trigger_scan_invalid_config(self, db):
|
||||
"""Test triggering a scan with invalid config ID."""
|
||||
service = ScanService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid config file"):
|
||||
service.trigger_scan(sample_invalid_config_file)
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
service.trigger_scan(config_id=99999)
|
||||
|
||||
def test_trigger_scan_nonexistent_file(self, test_db):
|
||||
"""Test triggering a scan with nonexistent config file."""
|
||||
service = ScanService(test_db)
|
||||
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
service.trigger_scan('/nonexistent/config.yaml')
|
||||
|
||||
def test_trigger_scan_with_schedule(self, test_db, sample_config_file):
|
||||
def test_trigger_scan_with_schedule(self, db, sample_db_config):
|
||||
"""Test triggering a scan via schedule."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
scan_id = service.trigger_scan(
|
||||
sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
triggered_by='scheduled',
|
||||
schedule_id=42
|
||||
)
|
||||
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
scan = db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
assert scan.triggered_by == 'scheduled'
|
||||
assert scan.schedule_id == 42
|
||||
|
||||
@@ -63,19 +56,19 @@ class TestScanServiceTrigger:
|
||||
class TestScanServiceGet:
|
||||
"""Tests for retrieving scans."""
|
||||
|
||||
def test_get_scan_not_found(self, test_db):
|
||||
def test_get_scan_not_found(self, db):
|
||||
"""Test getting a nonexistent scan."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
result = service.get_scan(999)
|
||||
assert result is None
|
||||
|
||||
def test_get_scan_found(self, test_db, sample_config_file):
|
||||
def test_get_scan_found(self, db, sample_db_config):
|
||||
"""Test getting an existing scan."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create a scan
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
|
||||
# Retrieve it
|
||||
result = service.get_scan(scan_id)
|
||||
@@ -90,9 +83,9 @@ class TestScanServiceGet:
|
||||
class TestScanServiceList:
|
||||
"""Tests for listing scans."""
|
||||
|
||||
def test_list_scans_empty(self, test_db):
|
||||
def test_list_scans_empty(self, db):
|
||||
"""Test listing scans when database is empty."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
result = service.list_scans(page=1, per_page=20)
|
||||
|
||||
@@ -100,13 +93,13 @@ class TestScanServiceList:
|
||||
assert len(result.items) == 0
|
||||
assert result.pages == 0
|
||||
|
||||
def test_list_scans_with_data(self, test_db, sample_config_file):
|
||||
def test_list_scans_with_data(self, db, sample_db_config):
|
||||
"""Test listing scans with multiple scans."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create 3 scans
|
||||
for i in range(3):
|
||||
service.trigger_scan(sample_config_file, triggered_by='api')
|
||||
service.trigger_scan(config_id=sample_db_config.id, triggered_by='api')
|
||||
|
||||
# List all scans
|
||||
result = service.list_scans(page=1, per_page=20)
|
||||
@@ -115,13 +108,13 @@ class TestScanServiceList:
|
||||
assert len(result.items) == 3
|
||||
assert result.pages == 1
|
||||
|
||||
def test_list_scans_pagination(self, test_db, sample_config_file):
|
||||
def test_list_scans_pagination(self, db, sample_db_config):
|
||||
"""Test pagination."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create 5 scans
|
||||
for i in range(5):
|
||||
service.trigger_scan(sample_config_file)
|
||||
service.trigger_scan(config_id=sample_db_config.id)
|
||||
|
||||
# Get page 1 (2 items per page)
|
||||
result = service.list_scans(page=1, per_page=2)
|
||||
@@ -141,18 +134,18 @@ class TestScanServiceList:
|
||||
assert len(result.items) == 1
|
||||
assert result.has_next is False
|
||||
|
||||
def test_list_scans_filter_by_status(self, test_db, sample_config_file):
|
||||
def test_list_scans_filter_by_status(self, db, sample_db_config):
|
||||
"""Test filtering scans by status."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create scans with different statuses
|
||||
scan_id_1 = service.trigger_scan(sample_config_file)
|
||||
scan_id_2 = service.trigger_scan(sample_config_file)
|
||||
scan_id_1 = service.trigger_scan(config_id=sample_db_config.id)
|
||||
scan_id_2 = service.trigger_scan(config_id=sample_db_config.id)
|
||||
|
||||
# Mark one as completed
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id_1).first()
|
||||
scan = db.query(Scan).filter(Scan.id == scan_id_1).first()
|
||||
scan.status = 'completed'
|
||||
test_db.commit()
|
||||
db.commit()
|
||||
|
||||
# Filter by running
|
||||
result = service.list_scans(status_filter='running')
|
||||
@@ -162,9 +155,9 @@ class TestScanServiceList:
|
||||
result = service.list_scans(status_filter='completed')
|
||||
assert result.total == 1
|
||||
|
||||
def test_list_scans_invalid_status_filter(self, test_db):
|
||||
def test_list_scans_invalid_status_filter(self, db):
|
||||
"""Test filtering with invalid status."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid status"):
|
||||
service.list_scans(status_filter='invalid_status')
|
||||
@@ -173,46 +166,46 @@ class TestScanServiceList:
|
||||
class TestScanServiceDelete:
|
||||
"""Tests for deleting scans."""
|
||||
|
||||
def test_delete_scan_not_found(self, test_db):
|
||||
def test_delete_scan_not_found(self, db):
|
||||
"""Test deleting a nonexistent scan."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
service.delete_scan(999)
|
||||
|
||||
def test_delete_scan_success(self, test_db, sample_config_file):
|
||||
def test_delete_scan_success(self, db, sample_db_config):
|
||||
"""Test successful scan deletion."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create a scan
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
|
||||
# Verify it exists
|
||||
assert test_db.query(Scan).filter(Scan.id == scan_id).first() is not None
|
||||
assert db.query(Scan).filter(Scan.id == scan_id).first() is not None
|
||||
|
||||
# Delete it
|
||||
result = service.delete_scan(scan_id)
|
||||
assert result is True
|
||||
|
||||
# Verify it's gone
|
||||
assert test_db.query(Scan).filter(Scan.id == scan_id).first() is None
|
||||
assert db.query(Scan).filter(Scan.id == scan_id).first() is None
|
||||
|
||||
|
||||
class TestScanServiceStatus:
|
||||
"""Tests for scan status retrieval."""
|
||||
|
||||
def test_get_scan_status_not_found(self, test_db):
|
||||
def test_get_scan_status_not_found(self, db):
|
||||
"""Test getting status of nonexistent scan."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
result = service.get_scan_status(999)
|
||||
assert result is None
|
||||
|
||||
def test_get_scan_status_running(self, test_db, sample_config_file):
|
||||
def test_get_scan_status_running(self, db, sample_db_config):
|
||||
"""Test getting status of running scan."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
status = service.get_scan_status(scan_id)
|
||||
|
||||
assert status is not None
|
||||
@@ -221,16 +214,16 @@ class TestScanServiceStatus:
|
||||
assert status['progress'] == 'In progress'
|
||||
assert status['title'] == 'Test Scan'
|
||||
|
||||
def test_get_scan_status_completed(self, test_db, sample_config_file):
|
||||
def test_get_scan_status_completed(self, db, sample_db_config):
|
||||
"""Test getting status of completed scan."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create and mark as completed
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
scan = db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
scan.status = 'completed'
|
||||
scan.duration = 125.5
|
||||
test_db.commit()
|
||||
db.commit()
|
||||
|
||||
status = service.get_scan_status(scan_id)
|
||||
|
||||
@@ -242,35 +235,35 @@ class TestScanServiceStatus:
|
||||
class TestScanServiceDatabaseMapping:
|
||||
"""Tests for mapping scan reports to database models."""
|
||||
|
||||
def test_save_scan_to_db(self, test_db, sample_config_file, sample_scan_report):
|
||||
def test_save_scan_to_db(self, db, sample_db_config, sample_scan_report):
|
||||
"""Test saving a complete scan report to database."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
# Create a scan
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
|
||||
# Save report to database
|
||||
service._save_scan_to_db(sample_scan_report, scan_id, status='completed')
|
||||
|
||||
# Verify scan updated
|
||||
scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
scan = db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
assert scan.status == 'completed'
|
||||
assert scan.duration == 125.5
|
||||
|
||||
# Verify sites created
|
||||
sites = test_db.query(ScanSite).filter(ScanSite.scan_id == scan_id).all()
|
||||
sites = db.query(ScanSite).filter(ScanSite.scan_id == scan_id).all()
|
||||
assert len(sites) == 1
|
||||
assert sites[0].site_name == 'Test Site'
|
||||
|
||||
# Verify IPs created
|
||||
ips = test_db.query(ScanIP).filter(ScanIP.scan_id == scan_id).all()
|
||||
ips = db.query(ScanIP).filter(ScanIP.scan_id == scan_id).all()
|
||||
assert len(ips) == 1
|
||||
assert ips[0].ip_address == '192.168.1.10'
|
||||
assert ips[0].ping_expected is True
|
||||
assert ips[0].ping_actual is True
|
||||
|
||||
# Verify ports created (TCP: 22, 80, 443, 8080 | UDP: 53)
|
||||
ports = test_db.query(ScanPort).filter(ScanPort.scan_id == scan_id).all()
|
||||
ports = db.query(ScanPort).filter(ScanPort.scan_id == scan_id).all()
|
||||
assert len(ports) == 5 # 4 TCP + 1 UDP
|
||||
|
||||
# Verify TCP ports
|
||||
@@ -285,7 +278,7 @@ class TestScanServiceDatabaseMapping:
|
||||
assert udp_ports[0].port == 53
|
||||
|
||||
# Verify services created
|
||||
services = test_db.query(ScanServiceModel).filter(
|
||||
services = db.query(ScanServiceModel).filter(
|
||||
ScanServiceModel.scan_id == scan_id
|
||||
).all()
|
||||
assert len(services) == 4 # SSH, HTTP (80), HTTPS, HTTP (8080)
|
||||
@@ -300,15 +293,15 @@ class TestScanServiceDatabaseMapping:
|
||||
assert https_service.http_protocol == 'https'
|
||||
assert https_service.screenshot_path == 'screenshots/192_168_1_10_443.png'
|
||||
|
||||
def test_map_port_expected_vs_actual(self, test_db, sample_config_file, sample_scan_report):
|
||||
def test_map_port_expected_vs_actual(self, db, sample_db_config, sample_scan_report):
|
||||
"""Test that expected vs actual ports are correctly flagged."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
service._save_scan_to_db(sample_scan_report, scan_id)
|
||||
|
||||
# Check TCP ports
|
||||
tcp_ports = test_db.query(ScanPort).filter(
|
||||
tcp_ports = db.query(ScanPort).filter(
|
||||
ScanPort.scan_id == scan_id,
|
||||
ScanPort.protocol == 'tcp'
|
||||
).all()
|
||||
@@ -322,15 +315,15 @@ class TestScanServiceDatabaseMapping:
|
||||
# Port 8080 was not expected
|
||||
assert port.expected is False, f"Port {port.port} should not be expected"
|
||||
|
||||
def test_map_certificate_and_tls(self, test_db, sample_config_file, sample_scan_report):
|
||||
def test_map_certificate_and_tls(self, db, sample_db_config, sample_scan_report):
|
||||
"""Test that certificate and TLS data are correctly mapped."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
service._save_scan_to_db(sample_scan_report, scan_id)
|
||||
|
||||
# Find HTTPS service
|
||||
https_service = test_db.query(ScanServiceModel).filter(
|
||||
https_service = db.query(ScanServiceModel).filter(
|
||||
ScanServiceModel.scan_id == scan_id,
|
||||
ScanServiceModel.service_name == 'https'
|
||||
).first()
|
||||
@@ -363,11 +356,11 @@ class TestScanServiceDatabaseMapping:
|
||||
assert tls_13 is not None
|
||||
assert tls_13.supported is True
|
||||
|
||||
def test_get_scan_with_full_details(self, test_db, sample_config_file, sample_scan_report):
|
||||
def test_get_scan_with_full_details(self, db, sample_db_config, sample_scan_report):
|
||||
"""Test retrieving scan with all nested relationships."""
|
||||
service = ScanService(test_db)
|
||||
service = ScanService(db)
|
||||
|
||||
scan_id = service.trigger_scan(sample_config_file)
|
||||
scan_id = service.trigger_scan(config_id=sample_db_config.id)
|
||||
service._save_scan_to_db(sample_scan_report, scan_id)
|
||||
|
||||
# Get full scan details
|
||||
|
||||
@@ -13,20 +13,20 @@ from web.models import Schedule, Scan
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_schedule(db, sample_config_file):
|
||||
def sample_schedule(db, sample_db_config):
|
||||
"""
|
||||
Create a sample schedule in the database for testing.
|
||||
|
||||
Args:
|
||||
db: Database session fixture
|
||||
sample_config_file: Path to test config file
|
||||
sample_db_config: Path to test config file
|
||||
|
||||
Returns:
|
||||
Schedule model instance
|
||||
"""
|
||||
schedule = Schedule(
|
||||
name='Daily Test Scan',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True,
|
||||
last_run=None,
|
||||
@@ -68,13 +68,13 @@ class TestScheduleAPIEndpoints:
|
||||
assert data['schedules'][0]['name'] == sample_schedule.name
|
||||
assert data['schedules'][0]['cron_expression'] == sample_schedule.cron_expression
|
||||
|
||||
def test_list_schedules_pagination(self, client, db, sample_config_file):
|
||||
def test_list_schedules_pagination(self, client, db, sample_db_config):
|
||||
"""Test schedule list pagination."""
|
||||
# Create 25 schedules
|
||||
for i in range(25):
|
||||
schedule = Schedule(
|
||||
name=f'Schedule {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True,
|
||||
created_at=datetime.utcnow()
|
||||
@@ -101,13 +101,13 @@ class TestScheduleAPIEndpoints:
|
||||
assert len(data['schedules']) == 10
|
||||
assert data['page'] == 2
|
||||
|
||||
def test_list_schedules_filter_enabled(self, client, db, sample_config_file):
|
||||
def test_list_schedules_filter_enabled(self, client, db, sample_db_config):
|
||||
"""Test filtering schedules by enabled status."""
|
||||
# Create enabled and disabled schedules
|
||||
for i in range(3):
|
||||
schedule = Schedule(
|
||||
name=f'Enabled Schedule {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True,
|
||||
created_at=datetime.utcnow()
|
||||
@@ -117,7 +117,7 @@ class TestScheduleAPIEndpoints:
|
||||
for i in range(2):
|
||||
schedule = Schedule(
|
||||
name=f'Disabled Schedule {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 3 * * *',
|
||||
enabled=False,
|
||||
created_at=datetime.utcnow()
|
||||
@@ -165,11 +165,11 @@ class TestScheduleAPIEndpoints:
|
||||
assert 'error' in data
|
||||
assert 'not found' in data['error'].lower()
|
||||
|
||||
def test_create_schedule(self, client, db, sample_config_file):
|
||||
def test_create_schedule(self, client, db, sample_db_config):
|
||||
"""Test creating a new schedule."""
|
||||
schedule_data = {
|
||||
'name': 'New Test Schedule',
|
||||
'config_file': sample_config_file,
|
||||
'config_file': sample_db_config,
|
||||
'cron_expression': '0 3 * * *',
|
||||
'enabled': True
|
||||
}
|
||||
@@ -211,11 +211,11 @@ class TestScheduleAPIEndpoints:
|
||||
assert 'error' in data
|
||||
assert 'missing' in data['error'].lower()
|
||||
|
||||
def test_create_schedule_invalid_cron(self, client, db, sample_config_file):
|
||||
def test_create_schedule_invalid_cron(self, client, db, sample_db_config):
|
||||
"""Test creating schedule with invalid cron expression."""
|
||||
schedule_data = {
|
||||
'name': 'Invalid Cron Schedule',
|
||||
'config_file': sample_config_file,
|
||||
'config_file': sample_db_config,
|
||||
'cron_expression': 'invalid cron'
|
||||
}
|
||||
|
||||
@@ -360,13 +360,13 @@ class TestScheduleAPIEndpoints:
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_delete_schedule_preserves_scans(self, client, db, sample_schedule, sample_config_file):
|
||||
def test_delete_schedule_preserves_scans(self, client, db, sample_schedule, sample_db_config):
|
||||
"""Test that deleting schedule preserves associated scans."""
|
||||
# Create a scan associated with the schedule
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='completed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title='Test Scan',
|
||||
triggered_by='scheduled',
|
||||
schedule_id=sample_schedule.id
|
||||
@@ -409,14 +409,14 @@ class TestScheduleAPIEndpoints:
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_get_schedule_with_history(self, client, db, sample_schedule, sample_config_file):
|
||||
def test_get_schedule_with_history(self, client, db, sample_schedule, sample_db_config):
|
||||
"""Test getting schedule includes execution history."""
|
||||
# Create some scans for this schedule
|
||||
for i in range(5):
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='completed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title=f'Scheduled Scan {i}',
|
||||
triggered_by='scheduled',
|
||||
schedule_id=sample_schedule.id
|
||||
@@ -431,12 +431,12 @@ class TestScheduleAPIEndpoints:
|
||||
assert 'history' in data
|
||||
assert len(data['history']) == 5
|
||||
|
||||
def test_schedule_workflow_integration(self, client, db, sample_config_file):
|
||||
def test_schedule_workflow_integration(self, client, db, sample_db_config):
|
||||
"""Test complete schedule workflow: create → update → trigger → delete."""
|
||||
# 1. Create schedule
|
||||
schedule_data = {
|
||||
'name': 'Integration Test Schedule',
|
||||
'config_file': sample_config_file,
|
||||
'config_file': sample_db_config,
|
||||
'cron_expression': '0 2 * * *',
|
||||
'enabled': True
|
||||
}
|
||||
@@ -482,14 +482,14 @@ class TestScheduleAPIEndpoints:
|
||||
scan = db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
assert scan is not None
|
||||
|
||||
def test_list_schedules_ordering(self, client, db, sample_config_file):
|
||||
def test_list_schedules_ordering(self, client, db, sample_db_config):
|
||||
"""Test that schedules are ordered by next_run time."""
|
||||
# Create schedules with different next_run times
|
||||
schedules = []
|
||||
for i in range(3):
|
||||
schedule = Schedule(
|
||||
name=f'Schedule {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True,
|
||||
next_run=datetime(2025, 11, 15 + i, 2, 0, 0),
|
||||
@@ -501,7 +501,7 @@ class TestScheduleAPIEndpoints:
|
||||
# Create a disabled schedule (next_run is None)
|
||||
disabled_schedule = Schedule(
|
||||
name='Disabled Schedule',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 3 * * *',
|
||||
enabled=False,
|
||||
next_run=None,
|
||||
@@ -523,11 +523,11 @@ class TestScheduleAPIEndpoints:
|
||||
assert returned_schedules[2]['id'] == schedules[2].id
|
||||
assert returned_schedules[3]['id'] == disabled_schedule.id
|
||||
|
||||
def test_create_schedule_with_disabled(self, client, db, sample_config_file):
|
||||
def test_create_schedule_with_disabled(self, client, db, sample_db_config):
|
||||
"""Test creating a disabled schedule."""
|
||||
schedule_data = {
|
||||
'name': 'Disabled Schedule',
|
||||
'config_file': sample_config_file,
|
||||
'config_file': sample_db_config,
|
||||
'cron_expression': '0 2 * * *',
|
||||
'enabled': False
|
||||
}
|
||||
@@ -587,7 +587,7 @@ class TestScheduleAPIAuthentication:
|
||||
class TestScheduleAPICronValidation:
|
||||
"""Test suite for cron expression validation."""
|
||||
|
||||
def test_valid_cron_expressions(self, client, db, sample_config_file):
|
||||
def test_valid_cron_expressions(self, client, db, sample_db_config):
|
||||
"""Test various valid cron expressions."""
|
||||
valid_expressions = [
|
||||
'0 2 * * *', # Daily at 2am
|
||||
@@ -600,7 +600,7 @@ class TestScheduleAPICronValidation:
|
||||
for cron_expr in valid_expressions:
|
||||
schedule_data = {
|
||||
'name': f'Schedule for {cron_expr}',
|
||||
'config_file': sample_config_file,
|
||||
'config_file': sample_db_config,
|
||||
'cron_expression': cron_expr
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ class TestScheduleAPICronValidation:
|
||||
assert response.status_code == 201, \
|
||||
f"Valid cron expression '{cron_expr}' should be accepted"
|
||||
|
||||
def test_invalid_cron_expressions(self, client, db, sample_config_file):
|
||||
def test_invalid_cron_expressions(self, client, db, sample_db_config):
|
||||
"""Test various invalid cron expressions."""
|
||||
invalid_expressions = [
|
||||
'invalid',
|
||||
@@ -626,7 +626,7 @@ class TestScheduleAPICronValidation:
|
||||
for cron_expr in invalid_expressions:
|
||||
schedule_data = {
|
||||
'name': f'Schedule for {cron_expr}',
|
||||
'config_file': sample_config_file,
|
||||
'config_file': sample_db_config,
|
||||
'cron_expression': cron_expr
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ from web.services.schedule_service import ScheduleService
|
||||
class TestScheduleServiceCreate:
|
||||
"""Tests for creating schedules."""
|
||||
|
||||
def test_create_schedule_valid(self, test_db, sample_config_file):
|
||||
def test_create_schedule_valid(self, db, sample_db_config):
|
||||
"""Test creating a schedule with valid parameters."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Daily Scan',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -31,57 +31,57 @@ class TestScheduleServiceCreate:
|
||||
assert isinstance(schedule_id, int)
|
||||
|
||||
# Verify schedule in database
|
||||
schedule = test_db.query(Schedule).filter(Schedule.id == schedule_id).first()
|
||||
schedule = db.query(Schedule).filter(Schedule.id == schedule_id).first()
|
||||
assert schedule is not None
|
||||
assert schedule.name == 'Daily Scan'
|
||||
assert schedule.config_file == sample_config_file
|
||||
assert schedule.config_id == sample_db_config.id
|
||||
assert schedule.cron_expression == '0 2 * * *'
|
||||
assert schedule.enabled is True
|
||||
assert schedule.next_run is not None
|
||||
assert schedule.last_run is None
|
||||
|
||||
def test_create_schedule_disabled(self, test_db, sample_config_file):
|
||||
def test_create_schedule_disabled(self, db, sample_db_config):
|
||||
"""Test creating a disabled schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Disabled Scan',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 3 * * *',
|
||||
enabled=False
|
||||
)
|
||||
|
||||
schedule = test_db.query(Schedule).filter(Schedule.id == schedule_id).first()
|
||||
schedule = db.query(Schedule).filter(Schedule.id == schedule_id).first()
|
||||
assert schedule.enabled is False
|
||||
assert schedule.next_run is None
|
||||
|
||||
def test_create_schedule_invalid_cron(self, test_db, sample_config_file):
|
||||
def test_create_schedule_invalid_cron(self, db, sample_db_config):
|
||||
"""Test creating a schedule with invalid cron expression."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid cron expression"):
|
||||
service.create_schedule(
|
||||
name='Invalid Schedule',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='invalid cron',
|
||||
enabled=True
|
||||
)
|
||||
|
||||
def test_create_schedule_nonexistent_config(self, test_db):
|
||||
"""Test creating a schedule with nonexistent config file."""
|
||||
service = ScheduleService(test_db)
|
||||
def test_create_schedule_nonexistent_config(self, db):
|
||||
"""Test creating a schedule with nonexistent config."""
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Config file not found"):
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
service.create_schedule(
|
||||
name='Bad Config',
|
||||
config_file='/nonexistent/config.yaml',
|
||||
config_id=99999,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
|
||||
def test_create_schedule_various_cron_expressions(self, test_db, sample_config_file):
|
||||
def test_create_schedule_various_cron_expressions(self, db, sample_db_config):
|
||||
"""Test creating schedules with various valid cron expressions."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
cron_expressions = [
|
||||
'0 0 * * *', # Daily at midnight
|
||||
@@ -94,7 +94,7 @@ class TestScheduleServiceCreate:
|
||||
for i, cron in enumerate(cron_expressions):
|
||||
schedule_id = service.create_schedule(
|
||||
name=f'Schedule {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression=cron,
|
||||
enabled=True
|
||||
)
|
||||
@@ -104,21 +104,21 @@ class TestScheduleServiceCreate:
|
||||
class TestScheduleServiceGet:
|
||||
"""Tests for retrieving schedules."""
|
||||
|
||||
def test_get_schedule_not_found(self, test_db):
|
||||
def test_get_schedule_not_found(self, db):
|
||||
"""Test getting a nonexistent schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Schedule .* not found"):
|
||||
service.get_schedule(999)
|
||||
|
||||
def test_get_schedule_found(self, test_db, sample_config_file):
|
||||
def test_get_schedule_found(self, db, sample_db_config):
|
||||
"""Test getting an existing schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Create a schedule
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test Schedule',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -134,14 +134,14 @@ class TestScheduleServiceGet:
|
||||
assert 'history' in result
|
||||
assert isinstance(result['history'], list)
|
||||
|
||||
def test_get_schedule_with_history(self, test_db, sample_config_file):
|
||||
def test_get_schedule_with_history(self, db, sample_db_config):
|
||||
"""Test getting schedule includes execution history."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Create schedule
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test Schedule',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -151,13 +151,13 @@ class TestScheduleServiceGet:
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow() - timedelta(days=i),
|
||||
status='completed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title=f'Scan {i}',
|
||||
triggered_by='scheduled',
|
||||
schedule_id=schedule_id
|
||||
)
|
||||
test_db.add(scan)
|
||||
test_db.commit()
|
||||
db.add(scan)
|
||||
db.commit()
|
||||
|
||||
# Get schedule
|
||||
result = service.get_schedule(schedule_id)
|
||||
@@ -169,9 +169,9 @@ class TestScheduleServiceGet:
|
||||
class TestScheduleServiceList:
|
||||
"""Tests for listing schedules."""
|
||||
|
||||
def test_list_schedules_empty(self, test_db):
|
||||
def test_list_schedules_empty(self, db):
|
||||
"""Test listing schedules when database is empty."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
result = service.list_schedules(page=1, per_page=20)
|
||||
|
||||
@@ -180,15 +180,15 @@ class TestScheduleServiceList:
|
||||
assert result['page'] == 1
|
||||
assert result['per_page'] == 20
|
||||
|
||||
def test_list_schedules_populated(self, test_db, sample_config_file):
|
||||
def test_list_schedules_populated(self, db, sample_db_config):
|
||||
"""Test listing schedules with data."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Create multiple schedules
|
||||
for i in range(5):
|
||||
service.create_schedule(
|
||||
name=f'Schedule {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -199,15 +199,15 @@ class TestScheduleServiceList:
|
||||
assert len(result['schedules']) == 5
|
||||
assert all('name' in s for s in result['schedules'])
|
||||
|
||||
def test_list_schedules_pagination(self, test_db, sample_config_file):
|
||||
def test_list_schedules_pagination(self, db, sample_db_config):
|
||||
"""Test schedule pagination."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Create 25 schedules
|
||||
for i in range(25):
|
||||
service.create_schedule(
|
||||
name=f'Schedule {i:02d}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -226,22 +226,22 @@ class TestScheduleServiceList:
|
||||
result_page3 = service.list_schedules(page=3, per_page=10)
|
||||
assert len(result_page3['schedules']) == 5
|
||||
|
||||
def test_list_schedules_filter_enabled(self, test_db, sample_config_file):
|
||||
def test_list_schedules_filter_enabled(self, db, sample_db_config):
|
||||
"""Test filtering schedules by enabled status."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Create enabled and disabled schedules
|
||||
for i in range(3):
|
||||
service.create_schedule(
|
||||
name=f'Enabled {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
for i in range(2):
|
||||
service.create_schedule(
|
||||
name=f'Disabled {i}',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=False
|
||||
)
|
||||
@@ -262,13 +262,13 @@ class TestScheduleServiceList:
|
||||
class TestScheduleServiceUpdate:
|
||||
"""Tests for updating schedules."""
|
||||
|
||||
def test_update_schedule_name(self, test_db, sample_config_file):
|
||||
def test_update_schedule_name(self, db, sample_db_config):
|
||||
"""Test updating schedule name."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Old Name',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -278,13 +278,13 @@ class TestScheduleServiceUpdate:
|
||||
assert result['name'] == 'New Name'
|
||||
assert result['cron_expression'] == '0 2 * * *'
|
||||
|
||||
def test_update_schedule_cron(self, test_db, sample_config_file):
|
||||
def test_update_schedule_cron(self, db, sample_db_config):
|
||||
"""Test updating cron expression recalculates next_run."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -302,13 +302,13 @@ class TestScheduleServiceUpdate:
|
||||
assert result['cron_expression'] == '0 3 * * *'
|
||||
assert result['next_run'] != original_next_run
|
||||
|
||||
def test_update_schedule_invalid_cron(self, test_db, sample_config_file):
|
||||
def test_update_schedule_invalid_cron(self, db, sample_db_config):
|
||||
"""Test updating with invalid cron expression fails."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -316,67 +316,67 @@ class TestScheduleServiceUpdate:
|
||||
with pytest.raises(ValueError, match="Invalid cron expression"):
|
||||
service.update_schedule(schedule_id, cron_expression='invalid')
|
||||
|
||||
def test_update_schedule_not_found(self, test_db):
|
||||
def test_update_schedule_not_found(self, db):
|
||||
"""Test updating nonexistent schedule fails."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Schedule .* not found"):
|
||||
service.update_schedule(999, name='New Name')
|
||||
|
||||
def test_update_schedule_invalid_config_file(self, test_db, sample_config_file):
|
||||
"""Test updating with nonexistent config file fails."""
|
||||
service = ScheduleService(test_db)
|
||||
def test_update_schedule_invalid_config_id(self, db, sample_db_config):
|
||||
"""Test updating with nonexistent config ID fails."""
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Config file not found"):
|
||||
service.update_schedule(schedule_id, config_file='/nonexistent.yaml')
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
service.update_schedule(schedule_id, config_id=99999)
|
||||
|
||||
|
||||
class TestScheduleServiceDelete:
|
||||
"""Tests for deleting schedules."""
|
||||
|
||||
def test_delete_schedule(self, test_db, sample_config_file):
|
||||
def test_delete_schedule(self, db, sample_db_config):
|
||||
"""Test deleting a schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='To Delete',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
|
||||
# Verify exists
|
||||
assert test_db.query(Schedule).filter(Schedule.id == schedule_id).first() is not None
|
||||
assert db.query(Schedule).filter(Schedule.id == schedule_id).first() is not None
|
||||
|
||||
# Delete
|
||||
result = service.delete_schedule(schedule_id)
|
||||
assert result is True
|
||||
|
||||
# Verify deleted
|
||||
assert test_db.query(Schedule).filter(Schedule.id == schedule_id).first() is None
|
||||
assert db.query(Schedule).filter(Schedule.id == schedule_id).first() is None
|
||||
|
||||
def test_delete_schedule_not_found(self, test_db):
|
||||
def test_delete_schedule_not_found(self, db):
|
||||
"""Test deleting nonexistent schedule fails."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Schedule .* not found"):
|
||||
service.delete_schedule(999)
|
||||
|
||||
def test_delete_schedule_preserves_scans(self, test_db, sample_config_file):
|
||||
def test_delete_schedule_preserves_scans(self, db, sample_db_config):
|
||||
"""Test that deleting schedule preserves associated scans."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Create schedule
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -385,20 +385,20 @@ class TestScheduleServiceDelete:
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow(),
|
||||
status='completed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title='Test Scan',
|
||||
triggered_by='scheduled',
|
||||
schedule_id=schedule_id
|
||||
)
|
||||
test_db.add(scan)
|
||||
test_db.commit()
|
||||
db.add(scan)
|
||||
db.commit()
|
||||
scan_id = scan.id
|
||||
|
||||
# Delete schedule
|
||||
service.delete_schedule(schedule_id)
|
||||
|
||||
# Verify scan still exists (schedule_id becomes null)
|
||||
remaining_scan = test_db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
remaining_scan = db.query(Scan).filter(Scan.id == scan_id).first()
|
||||
assert remaining_scan is not None
|
||||
assert remaining_scan.schedule_id is None
|
||||
|
||||
@@ -406,13 +406,13 @@ class TestScheduleServiceDelete:
|
||||
class TestScheduleServiceToggle:
|
||||
"""Tests for toggling schedule enabled status."""
|
||||
|
||||
def test_toggle_enabled_to_disabled(self, test_db, sample_config_file):
|
||||
def test_toggle_enabled_to_disabled(self, db, sample_db_config):
|
||||
"""Test disabling an enabled schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -422,13 +422,13 @@ class TestScheduleServiceToggle:
|
||||
assert result['enabled'] is False
|
||||
assert result['next_run'] is None
|
||||
|
||||
def test_toggle_disabled_to_enabled(self, test_db, sample_config_file):
|
||||
def test_toggle_disabled_to_enabled(self, db, sample_db_config):
|
||||
"""Test enabling a disabled schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=False
|
||||
)
|
||||
@@ -442,13 +442,13 @@ class TestScheduleServiceToggle:
|
||||
class TestScheduleServiceRunTimes:
|
||||
"""Tests for updating run times."""
|
||||
|
||||
def test_update_run_times(self, test_db, sample_config_file):
|
||||
def test_update_run_times(self, db, sample_db_config):
|
||||
"""Test updating last_run and next_run."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -463,9 +463,9 @@ class TestScheduleServiceRunTimes:
|
||||
assert schedule['last_run'] is not None
|
||||
assert schedule['next_run'] is not None
|
||||
|
||||
def test_update_run_times_not_found(self, test_db):
|
||||
def test_update_run_times_not_found(self, db):
|
||||
"""Test updating run times for nonexistent schedule."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Schedule .* not found"):
|
||||
service.update_run_times(
|
||||
@@ -478,9 +478,9 @@ class TestScheduleServiceRunTimes:
|
||||
class TestCronValidation:
|
||||
"""Tests for cron expression validation."""
|
||||
|
||||
def test_validate_cron_valid_expressions(self, test_db):
|
||||
def test_validate_cron_valid_expressions(self, db):
|
||||
"""Test validating various valid cron expressions."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
valid_expressions = [
|
||||
'0 0 * * *', # Daily at midnight
|
||||
@@ -496,9 +496,9 @@ class TestCronValidation:
|
||||
assert is_valid is True, f"Expression '{expr}' should be valid"
|
||||
assert error is None
|
||||
|
||||
def test_validate_cron_invalid_expressions(self, test_db):
|
||||
def test_validate_cron_invalid_expressions(self, db):
|
||||
"""Test validating invalid cron expressions."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
invalid_expressions = [
|
||||
'invalid',
|
||||
@@ -518,9 +518,9 @@ class TestCronValidation:
|
||||
class TestNextRunCalculation:
|
||||
"""Tests for next run time calculation."""
|
||||
|
||||
def test_calculate_next_run(self, test_db):
|
||||
def test_calculate_next_run(self, db):
|
||||
"""Test calculating next run time."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
# Daily at 2 AM
|
||||
next_run = service.calculate_next_run('0 2 * * *')
|
||||
@@ -529,9 +529,9 @@ class TestNextRunCalculation:
|
||||
assert isinstance(next_run, datetime)
|
||||
assert next_run > datetime.utcnow()
|
||||
|
||||
def test_calculate_next_run_from_time(self, test_db):
|
||||
def test_calculate_next_run_from_time(self, db):
|
||||
"""Test calculating next run from specific time."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
base_time = datetime(2025, 1, 1, 0, 0, 0)
|
||||
next_run = service.calculate_next_run('0 2 * * *', from_time=base_time)
|
||||
@@ -540,9 +540,9 @@ class TestNextRunCalculation:
|
||||
assert next_run.hour == 2
|
||||
assert next_run.minute == 0
|
||||
|
||||
def test_calculate_next_run_invalid_cron(self, test_db):
|
||||
def test_calculate_next_run_invalid_cron(self, db):
|
||||
"""Test calculating next run with invalid cron raises error."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid cron expression"):
|
||||
service.calculate_next_run('invalid cron')
|
||||
@@ -551,13 +551,13 @@ class TestNextRunCalculation:
|
||||
class TestScheduleHistory:
|
||||
"""Tests for schedule execution history."""
|
||||
|
||||
def test_get_schedule_history_empty(self, test_db, sample_config_file):
|
||||
def test_get_schedule_history_empty(self, db, sample_db_config):
|
||||
"""Test getting history for schedule with no executions."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -565,13 +565,13 @@ class TestScheduleHistory:
|
||||
history = service.get_schedule_history(schedule_id)
|
||||
assert len(history) == 0
|
||||
|
||||
def test_get_schedule_history_with_scans(self, test_db, sample_config_file):
|
||||
def test_get_schedule_history_with_scans(self, db, sample_db_config):
|
||||
"""Test getting history with multiple scans."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -581,26 +581,26 @@ class TestScheduleHistory:
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow() - timedelta(days=i),
|
||||
status='completed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title=f'Scan {i}',
|
||||
triggered_by='scheduled',
|
||||
schedule_id=schedule_id
|
||||
)
|
||||
test_db.add(scan)
|
||||
test_db.commit()
|
||||
db.add(scan)
|
||||
db.commit()
|
||||
|
||||
# Get history (default limit 10)
|
||||
history = service.get_schedule_history(schedule_id, limit=10)
|
||||
assert len(history) == 10
|
||||
assert history[0]['title'] == 'Scan 0' # Most recent first
|
||||
|
||||
def test_get_schedule_history_custom_limit(self, test_db, sample_config_file):
|
||||
def test_get_schedule_history_custom_limit(self, db, sample_db_config):
|
||||
"""Test getting history with custom limit."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -610,13 +610,13 @@ class TestScheduleHistory:
|
||||
scan = Scan(
|
||||
timestamp=datetime.utcnow() - timedelta(days=i),
|
||||
status='completed',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
title=f'Scan {i}',
|
||||
triggered_by='scheduled',
|
||||
schedule_id=schedule_id
|
||||
)
|
||||
test_db.add(scan)
|
||||
test_db.commit()
|
||||
db.add(scan)
|
||||
db.commit()
|
||||
|
||||
# Get only 5
|
||||
history = service.get_schedule_history(schedule_id, limit=5)
|
||||
@@ -626,13 +626,13 @@ class TestScheduleHistory:
|
||||
class TestScheduleSerialization:
|
||||
"""Tests for schedule serialization."""
|
||||
|
||||
def test_schedule_to_dict(self, test_db, sample_config_file):
|
||||
def test_schedule_to_dict(self, db, sample_db_config):
|
||||
"""Test converting schedule to dictionary."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test Schedule',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
@@ -642,7 +642,7 @@ class TestScheduleSerialization:
|
||||
# Verify all required fields
|
||||
assert 'id' in result
|
||||
assert 'name' in result
|
||||
assert 'config_file' in result
|
||||
assert 'config_id' in result
|
||||
assert 'cron_expression' in result
|
||||
assert 'enabled' in result
|
||||
assert 'last_run' in result
|
||||
@@ -652,13 +652,13 @@ class TestScheduleSerialization:
|
||||
assert 'updated_at' in result
|
||||
assert 'history' in result
|
||||
|
||||
def test_schedule_relative_time_formatting(self, test_db, sample_config_file):
|
||||
def test_schedule_relative_time_formatting(self, db, sample_db_config):
|
||||
"""Test relative time formatting in schedule dict."""
|
||||
service = ScheduleService(test_db)
|
||||
service = ScheduleService(db)
|
||||
|
||||
schedule_id = service.create_schedule(
|
||||
name='Test',
|
||||
config_file=sample_config_file,
|
||||
config_id=sample_db_config.id,
|
||||
cron_expression='0 2 * * *',
|
||||
enabled=True
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestStatsAPI:
|
||||
scan_date = today - timedelta(days=i)
|
||||
for j in range(i + 1): # Create 1, 2, 3, 4, 5 scans per day
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=scan_date,
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -56,7 +56,7 @@ class TestStatsAPI:
|
||||
today = datetime.utcnow()
|
||||
for i in range(10):
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today - timedelta(days=i),
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -105,7 +105,7 @@ class TestStatsAPI:
|
||||
|
||||
# Create scan 5 days ago
|
||||
scan1 = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today - timedelta(days=5),
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -114,7 +114,7 @@ class TestStatsAPI:
|
||||
|
||||
# Create scan 10 days ago
|
||||
scan2 = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today - timedelta(days=10),
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -148,7 +148,7 @@ class TestStatsAPI:
|
||||
# 5 completed scans
|
||||
for i in range(5):
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today - timedelta(days=i),
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -158,7 +158,7 @@ class TestStatsAPI:
|
||||
# 2 failed scans
|
||||
for i in range(2):
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today - timedelta(days=i),
|
||||
status='failed',
|
||||
duration=5.0
|
||||
@@ -167,7 +167,7 @@ class TestStatsAPI:
|
||||
|
||||
# 1 running scan
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today,
|
||||
status='running',
|
||||
duration=None
|
||||
@@ -217,7 +217,7 @@ class TestStatsAPI:
|
||||
# Create 3 scans today
|
||||
for i in range(3):
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today,
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -227,7 +227,7 @@ class TestStatsAPI:
|
||||
# Create 2 scans yesterday
|
||||
for i in range(2):
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=yesterday,
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -250,7 +250,7 @@ class TestStatsAPI:
|
||||
# Create scans over the last 10 days
|
||||
for i in range(10):
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=today - timedelta(days=i),
|
||||
status='completed',
|
||||
duration=10.5
|
||||
@@ -275,7 +275,7 @@ class TestStatsAPI:
|
||||
"""Test scan trend returns dates in correct format."""
|
||||
# Create a scan
|
||||
scan = Scan(
|
||||
config_file='/app/configs/test.yaml',
|
||||
config_id=1,
|
||||
timestamp=datetime.utcnow(),
|
||||
status='completed',
|
||||
duration=10.5
|
||||
|
||||
Reference in New Issue
Block a user