mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 07:10:00 -07:00
Parser expected pipe-delimited table rows but grgsm_scanner outputs comma-separated key-value pairs like: ARFCN: 975, Freq: 925.2M, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58 This was the root cause of no data appearing in GSM Spy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
304 lines
11 KiB
Python
304 lines
11 KiB
Python
"""Unit tests for GSM Spy parsing and validation functions."""
|
|
|
|
import pytest
|
|
from routes.gsm_spy import (
|
|
parse_grgsm_scanner_output,
|
|
parse_tshark_output,
|
|
arfcn_to_frequency,
|
|
validate_band_names,
|
|
REGIONAL_BANDS
|
|
)
|
|
|
|
|
|
class TestParseGrgsmScannerOutput:
|
|
"""Tests for parse_grgsm_scanner_output()."""
|
|
|
|
def test_valid_output_line(self):
|
|
"""Test parsing a valid grgsm_scanner output line."""
|
|
line = "ARFCN: 23, Freq: 940.6M, CID: 31245, LAC: 1234, MCC: 214, MNC: 01, Pwr: -48"
|
|
result = parse_grgsm_scanner_output(line)
|
|
|
|
assert result is not None
|
|
assert result['type'] == 'tower'
|
|
assert result['arfcn'] == 23
|
|
assert result['frequency'] == 940.6
|
|
assert result['cid'] == 31245
|
|
assert result['lac'] == 1234
|
|
assert result['mcc'] == 214
|
|
assert result['mnc'] == 1
|
|
assert result['signal_strength'] == -48.0
|
|
assert 'timestamp' in result
|
|
|
|
def test_freq_without_suffix(self):
|
|
"""Test parsing frequency without M suffix."""
|
|
line = "ARFCN: 975, Freq: 925.2, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58"
|
|
result = parse_grgsm_scanner_output(line)
|
|
assert result is not None
|
|
assert result['frequency'] == 925.2
|
|
|
|
def test_config_line(self):
|
|
"""Test that configuration lines are skipped."""
|
|
line = " Configuration: 1 CCCH, not combined"
|
|
result = parse_grgsm_scanner_output(line)
|
|
assert result is None
|
|
|
|
def test_neighbour_line(self):
|
|
"""Test that neighbour cell lines are skipped."""
|
|
line = " Neighbour Cells: 57, 61, 70, 71, 72, 86"
|
|
result = parse_grgsm_scanner_output(line)
|
|
assert result is None
|
|
|
|
def test_cell_arfcn_line(self):
|
|
"""Test that cell ARFCN lines are skipped."""
|
|
line = " Cell ARFCNs: 63, 76"
|
|
result = parse_grgsm_scanner_output(line)
|
|
assert result is None
|
|
|
|
def test_progress_line(self):
|
|
"""Test that progress/status lines are skipped."""
|
|
line = "Scanning GSM900 band..."
|
|
result = parse_grgsm_scanner_output(line)
|
|
assert result is None
|
|
|
|
def test_empty_line(self):
|
|
"""Test handling of empty lines."""
|
|
result = parse_grgsm_scanner_output("")
|
|
assert result is None
|
|
|
|
def test_invalid_data(self):
|
|
"""Test handling of non-numeric values."""
|
|
line = "ARFCN: abc, Freq: xyz, CID: bad, LAC: data, MCC: bad, MNC: bad, Pwr: bad"
|
|
result = parse_grgsm_scanner_output(line)
|
|
assert result is None
|
|
|
|
|
|
class TestParseTsharkOutput:
|
|
"""Tests for parse_tshark_output()."""
|
|
|
|
def test_valid_full_output(self):
|
|
"""Test parsing tshark output with all fields."""
|
|
line = "5\t0xABCD1234\t123456789012345\t1234\t31245"
|
|
result = parse_tshark_output(line)
|
|
|
|
assert result is not None
|
|
assert result['type'] == 'device'
|
|
assert result['ta_value'] == 5
|
|
assert result['tmsi'] == '0xABCD1234'
|
|
assert result['imsi'] == '123456789012345'
|
|
assert result['lac'] == 1234
|
|
assert result['cid'] == 31245
|
|
assert result['distance_meters'] == 5 * 554 # TA * 554 meters
|
|
assert 'timestamp' in result
|
|
|
|
def test_missing_optional_fields(self):
|
|
"""Test parsing with missing optional fields (empty tabs)."""
|
|
line = "3\t\t\t1234\t31245"
|
|
result = parse_tshark_output(line)
|
|
|
|
assert result is not None
|
|
assert result['ta_value'] == 3
|
|
assert result['tmsi'] is None
|
|
assert result['imsi'] is None
|
|
assert result['lac'] == 1234
|
|
assert result['cid'] == 31245
|
|
|
|
def test_no_ta_value(self):
|
|
"""Test parsing without TA value (empty field)."""
|
|
# When TA is empty, int('') will fail, so the parse returns None
|
|
# This is the current behavior - the function expects valid integers or valid empty handling
|
|
line = "\t0xABCD1234\t123456789012345\t1234\t31245"
|
|
result = parse_tshark_output(line)
|
|
# Current implementation will fail to parse this due to int('') failing
|
|
assert result is None
|
|
|
|
def test_invalid_line(self):
|
|
"""Test handling of invalid tshark output."""
|
|
line = "invalid data"
|
|
result = parse_tshark_output(line)
|
|
assert result is None
|
|
|
|
def test_empty_line(self):
|
|
"""Test handling of empty lines."""
|
|
result = parse_tshark_output("")
|
|
assert result is None
|
|
|
|
def test_partial_fields(self):
|
|
"""Test with fewer than 5 fields."""
|
|
line = "5\t0xABCD1234" # Only 2 fields
|
|
result = parse_tshark_output(line)
|
|
assert result is None
|
|
|
|
|
|
class TestArfcnToFrequency:
|
|
"""Tests for arfcn_to_frequency()."""
|
|
|
|
def test_gsm850_arfcn(self):
|
|
"""Test ARFCN in GSM850 band."""
|
|
# GSM850: ARFCN 128-251, 869-894 MHz
|
|
arfcn = 128
|
|
freq = arfcn_to_frequency(arfcn)
|
|
assert freq == 869000000 # 869 MHz
|
|
|
|
arfcn = 251
|
|
freq = arfcn_to_frequency(arfcn)
|
|
assert freq == 893600000 # 893.6 MHz
|
|
|
|
def test_egsm900_arfcn(self):
|
|
"""Test ARFCN in EGSM900 band."""
|
|
# EGSM900: ARFCN 0-124, 925-960 MHz
|
|
arfcn = 0
|
|
freq = arfcn_to_frequency(arfcn)
|
|
assert freq == 925000000 # 925 MHz
|
|
|
|
arfcn = 124
|
|
freq = arfcn_to_frequency(arfcn)
|
|
assert freq == 949800000 # 949.8 MHz
|
|
|
|
def test_dcs1800_arfcn(self):
|
|
"""Test ARFCN in DCS1800 band."""
|
|
# DCS1800: ARFCN 512-885, 1805-1880 MHz
|
|
# Note: ARFCN 512 also exists in PCS1900 and will match that first
|
|
# Use ARFCN 811+ which is only in DCS1800
|
|
arfcn = 811 # Beyond PCS1900 range (512-810)
|
|
freq = arfcn_to_frequency(arfcn)
|
|
# 811 is ARFCN offset from 512, so freq = 1805MHz + (811-512)*200kHz
|
|
expected = 1805000000 + (811 - 512) * 200000
|
|
assert freq == expected
|
|
|
|
arfcn = 885
|
|
freq = arfcn_to_frequency(arfcn)
|
|
assert freq == 1879600000 # 1879.6 MHz
|
|
|
|
def test_pcs1900_arfcn(self):
|
|
"""Test ARFCN in PCS1900 band."""
|
|
# PCS1900: ARFCN 512-810, 1930-1990 MHz
|
|
# Note: overlaps with DCS1800 ARFCN range, but different frequencies
|
|
arfcn = 512
|
|
freq = arfcn_to_frequency(arfcn)
|
|
# Will match first band (DCS1800 in Europe config)
|
|
assert freq > 0
|
|
|
|
def test_invalid_arfcn(self):
|
|
"""Test ARFCN outside known ranges."""
|
|
with pytest.raises(ValueError, match="not found in any known GSM band"):
|
|
arfcn_to_frequency(9999)
|
|
|
|
with pytest.raises(ValueError):
|
|
arfcn_to_frequency(-1)
|
|
|
|
def test_arfcn_200khz_spacing(self):
|
|
"""Test that ARFCNs are 200kHz apart."""
|
|
arfcn1 = 128
|
|
arfcn2 = 129
|
|
freq1 = arfcn_to_frequency(arfcn1)
|
|
freq2 = arfcn_to_frequency(arfcn2)
|
|
assert freq2 - freq1 == 200000 # 200 kHz
|
|
|
|
|
|
class TestValidateBandNames:
|
|
"""Tests for validate_band_names()."""
|
|
|
|
def test_valid_americas_bands(self):
|
|
"""Test valid band names for Americas region."""
|
|
bands = ['GSM850', 'PCS1900']
|
|
result, error = validate_band_names(bands, 'Americas')
|
|
assert result == bands
|
|
assert error is None
|
|
|
|
def test_valid_europe_bands(self):
|
|
"""Test valid band names for Europe region."""
|
|
# Note: Europe uses EGSM900, not GSM900
|
|
bands = ['EGSM900', 'DCS1800', 'GSM850', 'GSM800']
|
|
result, error = validate_band_names(bands, 'Europe')
|
|
assert result == bands
|
|
assert error is None
|
|
|
|
def test_valid_asia_bands(self):
|
|
"""Test valid band names for Asia region."""
|
|
# Note: Asia uses EGSM900, not GSM900
|
|
bands = ['EGSM900', 'DCS1800']
|
|
result, error = validate_band_names(bands, 'Asia')
|
|
assert result == bands
|
|
assert error is None
|
|
|
|
def test_invalid_band_for_region(self):
|
|
"""Test invalid band name for a region."""
|
|
bands = ['GSM900', 'INVALID_BAND']
|
|
result, error = validate_band_names(bands, 'Americas')
|
|
assert result == []
|
|
assert error is not None
|
|
assert 'Invalid bands' in error
|
|
assert 'INVALID_BAND' in error
|
|
|
|
def test_invalid_region(self):
|
|
"""Test invalid region name."""
|
|
bands = ['GSM900']
|
|
result, error = validate_band_names(bands, 'InvalidRegion')
|
|
assert result == []
|
|
assert error is not None
|
|
assert 'Invalid region' in error
|
|
|
|
def test_empty_bands_list(self):
|
|
"""Test with empty bands list."""
|
|
result, error = validate_band_names([], 'Americas')
|
|
assert result == []
|
|
assert error is None
|
|
|
|
def test_single_valid_band(self):
|
|
"""Test with single valid band."""
|
|
bands = ['GSM850']
|
|
result, error = validate_band_names(bands, 'Americas')
|
|
assert result == ['GSM850']
|
|
assert error is None
|
|
|
|
def test_case_sensitive_band_names(self):
|
|
"""Test that band names are case-sensitive."""
|
|
bands = ['gsm850'] # lowercase
|
|
result, error = validate_band_names(bands, 'Americas')
|
|
assert result == []
|
|
assert error is not None
|
|
|
|
def test_multiple_invalid_bands(self):
|
|
"""Test with multiple invalid bands."""
|
|
bands = ['INVALID1', 'GSM850', 'INVALID2']
|
|
result, error = validate_band_names(bands, 'Americas')
|
|
assert result == []
|
|
assert error is not None
|
|
assert 'INVALID1' in error
|
|
assert 'INVALID2' in error
|
|
|
|
|
|
class TestRegionalBandsConfig:
|
|
"""Tests for REGIONAL_BANDS configuration."""
|
|
|
|
def test_all_regions_defined(self):
|
|
"""Test that all expected regions are defined."""
|
|
assert 'Americas' in REGIONAL_BANDS
|
|
assert 'Europe' in REGIONAL_BANDS
|
|
assert 'Asia' in REGIONAL_BANDS
|
|
|
|
def test_all_bands_have_required_fields(self):
|
|
"""Test that all bands have required configuration fields."""
|
|
for region, bands in REGIONAL_BANDS.items():
|
|
for band_name, band_config in bands.items():
|
|
assert 'start' in band_config
|
|
assert 'end' in band_config
|
|
assert 'arfcn_start' in band_config
|
|
assert 'arfcn_end' in band_config
|
|
|
|
def test_frequency_ranges_valid(self):
|
|
"""Test that frequency ranges are positive and start < end."""
|
|
for region, bands in REGIONAL_BANDS.items():
|
|
for band_name, band_config in bands.items():
|
|
assert band_config['start'] > 0
|
|
assert band_config['end'] > 0
|
|
assert band_config['start'] < band_config['end']
|
|
|
|
def test_arfcn_ranges_valid(self):
|
|
"""Test that ARFCN ranges are valid."""
|
|
for region, bands in REGIONAL_BANDS.items():
|
|
for band_name, band_config in bands.items():
|
|
assert band_config['arfcn_start'] >= 0
|
|
assert band_config['arfcn_end'] >= 0
|
|
assert band_config['arfcn_start'] <= band_config['arfcn_end']
|