|
| 1 | +"""Test notification system deployment and functionality.""" |
| 2 | +import json |
| 3 | +import os |
| 4 | +import psycopg2 |
| 5 | +import psycopg2.extensions |
| 6 | +import requests |
| 7 | +import subprocess |
| 8 | +import time |
| 9 | +import pytest |
| 10 | +from datetime import datetime |
| 11 | + |
| 12 | + |
| 13 | +def test_eoapi_notifier_deployment(): |
| 14 | + """Test that eoapi-notifier deployment is running.""" |
| 15 | + # Check if eoapi-notifier deployment exists and is ready |
| 16 | + result = subprocess.run([ |
| 17 | + 'kubectl', 'get', 'deployment', |
| 18 | + '-l', 'app.kubernetes.io/name=eoapi-notifier', |
| 19 | + '--no-headers', '-o', 'custom-columns=READY:.status.readyReplicas' |
| 20 | + ], capture_output=True, text=True) |
| 21 | + |
| 22 | + if result.returncode != 0: |
| 23 | + pytest.skip("eoapi-notifier deployment not found - notifications not enabled") |
| 24 | + |
| 25 | + ready_replicas = result.stdout.strip() |
| 26 | + assert ready_replicas == "1", f"Expected 1 ready replica, got {ready_replicas}" |
| 27 | + |
| 28 | + |
| 29 | +def test_cloudevents_sink_exists(): |
| 30 | + """Test that Knative CloudEvents sink service exists and is accessible.""" |
| 31 | + # Check if Knative service exists |
| 32 | + result = subprocess.run([ |
| 33 | + 'kubectl', 'get', 'ksvc', |
| 34 | + '-l', 'app.kubernetes.io/component=cloudevents-sink', |
| 35 | + '--no-headers' |
| 36 | + ], capture_output=True, text=True) |
| 37 | + |
| 38 | + if result.returncode != 0 or not result.stdout.strip(): |
| 39 | + pytest.skip("Knative CloudEvents sink not found - notifications not configured") |
| 40 | + |
| 41 | + assert "cloudevents-sink" in result.stdout, "Knative CloudEvents sink should exist" |
| 42 | + |
| 43 | + |
| 44 | +def test_notification_configuration(): |
| 45 | + """Test that eoapi-notifier is configured correctly.""" |
| 46 | + # Get the configmap for eoapi-notifier |
| 47 | + result = subprocess.run([ |
| 48 | + 'kubectl', 'get', 'configmap', |
| 49 | + '-l', 'app.kubernetes.io/name=eoapi-notifier', |
| 50 | + '-o', r'jsonpath={.items[0].data.config\.yaml}' |
| 51 | + ], capture_output=True, text=True) |
| 52 | + |
| 53 | + if result.returncode != 0: |
| 54 | + pytest.skip("eoapi-notifier configmap not found") |
| 55 | + |
| 56 | + config_yaml = result.stdout.strip() |
| 57 | + assert "postgres" in config_yaml, "Should have postgres source configured" |
| 58 | + assert "cloudevents" in config_yaml, "Should have cloudevents output configured" |
| 59 | + assert "pgstac_items_change" in config_yaml, "Should listen to pgstac_items_change channel" |
| 60 | + |
| 61 | + |
| 62 | +def test_cloudevents_sink_logs_show_startup(): |
| 63 | + """Test that Knative CloudEvents sink started successfully.""" |
| 64 | + # Get Knative CloudEvents sink pod logs |
| 65 | + result = subprocess.run([ |
| 66 | + 'kubectl', 'logs', |
| 67 | + '-l', 'serving.knative.dev/service', |
| 68 | + '--tail=20' |
| 69 | + ], capture_output=True, text=True) |
| 70 | + |
| 71 | + if result.returncode != 0: |
| 72 | + pytest.skip("Cannot get Knative CloudEvents sink logs") |
| 73 | + |
| 74 | + logs = result.stdout |
| 75 | + assert "CloudEvents sink started on port" in logs, "Knative CloudEvents sink should have started successfully" |
| 76 | + |
| 77 | + |
| 78 | +def test_eoapi_notifier_logs_show_connection(): |
| 79 | + """Test that eoapi-notifier connects to database successfully.""" |
| 80 | + # Give some time for the notifier to start |
| 81 | + time.sleep(5) |
| 82 | + |
| 83 | + # Get eoapi-notifier pod logs |
| 84 | + result = subprocess.run([ |
| 85 | + 'kubectl', 'logs', |
| 86 | + '-l', 'app.kubernetes.io/name=eoapi-notifier', |
| 87 | + '--tail=50' |
| 88 | + ], capture_output=True, text=True) |
| 89 | + |
| 90 | + if result.returncode != 0: |
| 91 | + pytest.skip("Cannot get eoapi-notifier logs") |
| 92 | + |
| 93 | + logs = result.stdout |
| 94 | + # Should not have connection errors |
| 95 | + assert "Connection refused" not in logs, "Should not have connection errors" |
| 96 | + assert "Authentication failed" not in logs, "Should not have auth errors" |
| 97 | + |
| 98 | + |
| 99 | +def test_database_notification_triggers_exist(): |
| 100 | + """Test that pgstac notification triggers are installed.""" |
| 101 | + # Port forward to database if not already done |
| 102 | + import psycopg2 |
| 103 | + import os |
| 104 | + |
| 105 | + try: |
| 106 | + # Try to connect using environment variables set by the test runner |
| 107 | + conn = psycopg2.connect( |
| 108 | + host=os.getenv('PGHOST', 'localhost'), |
| 109 | + port=int(os.getenv('PGPORT', '5432')), |
| 110 | + database=os.getenv('PGDATABASE', 'eoapi'), |
| 111 | + user=os.getenv('PGUSER', 'eoapi'), |
| 112 | + password=os.getenv('PGPASSWORD', '') |
| 113 | + ) |
| 114 | + |
| 115 | + with conn.cursor() as cur: |
| 116 | + # Check if the notification function exists |
| 117 | + cur.execute(""" |
| 118 | + SELECT EXISTS( |
| 119 | + SELECT 1 FROM pg_proc p |
| 120 | + JOIN pg_namespace n ON p.pronamespace = n.oid |
| 121 | + WHERE n.nspname = 'public' |
| 122 | + AND p.proname = 'notify_items_change_func' |
| 123 | + ); |
| 124 | + """) |
| 125 | + function_exists = cur.fetchone()[0] |
| 126 | + assert function_exists, "notify_items_change_func should exist" |
| 127 | + |
| 128 | + # Check if triggers exist |
| 129 | + cur.execute(""" |
| 130 | + SELECT COUNT(*) FROM information_schema.triggers |
| 131 | + WHERE trigger_name LIKE 'notify_items_change_%' |
| 132 | + AND event_object_table = 'items' |
| 133 | + AND event_object_schema = 'pgstac'; |
| 134 | + """) |
| 135 | + trigger_count = cur.fetchone()[0] |
| 136 | + assert trigger_count >= 3, f"Should have at least 3 triggers (INSERT, UPDATE, DELETE), found {trigger_count}" |
| 137 | + |
| 138 | + conn.close() |
| 139 | + |
| 140 | + except (psycopg2.Error, ConnectionError, OSError): |
| 141 | + pytest.skip("Cannot connect to database for trigger verification") |
| 142 | + |
| 143 | + |
| 144 | +def test_end_to_end_notification_flow(): |
| 145 | + """Test complete flow: database → eoapi-notifier → Knative CloudEvents sink.""" |
| 146 | + |
| 147 | + # Skip if notifications not enabled |
| 148 | + if not subprocess.run(['kubectl', 'get', 'deployment', '-l', 'app.kubernetes.io/name=eoapi-notifier', '--no-headers'], capture_output=True).stdout.strip(): |
| 149 | + pytest.skip("eoapi-notifier not deployed") |
| 150 | + |
| 151 | + # Find Knative CloudEvents sink pod |
| 152 | + result = subprocess.run(['kubectl', 'get', 'pods', '-l', 'serving.knative.dev/service', '-o', 'jsonpath={.items[0].metadata.name}'], capture_output=True, text=True) |
| 153 | + |
| 154 | + if result.returncode != 0 or not result.stdout.strip(): |
| 155 | + pytest.skip("Knative CloudEvents sink pod not found") |
| 156 | + |
| 157 | + sink_pod = result.stdout.strip() |
| 158 | + |
| 159 | + # Connect to database using test environment |
| 160 | + try: |
| 161 | + conn = psycopg2.connect( |
| 162 | + host=os.getenv('PGHOST', 'localhost'), |
| 163 | + port=int(os.getenv('PGPORT', '5432')), |
| 164 | + database=os.getenv('PGDATABASE', 'eoapi'), |
| 165 | + user=os.getenv('PGUSER', 'eoapi'), |
| 166 | + password=os.getenv('PGPASSWORD', '') |
| 167 | + ) |
| 168 | + conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) |
| 169 | + except psycopg2.Error as e: |
| 170 | + pytest.skip(f"Cannot connect to database: {e}") |
| 171 | + |
| 172 | + # Insert test item and check for CloudEvent |
| 173 | + test_item_id = f"e2e-test-{int(time.time())}" |
| 174 | + try: |
| 175 | + with conn.cursor() as cursor: |
| 176 | + cursor.execute("SELECT pgstac.create_item(%s);", (json.dumps({ |
| 177 | + "id": test_item_id, |
| 178 | + "type": "Feature", |
| 179 | + "stac_version": "1.0.0", |
| 180 | + "collection": "noaa-emergency-response", |
| 181 | + "geometry": {"type": "Point", "coordinates": [0, 0]}, |
| 182 | + "bbox": [0, 0, 0, 0], |
| 183 | + "properties": {"datetime": "2020-01-01T00:00:00Z"}, |
| 184 | + "assets": {} |
| 185 | + }),)) |
| 186 | + |
| 187 | + # Check CloudEvents sink logs for CloudEvent |
| 188 | + found_event = False |
| 189 | + for _ in range(20): # 20 second timeout |
| 190 | + time.sleep(1) |
| 191 | + result = subprocess.run(['kubectl', 'logs', sink_pod, '--since=30s'], capture_output=True, text=True) |
| 192 | + if result.returncode == 0 and "CloudEvent received" in result.stdout and test_item_id in result.stdout: |
| 193 | + found_event = True |
| 194 | + break |
| 195 | + |
| 196 | + assert found_event, f"CloudEvent for {test_item_id} not received by CloudEvents sink" |
| 197 | + |
| 198 | + finally: |
| 199 | + # Cleanup |
| 200 | + with conn.cursor() as cursor: |
| 201 | + cursor.execute("SELECT pgstac.delete_item(%s);", (test_item_id,)) |
| 202 | + conn.close() |
| 203 | + |
| 204 | + |
| 205 | +def test_k_sink_injection(): |
| 206 | + """Test that SinkBinding injects K_SINK into eoapi-notifier deployment.""" |
| 207 | + # Check if eoapi-notifier deployment exists |
| 208 | + result = subprocess.run([ |
| 209 | + 'kubectl', 'get', 'deployment', |
| 210 | + '-l', 'app.kubernetes.io/name=eoapi-notifier', |
| 211 | + '-o', 'jsonpath={.items[0].spec.template.spec.containers[0].env[?(@.name=="K_SINK")].value}' |
| 212 | + ], capture_output=True, text=True) |
| 213 | + |
| 214 | + if result.returncode != 0: |
| 215 | + pytest.skip("eoapi-notifier deployment not found") |
| 216 | + |
| 217 | + k_sink_value = result.stdout.strip() |
| 218 | + if k_sink_value: |
| 219 | + assert "cloudevents-sink" in k_sink_value, f"K_SINK should point to CloudEvents sink service, got: {k_sink_value}" |
| 220 | + print(f"✅ K_SINK properly injected: {k_sink_value}") |
| 221 | + else: |
| 222 | + # Check if SinkBinding exists - it may take time to inject |
| 223 | + sinkbinding_result = subprocess.run([ |
| 224 | + 'kubectl', 'get', 'sinkbinding', |
| 225 | + '-l', 'app.kubernetes.io/component=sink-binding', |
| 226 | + '--no-headers' |
| 227 | + ], capture_output=True, text=True) |
| 228 | + |
| 229 | + if sinkbinding_result.returncode == 0 and sinkbinding_result.stdout.strip(): |
| 230 | + pytest.skip("SinkBinding exists but K_SINK not yet injected - may need more time") |
| 231 | + else: |
| 232 | + pytest.fail("No K_SINK found and no SinkBinding exists") |
| 233 | + |
| 234 | + |
| 235 | +if __name__ == "__main__": |
| 236 | + pytest.main([__file__, "-v"]) |
0 commit comments