2026-07-09 23:44:30 +02:00
|
|
|
import sys
|
|
|
|
|
import asyncio
|
2026-08-30 14:34:43 +02:00
|
|
|
import io
|
|
|
|
|
import zipfile
|
2026-07-09 23:44:30 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
|
from main import app
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
def _build_ip_nordic_test_xlsx():
|
|
|
|
|
rows = [
|
|
|
|
|
["Company", "Name", "Startdate", "Salgspris", "Kostpris", "InstallationAddress"],
|
|
|
|
|
["99773", "BMC Denmark ApS", "43418", "2495", "1386", "Engholm Parkvej 8, 3450 Allerød"],
|
|
|
|
|
["99773", "BMC Denmark ApS", "43418", "129", "88", "Engholm Parkvej 8, 3450 Allerød "],
|
|
|
|
|
]
|
|
|
|
|
xml_rows = []
|
|
|
|
|
for row_number, values in enumerate(rows, start=1):
|
|
|
|
|
cells = []
|
|
|
|
|
for column_number, value in enumerate(values):
|
|
|
|
|
column = chr(ord('A') + column_number)
|
|
|
|
|
if row_number == 1 or column in {'A', 'B', 'F'}:
|
|
|
|
|
cells.append(f'<c r="{column}{row_number}" t="inlineStr"><is><t>{value}</t></is></c>')
|
|
|
|
|
else:
|
|
|
|
|
cells.append(f'<c r="{column}{row_number}"><v>{value}</v></c>')
|
|
|
|
|
xml_rows.append(f'<row r="{row_number}">{"".join(cells)}</row>')
|
|
|
|
|
sheet = (
|
|
|
|
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
|
|
|
|
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
|
|
|
|
f'<sheetData>{"".join(xml_rows)}</sheetData></worksheet>'
|
|
|
|
|
)
|
|
|
|
|
output = io.BytesIO()
|
|
|
|
|
with zipfile.ZipFile(output, 'w') as archive:
|
|
|
|
|
archive.writestr('xl/worksheets/sheet1.xml', sheet)
|
|
|
|
|
return output.getvalue()
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 23:44:30 +02:00
|
|
|
def test_internet_connections_module_routes_are_available():
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
|
|
|
|
health_response = client.get('/api/v1/internet-connections/health')
|
|
|
|
|
assert health_response.status_code == 200
|
|
|
|
|
assert health_response.json()['service'] == 'internet-connections-module'
|
|
|
|
|
|
|
|
|
|
page_response = client.get('/economy/internet-connections')
|
|
|
|
|
assert page_response.status_code == 200
|
|
|
|
|
assert (
|
|
|
|
|
'Internetforbindelser' in page_response.text
|
|
|
|
|
or "window.location.href = '/login'" in page_response.text
|
|
|
|
|
)
|
|
|
|
|
detail_response = client.get('/economy/internet-connections/1')
|
|
|
|
|
assert detail_response.status_code == 200
|
|
|
|
|
assert (
|
|
|
|
|
'IP-ranges' in detail_response.text
|
|
|
|
|
or "window.location.href = '/login'" in detail_response.text
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
def test_ip_nordic_xlsx_parser_groups_lines_by_company_and_address():
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
items = internet_router._parse_ip_nordic_xlsx(_build_ip_nordic_test_xlsx())
|
|
|
|
|
|
|
|
|
|
assert len(items) == 1
|
|
|
|
|
assert items[0]['line_count'] == 2
|
|
|
|
|
assert float(items[0]['monthly_cost']) == 1474
|
|
|
|
|
assert float(items[0]['sales_price']) == 2624
|
|
|
|
|
assert items[0]['address'] == 'Engholm Parkvej 8, 3450 Allerød'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ip_nordic_import_preview_never_assigns_customer(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query_single', lambda query, params=None: None)
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post(
|
|
|
|
|
'/api/v1/internet-connections/import/ip-nordic',
|
|
|
|
|
files={'file': ('IP_Nordic.xlsx', _build_ip_nordic_test_xlsx(), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')},
|
|
|
|
|
data={'commit': 'false'},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()['create_count'] == 1
|
|
|
|
|
assert response.json()['customer_auto_assignment'] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_internet_connections_page_has_ip_nordic_preview_import():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert 'Importér IP Nordic' in template
|
|
|
|
|
assert 'previewIpNordicImport()' in template
|
|
|
|
|
assert 'commitIpNordicImport()' in template
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 23:44:30 +02:00
|
|
|
def test_create_ip_range_rejects_invalid_cidr(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections/2/ip-ranges', json={
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': 'not-a-cidr',
|
|
|
|
|
'description': 'Test range',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert 'CIDR' in response.json()['detail']
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
def test_document_entity_extraction_canonicalizes_cidr_with_host_bits_and_spaces():
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
entities = internet_router._extract_segment_entities(
|
|
|
|
|
"WAN range 192.0.2.1 / 29 og gateway 192.0.2.2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert entities["cidr_blocks"] == ["192.0.2.0/29"]
|
|
|
|
|
assert entities["ip_addresses"] == ["192.0.2.2"]
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 23:44:30 +02:00
|
|
|
def test_create_connection_requires_address(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: [])
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections', json={
|
|
|
|
|
'name': 'Uden adresse',
|
|
|
|
|
'provider': 'GlobalConnect A/S',
|
|
|
|
|
'status': 'active',
|
|
|
|
|
'allocation_model': 'shared',
|
|
|
|
|
'value_type': 'bmc_networks',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert 'address is required' in response.json()['detail']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_ip_ranges_includes_ipam_details(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'FROM internet_connections_ip_ranges' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 9,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '10.0.0.0/24',
|
|
|
|
|
'description': 'Test range',
|
|
|
|
|
}]
|
|
|
|
|
if 'FROM internet_connections_ip_addresses' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 1,
|
|
|
|
|
'range_id': 9,
|
|
|
|
|
'ip_address': '10.0.0.1',
|
|
|
|
|
'status': 'in_use',
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/2/ip-ranges')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
payload = response.json()[0]
|
|
|
|
|
assert payload['network_address'] == '10.0.0.0'
|
|
|
|
|
assert payload['usable_hosts'] == 254
|
|
|
|
|
assert payload['used_addresses'] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_wizard_v2_query_requires_real_segment_match(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if "FROM internet_connections_customer_documents" in query:
|
|
|
|
|
return [{
|
|
|
|
|
"id": 101,
|
|
|
|
|
"customer_id": 77,
|
|
|
|
|
"connection_id": None,
|
|
|
|
|
"original_filename": "management_net.txt",
|
|
|
|
|
"filename": "management_net.txt",
|
|
|
|
|
"file_size": 1024,
|
|
|
|
|
"mime_type": "text/plain",
|
|
|
|
|
"extracted_text": "1 UNTAGGED Native Management\nVlan 50 Not in Use\nIP Informationer",
|
|
|
|
|
"notes": None,
|
|
|
|
|
"created_at": None,
|
|
|
|
|
}]
|
|
|
|
|
if "FROM internet_connections_customer_document_segments" in query:
|
|
|
|
|
return [{
|
|
|
|
|
"id": 201,
|
|
|
|
|
"document_id": 101,
|
|
|
|
|
"block_index": 0,
|
|
|
|
|
"block_title": "IP Informationer",
|
|
|
|
|
"content": "1 UNTAGGED Native Management\nVlan 50 Not in Use\nIP Informationer",
|
|
|
|
|
"ip_addresses": [],
|
|
|
|
|
"cidr_blocks": [],
|
|
|
|
|
"references_json": [],
|
|
|
|
|
"socket_numbers": [],
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, "execute_query", fake_execute_query)
|
|
|
|
|
monkeypatch.setattr(internet_router, "_ensure_document_segments", lambda document_id, extracted_text: 1)
|
|
|
|
|
|
|
|
|
|
payload = asyncio.run(internet_router._build_customer_document_hits(77, "Karise", "stageone"))
|
|
|
|
|
|
|
|
|
|
assert payload["segments"] == []
|
|
|
|
|
assert payload["documents"] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_wizard_v2_query_keeps_precise_segment_hits(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if "FROM internet_connections_customer_documents" in query:
|
|
|
|
|
return [{
|
|
|
|
|
"id": 102,
|
|
|
|
|
"customer_id": 77,
|
|
|
|
|
"connection_id": None,
|
|
|
|
|
"original_filename": "karise.txt",
|
|
|
|
|
"filename": "karise.txt",
|
|
|
|
|
"file_size": 2048,
|
|
|
|
|
"mime_type": "text/plain",
|
|
|
|
|
"extracted_text": "StageOne uplink til Karise\nPort 1 StageOne WAN U20",
|
|
|
|
|
"notes": None,
|
|
|
|
|
"created_at": None,
|
|
|
|
|
}]
|
|
|
|
|
if "FROM internet_connections_customer_document_segments" in query:
|
|
|
|
|
return [{
|
|
|
|
|
"id": 202,
|
|
|
|
|
"document_id": 102,
|
|
|
|
|
"block_index": 0,
|
|
|
|
|
"block_title": "StageOne uplink til Karise",
|
|
|
|
|
"content": "StageOne uplink til Karise\nPort 1 StageOne WAN U20",
|
|
|
|
|
"ip_addresses": [],
|
|
|
|
|
"cidr_blocks": [],
|
|
|
|
|
"references_json": [],
|
|
|
|
|
"socket_numbers": ["U20"],
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, "execute_query", fake_execute_query)
|
|
|
|
|
monkeypatch.setattr(internet_router, "_ensure_document_segments", lambda document_id, extracted_text: 1)
|
|
|
|
|
|
|
|
|
|
payload = asyncio.run(internet_router._build_customer_document_hits(77, "Karise", "stageone"))
|
|
|
|
|
|
|
|
|
|
assert len(payload["segments"]) == 1
|
|
|
|
|
assert payload["segments"][0]["title"] == "StageOne uplink til Karise"
|
|
|
|
|
assert payload["documents"][0]["snippet_count"] == 1
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 01:58:02 +02:00
|
|
|
def test_migration_wizard_v2_returns_full_text_for_selected_segment(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, "execute_query_single", lambda query, params: {
|
|
|
|
|
"segment_id": 202,
|
|
|
|
|
"document_id": 102,
|
|
|
|
|
"block_index": 3,
|
|
|
|
|
"title": "StageOne uplink",
|
|
|
|
|
"original_filename": "karise.txt",
|
|
|
|
|
"content": "Hele den valgte tekstblok\nmed alle linjer.",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
payload = asyncio.run(internet_router.get_customer_document_segment(202))
|
|
|
|
|
|
|
|
|
|
assert payload["title"] == "StageOne uplink"
|
|
|
|
|
assert payload["content"] == "Hele den valgte tekstblok\nmed alle linjer."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_wizard_block_search_requires_all_words(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if "FROM internet_connections_customer_documents" in query:
|
|
|
|
|
return [{
|
|
|
|
|
"id": 103, "customer_id": 77, "connection_id": None,
|
|
|
|
|
"original_filename": "sales.txt", "filename": "sales.txt",
|
|
|
|
|
"file_size": 1, "mime_type": "text/plain", "notes": None,
|
|
|
|
|
"created_at": None, "extracted_text": "Management network notes",
|
|
|
|
|
}]
|
|
|
|
|
if "FROM internet_connections_customer_document_segments" in query:
|
|
|
|
|
return [{
|
|
|
|
|
"id": 203, "document_id": 103, "block_index": 0,
|
|
|
|
|
"block_title": "Management", "content": "Management network notes",
|
|
|
|
|
"ip_addresses": [], "cidr_blocks": [], "references_json": [], "socket_numbers": [],
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, "execute_query", fake_execute_query)
|
|
|
|
|
monkeypatch.setattr(internet_router, "_ensure_document_segments", lambda document_id, extracted_text: 1)
|
|
|
|
|
|
|
|
|
|
payload = asyncio.run(internet_router._build_customer_document_hits(77, "Karise", "sales management"))
|
|
|
|
|
|
|
|
|
|
assert payload["segments"] == []
|
|
|
|
|
assert payload["documents"] == []
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 23:44:30 +02:00
|
|
|
def test_create_ip_range_auto_generates_addresses_from_cidr(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
created_addresses = []
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'INSERT INTO internet_connections_ip_ranges' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 15,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '192.168.1.0/30',
|
|
|
|
|
'description': 'Auto generated',
|
|
|
|
|
}]
|
|
|
|
|
if 'INSERT INTO internet_connections_ip_addresses' in query:
|
|
|
|
|
created_addresses.append(params[1])
|
|
|
|
|
return [{
|
|
|
|
|
'id': len(created_addresses),
|
|
|
|
|
'range_id': 15,
|
|
|
|
|
'ip_address': params[1],
|
|
|
|
|
'status': 'available',
|
|
|
|
|
}]
|
|
|
|
|
if 'INSERT INTO internet_connections_history' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 20,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'event_type': 'ip_range_created',
|
|
|
|
|
'summary': 'Created IP range',
|
|
|
|
|
'details': {},
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections/2/ip-ranges', json={
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '192.168.1.0/30',
|
|
|
|
|
'description': 'Auto generated',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert len(created_addresses) == 2
|
|
|
|
|
assert created_addresses == ['192.168.1.1', '192.168.1.2']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_ip_range_skips_duplicate_existing_ip_addresses(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
created_addresses = []
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'INSERT INTO internet_connections_ip_ranges' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 15,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '192.168.1.0/30',
|
|
|
|
|
'description': 'Auto generated',
|
|
|
|
|
}]
|
|
|
|
|
if 'FROM internet_connections_ip_addresses' in query and 'WHERE ip_address = %s' in query:
|
|
|
|
|
if params[0] == '192.168.1.1':
|
|
|
|
|
return [{'id': 99, 'ip_address': '192.168.1.1'}]
|
|
|
|
|
return []
|
|
|
|
|
if 'INSERT INTO internet_connections_ip_addresses' in query:
|
|
|
|
|
created_addresses.append(params[1])
|
|
|
|
|
return [{
|
|
|
|
|
'id': len(created_addresses),
|
|
|
|
|
'range_id': 15,
|
|
|
|
|
'ip_address': params[1],
|
|
|
|
|
'status': 'available',
|
|
|
|
|
}]
|
|
|
|
|
if 'INSERT INTO internet_connections_history' in query:
|
|
|
|
|
return [{'id': 20}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections/2/ip-ranges', json={
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '192.168.1.0/30',
|
|
|
|
|
'description': 'Auto generated',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert created_addresses == ['192.168.1.2']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_update_ip_address_status_changes_record(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
updated = []
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'UPDATE internet_connections_ip_addresses' in query:
|
|
|
|
|
updated.append(params)
|
|
|
|
|
return [{'id': 4, 'status': 'reserved'}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.put('/api/v1/internet-connections/2/ip-addresses/4', json={
|
|
|
|
|
'status': 'reserved',
|
|
|
|
|
'assigned_to': 'Test device',
|
|
|
|
|
'comment': 'Reserved for switch',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert updated[0][0] == 'reserved'
|
|
|
|
|
assert updated[0][1] == 'Test device'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_ip_address_rejects_duplicate_ip(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'FROM internet_connections_ip_addresses' in query and 'WHERE ip_address = %s' in query:
|
|
|
|
|
return [{'id': 4, 'range_id': 2}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections/2/ip-addresses', json={
|
|
|
|
|
'range_id': 7,
|
|
|
|
|
'ip_address': '10.0.0.10',
|
|
|
|
|
'status': 'available',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
assert 'findes allerede' in response.json()['detail']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_ip_addresses_returns_structured_payload(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'FROM internet_connections_ip_addresses' in query and 'JOIN internet_connections_ip_ranges' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 7,
|
|
|
|
|
'range_id': 3,
|
|
|
|
|
'ip_address': '10.0.0.10',
|
|
|
|
|
'status': 'in_use',
|
|
|
|
|
'assigned_to': 'Router',
|
|
|
|
|
'assigned_type': 'device',
|
|
|
|
|
'comment': 'Main gateway',
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/2/ip-addresses')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
payload = response.json()[0]
|
|
|
|
|
assert payload['status_label'] == 'I brug'
|
|
|
|
|
assert payload['badge_class'] == 'bg-primary'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_ip_range_writes_history(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
calls = []
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
calls.append((query, params))
|
|
|
|
|
if 'INSERT INTO internet_connections_ip_ranges' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 1,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '10.0.0.0/24',
|
|
|
|
|
'description': 'Test range',
|
|
|
|
|
}]
|
|
|
|
|
if 'INSERT INTO internet_connections_history' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 11,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'event_type': 'ip_range_created',
|
|
|
|
|
'summary': 'Created IP range',
|
|
|
|
|
'details': {},
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections/2/ip-ranges', json={
|
|
|
|
|
'name': 'LAN',
|
|
|
|
|
'cidr': '10.0.0.0/24',
|
|
|
|
|
'description': 'Test range',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert any('INSERT INTO internet_connections_history' in query for query, _ in calls)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ip_address_status_summary_endpoint(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'COUNT(*)' in query and 'internet_connections_ip_addresses' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'available': 2,
|
|
|
|
|
'in_use': 1,
|
|
|
|
|
'reserved': 1,
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/2/ip-addresses/summary')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()['available'] == 2
|
|
|
|
|
assert response.json()['in_use'] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pricing_history_can_be_created(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'INSERT INTO internet_connections_pricing' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 7,
|
|
|
|
|
'connection_id': 2,
|
|
|
|
|
'effective_from': '2026-07-01',
|
|
|
|
|
'purchase_price': 1500,
|
|
|
|
|
'sales_price': 1800,
|
|
|
|
|
'notes': 'Ny aftale',
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections/2/pricing', json={
|
|
|
|
|
'effective_from': '2026-07-01',
|
|
|
|
|
'purchase_price': 1500,
|
|
|
|
|
'sales_price': 1800,
|
|
|
|
|
'notes': 'Ny aftale',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()['sales_price'] == 1800
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_contract_overview_endpoint_reports_status(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'FROM internet_connections_connections' in query and 'contract_end' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 3,
|
|
|
|
|
'name': 'Test connection',
|
|
|
|
|
'provider': 'BMC',
|
|
|
|
|
'contract_start': '2025-01-01',
|
|
|
|
|
'contract_end': '2025-12-31',
|
|
|
|
|
'status': 'active',
|
|
|
|
|
'sales_price': 900,
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/contracts')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()[0]['contract_status'] == 'expired'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_contract_overview_endpoint_supports_status_filter(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'status = %s' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 4,
|
|
|
|
|
'name': 'Filtered contract',
|
|
|
|
|
'provider': 'Nordic',
|
|
|
|
|
'contract_start': '2026-01-01',
|
|
|
|
|
'contract_end': '2026-12-31',
|
|
|
|
|
'status': 'active',
|
|
|
|
|
'sales_price': 1200,
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/contracts', params={'status': 'active'})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()[0]['status'] == 'active'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_contract_overview_page_contains_table_controls():
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/economy/internet-connections')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert (
|
|
|
|
|
'connectionsTableBody' in response.text
|
|
|
|
|
or "window.location.href = '/login'" in response.text
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
'pageSummaryText' in response.text
|
|
|
|
|
or "window.location.href = '/login'" in response.text
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_contract_overview_returns_empty_list_when_query_fails(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fail_execute_query(query, params=None):
|
|
|
|
|
raise RuntimeError('database unavailable')
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fail_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/contracts')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == []
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
def test_list_connections_returns_visible_error_when_query_fails(monkeypatch):
|
2026-07-09 23:44:30 +02:00
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fail_execute_query(query, params=None):
|
|
|
|
|
raise RuntimeError('database unavailable')
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fail_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections')
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
assert response.status_code == 500
|
|
|
|
|
assert response.json()['detail'] == 'Kunne ikke hente internetforbindelser'
|
2026-07-09 23:44:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pricing_summary_returns_zeroes_when_query_fails(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fail_execute_query(query, params=None):
|
|
|
|
|
raise RuntimeError('database unavailable')
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fail_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/pricing/summary')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == {
|
|
|
|
|
'total_connections': 0,
|
|
|
|
|
'active_connections': 0,
|
|
|
|
|
'shared_head_connections': 0,
|
|
|
|
|
'total_purchase_cost': 0,
|
|
|
|
|
'total_sales_price': 0,
|
|
|
|
|
'total_margin': 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_connection_requires_subscription_id_for_subscription_value(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: [])
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections', json={
|
|
|
|
|
'name': 'Shared transit',
|
|
|
|
|
'address': 'Testvej 1, 8000 Aarhus C',
|
|
|
|
|
'allocation_model': 'shared',
|
|
|
|
|
'value_type': 'subscription',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert 'subscription_id' in response.json()['detail']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_connection_requires_value_label_for_other(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: [])
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.post('/api/v1/internet-connections', json={
|
|
|
|
|
'name': 'Carrier edge',
|
|
|
|
|
'address': 'Testvej 1, 8000 Aarhus C',
|
|
|
|
|
'allocation_model': 'dedicated',
|
|
|
|
|
'value_type': 'other',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert 'value_label' in response.json()['detail']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_connections_supports_shared_only_filter(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
assert "ic.allocation_model = 'shared' AND ic.parent_id IS NULL" in query
|
|
|
|
|
return [{
|
|
|
|
|
'id': 10,
|
|
|
|
|
'parent_id': None,
|
|
|
|
|
'name': 'BMC Networks · NKA008225',
|
|
|
|
|
'provider': 'GlobalConnect',
|
|
|
|
|
'customer_id': 1662,
|
|
|
|
|
'customer_name': 'BMC Networks',
|
|
|
|
|
'parent_name': None,
|
|
|
|
|
'address': None,
|
|
|
|
|
'status': 'active',
|
|
|
|
|
'monthly_cost': 256,
|
|
|
|
|
'sales_price': 0,
|
|
|
|
|
'margin_amount': -256,
|
|
|
|
|
'technology': 'Fiber',
|
|
|
|
|
'connection_type': 'fiber',
|
|
|
|
|
'circuit_number': 'NKA008225',
|
|
|
|
|
'speed_mbps': 100,
|
|
|
|
|
'upload_mbps': 100,
|
|
|
|
|
'download_mbps': 100,
|
|
|
|
|
'monitoring_url': None,
|
|
|
|
|
'contract_start': None,
|
|
|
|
|
'contract_end': None,
|
|
|
|
|
'allocation_model': 'shared',
|
|
|
|
|
'value_type': 'bmc_networks',
|
|
|
|
|
'value_label': None,
|
|
|
|
|
'subscription_id': None,
|
|
|
|
|
'subscription_number': None,
|
|
|
|
|
'subscription_product_name': None,
|
|
|
|
|
'subscription_customer_name': None,
|
|
|
|
|
'ip_range_count': 3,
|
|
|
|
|
'total_ip_addresses': 62,
|
|
|
|
|
'in_use_ip_addresses': 0,
|
|
|
|
|
'reserved_ip_addresses': 0,
|
|
|
|
|
'available_ip_addresses': 62,
|
|
|
|
|
}]
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections', params={'shared_only': 'true'})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
payload = response.json()[0]
|
|
|
|
|
assert payload['allocation_model_label'] == 'Delt'
|
|
|
|
|
assert payload['value_type_label'] == 'BMC Networks'
|
|
|
|
|
assert payload['is_shared_head'] is True
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
def test_first_bmcnet_child_promotes_head_and_preserves_customer(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
single_results = iter([
|
|
|
|
|
{'id': 12, 'allocation_model': 'dedicated', 'value_type': 'other', 'value_label': 'Internetforbindelse'},
|
|
|
|
|
{'child_count': 1},
|
|
|
|
|
])
|
|
|
|
|
writes = []
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query_single', lambda query, params=None: next(single_results))
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: writes.append((query, params)) or [])
|
|
|
|
|
|
|
|
|
|
assert internet_router._sync_bmcnet_parent_classification(12) is True
|
|
|
|
|
classification_query, params = writes[0]
|
|
|
|
|
assert 'customer_id' not in classification_query
|
|
|
|
|
assert params == ('shared', 'delefiber', None, 12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_last_bmcnet_child_removal_returns_head_to_dedicated(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
single_results = iter([
|
|
|
|
|
{'id': 12, 'allocation_model': 'shared', 'value_type': 'delefiber', 'value_label': None},
|
|
|
|
|
{'child_count': 0},
|
|
|
|
|
])
|
|
|
|
|
writes = []
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query_single', lambda query, params=None: next(single_results))
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: writes.append((query, params)) or [])
|
|
|
|
|
|
|
|
|
|
assert internet_router._sync_bmcnet_parent_classification(12) is True
|
|
|
|
|
assert writes[0][1] == ('dedicated', 'other', 'Internetforbindelse', 12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manually_marked_delefiber_stays_shared_without_children(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
single_results = iter([
|
|
|
|
|
{'id': 12, 'allocation_model': 'shared', 'value_type': 'delefiber', 'value_label': None, 'is_manual_shared': True},
|
|
|
|
|
{'child_count': 0},
|
|
|
|
|
])
|
|
|
|
|
writes = []
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query_single', lambda query, params=None: next(single_results))
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: writes.append((query, params)) or [])
|
|
|
|
|
|
|
|
|
|
assert internet_router._sync_bmcnet_parent_classification(12) is False
|
|
|
|
|
assert writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bmcnet_wizard_is_available_on_dedicated_root_connection():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/detail.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert "const canCreateBmcnet = Boolean(connection && !connection.parent_id);" in template
|
|
|
|
|
assert "filter((item) => !item?.parent_id)" in template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_connections_supports_unallocated_filter(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
assert "ic.customer_id IS NULL" in query
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
response = TestClient(app).get('/api/v1/internet-connections', params={'unallocated_only': 'true'})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_internet_connection_tabs_include_dedicated_and_unallocated():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert "setActiveTab('dedicated')" in template
|
|
|
|
|
assert "setActiveTab('unallocated')" in template
|
|
|
|
|
assert "params.set('allocation_model', 'dedicated')" in template
|
|
|
|
|
assert "params.set('allocated_only', 'true')" in template
|
|
|
|
|
assert "params.set('unallocated_only', 'true')" in template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_processed_internet_invoices_have_their_own_tab():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert "setActiveTab('invoices')" in template
|
|
|
|
|
assert 'id="invoiceProcessingOverview"' in template
|
|
|
|
|
assert "document.getElementById('connectionsOverview').classList.toggle('d-none', invoiceMode)" in template
|
|
|
|
|
assert "document.getElementById('invoiceProcessingOverview').classList.toggle('d-none', !invoiceMode)" in template
|
|
|
|
|
assert "if (activeTab === 'invoices') return loadInvoiceSyncRuns();" in template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_connections_show_and_allocate_sla_subscriptions():
|
|
|
|
|
index_template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
detail_template = Path('app/modules/internet_connections/templates/detail.html').read_text()
|
|
|
|
|
migration = Path('migrations/1025_internet_connections_sla_subscription.sql').read_text()
|
|
|
|
|
|
|
|
|
|
assert 'sla_subscription_id' in migration
|
|
|
|
|
assert 'Ingen SLA-aftale' in index_template
|
|
|
|
|
assert 'id="slaSubscriptionSelect"' in detail_template
|
|
|
|
|
assert 'Prisen skal kontrolleres' in detail_template
|
|
|
|
|
assert "JSON.stringify({ sla_subscription_id: value })" in detail_template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manual_bmc_shared_fiber_does_not_suggest_customer_allocation():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/detail.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert 'const isBmcSharedFiber = Boolean(' in template
|
|
|
|
|
assert 'connection.is_manual_shared' in template
|
|
|
|
|
assert 'const shouldSuggestCustomer = !connection.customer_id && !isBmcSharedFiber;' in template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_invoice_review_reconciles_ranges_that_are_already_allocated():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
assert "invoice-sync-runs/reconcile', { method: 'POST' }" in template
|
|
|
|
|
assert hasattr(internet_router, 'reconcile_internet_invoice_reviews')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_invoice_reconciliation_is_explicit_and_shared_fiber_has_no_sla_warning():
|
|
|
|
|
index_template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
detail_template = Path('app/modules/internet_connections/templates/detail.html').read_text()
|
|
|
|
|
|
|
|
|
|
load_body = index_template.split('async function loadInvoiceSyncRuns()', 1)[1].split('async function reconcileInvoiceSyncRuns', 1)[0]
|
|
|
|
|
assert "await fetch('/api/v1/internet-connections/invoice-sync-runs/reconcile'" not in load_body
|
|
|
|
|
assert 'onclick="reconcileInvoiceSyncRuns()"' in index_template
|
|
|
|
|
assert "if (isBmcSharedFiber)" in detail_template
|
|
|
|
|
assert "banner.className = 'd-none';" in detail_template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unallocated_tab_has_compact_customer_suggestion_workflow(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
query_results = iter([[], []])
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: next(query_results))
|
|
|
|
|
response = TestClient(app).get('/api/v1/internet-connections/allocation-overview')
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/index.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == {'items': []}
|
|
|
|
|
assert 'assignSuggestedCustomer' in template
|
|
|
|
|
assert 'unique_suggestion' in template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_connections_supports_allocated_filter(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
assert "ic.customer_id IS NOT NULL" in query
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
response = TestClient(app).get('/api/v1/internet-connections', params={
|
|
|
|
|
'allocation_model': 'dedicated', 'allocated_only': 'true',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_allocation_suggestions_return_all_customers_on_exact_address(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query_single', lambda query, params=None: {
|
|
|
|
|
'id': 155, 'address': 'Testvej 1, 8000 Aarhus C', 'customer_id': None,
|
|
|
|
|
})
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: [
|
|
|
|
|
{'customer_id': 10, 'customer_name': 'Firma A', 'candidate_address': 'Testvej 1, 8000 Aarhus C', 'address_source': 'customer', 'location_name': None},
|
|
|
|
|
{'customer_id': 11, 'customer_name': 'Firma B', 'candidate_address': 'Testvej 1, 8000 Aarhus C', 'address_source': 'location', 'location_name': 'Kontor'},
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
response = TestClient(app).get('/api/v1/internet-connections/155/allocation-suggestions')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert [item['customer_id'] for item in response.json()['items']] == [10, 11]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_allocation_suggestions_match_boulevard_abbreviation_and_house_range(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query_single', lambda query, params=None: {
|
|
|
|
|
'id': 155, 'address': 'Arnold Nielsens Boulevard 81, 2650 Hvidovre', 'customer_id': None,
|
|
|
|
|
})
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', lambda query, params=None: [{
|
|
|
|
|
'customer_id': 214, 'customer_name': 'Glarmester Svensson ApS',
|
|
|
|
|
'candidate_address': 'Arnold Nielsens Blv. 81 - 83, 2650 Hvidovre',
|
|
|
|
|
'address_source': 'customer', 'location_name': None,
|
|
|
|
|
}])
|
|
|
|
|
|
|
|
|
|
response = TestClient(app).get('/api/v1/internet-connections/155/allocation-suggestions')
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()['items'][0]['customer_id'] == 214
|
|
|
|
|
assert response.json()['items'][0]['match_score'] == 90
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_connection_detail_has_unallocated_customer_banner_and_subscription_linking():
|
|
|
|
|
template = Path('app/modules/internet_connections/templates/detail.html').read_text()
|
|
|
|
|
|
|
|
|
|
assert 'Forbindelsen er ikke tildelt en kunde' in template
|
|
|
|
|
assert '/allocation-suggestions' in template
|
|
|
|
|
assert 'customer_id=${customerId}' in template
|
|
|
|
|
assert 'saveConnectionAllocation()' in template
|
|
|
|
|
assert 'BMC Delefiber' in template
|
|
|
|
|
assert 'fieldManualShared' in template
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 23:44:30 +02:00
|
|
|
def test_subscription_options_endpoint_returns_lookup_rows(monkeypatch):
|
|
|
|
|
from app.modules.internet_connections.backend import router as internet_router
|
|
|
|
|
|
|
|
|
|
def fake_execute_query(query, params=None):
|
|
|
|
|
if 'FROM sag_subscriptions s' in query:
|
|
|
|
|
return [{
|
|
|
|
|
'id': 9,
|
|
|
|
|
'subscription_number': 'SUB-1001',
|
|
|
|
|
'product_name': 'Internet 1G',
|
|
|
|
|
'customer_id': 1662,
|
|
|
|
|
'customer_name': 'BMC Networks',
|
|
|
|
|
'status': 'active',
|
|
|
|
|
}]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(internet_router, 'execute_query', fake_execute_query)
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
response = client.get('/api/v1/internet-connections/subscription-options', params={'q': '1G'})
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.json()[0]['subscription_number'] == 'SUB-1001'
|
2026-08-30 14:34:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_connection_detail_has_polished_network_header_and_ip_overview():
|
|
|
|
|
template = Path("app/modules/internet_connections/templates/detail.html").read_text()
|
|
|
|
|
|
|
|
|
|
assert 'id="detailCircuitBadge"' in template
|
|
|
|
|
assert 'class="detail-section-nav"' in template
|
|
|
|
|
assert 'class="detail-metrics-grid mb-4"' in template
|
|
|
|
|
assert 'id="connection-ip"' in template
|
|
|
|
|
assert "(summary.available || 0) + (summary.in_use || 0) + (summary.reserved || 0)" in template
|
|
|
|
|
assert "detail-grid-card ip-range-card" in template
|
|
|
|
|
assert "<span class=\"label\">Binding</span>" not in template
|
|
|
|
|
assert "Netværksmodel" in template
|