From 1c454e6435f66b6f2bba1b68393a510a327a53a6 Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Mon, 3 Jun 2024 12:57:58 -0400 Subject: [PATCH] refactor last location tests --- mobile_verifier/src/coverage.rs | 3 +- .../tests/integrations/heartbeats.rs | 367 +----------------- .../tests/integrations/last_location.rs | 333 ++++++++++++++++ mobile_verifier/tests/integrations/main.rs | 1 + 4 files changed, 339 insertions(+), 365 deletions(-) create mode 100644 mobile_verifier/tests/integrations/last_location.rs diff --git a/mobile_verifier/src/coverage.rs b/mobile_verifier/src/coverage.rs index 3da4e98b4..4546b3249 100644 --- a/mobile_verifier/src/coverage.rs +++ b/mobile_verifier/src/coverage.rs @@ -223,6 +223,7 @@ pub fn new_coverage_object_notification_channel( ) } +#[derive(Clone)] pub struct CoverageObject { pub coverage_object: file_store::coverage::CoverageObject, pub validity: CoverageObjectValidity, @@ -298,7 +299,7 @@ impl CoverageObject { Ok(()) } - pub async fn save(self, transaction: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> { + pub async fn save(&self, transaction: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> { let insertion_time = Utc::now(); let key = self.key(); let hb_type = key.hb_type(); diff --git a/mobile_verifier/tests/integrations/heartbeats.rs b/mobile_verifier/tests/integrations/heartbeats.rs index 167717380..d79f259d5 100644 --- a/mobile_verifier/tests/integrations/heartbeats.rs +++ b/mobile_verifier/tests/integrations/heartbeats.rs @@ -1,17 +1,10 @@ -use chrono::{DateTime, Duration, Utc}; -use file_store::coverage::RadioHexSignalLevel; +use chrono::{DateTime, Utc}; use futures_util::TryStreamExt; -use h3o::{CellIndex, LatLng}; use helium_crypto::PublicKeyBinary; -use helium_proto::services::poc_mobile::{CoverageObjectValidity, HeartbeatValidity, SignalLevel}; +use helium_proto::services::poc_mobile::HeartbeatValidity; use mobile_verifier::{ cell_type::CellType, - coverage::{CoverageObject, CoverageObjectCache}, - geofence::GeofenceValidator, - heartbeats::{ - last_location::LocationCache, HbType, Heartbeat, HeartbeatReward, ValidatedHeartbeat, - }, - GatewayResolution, GatewayResolver, + heartbeats::{HbType, Heartbeat, HeartbeatReward, ValidatedHeartbeat}, }; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -375,357 +368,3 @@ VALUES Ok(()) } - -fn signal_level(hex: &str, signal_level: SignalLevel) -> anyhow::Result { - Ok(RadioHexSignalLevel { - location: hex.parse()?, - signal_level, - signal_power: 0, // Unused - }) -} - -#[derive(Clone)] -struct MockGeofence; - -impl GeofenceValidator for MockGeofence { - fn in_valid_region(&self, _heartbeat: &Heartbeat) -> bool { - true - } -} - -#[derive(Copy, Clone)] -struct AllOwnersValid; - -#[async_trait::async_trait] -impl GatewayResolver for AllOwnersValid { - type Error = std::convert::Infallible; - - async fn resolve_gateway( - &self, - _address: &PublicKeyBinary, - ) -> Result { - Ok(GatewayResolution::AssertedLocation(0x8c2681a3064d9ff)) - } -} - -#[sqlx::test] -async fn use_previous_location_if_timestamp_is_none(pool: PgPool) -> anyhow::Result<()> { - let hotspot: PublicKeyBinary = - "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6".parse()?; - let hotspot_2: PublicKeyBinary = - "11sctWiP9r5wDJVuDe1Th4XSL2vaawaLLSQF8f8iokAoMAJHxqp".parse()?; - - let coverage_obj = Uuid::new_v4(); - let coverage_object = file_store::coverage::CoverageObject { - pub_key: hotspot.clone(), - uuid: coverage_obj, - key_type: file_store::coverage::KeyType::HotspotKey(hotspot.clone()), - coverage_claim_time: "2022-01-01 00:00:00.000000000 UTC".parse()?, - indoor: true, - signature: Vec::new(), - coverage: vec![signal_level("8c2681a3064d9ff", SignalLevel::High)?], - trust_score: 0, - }; - - let coverage_obj_2 = Uuid::new_v4(); - let coverage_object_2 = file_store::coverage::CoverageObject { - pub_key: hotspot_2.clone(), - uuid: coverage_obj_2, - key_type: file_store::coverage::KeyType::HotspotKey(hotspot_2.clone()), - coverage_claim_time: "2022-01-01 00:00:00.000000000 UTC".parse()?, - indoor: true, - signature: Vec::new(), - coverage: vec![signal_level("8c2681a3064d9ff", SignalLevel::High)?], - trust_score: 0, - }; - - let mut transaction = pool.begin().await?; - CoverageObject { - coverage_object, - validity: CoverageObjectValidity::Valid, - } - .save(&mut transaction) - .await?; - CoverageObject { - coverage_object: coverage_object_2, - validity: CoverageObjectValidity::Valid, - } - .save(&mut transaction) - .await?; - transaction.commit().await?; - - let coverage_objects = CoverageObjectCache::new(&pool); - let location_cache = LocationCache::new(&pool); - - let cell: CellIndex = "8c2681a3064d9ff".parse().unwrap(); - let lat_lng: LatLng = cell.into(); - - let epoch_start: DateTime = "2023-08-20 00:00:00.000000000 UTC".parse().unwrap(); - let epoch_end: DateTime = Utc::now() + Duration::days(1); - - let first_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: lat_lng.lat(), - lon: lat_lng.lng(), - coverage_object: Some(coverage_obj), - location_validation_timestamp: Some(Utc::now()), - timestamp: Utc::now(), - }; - - let first_heartbeat = ValidatedHeartbeat::validate( - first_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - // First heartbeat should have a 1.0 trust score: - assert_eq!(first_heartbeat.location_trust_score_multiplier, dec!(1.0)); - - let second_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(coverage_obj), - location_validation_timestamp: None, - timestamp: Utc::now(), - }; - - let second_heartbeat = ValidatedHeartbeat::validate( - second_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - // Despite having no location set, we should still have a 1.0 trust score - // for the second heartbeat: - assert_eq!(second_heartbeat.location_trust_score_multiplier, dec!(1.0)); - // Additionally, the lat and lon should be set to the correct value - assert_eq!(second_heartbeat.heartbeat.lat, lat_lng.lat()); - assert_eq!(second_heartbeat.heartbeat.lon, lat_lng.lng()); - - // If we remove the radio from the location cache, then we should not see - // the same behavior: - - location_cache.delete_last_location(&hotspot).await; - - let third_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(coverage_obj), - location_validation_timestamp: None, - timestamp: Utc::now(), - }; - - let third_heartbeat = ValidatedHeartbeat::validate( - third_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - assert_ne!(third_heartbeat.location_trust_score_multiplier, dec!(1.0)); - assert_eq!(third_heartbeat.heartbeat.lat, 0.0); - assert_eq!(third_heartbeat.heartbeat.lon, 0.0); - - let hotspot_2_hb_1 = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_2.clone(), - cbsd_id: None, - operation_mode: true, - lat: 48.385318100686, - lon: -104.697568066261, - coverage_object: Some(coverage_obj_2), - location_validation_timestamp: Some(Utc::now()), - timestamp: Utc::now(), - }; - - let hotspot_2_hb_1 = ValidatedHeartbeat::validate( - hotspot_2_hb_1, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - // We also want to ensure that if the first heartbeat is saved into the - // db that it is properly fetched: - let mut transaction = pool.begin().await?; - first_heartbeat.save(&mut transaction).await?; - hotspot_2_hb_1.save(&mut transaction).await?; - transaction.commit().await?; - - // Also check to make sure that fetching from the DB gives us the same location - // for hotspot 2: - location_cache.delete_last_location(&hotspot_2).await; - let hotspot_2_last_location = location_cache - .fetch_last_location(&hotspot_2) - .await - .unwrap() - .unwrap(); - assert_eq!(hotspot_2_last_location.lat, 48.385318100686); - assert_eq!(hotspot_2_last_location.lon, -104.697568066261); - - // We have to remove the last location again, as the lack of previous - // locations was added to the cache: - location_cache.delete_last_location(&hotspot).await; - - let fourth_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(coverage_obj), - location_validation_timestamp: None, - timestamp: Utc::now(), - }; - - let fourth_heartbeat = ValidatedHeartbeat::validate( - fourth_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - assert_eq!(fourth_heartbeat.location_trust_score_multiplier, dec!(1.0)); - assert_eq!(fourth_heartbeat.heartbeat.lat, lat_lng.lat()); - assert_eq!(fourth_heartbeat.heartbeat.lon, lat_lng.lng()); - - // Lastly, check that if the valid heartbeat was saved over 12 hours ago - // that it is not used: - sqlx::query("TRUNCATE TABLE wifi_heartbeats") - .execute(&pool) - .await?; - location_cache.delete_last_location(&hotspot).await; - - let fifth_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: lat_lng.lat(), - lon: lat_lng.lng(), - coverage_object: Some(coverage_obj), - location_validation_timestamp: Some( - Utc::now() - (Duration::hours(12) + Duration::seconds(1)), - ), - timestamp: Utc::now() - (Duration::hours(12) + Duration::seconds(1)), - }; - - let fifth_heartbeat = ValidatedHeartbeat::validate( - fifth_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - let mut transaction = pool.begin().await?; - fifth_heartbeat.save(&mut transaction).await?; - transaction.commit().await?; - - let sixth_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(coverage_obj), - location_validation_timestamp: None, - timestamp: "2023-08-23 00:00:00.000000000 UTC".parse().unwrap(), - }; - - let sixth_heartbeat = ValidatedHeartbeat::validate( - sixth_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - assert_ne!(sixth_heartbeat.location_trust_score_multiplier, dec!(1.0)); - - location_cache.delete_last_location(&hotspot).await; - - let seventh_heartbeat = Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot.clone(), - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(coverage_obj), - location_validation_timestamp: None, - timestamp: "2023-08-23 00:00:00.000000000 UTC".parse().unwrap(), - }; - - let seventh_heartbeat = ValidatedHeartbeat::validate( - seventh_heartbeat, - &AllOwnersValid, - &coverage_objects, - &location_cache, - 1, - u32::MAX, - &(epoch_start..epoch_end), - &MockGeofence, - ) - .await - .unwrap(); - - assert_ne!(seventh_heartbeat.location_trust_score_multiplier, dec!(1.0)); - - Ok(()) -} diff --git a/mobile_verifier/tests/integrations/last_location.rs b/mobile_verifier/tests/integrations/last_location.rs new file mode 100644 index 000000000..c5ba6a150 --- /dev/null +++ b/mobile_verifier/tests/integrations/last_location.rs @@ -0,0 +1,333 @@ +use std::str::FromStr; + +use chrono::{DateTime, Duration, Utc}; +use file_store::coverage::RadioHexSignalLevel; +use h3o::LatLng; +use helium_crypto::PublicKeyBinary; +use helium_proto::services::poc_mobile as proto; +use mobile_verifier::{ + coverage::{CoverageObject, CoverageObjectCache}, + geofence::GeofenceValidator, + heartbeats::{last_location::LocationCache, HbType, Heartbeat, ValidatedHeartbeat}, + GatewayResolution, GatewayResolver, +}; +use rust_decimal_macros::dec; +use sqlx::{PgPool, Postgres, Transaction}; +use uuid::Uuid; + +const PUB_KEY: &str = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6"; + +#[derive(Clone)] +struct MockGeofence; + +impl GeofenceValidator for MockGeofence { + fn in_valid_region(&self, _heartbeat: &Heartbeat) -> bool { + true + } +} + +#[derive(Copy, Clone)] +struct AllOwnersValid; + +#[async_trait::async_trait] +impl GatewayResolver for AllOwnersValid { + type Error = std::convert::Infallible; + + async fn resolve_gateway( + &self, + _address: &PublicKeyBinary, + ) -> Result { + Ok(GatewayResolution::AssertedLocation(0x8c2681a3064d9ff)) + } +} + +#[sqlx::test] +async fn heatbeat_uses_last_good_location_when_invalid_location( + pool: PgPool, +) -> anyhow::Result<()> { + let hotspot = PublicKeyBinary::from_str(PUB_KEY)?; + let epoch_start = Utc::now() - Duration::days(1); + let epoch_end = epoch_start + Duration::days(2); + + let coverage_objects = CoverageObjectCache::new(&pool); + let location_cache = LocationCache::new(&pool); + + let mut transaction = pool.begin().await?; + let coverage_object = coverage_object(&hotspot, &mut transaction).await?; + transaction.commit().await?; + + let validated_heartbeat_1 = ValidatedHeartbeat::validate( + heartbeat(&hotspot, &coverage_object) + .location_validation_timestamp(Utc::now()) + .build(), + &AllOwnersValid, + &coverage_objects, + &location_cache, + 1, + u32::MAX, + &(epoch_start..epoch_end), + &MockGeofence, + ) + .await?; + + assert_eq!( + validated_heartbeat_1.location_trust_score_multiplier, + dec!(1.0) + ); + + let validated_heartbeat_2 = ValidatedHeartbeat::validate( + heartbeat(&hotspot, &coverage_object) + .latlng((0.0, 0.0)) + .build(), + &AllOwnersValid, + &coverage_objects, + &location_cache, + 1, + u32::MAX, + &(epoch_start..epoch_end), + &MockGeofence, + ) + .await?; + + // Despite having no location set, we should still have a 1.0 trust score + // for the second heartbeat: + assert_eq!( + validated_heartbeat_2.location_trust_score_multiplier, + dec!(1.0) + ); + assert_eq!( + validated_heartbeat_1.heartbeat.lat, + validated_heartbeat_2.heartbeat.lat + ); + assert_eq!( + validated_heartbeat_1.heartbeat.lon, + validated_heartbeat_2.heartbeat.lon + ); + + Ok(()) +} + +#[sqlx::test] +async fn heatbeat_will_use_last_good_location_from_db(pool: PgPool) -> anyhow::Result<()> { + let hotspot = PublicKeyBinary::from_str(PUB_KEY)?; + let epoch_start = Utc::now() - Duration::days(1); + let epoch_end = epoch_start + Duration::days(2); + + let coverage_objects = CoverageObjectCache::new(&pool); + let location_cache = LocationCache::new(&pool); + + let mut transaction = pool.begin().await?; + let coverage_object = coverage_object(&hotspot, &mut transaction).await?; + transaction.commit().await?; + + let validated_heartbeat_1 = ValidatedHeartbeat::validate( + heartbeat(&hotspot, &coverage_object) + .location_validation_timestamp(Utc::now()) + .build(), + &AllOwnersValid, + &coverage_objects, + &location_cache, + 1, + u32::MAX, + &(epoch_start..epoch_end), + &MockGeofence, + ) + .await?; + + assert_eq!( + validated_heartbeat_1.location_trust_score_multiplier, + dec!(1.0) + ); + + location_cache.delete_last_location(&hotspot).await; + transaction = pool.begin().await?; + validated_heartbeat_1.clone().save(&mut transaction).await?; + transaction.commit().await?; + + let validated_heartbeat_2 = ValidatedHeartbeat::validate( + heartbeat(&hotspot, &coverage_object) + .latlng((0.0, 0.0)) + .build(), + &AllOwnersValid, + &coverage_objects, + &location_cache, + 1, + u32::MAX, + &(epoch_start..epoch_end), + &MockGeofence, + ) + .await?; + + // Despite having no location set, we should still have a 1.0 trust score + // for the second heartbeat: + assert_eq!( + validated_heartbeat_2.location_trust_score_multiplier, + dec!(1.0) + ); + assert_eq!( + validated_heartbeat_1.heartbeat.lat, + validated_heartbeat_2.heartbeat.lat + ); + assert_eq!( + validated_heartbeat_1.heartbeat.lon, + validated_heartbeat_2.heartbeat.lon + ); + + Ok(()) +} + +#[sqlx::test] +async fn heatbeat_does_not_use_last_good_location_when_more_than_12_hours( + pool: PgPool, +) -> anyhow::Result<()> { + let hotspot = PublicKeyBinary::from_str(PUB_KEY)?; + let epoch_start = Utc::now() - Duration::days(1); + let epoch_end = epoch_start + Duration::days(2); + + let coverage_objects = CoverageObjectCache::new(&pool); + let location_cache = LocationCache::new(&pool); + + let mut transaction = pool.begin().await?; + let coverage_object = coverage_object(&hotspot, &mut transaction).await?; + transaction.commit().await?; + + let validated_heartbeat_1 = ValidatedHeartbeat::validate( + heartbeat(&hotspot, &coverage_object) + .location_validation_timestamp(Utc::now()) + .timestamp(Utc::now() - Duration::hours(12) - Duration::seconds(1)) + .build(), + &AllOwnersValid, + &coverage_objects, + &location_cache, + 1, + u32::MAX, + &(epoch_start..epoch_end), + &MockGeofence, + ) + .await?; + + assert_eq!( + validated_heartbeat_1.location_trust_score_multiplier, + dec!(1.0) + ); + + let validated_heartbeat_2 = ValidatedHeartbeat::validate( + heartbeat(&hotspot, &coverage_object) + .latlng((0.0, 0.0)) + .build(), + &AllOwnersValid, + &coverage_objects, + &location_cache, + 1, + u32::MAX, + &(epoch_start..epoch_end), + &MockGeofence, + ) + .await?; + + assert_eq!( + validated_heartbeat_2.location_trust_score_multiplier, + dec!(0.25) + ); + + Ok(()) +} + +struct HeartbeatBuilder { + hotspot: PublicKeyBinary, + coverage_object: CoverageObject, + location_validation_timestamp: Option>, + latlng: Option<(f64, f64)>, + timestamp: Option>, +} + +impl HeartbeatBuilder { + fn new(hotspot: PublicKeyBinary, coverage_object: CoverageObject) -> Self { + Self { + hotspot, + coverage_object, + location_validation_timestamp: None, + latlng: None, + timestamp: None, + } + } + + fn location_validation_timestamp(mut self, ts: DateTime) -> Self { + self.location_validation_timestamp = Some(ts); + self + } + + fn latlng(mut self, latlng: (f64, f64)) -> Self { + self.latlng = Some(latlng); + self + } + + fn timestamp(mut self, ts: DateTime) -> Self { + self.timestamp = Some(ts); + self + } + + fn build(self) -> Heartbeat { + let (lat, lon) = self.latlng.unwrap_or_else(|| { + let lat_lng: LatLng = self + .coverage_object + .coverage_object + .coverage + .first() + .unwrap() + .location + .into(); + + (lat_lng.lat(), lat_lng.lng()) + }); + + Heartbeat { + hb_type: HbType::Wifi, + hotspot_key: self.hotspot, + cbsd_id: None, + operation_mode: true, + lat, + lon, + coverage_object: Some(self.coverage_object.coverage_object.uuid), + location_validation_timestamp: self.location_validation_timestamp, + timestamp: self.timestamp.unwrap_or(Utc::now()), + } + } +} + +fn heartbeat(hotspot: &PublicKeyBinary, coverage_object: &CoverageObject) -> HeartbeatBuilder { + HeartbeatBuilder::new(hotspot.clone(), coverage_object.clone()) +} + +async fn coverage_object( + hotspot: &PublicKeyBinary, + transaction: &mut Transaction<'_, Postgres>, +) -> anyhow::Result { + let coverage_object = CoverageObject { + coverage_object: file_store::coverage::CoverageObject { + pub_key: hotspot.clone(), + uuid: Uuid::new_v4(), + key_type: file_store::coverage::KeyType::HotspotKey(hotspot.clone()), + coverage_claim_time: Utc::now(), + coverage: vec![signal_level("8c2681a3064d9ff", proto::SignalLevel::High)?], + indoor: true, + trust_score: 0, + signature: vec![], + }, + validity: proto::CoverageObjectValidity::Valid, + }; + coverage_object.save(transaction).await?; + + Ok(coverage_object) +} + +fn signal_level( + hex: &str, + signal_level: proto::SignalLevel, +) -> anyhow::Result { + Ok(RadioHexSignalLevel { + location: hex.parse()?, + signal_level, + signal_power: 0, // Unused + }) +} diff --git a/mobile_verifier/tests/integrations/main.rs b/mobile_verifier/tests/integrations/main.rs index 01d85ca78..6505c40ad 100644 --- a/mobile_verifier/tests/integrations/main.rs +++ b/mobile_verifier/tests/integrations/main.rs @@ -3,6 +3,7 @@ mod common; mod boosting_oracles; mod heartbeats; mod hex_boosting; +mod last_location; mod modeled_coverage; mod rewarder_mappers; mod rewarder_oracles;