Files
attune/crates/common/tests/sensor_repository_tests.rs
David Culbreth f96861d417
Some checks failed
CI / Clippy (push) Failing after 3m6s
CI / Rustfmt (push) Failing after 3m9s
CI / Cargo Audit & Deny (push) Successful in 5m2s
CI / Tests (push) Successful in 8m15s
CI / Security Blocking Checks (push) Successful in 10s
CI / Web Advisory Checks (push) Successful in 1m4s
CI / Web Blocking Checks (push) Failing after 4m52s
Publish Images And Chart / Resolve Publish Metadata (push) Successful in 2s
CI / Security Advisory Checks (push) Successful in 1m31s
Publish Images And Chart / Publish init-user (push) Failing after 30s
Publish Images And Chart / Publish init-packs (push) Failing after 1m41s
Publish Images And Chart / Publish migrations (push) Failing after 10s
Publish Images And Chart / Publish web (push) Failing after 11s
Publish Images And Chart / Publish sensor (push) Failing after 32s
Publish Images And Chart / Publish worker (push) Failing after 11s
Publish Images And Chart / Publish executor (push) Failing after 11s
Publish Images And Chart / Publish notifier (push) Failing after 9s
Publish Images And Chart / Publish api (push) Failing after 31s
Publish Images And Chart / Publish Helm Chart (push) Has been skipped
properly handling patch updates
2026-03-17 12:17:58 -05:00

1779 lines
48 KiB
Rust

//! Integration tests for Sensor repository
//!
//! These tests verify CRUD operations, queries, and constraints
//! for the Sensor repository.
mod helpers;
use attune_common::{
repositories::{
trigger::{CreateSensorInput, SensorRepository, UpdateSensorInput},
Create, Delete, FindById, FindByRef, List, Patch, Update,
},
Error,
};
use helpers::*;
use serde_json::json;
// ============================================================================
// CREATE Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_minimal() {
let pool = create_test_pool().await.unwrap();
// Create dependencies
let pack = PackFixture::new_unique("sensor_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Create sensor
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"webhook_sensor",
)
.create(&pool)
.await
.unwrap();
assert!(sensor.id > 0);
assert!(sensor.r#ref.contains(&pack.r#ref));
assert_eq!(sensor.pack, Some(pack.id));
assert_eq!(sensor.pack_ref, Some(pack.r#ref));
assert_eq!(sensor.runtime, runtime.id);
assert_eq!(sensor.runtime_ref, runtime.r#ref);
assert_eq!(sensor.trigger, trigger.id);
assert_eq!(sensor.trigger_ref, trigger.r#ref);
assert!(sensor.enabled);
assert_eq!(sensor.param_schema, None);
assert!(sensor.created.timestamp() > 0);
assert!(sensor.updated.timestamp() > 0);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_with_param_schema() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("schema_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let param_schema = json!({
"type": "object",
"properties": {
"interval": {
"type": "integer",
"minimum": 1
},
"endpoint": {
"type": "string",
"format": "uri"
}
},
"required": ["interval"]
});
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"polling_sensor",
)
.with_param_schema(param_schema.clone())
.create(&pool)
.await
.unwrap();
assert_eq!(sensor.param_schema, Some(param_schema));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_without_pack() {
let pool = create_test_pool().await.unwrap();
let trigger = TriggerFixture::new_unique(None, None, "webhook")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(None, None, "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
None,
None,
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"system_sensor",
)
.create(&pool)
.await
.unwrap();
assert_eq!(sensor.pack, None);
assert_eq!(sensor.pack_ref, None);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_duplicate_ref_fails() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("dup_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Create first sensor
let sensor_ref = format!("{}.duplicate_sensor", pack.r#ref);
let input = CreateSensorInput {
r#ref: sensor_ref.clone(),
pack: Some(pack.id),
pack_ref: Some(pack.r#ref.clone()),
label: "Duplicate Sensor".to_string(),
description: "Test sensor".to_string(),
entrypoint: "sensors/dup.py".to_string(),
runtime: runtime.id,
runtime_ref: runtime.r#ref.clone(),
runtime_version_constraint: None,
trigger: trigger.id,
trigger_ref: trigger.r#ref.clone(),
enabled: true,
param_schema: None,
config: None,
};
SensorRepository::create(&pool, input.clone())
.await
.unwrap();
// Try to create second sensor with same ref
let result = SensorRepository::create(&pool, input).await;
assert!(result.is_err());
// Should fail with database error due to unique constraint violation
assert!(result.is_err());
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_invalid_ref_format_fails() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("invalid_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Try invalid ref formats
let invalid_refs = vec![
"no_dot", // Missing dot
"too.many.dots.here", // Too many dots
"UPPERCASE.sensor", // Uppercase not allowed
];
for invalid_ref in invalid_refs {
let input = CreateSensorInput {
r#ref: invalid_ref.to_string(),
pack: Some(pack.id),
pack_ref: Some(pack.r#ref.clone()),
label: "Invalid Sensor".to_string(),
description: "Test sensor".to_string(),
entrypoint: "sensors/invalid.py".to_string(),
runtime: runtime.id,
runtime_ref: runtime.r#ref.clone(),
runtime_version_constraint: None,
trigger: trigger.id,
trigger_ref: trigger.r#ref.clone(),
enabled: true,
param_schema: None,
config: None,
};
let result = SensorRepository::create(&pool, input).await;
assert!(
result.is_err(),
"Expected error for invalid ref: {}",
invalid_ref
);
}
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_invalid_pack_fails() {
let pool = create_test_pool().await.unwrap();
let trigger = TriggerFixture::new_unique(None, None, "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(None, None, "python3")
.create(&pool)
.await
.unwrap();
let input = CreateSensorInput {
r#ref: "invalid.sensor".to_string(),
pack: Some(99999), // Non-existent pack
pack_ref: Some("invalid".to_string()),
label: "Invalid Pack Sensor".to_string(),
description: "Test sensor".to_string(),
entrypoint: "sensors/invalid.py".to_string(),
runtime: runtime.id,
runtime_ref: runtime.r#ref.clone(),
runtime_version_constraint: None,
trigger: trigger.id,
trigger_ref: trigger.r#ref.clone(),
enabled: true,
param_schema: None,
config: None,
};
let result = SensorRepository::create(&pool, input).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Database(_)));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_invalid_trigger_fails() {
let pool = create_test_pool().await.unwrap();
let runtime = RuntimeFixture::new_unique(None, None, "python3")
.create(&pool)
.await
.unwrap();
let input = CreateSensorInput {
r#ref: "invalid.sensor".to_string(),
pack: None,
pack_ref: None,
label: "Invalid Trigger Sensor".to_string(),
description: "Test sensor".to_string(),
entrypoint: "sensors/invalid.py".to_string(),
runtime: runtime.id,
runtime_ref: runtime.r#ref.clone(),
runtime_version_constraint: None,
trigger: 99999, // Non-existent trigger
trigger_ref: "invalid.trigger".to_string(),
enabled: true,
param_schema: None,
config: None,
};
let result = SensorRepository::create(&pool, input).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Database(_)));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_create_sensor_invalid_runtime_fails() {
let pool = create_test_pool().await.unwrap();
let trigger = TriggerFixture::new_unique(None, None, "event")
.create(&pool)
.await
.unwrap();
let input = CreateSensorInput {
r#ref: "invalid.sensor".to_string(),
pack: None,
pack_ref: None,
label: "Invalid Runtime Sensor".to_string(),
description: "Test sensor".to_string(),
entrypoint: "sensors/invalid.py".to_string(),
runtime: 99999, // Non-existent runtime
runtime_ref: "invalid.runtime".to_string(),
runtime_version_constraint: None,
trigger: trigger.id,
trigger_ref: trigger.r#ref.clone(),
enabled: true,
param_schema: None,
config: None,
};
let result = SensorRepository::create(&pool, input).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Database(_)));
}
// ============================================================================
// READ Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_id_exists() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("find_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"find_sensor",
)
.create(&pool)
.await
.unwrap();
let found = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap();
assert!(found.is_some());
let found = found.unwrap();
assert_eq!(found.id, sensor.id);
assert_eq!(found.r#ref, sensor.r#ref);
assert_eq!(found.label, sensor.label);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_id_not_exists() {
let pool = create_test_pool().await.unwrap();
let result = SensorRepository::find_by_id(&pool, 99999).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_get_by_id_exists() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("get_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"get_sensor",
)
.create(&pool)
.await
.unwrap();
let found = SensorRepository::get_by_id(&pool, sensor.id).await.unwrap();
assert_eq!(found.id, sensor.id);
assert_eq!(found.r#ref, sensor.r#ref);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_get_by_id_not_exists_fails() {
let pool = create_test_pool().await.unwrap();
let result = SensorRepository::get_by_id(&pool, 99999).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NotFound { .. }));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_ref_exists() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("ref_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"ref_sensor",
)
.create(&pool)
.await
.unwrap();
let found = SensorRepository::find_by_ref(&pool, &sensor.r#ref)
.await
.unwrap();
assert!(found.is_some());
let found = found.unwrap();
assert_eq!(found.id, sensor.id);
assert_eq!(found.r#ref, sensor.r#ref);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_ref_not_exists() {
let pool = create_test_pool().await.unwrap();
let result = SensorRepository::find_by_ref(&pool, "nonexistent.sensor")
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_get_by_ref_exists() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("getref_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"getref_sensor",
)
.create(&pool)
.await
.unwrap();
let found = SensorRepository::get_by_ref(&pool, &sensor.r#ref)
.await
.unwrap();
assert_eq!(found.id, sensor.id);
assert_eq!(found.r#ref, sensor.r#ref);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_get_by_ref_not_exists_fails() {
let pool = create_test_pool().await.unwrap();
let result = SensorRepository::get_by_ref(&pool, "nonexistent.sensor").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NotFound { .. }));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_list_all_sensors() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("list_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Create multiple sensors
let _sensor1 = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"sensor_a",
)
.create(&pool)
.await
.unwrap();
let _sensor2 = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"sensor_b",
)
.create(&pool)
.await
.unwrap();
let sensors = SensorRepository::list(&pool).await.unwrap();
// Should have at least our 2 sensors (may have more from other parallel tests)
assert!(sensors.len() >= 2);
// Verify sensors are sorted by ref
for i in 1..sensors.len() {
assert!(sensors[i - 1].r#ref <= sensors[i].r#ref);
}
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_list_empty() {
let pool = create_test_pool().await.unwrap();
// Count should be at least 0 (may have sensors from parallel tests)
let sensors = SensorRepository::list(&pool).await.unwrap();
// Just verify we can list sensors without error
drop(sensors);
}
// ============================================================================
// UPDATE Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_label() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("update_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"update_sensor",
)
.create(&pool)
.await
.unwrap();
let original_updated = sensor.updated;
// Small delay to ensure updated timestamp changes
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let input = UpdateSensorInput {
label: Some("Updated Sensor Label".to_string()),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.id, sensor.id);
assert_eq!(updated.label, "Updated Sensor Label");
assert_eq!(updated.description, sensor.description); // Unchanged
assert!(updated.updated > original_updated);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_description() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("desc_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"desc_sensor",
)
.create(&pool)
.await
.unwrap();
let input = UpdateSensorInput {
description: Some("New description for the sensor".to_string()),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.description, "New description for the sensor");
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_entrypoint() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("entry_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"entry_sensor",
)
.create(&pool)
.await
.unwrap();
let input = UpdateSensorInput {
entrypoint: Some("sensors/new_entrypoint.py".to_string()),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.entrypoint, "sensors/new_entrypoint.py");
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_enabled_status() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("enabled_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"enabled_sensor",
)
.with_enabled(true)
.create(&pool)
.await
.unwrap();
assert!(sensor.enabled);
let input = UpdateSensorInput {
enabled: Some(false),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert!(!updated.enabled);
// Enable it again
let input = UpdateSensorInput {
enabled: Some(true),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert!(updated.enabled);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_param_schema() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("schema_update_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"schema_sensor",
)
.create(&pool)
.await
.unwrap();
let new_schema = json!({
"type": "object",
"properties": {
"timeout": {
"type": "integer",
"minimum": 0
}
}
});
let input = UpdateSensorInput {
param_schema: Some(Patch::Set(new_schema.clone())),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.param_schema, Some(new_schema));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_multiple_fields() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("multi_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"multi_sensor",
)
.create(&pool)
.await
.unwrap();
let input = UpdateSensorInput {
label: Some("Multi Update".to_string()),
description: Some("Updated multiple fields".to_string()),
entrypoint: Some("sensors/multi.py".to_string()),
enabled: Some(false),
param_schema: Some(Patch::Set(json!({"type": "object"}))),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.label, "Multi Update");
assert_eq!(updated.description, "Updated multiple fields");
assert_eq!(updated.entrypoint, "sensors/multi.py");
assert!(!updated.enabled);
assert_eq!(updated.param_schema, Some(json!({"type": "object"})));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_no_changes() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("nochange_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"nochange_sensor",
)
.create(&pool)
.await
.unwrap();
let original_updated = sensor.updated;
let input = UpdateSensorInput::default();
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.id, sensor.id);
assert_eq!(updated.label, sensor.label);
assert_eq!(updated.description, sensor.description);
assert_eq!(updated.entrypoint, sensor.entrypoint);
assert_eq!(updated.enabled, sensor.enabled);
// Updated timestamp should not change when no fields are updated
assert_eq!(updated.updated, original_updated);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_update_nonexistent_sensor_fails() {
let pool = create_test_pool().await.unwrap();
let input = UpdateSensorInput {
label: Some("Updated".to_string()),
..Default::default()
};
let result = SensorRepository::update(&pool, 99999, input).await;
assert!(result.is_err());
}
// ============================================================================
// DELETE Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_existing_sensor() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("delete_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"delete_sensor",
)
.create(&pool)
.await
.unwrap();
let deleted = SensorRepository::delete(&pool, sensor.id).await.unwrap();
assert!(deleted);
// Verify sensor is gone
let result = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_nonexistent_sensor() {
let pool = create_test_pool().await.unwrap();
let deleted = SensorRepository::delete(&pool, 99999).await.unwrap();
assert!(!deleted);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_sensor_when_pack_deleted() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("cascade_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"cascade_sensor",
)
.create(&pool)
.await
.unwrap();
// Delete the pack
use attune_common::repositories::{pack::PackRepository, Delete as _};
PackRepository::delete(&pool, pack.id).await.unwrap();
// Sensor should also be deleted due to CASCADE
let result = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_sensor_when_trigger_deleted() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("trigger_cascade_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"trigger_cascade_sensor",
)
.create(&pool)
.await
.unwrap();
// Delete the trigger
use attune_common::repositories::{trigger::TriggerRepository, Delete as _};
TriggerRepository::delete(&pool, trigger.id).await.unwrap();
// Sensor should also be deleted due to CASCADE
let result = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_delete_sensor_when_runtime_deleted() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("runtime_cascade_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"runtime_cascade_sensor",
)
.create(&pool)
.await
.unwrap();
// Delete the runtime
use attune_common::repositories::{runtime::RuntimeRepository, Delete as _};
RuntimeRepository::delete(&pool, runtime.id).await.unwrap();
// Sensor should also be deleted due to CASCADE
let result = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap();
assert!(result.is_none());
}
// ============================================================================
// Specialized Query Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_trigger() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("trigger_find_pack")
.create(&pool)
.await
.unwrap();
let trigger1 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event1")
.create(&pool)
.await
.unwrap();
let trigger2 = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event2")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Create sensors for trigger1
let sensor1 = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger1.id,
trigger1.r#ref.clone(),
"sensor1",
)
.create(&pool)
.await
.unwrap();
let sensor2 = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger1.id,
trigger1.r#ref.clone(),
"sensor2",
)
.create(&pool)
.await
.unwrap();
// Create sensor for trigger2
let _sensor3 = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger2.id,
trigger2.r#ref.clone(),
"sensor3",
)
.create(&pool)
.await
.unwrap();
let sensors = SensorRepository::find_by_trigger(&pool, trigger1.id)
.await
.unwrap();
assert_eq!(sensors.len(), 2);
assert!(sensors.iter().any(|s| s.id == sensor1.id));
assert!(sensors.iter().any(|s| s.id == sensor2.id));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_trigger_no_sensors() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("empty_trigger_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let sensors = SensorRepository::find_by_trigger(&pool, trigger.id)
.await
.unwrap();
assert_eq!(sensors.len(), 0);
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_enabled() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("enabled_find_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Create enabled sensor
let enabled_sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"enabled_sensor",
)
.with_enabled(true)
.create(&pool)
.await
.unwrap();
// Create disabled sensor
let _disabled_sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"disabled_sensor",
)
.with_enabled(false)
.create(&pool)
.await
.unwrap();
let enabled_sensors = SensorRepository::find_enabled(&pool).await.unwrap();
// Should only contain enabled sensors
assert!(enabled_sensors.iter().all(|s| s.enabled));
assert!(enabled_sensors.iter().any(|s| s.id == enabled_sensor.id));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_enabled_empty() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("disabled_only_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
// Create only disabled sensor
let disabled = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"disabled",
)
.with_enabled(false)
.create(&pool)
.await
.unwrap();
let enabled_sensors = SensorRepository::find_enabled(&pool).await.unwrap();
// May have enabled sensors from other parallel tests, just verify our disabled sensor is not in the list
assert!(enabled_sensors.iter().all(|s| s.id != disabled.id));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_pack() {
let pool = create_test_pool().await.unwrap();
let pack1 = PackFixture::new_unique("pack_find1")
.create(&pool)
.await
.unwrap();
let pack2 = PackFixture::new_unique("pack_find2")
.create(&pool)
.await
.unwrap();
let trigger1 = TriggerFixture::new_unique(Some(pack1.id), Some(pack1.r#ref.clone()), "event1")
.create(&pool)
.await
.unwrap();
let trigger2 = TriggerFixture::new_unique(Some(pack2.id), Some(pack2.r#ref.clone()), "event2")
.create(&pool)
.await
.unwrap();
let runtime1 = RuntimeFixture::new_unique(Some(pack1.id), Some(pack1.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let runtime2 = RuntimeFixture::new_unique(Some(pack2.id), Some(pack2.r#ref.clone()), "nodejs")
.create(&pool)
.await
.unwrap();
// Create sensors for pack1
let sensor1 = SensorFixture::new_unique(
Some(pack1.id),
Some(pack1.r#ref.clone()),
runtime1.id,
runtime1.r#ref.clone(),
trigger1.id,
trigger1.r#ref.clone(),
"pack1_sensor1",
)
.create(&pool)
.await
.unwrap();
let sensor2 = SensorFixture::new_unique(
Some(pack1.id),
Some(pack1.r#ref.clone()),
runtime1.id,
runtime1.r#ref.clone(),
trigger1.id,
trigger1.r#ref.clone(),
"pack1_sensor2",
)
.create(&pool)
.await
.unwrap();
// Create sensor for pack2
let _sensor3 = SensorFixture::new_unique(
Some(pack2.id),
Some(pack2.r#ref.clone()),
runtime2.id,
runtime2.r#ref.clone(),
trigger2.id,
trigger2.r#ref.clone(),
"pack2_sensor",
)
.create(&pool)
.await
.unwrap();
let pack1_sensors = SensorRepository::find_by_pack(&pool, pack1.id)
.await
.unwrap();
assert_eq!(pack1_sensors.len(), 2);
assert!(pack1_sensors.iter().all(|s| s.pack == Some(pack1.id)));
assert!(pack1_sensors.iter().any(|s| s.id == sensor1.id));
assert!(pack1_sensors.iter().any(|s| s.id == sensor2.id));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_find_by_pack_no_sensors() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("empty_pack")
.create(&pool)
.await
.unwrap();
let sensors = SensorRepository::find_by_pack(&pool, pack.id)
.await
.unwrap();
assert_eq!(sensors.len(), 0);
}
// ============================================================================
// Timestamp Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_created_timestamp_set_automatically() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("timestamp_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let before = chrono::Utc::now();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"timestamp_sensor",
)
.create(&pool)
.await
.unwrap();
let after = chrono::Utc::now();
assert!(sensor.created >= before);
assert!(sensor.created <= after);
assert_eq!(sensor.created, sensor.updated); // Should be equal on creation
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_updated_timestamp_changes_on_update() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("update_time_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"update_time_sensor",
)
.create(&pool)
.await
.unwrap();
let original_updated = sensor.updated;
// Small delay to ensure timestamp changes
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let input = UpdateSensorInput {
label: Some("Updated".to_string()),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert!(updated.updated > original_updated);
assert_eq!(updated.created, sensor.created); // Created should not change
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_updated_timestamp_unchanged_on_read() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("read_time_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"read_time_sensor",
)
.create(&pool)
.await
.unwrap();
let original_updated = sensor.updated;
// Small delay
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
// Read the sensor
let found = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap()
.unwrap();
assert_eq!(found.updated, original_updated); // Should not change
}
// ============================================================================
// JSON Field Tests
// ============================================================================
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_param_schema_complex_structure() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("complex_schema_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let complex_schema = json!({
"type": "object",
"properties": {
"connection": {
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer" },
"ssl": { "type": "boolean" }
},
"required": ["host", "port"]
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": { "type": "string" },
"operator": { "enum": ["eq", "ne", "gt", "lt"] },
"value": {}
}
}
},
"poll_interval": {
"type": "integer",
"minimum": 1,
"maximum": 3600
}
},
"required": ["connection", "poll_interval"]
});
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"complex_sensor",
)
.with_param_schema(complex_schema.clone())
.create(&pool)
.await
.unwrap();
// Retrieve and verify
let found = SensorRepository::find_by_id(&pool, sensor.id)
.await
.unwrap()
.unwrap();
assert_eq!(found.param_schema, Some(complex_schema));
}
#[tokio::test]
#[ignore = "integration test — requires database"]
async fn test_param_schema_can_be_null() {
let pool = create_test_pool().await.unwrap();
let pack = PackFixture::new_unique("null_schema_pack")
.create(&pool)
.await
.unwrap();
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "event")
.create(&pool)
.await
.unwrap();
let runtime = RuntimeFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "python3")
.create(&pool)
.await
.unwrap();
let sensor = SensorFixture::new_unique(
Some(pack.id),
Some(pack.r#ref.clone()),
runtime.id,
runtime.r#ref.clone(),
trigger.id,
trigger.r#ref.clone(),
"null_schema_sensor",
)
.create(&pool)
.await
.unwrap();
assert_eq!(sensor.param_schema, None);
// Update to add schema
let schema = json!({"type": "object"});
let input = UpdateSensorInput {
param_schema: Some(Patch::Set(schema.clone())),
..Default::default()
};
let updated = SensorRepository::update(&pool, sensor.id, input)
.await
.unwrap();
assert_eq!(updated.param_schema, Some(schema));
}