refactor: reorganize project into monorepo with frontend scaffolding
Convert project from single backend to monorepo structure with separate frontend (Vue 3 + TypeScript + Vite) and backend directories. Updates all configuration files and build system to support both workspaces. Ref: T007 (specs/001-modbus-relay-control)
This commit is contained in:
266
backend/tests/sqlite_repository_test.rs
Normal file
266
backend/tests/sqlite_repository_test.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
//! Integration tests for `SqliteRelayLabelRepository`.
|
||||
//!
|
||||
//! These tests verify that the SQLite repository correctly:
|
||||
//! - Creates an in-memory database
|
||||
//! - Applies schema migrations
|
||||
//! - Validates table structure and constraints
|
||||
|
||||
use sta::domain::relay::repository::RepositoryError;
|
||||
use sta::infrastructure::persistence::sqlite_repository::SqliteRelayLabelRepository;
|
||||
|
||||
/// Test that `in_memory()` successfully creates an in-memory database.
|
||||
///
|
||||
/// **T006 Requirement**: `SqliteRelayLabelRepository::in_memory()` creates in-memory DB with schema
|
||||
#[tokio::test]
|
||||
async fn test_in_memory_creates_database() {
|
||||
let result = SqliteRelayLabelRepository::in_memory().await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to create in-memory database: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the schema migration creates the `RelayLabels` table.
|
||||
///
|
||||
/// **T006 Requirement**: Verify schema is applied correctly
|
||||
#[tokio::test]
|
||||
async fn test_in_memory_applies_schema() {
|
||||
let repo = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create in-memory database");
|
||||
|
||||
// Verify the table exists by querying it
|
||||
let result: Result<(String,), sqlx::Error> =
|
||||
sqlx::query_as("SELECT name FROM sqlite_master WHERE type='table' AND name='RelayLabels'")
|
||||
.fetch_one(repo.pool())
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"RelayLabels table should exist after migration"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the `RelayLabels` table has the correct schema.
|
||||
///
|
||||
/// **T006 Requirement**: Verify table structure matches migration
|
||||
#[tokio::test]
|
||||
async fn test_relay_labels_table_structure() {
|
||||
let repo = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create in-memory database");
|
||||
|
||||
// Query table info to verify column structure
|
||||
let columns: Vec<(String, String)> =
|
||||
sqlx::query_as("SELECT name, type FROM pragma_table_info('RelayLabels') ORDER BY cid")
|
||||
.fetch_all(repo.pool())
|
||||
.await
|
||||
.expect("Failed to query table structure");
|
||||
|
||||
assert_eq!(columns.len(), 2, "RelayLabels table should have 2 columns");
|
||||
|
||||
// Verify relay_id column
|
||||
assert_eq!(columns[0].0, "relay_id", "First column should be relay_id");
|
||||
assert_eq!(columns[0].1, "INTEGER", "relay_id should be INTEGER");
|
||||
|
||||
// Verify label column
|
||||
assert_eq!(columns[1].0, "label", "Second column should be label");
|
||||
assert_eq!(columns[1].1, "TEXT", "label should be TEXT");
|
||||
}
|
||||
|
||||
/// Test that `relay_id` is the primary key.
|
||||
///
|
||||
/// **T006 Requirement**: Verify primary key constraint
|
||||
#[tokio::test]
|
||||
async fn test_relay_id_primary_key() {
|
||||
let repo = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create in-memory database");
|
||||
|
||||
// Insert first row with relay_id = 1
|
||||
let insert1: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, 'Test')")
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(insert1.is_ok(), "First insert should succeed");
|
||||
|
||||
// Try to insert duplicate relay_id = 1
|
||||
let insert2: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, 'Duplicate')")
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
insert2.is_err(),
|
||||
"Duplicate relay_id should fail due to PRIMARY KEY constraint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that `relay_id` must be between 1 and 8.
|
||||
///
|
||||
/// **T006 Requirement**: Verify CHECK constraint on relay_id range
|
||||
#[tokio::test]
|
||||
async fn test_relay_id_range_constraint() {
|
||||
let repo = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create in-memory database");
|
||||
|
||||
// Valid range: 1-8 should succeed
|
||||
for id in 1..=8 {
|
||||
let result: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (?, ?)")
|
||||
.bind(id)
|
||||
.bind(format!("Relay {}", id))
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"relay_id {} should be valid (range 1-8)",
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
// Below valid range: 0 should fail
|
||||
let result_below: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (0, 'Invalid')")
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
result_below.is_err(),
|
||||
"relay_id = 0 should fail CHECK constraint"
|
||||
);
|
||||
|
||||
// Above valid range: 9 should fail
|
||||
let result_above: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (9, 'Invalid')")
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
result_above.is_err(),
|
||||
"relay_id = 9 should fail CHECK constraint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that `label` cannot exceed 50 characters.
|
||||
///
|
||||
/// **T006 Requirement**: Verify CHECK constraint on label length
|
||||
#[tokio::test]
|
||||
async fn test_label_length_constraint() {
|
||||
let repo = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create in-memory database");
|
||||
|
||||
// Valid length: 50 characters should succeed
|
||||
let label_50 = "A".repeat(50);
|
||||
let result_valid: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, ?)")
|
||||
.bind(&label_50)
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
result_valid.is_ok(),
|
||||
"Label with 50 characters should be valid"
|
||||
);
|
||||
|
||||
// Invalid length: 51 characters should fail
|
||||
let label_51 = "B".repeat(51);
|
||||
let result_invalid: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (2, ?)")
|
||||
.bind(&label_51)
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
result_invalid.is_err(),
|
||||
"Label with 51 characters should fail CHECK constraint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that `label` cannot be NULL.
|
||||
///
|
||||
/// **T006 Requirement**: Verify NOT NULL constraint on label
|
||||
#[tokio::test]
|
||||
async fn test_label_not_null_constraint() {
|
||||
let repo = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create in-memory database");
|
||||
|
||||
// Attempt to insert NULL label
|
||||
let result: Result<sqlx::sqlite::SqliteQueryResult, sqlx::Error> =
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, NULL)")
|
||||
.execute(repo.pool())
|
||||
.await;
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"NULL label should fail NOT NULL constraint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that multiple in-memory repositories are isolated.
|
||||
///
|
||||
/// **T006 Requirement**: Verify in-memory instances are independent
|
||||
#[tokio::test]
|
||||
async fn test_multiple_in_memory_instances_isolated() {
|
||||
let repo1 = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create first in-memory database");
|
||||
|
||||
let repo2 = SqliteRelayLabelRepository::in_memory()
|
||||
.await
|
||||
.expect("Failed to create second in-memory database");
|
||||
|
||||
// Insert data into repo1
|
||||
sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, 'Repo1')")
|
||||
.execute(repo1.pool())
|
||||
.await
|
||||
.expect("Failed to insert into repo1");
|
||||
|
||||
// Verify repo2 is empty (no data from repo1)
|
||||
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM RelayLabels")
|
||||
.fetch_one(repo2.pool())
|
||||
.await
|
||||
.expect("Failed to query repo2");
|
||||
|
||||
assert_eq!(
|
||||
count.0, 0,
|
||||
"Second in-memory instance should be isolated from first"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that `new()` with file path creates a persistent database.
|
||||
///
|
||||
/// **T006 Requirement**: Verify file-based database creation
|
||||
#[tokio::test]
|
||||
async fn test_new_creates_file_database() {
|
||||
let temp_db = tempfile::NamedTempFile::new().expect("Failed to create temp file");
|
||||
let db_path = format!("sqlite://{}", temp_db.path().to_str().unwrap());
|
||||
|
||||
let result = SqliteRelayLabelRepository::new(&db_path).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to create file-based database: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
// Verify the file exists and has content
|
||||
let metadata = std::fs::metadata(temp_db.path()).expect("Database file should exist");
|
||||
assert!(metadata.len() > 0, "Database file should not be empty");
|
||||
}
|
||||
|
||||
/// Test that `new()` with invalid path returns error.
|
||||
///
|
||||
/// **T006 Requirement**: Verify error handling for invalid paths
|
||||
#[tokio::test]
|
||||
async fn test_new_invalid_path_returns_error() {
|
||||
let result =
|
||||
SqliteRelayLabelRepository::new("sqlite:///invalid/path/that/does/not/exist/db.sqlite")
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Invalid database path should return error");
|
||||
|
||||
match result {
|
||||
Err(RepositoryError::DatabaseError(_)) => {
|
||||
// Expected error type
|
||||
}
|
||||
_ => panic!("Expected RepositoryError::DatabaseError for invalid path"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user