"""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 def test_no_identity_filtered(self): """Test that MCC=0/MNC=0 entries (no network identity) are filtered out.""" line = "ARFCN: 115, Freq: 925.0M, CID: 0, LAC: 0, MCC: 0, MNC: 0, Pwr: -100" result = parse_grgsm_scanner_output(line) assert result is None def test_mcc_zero_mnc_zero_filtered(self): """Test that MCC=0/MNC=0 even with valid CID is filtered out.""" line = "ARFCN: 113, Freq: 924.6M, CID: 1234, LAC: 5678, MCC: 0, MNC: 0, Pwr: -90" result = parse_grgsm_scanner_output(line) assert result is None def test_cid_zero_valid_mcc_passes(self): """Test that CID=0 with valid MCC/MNC passes (partially decoded cell).""" line = "ARFCN: 115, Freq: 958.0M, CID: 0, LAC: 21864, MCC: 234, MNC: 10, Pwr: -51" result = parse_grgsm_scanner_output(line) assert result is not None assert result['cid'] == 0 assert result['mcc'] == 234 assert result['signal_strength'] == -51.0 def test_valid_cid_nonzero(self): """Test that valid non-zero CID/MCC entries pass through.""" line = "ARFCN: 115, Freq: 925.0M, CID: 19088, LAC: 21864, MCC: 234, MNC: 10, Pwr: -58" result = parse_grgsm_scanner_output(line) assert result is not None assert result['cid'] == 19088 assert result['signal_strength'] == -58.0 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']