Some checks failed
CI / Rustfmt (push) Successful in 23s
CI / Cargo Audit & Deny (push) Successful in 30s
CI / Web Blocking Checks (push) Successful in 48s
CI / Security Blocking Checks (push) Successful in 8s
CI / Clippy (push) Failing after 1m55s
CI / Web Advisory Checks (push) Successful in 35s
CI / Security Advisory Checks (push) Successful in 37s
CI / Tests (push) Successful in 8m5s
1779 lines
48 KiB
Rust
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, 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(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(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(schema.clone()),
|
|
..Default::default()
|
|
};
|
|
|
|
let updated = SensorRepository::update(&pool, sensor.id, input)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(updated.param_schema, Some(schema));
|
|
}
|