import pandas as pd import pytest from unittest.mock import Mock from backend.Property import Property from etl.epc_clean.EpcClean import EpcClean from etl.epc.Record import EPCRecord # Define some test data mock_epc_response = { "rows": [ { "tenure": "rental (social)", "lmk-key": 1, "uprn": 1, "number-habitable-rooms": 5, "property-type": "House", "built-form": "Detached", "inspection-date": "2023-06-01", 'lodgement-datetime': '2023-06-01 20:29:01', "some-other-key": "some-value", "roof-description": "pitched, no insulation", "walls-description": "Walls Description", "windows-description": "Windows Description", "mainheat-description": "Main Heating Description", "hotwater-description": "Hot Water Description", "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground" }, { "lmk-key": 2, "uprn": 2, "number-habitable-rooms": 5, "property-type": "House", "built-form": "Detached", "inspection-date": "2023-05-01", 'lodgement-datetime': '2023-05-01 20:29:01', "some-other-key": "some-other-value", "roof-description": "Roof Description", "walls-description": "Walls Description", "windows-description": "Windows Description", "mainheat-description": "Main Heating Description", "hotwater-description": "Hot Water Description", "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground" } ] } mock_epc_response_dupe = { 'rows': [ { "lmk-key": 1, "uprn": 1, "number-habitable-rooms": 5, "property-type": "House", 'inspection-date': '2023-06-01', 'lodgement-datetime': '2023-06-01 20:29:01', 'some-other-key': 'some-value', 'roof-description': 'Roof Description', 'walls-description': 'Walls Description', 'windows-description': 'Windows Description', 'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description', "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground" }, { "lmk-key": 2, "uprn": 2, "number-habitable-rooms": 5, "property-type": "House", 'inspection-date': '2023-05-01', 'lodgement-datetime': '2023-05-01 20:29:01', 'some-other-key': 'some-other-value', 'roof-description': 'Roof Description', 'walls-description': 'Walls Description', 'windows-description': 'Windows Description', 'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description', "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground" }, { "lmk-key": 3, "uprn": 3, "number-habitable-rooms": 5, "property-type": "House", 'inspection-date': '2023-06-01', 'lodgement-datetime': '2023-06-01 20:29:01', 'some-other-key': 'duplicate-date', 'roof-description': 'Roof Description', 'walls-description': 'Walls Description', 'windows-description': 'Windows Description', 'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description', "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", "co2-emissions-current": "123", "mechanical-ventilation": "natural", 'photo-supply': 0, "solar-water-heating-flag": "N", "wind-turbine-count": 0, "extension-count": 0, "heat-loss-corridor": "no corridor", "unheated-corridor-length": 0, "mains-gas-flag": "Y", "floor-height": 2.5, "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", "floor-level": "Ground" } ] } class TestProperty: @pytest.fixture(autouse=True) def mock_photo_supply_lookup(self): return pd.DataFrame( [ dict( tenure="rental (social)", built_form="Detached", property_type="House", construction_age_band="England and Wales: 1967-1975", is_flat=False, is_pitched=True, is_roof_room=False, floor_area_decile=2, photo_supply_median=40 ) ] ) @pytest.fixture(autouse=True) def mock_floor_area_decile_thresholds(self): return pd.DataFrame( {"floor_area_decile_thresholds": [0, 10, 30, 50]} ) @pytest.fixture(autouse=True) def property_instance(self, mock_cleaner): epc_record = EPCRecord() epc_record.prepared_epc = mock_epc_response["rows"][0] property_instance = Property(id=1, postcode="AB12CD", address="Test Address", epc_record=epc_record) property_instance.number_of_floors = 2 property_instance.number_of_rooms = 5 property_instance.floor_area = 100 property_instance.floor_height = 2.5 return property_instance @pytest.fixture(autouse=True) def property_instance_dupe_data(self): epc_record = EPCRecord() epc_record.prepared_epc = mock_epc_response_dupe["rows"][0] property_instance_dupe_data = Property(id=2, postcode="AB12CD", address="Test Address", epc_record=epc_record) return property_instance_dupe_data # @pytest.fixture # def mock_epc_client(self): # mock_epc_client = Mock(spec=EpcClient(auth_token="mocked_auth_token")) # mock_epc_client.domestic.search.return_value = mock_epc_response.copy() # mock_epc_client.auth_token = "mocked_auth_token" # return mock_epc_client # # @pytest.fixture # def mock_epc_client_dupe_data(self): # mock_epc_client_dupe_data = Mock(spec=EpcClient(auth_token="mocked_auth_token")) # mock_epc_client_dupe_data.domestic.search.return_value = mock_epc_response_dupe.copy() # mock_epc_client_dupe_data.auth_token = "mocked_auth_token" # return mock_epc_client_dupe_data @pytest.fixture def mock_cleaner(self): lighting_averages = [ {'lighting-description': 'good lighting efficiency', 'low-energy-lighting': 99.26666666666667}, {'lighting-description': 'excellent lighting efficiency', 'low-energy-lighting': 100.0}, {'lighting-description': 'below average lighting efficiency', 'low-energy-lighting': 0.0} ] cleaner_spec = EpcClean( data=[ {"roof-description": "Roof Description"}, {"walls-description": "Walls Description"}, {"windows-description": "Windows Description"}, {"mainheat-description": "Main Heating Description"}, {"hotwater-description": "Hot Water Description"}, {"lighting-description": "Good Lighting Efficiency"}, {"low-energy-lighting": 0}, {"floor-description": "Floor Description"} ], lighting_averages=lighting_averages ) mock_cleaner = Mock(spec=cleaner_spec) walls_data = { "original_description": "Walls Description", "is_cavity_wall": True, "is_solid_brick": False, "is_timber_frame": False, "is_system_built": False, "is_park_home": False, "is_cob": False, "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } mock_cleaner.cleaned = { "roof-description": [ {"original_description": "Roof Description"}, {"original_description": "pitched, no insulation", "is_pitched": True, "is_flat": False, "is_roof_room": False} ], "walls-description": [walls_data], "windows-description": [{"original_description": "Windows Description"}], "mainheat-description": [{"original_description": "Main Heating Description"}], "hotwater-description": [{"original_description": "Hot Water Description"}], "lighting-description": [{"original_description": "Good Lighting Efficiency"}], "floor-description": [ {"original_description": "Floor Description", "is_suspended": True, "another_property_below": False}] } return mock_cleaner def test_init(self): epc_record = EPCRecord() epc_record.prepared_epc = {"uprn": 1} inst1 = Property(0, postcode="AB12CD", address="Test Address", epc_record=epc_record) assert inst1.data is not None inst2 = Property(3, "AB12CD", "Test Address", epc_record=epc_record) assert inst2.id == 3 inst3 = Property(4, "AB12CD", "Test Address", epc_record=epc_record) assert inst3.data == {"uprn": 1} def test_get_components( self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds ): property_instance.get_components( mock_cleaner.cleaned, photo_supply_lookup=mock_photo_supply_lookup, floor_area_decile_thresholds=mock_floor_area_decile_thresholds ) # Verify that the components are set correctly assert property_instance.roof == { 'original_description': 'pitched, no insulation', 'is_pitched': True, 'is_flat': False, 'is_roof_room': False } assert property_instance.walls == { "original_description": "Walls Description", "is_cavity_wall": True, "is_solid_brick": False, "is_timber_frame": False, "is_system_built": False, "is_park_home": False, "is_cob": False, "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } assert property_instance.windows == {"original_description": "Windows Description"} assert property_instance.main_heating == {"original_description": "Main Heating Description"} assert property_instance.hotwater == {"original_description": "Hot Water Description"} assert property_instance.wall_type == "cavity" def test_get_components_without_cleaned_data(self, property_instance, mock_cleaner): # Modify the mock EpcClean to not have cleaned data mock_cleaner.cleaned = {} # Verify that ValueError is raised when EpcClean doesn't contain cleaned data with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"): property_instance.get_components(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame()) def test_get_components_no_attributes( self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds ): # Modify the mock cleaner to have no attributes for a specific description mock_cleaner.cleaned = { "roof-description": [] } property_instance.data["roof-description"] = "Pitched, no insulation" property_instance.walls = { "original_description": "Walls Description", "is_cavity_wall": True, "is_solid_brick": False, "is_timber_frame": False, "is_system_built": False, "is_park_home": False, "is_cob": False, "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } property_instance.floor = { "is_suspended": False, "another_property_below": False, "is_solid": True } # Assert backup cleaning has been applied property_instance.get_components( mock_cleaner.cleaned, mock_photo_supply_lookup, mock_floor_area_decile_thresholds ) assert property_instance.roof["clean_description"] == "Pitched, no insulation" assert property_instance.roof["is_pitched"] def test_get_components_multiple_attributes( self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds ): # This shouldn't happen - it would mean a cleaning error property_instance.data["roof-description"] = "Roof Description" cleaned = { "roof-description": [ {"original_description": "Roof Description"}, {"original_description": "Roof Description"} ] } # Verify that ValueError is raised when multiple attributes are found with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"): property_instance.get_components(cleaned, mock_photo_supply_lookup, mock_floor_area_decile_thresholds) def test_set_spatial(self): epc_record = EPCRecord() epc_record.prepared_epc = mock_epc_response["rows"][0] prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) spatial1 = pd.DataFrame([{ 'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238, 'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True }]) prop.set_spatial(spatial1) assert prop.in_conservation_area assert not prop.is_listed assert prop.is_heritage assert prop.restricted_measures prop2 = Property(1, "AB12CD", "Test Address", epc_record=epc_record) spatial2 = pd.DataFrame([{ 'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238, 'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False }]) prop2.set_spatial(spatial2) assert prop2.in_conservation_area is None assert not prop2.is_listed assert not prop2.is_heritage assert not prop2.restricted_measures def test_set_floor_level(self): # In this case, we have a flat which looks looks it's on the first floor, but it's actually on the ground # floor, so we should set floor_level to 0 epc_record = EPCRecord() epc_record.prepared_epc = {'floor-level': '01', 'property-type': 'Flat'} prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop.floor = { 'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True, 'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop.set_floor_level() assert prop.floor_level == 0 # This property is labelled as being on the ground floor but actually has another property below # so we set floor level to 1 epc_record = EPCRecord() epc_record.prepared_epc = {'floor-level': 'Ground', 'property-type': 'Flat'} prop2 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop2.floor = { 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop2.set_floor_level() assert prop2.floor_level == 1 # this property is correctly labelled as being on the 2nd floor epc_record = EPCRecord() epc_record.prepared_epc = {'floor-level': '02', 'property-type': 'Flat'} prop3 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop3.floor = { 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop3.set_floor_level() assert prop3.floor_level == 2 # Example of a house epc_record = EPCRecord() epc_record.prepared_epc = {'floor-level': '', 'property-type': 'House'} prop4 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop4.floor = { 'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None, 'floor_insulation_thickness': 'none' } prop4.set_floor_level() assert prop4.floor_level is None