"""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_table_row(self): """Test parsing a valid scanner output table row.""" line = " 23 | 940.6 | 31245 | 1234 | 214 | 01 | -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_header_line(self): """Test that header lines are skipped.""" line = "ARFCN | Freq (MHz) | CID | LAC | MCC | MNC | Power (dB)" result = parse_grgsm_scanner_output(line) assert result is None def test_separator_line(self): """Test that separator lines are skipped.""" line = "--------------------------------------------------------------------" result = parse_grgsm_scanner_output(line) assert result is None def test_progress_line(self): """Test that progress lines are skipped.""" line = "Scanning: 50% complete" result = parse_grgsm_scanner_output(line) assert result is None def test_found_line(self): """Test that 'Found X towers' lines are skipped.""" line = "Found 5 towers" result = parse_grgsm_scanner_output(line) assert result is None def test_invalid_data(self): """Test handling of invalid data.""" line = " abc | xyz | invalid | data | bad | bad | bad" 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_partial_data(self): """Test handling of incomplete table rows.""" line = " 23 | 940.6 | 31245" # Missing fields 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']