diff --git a/api/Cargo.toml b/api/Cargo.toml index 9bb1b309f..135dce24a 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - ".", + "server", "libs/handlers", "libs/litellm", "libs/database", @@ -9,7 +9,9 @@ members = [ "libs/sharing", "libs/sql_analyzer", "libs/search", + "testkit", ] +resolver = "2" # Define shared dependencies for all workspace members [workspace.dependencies] @@ -99,90 +101,11 @@ rayon = "1.10.0" diesel_migrations = "2.0.0" html-escape = "0.2.13" -[package] -name = "bi_api" -version = "0.0.1" -edition = "2021" -default-run = "bi_api" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# Use workspace dependencies -anyhow = { workspace = true } -arrow = { workspace = true } -async-compression = { workspace = true } -async-trait = { workspace = true } -axum = { workspace = true } -base64 = { workspace = true } -bb8-redis = { workspace = true } -chrono = { workspace = true } -cohere-rust = { workspace = true } -diesel = { workspace = true } -diesel-async = { workspace = true } -diesel_migrations = { workspace = true } -dotenv = { workspace = true } -futures = { workspace = true } -futures-util = { workspace = true } -gcp-bigquery-client = { workspace = true } -html-escape = { workspace = true } -indexmap = { workspace = true } -jsonwebtoken = { workspace = true } -lazy_static = { workspace = true } -num-traits = { workspace = true } -once_cell = { workspace = true } -rand = { workspace = true } -rayon = { workspace = true } -redis = { workspace = true } -regex = { workspace = true } -reqwest = { workspace = true } -resend-rs = { workspace = true } -rustls = { workspace = true } -rustls-native-certs = { workspace = true } -sentry = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_urlencoded = { workspace = true } -serde_yaml = { workspace = true } -snowflake-api = { workspace = true } -sqlx = { workspace = true } -tempfile = { workspace = true } -tiberius = { workspace = true } -tiktoken-rs = { workspace = true } -tokio = { workspace = true } -tokio-postgres = { workspace = true } -tokio-postgres-rustls = { workspace = true } -tokio-stream = { workspace = true } -tokio-util = { workspace = true } -tower-http = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -url = { workspace = true } -uuid = { workspace = true } -sqlparser = { workspace = true } - -# Local dependencies -handlers = { path = "libs/handlers" } -litellm = { path = "libs/litellm" } -database = { path = "libs/database" } -agents = { path = "libs/agents" } -query_engine = { path = "libs/query_engine" } -braintrust = { path = "libs/braintrust" } -middleware = { path = "libs/middleware" } -sharing = { path = "libs/sharing" } -search = { path = "libs/search" } - -[dev-dependencies] -mockito = { workspace = true } -tokio-test = { workspace = true } -async-trait = { workspace = true } - [profile.release] debug = false incremental = true [profile.dev] incremental = true - opt-level = 0 # Ensure this is 0 for faster debug builds debug = 1 # Reduce debug info slightly while keeping enough for backtraces \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index ea7f2e69c..378cef7bd 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -9,7 +9,7 @@ FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release --bin bi_api +RUN cargo build --release --bin buster_server FROM debian:bookworm-slim AS runtime WORKDIR /app @@ -34,6 +34,6 @@ RUN update-ca-certificates ENV RUST_LOG=warn ENV RUST_BACKTRACE=0 -COPY --from=builder /app/target/release/bi_api . +COPY --from=builder /app/target/release/buster_server . EXPOSE 3001 -ENTRYPOINT ["./bi_api"] +ENTRYPOINT ["./buster_server"] diff --git a/api/libs/database/Cargo.toml b/api/libs/database/Cargo.toml index 66441df2c..c3e1ff866 100644 --- a/api/libs/database/Cargo.toml +++ b/api/libs/database/Cargo.toml @@ -30,4 +30,5 @@ reqwest = { workspace = true } [dev-dependencies] -tokio-test = { workspace = true } \ No newline at end of file +tokio-test = { workspace = true } +testkit = { path = "../../testkit" } \ No newline at end of file diff --git a/api/libs/database/tests/connection_test.rs b/api/libs/database/tests/connection_test.rs new file mode 100644 index 000000000..d8820d4e8 --- /dev/null +++ b/api/libs/database/tests/connection_test.rs @@ -0,0 +1,45 @@ +use anyhow::Result; + +#[tokio::test] +async fn test_database_connection() -> Result<()> { + // Get the database pool from testkit + let pool = testkit::get_pg_pool(); + + // Test the connection by getting a connection from the pool + let conn = pool.get().await?; + + // If we got here without errors, the connection is working + Ok(()) +} + +#[tokio::test] +async fn test_with_isolation() -> Result<()> { + // Get a unique test ID for data isolation + let test_id = testkit::test_id(); + + // Get database pool + let pool = testkit::get_pg_pool(); + + // Get a DB connection + let mut conn = pool.get().await?; + + // Here you would create test data with test_id for isolation + // For example: + // diesel::sql_query("INSERT INTO users (id, email, test_id) VALUES ($1, $2, $3)") + // .bind::(uuid::Uuid::new_v4()) + // .bind::("test@example.com") + // .bind::(&test_id) + // .execute(&mut conn) + // .await?; + + // Run assertions on the test data + + // Clean up after the test + // For example: + // diesel::sql_query("DELETE FROM users WHERE test_id = $1") + // .bind::(&test_id) + // .execute(&mut conn) + // .await?; + + Ok(()) +} \ No newline at end of file diff --git a/api/makefile b/api/makefile index eaf6afc45..e4a8aeb1f 100644 --- a/api/makefile +++ b/api/makefile @@ -9,7 +9,7 @@ dev: PGPASSWORD=postgres psql -h 127.0.0.1 -p 54322 -d postgres -U postgres -f libs/database/seed.sql && \ export RUST_LOG=debug export CARGO_INCREMENTAL=1 - nice cargo watch -x run + nice cargo watch -C server -x run stop: docker compose down && \ @@ -19,4 +19,4 @@ stop: fast: export RUST_LOG=debug export CARGO_INCREMENTAL=1 - nice cargo watch -x run \ No newline at end of file + nice cargo watch -C server -x run \ No newline at end of file diff --git a/api/server/Cargo.toml b/api/server/Cargo.toml new file mode 100644 index 000000000..ee9509fe2 --- /dev/null +++ b/api/server/Cargo.toml @@ -0,0 +1,77 @@ +[package] +name = "buster_server" +version = "0.0.1" +edition = "2021" +default-run = "buster_server" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# Use workspace dependencies +anyhow = { workspace = true } +arrow = { workspace = true } +async-compression = { workspace = true } +async-trait = { workspace = true } +axum = { workspace = true } +base64 = { workspace = true } +bb8-redis = { workspace = true } +chrono = { workspace = true } +cohere-rust = { workspace = true } +diesel = { workspace = true } +diesel-async = { workspace = true } +diesel_migrations = { workspace = true } +dotenv = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } +gcp-bigquery-client = { workspace = true } +html-escape = { workspace = true } +indexmap = { workspace = true } +jsonwebtoken = { workspace = true } +lazy_static = { workspace = true } +num-traits = { workspace = true } +once_cell = { workspace = true } +rand = { workspace = true } +rayon = { workspace = true } +redis = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +resend-rs = { workspace = true } +rustls = { workspace = true } +rustls-native-certs = { workspace = true } +sentry = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_urlencoded = { workspace = true } +serde_yaml = { workspace = true } +snowflake-api = { workspace = true } +sqlx = { workspace = true } +tempfile = { workspace = true } +tiberius = { workspace = true } +tiktoken-rs = { workspace = true } +tokio = { workspace = true } +tokio-postgres = { workspace = true } +tokio-postgres-rustls = { workspace = true } +tokio-stream = { workspace = true } +tokio-util = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } +sqlparser = { workspace = true } + +# Local dependencies +handlers = { path = "../libs/handlers" } +litellm = { path = "../libs/litellm" } +database = { path = "../libs/database" } +agents = { path = "../libs/agents" } +query_engine = { path = "../libs/query_engine" } +braintrust = { path = "../libs/braintrust" } +middleware = { path = "../libs/middleware" } +sharing = { path = "../libs/sharing" } +search = { path = "../libs/search" } + +[dev-dependencies] +mockito = { workspace = true } +tokio-test = { workspace = true } +async-trait = { workspace = true } \ No newline at end of file diff --git a/api/server/README.md b/api/server/README.md new file mode 100644 index 000000000..66ef60f87 --- /dev/null +++ b/api/server/README.md @@ -0,0 +1,35 @@ +# Buster Server + +This directory contains the main server code for the Buster API. It provides the API endpoints, WebSocket handlers, and application logic for the Buster application. + +## Structure + +- `src/` - Main server code + - `routes/` - API endpoints (REST, WebSocket) + - `utils/` - Shared utilities + - `types/` - Common type definitions + +## Development + +To run the server in development mode: + +```bash +# From the project root +make dev + +# Or to run with faster feedback loop +make fast +``` + +## Dependencies + +The server depends on the following local libraries: + +- `database` - Database access and models +- `handlers` - Business logic handlers +- `middleware` - HTTP middleware components +- `query_engine` - SQL query engine +- `sharing` - Asset sharing functionality +- `search` - Search functionality + +All dependencies are inherited from the workspace Cargo.toml. \ No newline at end of file diff --git a/api/src/main.rs b/api/server/src/main.rs similarity index 100% rename from api/src/main.rs rename to api/server/src/main.rs diff --git a/api/src/routes/mod.rs b/api/server/src/routes/mod.rs similarity index 100% rename from api/src/routes/mod.rs rename to api/server/src/routes/mod.rs diff --git a/api/src/routes/rest/mod.rs b/api/server/src/routes/rest/mod.rs similarity index 100% rename from api/src/routes/rest/mod.rs rename to api/server/src/routes/rest/mod.rs diff --git a/api/src/routes/rest/routes/api_keys/delete_api_key.rs b/api/server/src/routes/rest/routes/api_keys/delete_api_key.rs similarity index 100% rename from api/src/routes/rest/routes/api_keys/delete_api_key.rs rename to api/server/src/routes/rest/routes/api_keys/delete_api_key.rs diff --git a/api/src/routes/rest/routes/api_keys/get_api_key.rs b/api/server/src/routes/rest/routes/api_keys/get_api_key.rs similarity index 100% rename from api/src/routes/rest/routes/api_keys/get_api_key.rs rename to api/server/src/routes/rest/routes/api_keys/get_api_key.rs diff --git a/api/src/routes/rest/routes/api_keys/list_api_keys.rs b/api/server/src/routes/rest/routes/api_keys/list_api_keys.rs similarity index 100% rename from api/src/routes/rest/routes/api_keys/list_api_keys.rs rename to api/server/src/routes/rest/routes/api_keys/list_api_keys.rs diff --git a/api/src/routes/rest/routes/api_keys/mod.rs b/api/server/src/routes/rest/routes/api_keys/mod.rs similarity index 100% rename from api/src/routes/rest/routes/api_keys/mod.rs rename to api/server/src/routes/rest/routes/api_keys/mod.rs diff --git a/api/src/routes/rest/routes/api_keys/post_api_key.rs b/api/server/src/routes/rest/routes/api_keys/post_api_key.rs similarity index 100% rename from api/src/routes/rest/routes/api_keys/post_api_key.rs rename to api/server/src/routes/rest/routes/api_keys/post_api_key.rs diff --git a/api/src/routes/rest/routes/api_keys/validate_api_key.rs b/api/server/src/routes/rest/routes/api_keys/validate_api_key.rs similarity index 100% rename from api/src/routes/rest/routes/api_keys/validate_api_key.rs rename to api/server/src/routes/rest/routes/api_keys/validate_api_key.rs diff --git a/api/src/routes/rest/routes/assets/get_asset_access.rs b/api/server/src/routes/rest/routes/assets/get_asset_access.rs similarity index 100% rename from api/src/routes/rest/routes/assets/get_asset_access.rs rename to api/server/src/routes/rest/routes/assets/get_asset_access.rs diff --git a/api/src/routes/rest/routes/assets/mod.rs b/api/server/src/routes/rest/routes/assets/mod.rs similarity index 100% rename from api/src/routes/rest/routes/assets/mod.rs rename to api/server/src/routes/rest/routes/assets/mod.rs diff --git a/api/src/routes/rest/routes/chats/delete_chats.rs b/api/server/src/routes/rest/routes/chats/delete_chats.rs similarity index 100% rename from api/src/routes/rest/routes/chats/delete_chats.rs rename to api/server/src/routes/rest/routes/chats/delete_chats.rs diff --git a/api/src/routes/rest/routes/chats/duplicate_chat.rs b/api/server/src/routes/rest/routes/chats/duplicate_chat.rs similarity index 100% rename from api/src/routes/rest/routes/chats/duplicate_chat.rs rename to api/server/src/routes/rest/routes/chats/duplicate_chat.rs diff --git a/api/src/routes/rest/routes/chats/get_chat.rs b/api/server/src/routes/rest/routes/chats/get_chat.rs similarity index 100% rename from api/src/routes/rest/routes/chats/get_chat.rs rename to api/server/src/routes/rest/routes/chats/get_chat.rs diff --git a/api/src/routes/rest/routes/chats/get_chat_raw_llm_messages.rs b/api/server/src/routes/rest/routes/chats/get_chat_raw_llm_messages.rs similarity index 100% rename from api/src/routes/rest/routes/chats/get_chat_raw_llm_messages.rs rename to api/server/src/routes/rest/routes/chats/get_chat_raw_llm_messages.rs diff --git a/api/src/routes/rest/routes/chats/list_chats.rs b/api/server/src/routes/rest/routes/chats/list_chats.rs similarity index 100% rename from api/src/routes/rest/routes/chats/list_chats.rs rename to api/server/src/routes/rest/routes/chats/list_chats.rs diff --git a/api/src/routes/rest/routes/chats/mod.rs b/api/server/src/routes/rest/routes/chats/mod.rs similarity index 100% rename from api/src/routes/rest/routes/chats/mod.rs rename to api/server/src/routes/rest/routes/chats/mod.rs diff --git a/api/src/routes/rest/routes/chats/post_chat.rs b/api/server/src/routes/rest/routes/chats/post_chat.rs similarity index 100% rename from api/src/routes/rest/routes/chats/post_chat.rs rename to api/server/src/routes/rest/routes/chats/post_chat.rs diff --git a/api/src/routes/rest/routes/chats/restore_chat.rs b/api/server/src/routes/rest/routes/chats/restore_chat.rs similarity index 100% rename from api/src/routes/rest/routes/chats/restore_chat.rs rename to api/server/src/routes/rest/routes/chats/restore_chat.rs diff --git a/api/src/routes/rest/routes/chats/sharing/create_sharing.rs b/api/server/src/routes/rest/routes/chats/sharing/create_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/chats/sharing/create_sharing.rs rename to api/server/src/routes/rest/routes/chats/sharing/create_sharing.rs diff --git a/api/src/routes/rest/routes/chats/sharing/delete_sharing.rs b/api/server/src/routes/rest/routes/chats/sharing/delete_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/chats/sharing/delete_sharing.rs rename to api/server/src/routes/rest/routes/chats/sharing/delete_sharing.rs diff --git a/api/src/routes/rest/routes/chats/sharing/list_sharing.rs b/api/server/src/routes/rest/routes/chats/sharing/list_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/chats/sharing/list_sharing.rs rename to api/server/src/routes/rest/routes/chats/sharing/list_sharing.rs diff --git a/api/src/routes/rest/routes/chats/sharing/mod.rs b/api/server/src/routes/rest/routes/chats/sharing/mod.rs similarity index 100% rename from api/src/routes/rest/routes/chats/sharing/mod.rs rename to api/server/src/routes/rest/routes/chats/sharing/mod.rs diff --git a/api/src/routes/rest/routes/chats/sharing/update_sharing.rs b/api/server/src/routes/rest/routes/chats/sharing/update_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/chats/sharing/update_sharing.rs rename to api/server/src/routes/rest/routes/chats/sharing/update_sharing.rs diff --git a/api/src/routes/rest/routes/chats/update_chat.rs b/api/server/src/routes/rest/routes/chats/update_chat.rs similarity index 100% rename from api/src/routes/rest/routes/chats/update_chat.rs rename to api/server/src/routes/rest/routes/chats/update_chat.rs diff --git a/api/src/routes/rest/routes/chats/update_chats.rs b/api/server/src/routes/rest/routes/chats/update_chats.rs similarity index 100% rename from api/src/routes/rest/routes/chats/update_chats.rs rename to api/server/src/routes/rest/routes/chats/update_chats.rs diff --git a/api/src/routes/rest/routes/collections/add_assets_to_collection.rs b/api/server/src/routes/rest/routes/collections/add_assets_to_collection.rs similarity index 100% rename from api/src/routes/rest/routes/collections/add_assets_to_collection.rs rename to api/server/src/routes/rest/routes/collections/add_assets_to_collection.rs diff --git a/api/src/routes/rest/routes/collections/create_collection.rs b/api/server/src/routes/rest/routes/collections/create_collection.rs similarity index 100% rename from api/src/routes/rest/routes/collections/create_collection.rs rename to api/server/src/routes/rest/routes/collections/create_collection.rs diff --git a/api/src/routes/rest/routes/collections/delete_collection.rs b/api/server/src/routes/rest/routes/collections/delete_collection.rs similarity index 100% rename from api/src/routes/rest/routes/collections/delete_collection.rs rename to api/server/src/routes/rest/routes/collections/delete_collection.rs diff --git a/api/src/routes/rest/routes/collections/get_collection.rs b/api/server/src/routes/rest/routes/collections/get_collection.rs similarity index 100% rename from api/src/routes/rest/routes/collections/get_collection.rs rename to api/server/src/routes/rest/routes/collections/get_collection.rs diff --git a/api/src/routes/rest/routes/collections/list_collections.rs b/api/server/src/routes/rest/routes/collections/list_collections.rs similarity index 100% rename from api/src/routes/rest/routes/collections/list_collections.rs rename to api/server/src/routes/rest/routes/collections/list_collections.rs diff --git a/api/src/routes/rest/routes/collections/mod.rs b/api/server/src/routes/rest/routes/collections/mod.rs similarity index 100% rename from api/src/routes/rest/routes/collections/mod.rs rename to api/server/src/routes/rest/routes/collections/mod.rs diff --git a/api/src/routes/rest/routes/collections/remove_assets_from_collection.rs b/api/server/src/routes/rest/routes/collections/remove_assets_from_collection.rs similarity index 100% rename from api/src/routes/rest/routes/collections/remove_assets_from_collection.rs rename to api/server/src/routes/rest/routes/collections/remove_assets_from_collection.rs diff --git a/api/src/routes/rest/routes/collections/sharing/create_sharing.rs b/api/server/src/routes/rest/routes/collections/sharing/create_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/collections/sharing/create_sharing.rs rename to api/server/src/routes/rest/routes/collections/sharing/create_sharing.rs diff --git a/api/src/routes/rest/routes/collections/sharing/delete_sharing.rs b/api/server/src/routes/rest/routes/collections/sharing/delete_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/collections/sharing/delete_sharing.rs rename to api/server/src/routes/rest/routes/collections/sharing/delete_sharing.rs diff --git a/api/src/routes/rest/routes/collections/sharing/list_sharing.rs b/api/server/src/routes/rest/routes/collections/sharing/list_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/collections/sharing/list_sharing.rs rename to api/server/src/routes/rest/routes/collections/sharing/list_sharing.rs diff --git a/api/src/routes/rest/routes/collections/sharing/mod.rs b/api/server/src/routes/rest/routes/collections/sharing/mod.rs similarity index 100% rename from api/src/routes/rest/routes/collections/sharing/mod.rs rename to api/server/src/routes/rest/routes/collections/sharing/mod.rs diff --git a/api/src/routes/rest/routes/collections/sharing/update_sharing.rs b/api/server/src/routes/rest/routes/collections/sharing/update_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/collections/sharing/update_sharing.rs rename to api/server/src/routes/rest/routes/collections/sharing/update_sharing.rs diff --git a/api/src/routes/rest/routes/collections/update_collection.rs b/api/server/src/routes/rest/routes/collections/update_collection.rs similarity index 100% rename from api/src/routes/rest/routes/collections/update_collection.rs rename to api/server/src/routes/rest/routes/collections/update_collection.rs diff --git a/api/src/routes/rest/routes/dashboards/create_dashboard.rs b/api/server/src/routes/rest/routes/dashboards/create_dashboard.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/create_dashboard.rs rename to api/server/src/routes/rest/routes/dashboards/create_dashboard.rs diff --git a/api/src/routes/rest/routes/dashboards/delete_dashboard.rs b/api/server/src/routes/rest/routes/dashboards/delete_dashboard.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/delete_dashboard.rs rename to api/server/src/routes/rest/routes/dashboards/delete_dashboard.rs diff --git a/api/src/routes/rest/routes/dashboards/get_dashboard.rs b/api/server/src/routes/rest/routes/dashboards/get_dashboard.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/get_dashboard.rs rename to api/server/src/routes/rest/routes/dashboards/get_dashboard.rs diff --git a/api/src/routes/rest/routes/dashboards/list_dashboards.rs b/api/server/src/routes/rest/routes/dashboards/list_dashboards.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/list_dashboards.rs rename to api/server/src/routes/rest/routes/dashboards/list_dashboards.rs diff --git a/api/src/routes/rest/routes/dashboards/mod.rs b/api/server/src/routes/rest/routes/dashboards/mod.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/mod.rs rename to api/server/src/routes/rest/routes/dashboards/mod.rs diff --git a/api/src/routes/rest/routes/dashboards/sharing/create_sharing.rs b/api/server/src/routes/rest/routes/dashboards/sharing/create_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/sharing/create_sharing.rs rename to api/server/src/routes/rest/routes/dashboards/sharing/create_sharing.rs diff --git a/api/src/routes/rest/routes/dashboards/sharing/delete_sharing.rs b/api/server/src/routes/rest/routes/dashboards/sharing/delete_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/sharing/delete_sharing.rs rename to api/server/src/routes/rest/routes/dashboards/sharing/delete_sharing.rs diff --git a/api/src/routes/rest/routes/dashboards/sharing/list_sharing.rs b/api/server/src/routes/rest/routes/dashboards/sharing/list_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/sharing/list_sharing.rs rename to api/server/src/routes/rest/routes/dashboards/sharing/list_sharing.rs diff --git a/api/src/routes/rest/routes/dashboards/sharing/mod.rs b/api/server/src/routes/rest/routes/dashboards/sharing/mod.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/sharing/mod.rs rename to api/server/src/routes/rest/routes/dashboards/sharing/mod.rs diff --git a/api/src/routes/rest/routes/dashboards/sharing/update_sharing.rs b/api/server/src/routes/rest/routes/dashboards/sharing/update_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/sharing/update_sharing.rs rename to api/server/src/routes/rest/routes/dashboards/sharing/update_sharing.rs diff --git a/api/src/routes/rest/routes/dashboards/update_dashboard.rs b/api/server/src/routes/rest/routes/dashboards/update_dashboard.rs similarity index 100% rename from api/src/routes/rest/routes/dashboards/update_dashboard.rs rename to api/server/src/routes/rest/routes/dashboards/update_dashboard.rs diff --git a/api/src/routes/rest/routes/data_sources/create_data_source.rs b/api/server/src/routes/rest/routes/data_sources/create_data_source.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/create_data_source.rs rename to api/server/src/routes/rest/routes/data_sources/create_data_source.rs diff --git a/api/src/routes/rest/routes/data_sources/delete_data_source.rs b/api/server/src/routes/rest/routes/data_sources/delete_data_source.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/delete_data_source.rs rename to api/server/src/routes/rest/routes/data_sources/delete_data_source.rs diff --git a/api/src/routes/rest/routes/data_sources/get_data_source.rs b/api/server/src/routes/rest/routes/data_sources/get_data_source.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/get_data_source.rs rename to api/server/src/routes/rest/routes/data_sources/get_data_source.rs diff --git a/api/src/routes/rest/routes/data_sources/list_data_sources.rs b/api/server/src/routes/rest/routes/data_sources/list_data_sources.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/list_data_sources.rs rename to api/server/src/routes/rest/routes/data_sources/list_data_sources.rs diff --git a/api/src/routes/rest/routes/data_sources/mod.rs b/api/server/src/routes/rest/routes/data_sources/mod.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/mod.rs rename to api/server/src/routes/rest/routes/data_sources/mod.rs diff --git a/api/src/routes/rest/routes/data_sources/post_data_sources.rs b/api/server/src/routes/rest/routes/data_sources/post_data_sources.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/post_data_sources.rs rename to api/server/src/routes/rest/routes/data_sources/post_data_sources.rs diff --git a/api/src/routes/rest/routes/data_sources/update_data_source.rs b/api/server/src/routes/rest/routes/data_sources/update_data_source.rs similarity index 100% rename from api/src/routes/rest/routes/data_sources/update_data_source.rs rename to api/server/src/routes/rest/routes/data_sources/update_data_source.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/list_datasets.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/list_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/list_datasets.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/list_datasets.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/list_permission_groups.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/list_permission_groups.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/list_permission_groups.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/list_permission_groups.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/list_users.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/list_users.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/list_users.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/list_users.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/mod.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/mod.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/mod.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/mod.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/put_datasets.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/put_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/put_datasets.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/put_datasets.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/put_permission_groups.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/put_permission_groups.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/put_permission_groups.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/put_permission_groups.rs diff --git a/api/src/routes/rest/routes/dataset_groups/assets/put_users.rs b/api/server/src/routes/rest/routes/dataset_groups/assets/put_users.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/assets/put_users.rs rename to api/server/src/routes/rest/routes/dataset_groups/assets/put_users.rs diff --git a/api/src/routes/rest/routes/dataset_groups/delete_dataset_group.rs b/api/server/src/routes/rest/routes/dataset_groups/delete_dataset_group.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/delete_dataset_group.rs rename to api/server/src/routes/rest/routes/dataset_groups/delete_dataset_group.rs diff --git a/api/src/routes/rest/routes/dataset_groups/get_dataset_group.rs b/api/server/src/routes/rest/routes/dataset_groups/get_dataset_group.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/get_dataset_group.rs rename to api/server/src/routes/rest/routes/dataset_groups/get_dataset_group.rs diff --git a/api/src/routes/rest/routes/dataset_groups/list_dataset_groups.rs b/api/server/src/routes/rest/routes/dataset_groups/list_dataset_groups.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/list_dataset_groups.rs rename to api/server/src/routes/rest/routes/dataset_groups/list_dataset_groups.rs diff --git a/api/src/routes/rest/routes/dataset_groups/mod.rs b/api/server/src/routes/rest/routes/dataset_groups/mod.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/mod.rs rename to api/server/src/routes/rest/routes/dataset_groups/mod.rs diff --git a/api/src/routes/rest/routes/dataset_groups/post_dataset_group.rs b/api/server/src/routes/rest/routes/dataset_groups/post_dataset_group.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/post_dataset_group.rs rename to api/server/src/routes/rest/routes/dataset_groups/post_dataset_group.rs diff --git a/api/src/routes/rest/routes/dataset_groups/put_dataset_group.rs b/api/server/src/routes/rest/routes/dataset_groups/put_dataset_group.rs similarity index 100% rename from api/src/routes/rest/routes/dataset_groups/put_dataset_group.rs rename to api/server/src/routes/rest/routes/dataset_groups/put_dataset_group.rs diff --git a/api/src/routes/rest/routes/datasets/assets/get_dataset_overview.rs b/api/server/src/routes/rest/routes/datasets/assets/get_dataset_overview.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/assets/get_dataset_overview.rs rename to api/server/src/routes/rest/routes/datasets/assets/get_dataset_overview.rs diff --git a/api/src/routes/rest/routes/datasets/assets/list_dataset_assets.rs b/api/server/src/routes/rest/routes/datasets/assets/list_dataset_assets.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/assets/list_dataset_assets.rs rename to api/server/src/routes/rest/routes/datasets/assets/list_dataset_assets.rs diff --git a/api/src/routes/rest/routes/datasets/assets/mod.rs b/api/server/src/routes/rest/routes/datasets/assets/mod.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/assets/mod.rs rename to api/server/src/routes/rest/routes/datasets/assets/mod.rs diff --git a/api/src/routes/rest/routes/datasets/assets/put_dataset_assets.rs b/api/server/src/routes/rest/routes/datasets/assets/put_dataset_assets.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/assets/put_dataset_assets.rs rename to api/server/src/routes/rest/routes/datasets/assets/put_dataset_assets.rs diff --git a/api/src/routes/rest/routes/datasets/delete_dataset.rs b/api/server/src/routes/rest/routes/datasets/delete_dataset.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/delete_dataset.rs rename to api/server/src/routes/rest/routes/datasets/delete_dataset.rs diff --git a/api/src/routes/rest/routes/datasets/deploy_datasets.rs b/api/server/src/routes/rest/routes/datasets/deploy_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/deploy_datasets.rs rename to api/server/src/routes/rest/routes/datasets/deploy_datasets.rs diff --git a/api/src/routes/rest/routes/datasets/generate_datasets.rs b/api/server/src/routes/rest/routes/datasets/generate_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/generate_datasets.rs rename to api/server/src/routes/rest/routes/datasets/generate_datasets.rs diff --git a/api/src/routes/rest/routes/datasets/get_dataset.rs b/api/server/src/routes/rest/routes/datasets/get_dataset.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/get_dataset.rs rename to api/server/src/routes/rest/routes/datasets/get_dataset.rs diff --git a/api/src/routes/rest/routes/datasets/get_dataset_data_sample.rs b/api/server/src/routes/rest/routes/datasets/get_dataset_data_sample.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/get_dataset_data_sample.rs rename to api/server/src/routes/rest/routes/datasets/get_dataset_data_sample.rs diff --git a/api/src/routes/rest/routes/datasets/list_datasets.rs b/api/server/src/routes/rest/routes/datasets/list_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/list_datasets.rs rename to api/server/src/routes/rest/routes/datasets/list_datasets.rs diff --git a/api/src/routes/rest/routes/datasets/mod.rs b/api/server/src/routes/rest/routes/datasets/mod.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/mod.rs rename to api/server/src/routes/rest/routes/datasets/mod.rs diff --git a/api/src/routes/rest/routes/datasets/post_dataset.rs b/api/server/src/routes/rest/routes/datasets/post_dataset.rs similarity index 100% rename from api/src/routes/rest/routes/datasets/post_dataset.rs rename to api/server/src/routes/rest/routes/datasets/post_dataset.rs diff --git a/api/src/routes/rest/routes/helpers/mod.rs b/api/server/src/routes/rest/routes/helpers/mod.rs similarity index 100% rename from api/src/routes/rest/routes/helpers/mod.rs rename to api/server/src/routes/rest/routes/helpers/mod.rs diff --git a/api/src/routes/rest/routes/helpers/search_data_catalog.rs b/api/server/src/routes/rest/routes/helpers/search_data_catalog.rs similarity index 100% rename from api/src/routes/rest/routes/helpers/search_data_catalog.rs rename to api/server/src/routes/rest/routes/helpers/search_data_catalog.rs diff --git a/api/src/routes/rest/routes/logs/list_logs.rs b/api/server/src/routes/rest/routes/logs/list_logs.rs similarity index 100% rename from api/src/routes/rest/routes/logs/list_logs.rs rename to api/server/src/routes/rest/routes/logs/list_logs.rs diff --git a/api/src/routes/rest/routes/logs/mod.rs b/api/server/src/routes/rest/routes/logs/mod.rs similarity index 100% rename from api/src/routes/rest/routes/logs/mod.rs rename to api/server/src/routes/rest/routes/logs/mod.rs diff --git a/api/src/routes/rest/routes/messages/delete_message.rs b/api/server/src/routes/rest/routes/messages/delete_message.rs similarity index 100% rename from api/src/routes/rest/routes/messages/delete_message.rs rename to api/server/src/routes/rest/routes/messages/delete_message.rs diff --git a/api/src/routes/rest/routes/messages/mod.rs b/api/server/src/routes/rest/routes/messages/mod.rs similarity index 100% rename from api/src/routes/rest/routes/messages/mod.rs rename to api/server/src/routes/rest/routes/messages/mod.rs diff --git a/api/src/routes/rest/routes/messages/update_message.rs b/api/server/src/routes/rest/routes/messages/update_message.rs similarity index 100% rename from api/src/routes/rest/routes/messages/update_message.rs rename to api/server/src/routes/rest/routes/messages/update_message.rs diff --git a/api/src/routes/rest/routes/metrics/delete_metric.rs b/api/server/src/routes/rest/routes/metrics/delete_metric.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/delete_metric.rs rename to api/server/src/routes/rest/routes/metrics/delete_metric.rs diff --git a/api/src/routes/rest/routes/metrics/get_metric.rs b/api/server/src/routes/rest/routes/metrics/get_metric.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/get_metric.rs rename to api/server/src/routes/rest/routes/metrics/get_metric.rs diff --git a/api/src/routes/rest/routes/metrics/get_metric_data.rs b/api/server/src/routes/rest/routes/metrics/get_metric_data.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/get_metric_data.rs rename to api/server/src/routes/rest/routes/metrics/get_metric_data.rs diff --git a/api/src/routes/rest/routes/metrics/list_metrics.rs b/api/server/src/routes/rest/routes/metrics/list_metrics.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/list_metrics.rs rename to api/server/src/routes/rest/routes/metrics/list_metrics.rs diff --git a/api/src/routes/rest/routes/metrics/mod.rs b/api/server/src/routes/rest/routes/metrics/mod.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/mod.rs rename to api/server/src/routes/rest/routes/metrics/mod.rs diff --git a/api/src/routes/rest/routes/metrics/sharing/create_sharing.rs b/api/server/src/routes/rest/routes/metrics/sharing/create_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/sharing/create_sharing.rs rename to api/server/src/routes/rest/routes/metrics/sharing/create_sharing.rs diff --git a/api/src/routes/rest/routes/metrics/sharing/delete_sharing.rs b/api/server/src/routes/rest/routes/metrics/sharing/delete_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/sharing/delete_sharing.rs rename to api/server/src/routes/rest/routes/metrics/sharing/delete_sharing.rs diff --git a/api/src/routes/rest/routes/metrics/sharing/list_sharing.rs b/api/server/src/routes/rest/routes/metrics/sharing/list_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/sharing/list_sharing.rs rename to api/server/src/routes/rest/routes/metrics/sharing/list_sharing.rs diff --git a/api/src/routes/rest/routes/metrics/sharing/mod.rs b/api/server/src/routes/rest/routes/metrics/sharing/mod.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/sharing/mod.rs rename to api/server/src/routes/rest/routes/metrics/sharing/mod.rs diff --git a/api/src/routes/rest/routes/metrics/sharing/update_sharing.rs b/api/server/src/routes/rest/routes/metrics/sharing/update_sharing.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/sharing/update_sharing.rs rename to api/server/src/routes/rest/routes/metrics/sharing/update_sharing.rs diff --git a/api/src/routes/rest/routes/metrics/update_metric.rs b/api/server/src/routes/rest/routes/metrics/update_metric.rs similarity index 100% rename from api/src/routes/rest/routes/metrics/update_metric.rs rename to api/server/src/routes/rest/routes/metrics/update_metric.rs diff --git a/api/src/routes/rest/routes/mod.rs b/api/server/src/routes/rest/routes/mod.rs similarity index 100% rename from api/src/routes/rest/routes/mod.rs rename to api/server/src/routes/rest/routes/mod.rs diff --git a/api/src/routes/rest/routes/organizations/mod.rs b/api/server/src/routes/rest/routes/organizations/mod.rs similarity index 100% rename from api/src/routes/rest/routes/organizations/mod.rs rename to api/server/src/routes/rest/routes/organizations/mod.rs diff --git a/api/src/routes/rest/routes/organizations/update_organization.rs b/api/server/src/routes/rest/routes/organizations/update_organization.rs similarity index 100% rename from api/src/routes/rest/routes/organizations/update_organization.rs rename to api/server/src/routes/rest/routes/organizations/update_organization.rs diff --git a/api/src/routes/rest/routes/organizations/users.rs b/api/server/src/routes/rest/routes/organizations/users.rs similarity index 100% rename from api/src/routes/rest/routes/organizations/users.rs rename to api/server/src/routes/rest/routes/organizations/users.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/list_dataset_groups.rs b/api/server/src/routes/rest/routes/permission_groups/assets/list_dataset_groups.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/list_dataset_groups.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/list_dataset_groups.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/list_datasets.rs b/api/server/src/routes/rest/routes/permission_groups/assets/list_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/list_datasets.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/list_datasets.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/list_users.rs b/api/server/src/routes/rest/routes/permission_groups/assets/list_users.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/list_users.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/list_users.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/mod.rs b/api/server/src/routes/rest/routes/permission_groups/assets/mod.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/mod.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/mod.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/put_dataset_groups.rs b/api/server/src/routes/rest/routes/permission_groups/assets/put_dataset_groups.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/put_dataset_groups.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/put_dataset_groups.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/put_datasets.rs b/api/server/src/routes/rest/routes/permission_groups/assets/put_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/put_datasets.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/put_datasets.rs diff --git a/api/src/routes/rest/routes/permission_groups/assets/put_users.rs b/api/server/src/routes/rest/routes/permission_groups/assets/put_users.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/assets/put_users.rs rename to api/server/src/routes/rest/routes/permission_groups/assets/put_users.rs diff --git a/api/src/routes/rest/routes/permission_groups/delete_permission_group.rs b/api/server/src/routes/rest/routes/permission_groups/delete_permission_group.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/delete_permission_group.rs rename to api/server/src/routes/rest/routes/permission_groups/delete_permission_group.rs diff --git a/api/src/routes/rest/routes/permission_groups/get_permission_group.rs b/api/server/src/routes/rest/routes/permission_groups/get_permission_group.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/get_permission_group.rs rename to api/server/src/routes/rest/routes/permission_groups/get_permission_group.rs diff --git a/api/src/routes/rest/routes/permission_groups/list_permission_groups.rs b/api/server/src/routes/rest/routes/permission_groups/list_permission_groups.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/list_permission_groups.rs rename to api/server/src/routes/rest/routes/permission_groups/list_permission_groups.rs diff --git a/api/src/routes/rest/routes/permission_groups/mod.rs b/api/server/src/routes/rest/routes/permission_groups/mod.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/mod.rs rename to api/server/src/routes/rest/routes/permission_groups/mod.rs diff --git a/api/src/routes/rest/routes/permission_groups/post_permission_group.rs b/api/server/src/routes/rest/routes/permission_groups/post_permission_group.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/post_permission_group.rs rename to api/server/src/routes/rest/routes/permission_groups/post_permission_group.rs diff --git a/api/src/routes/rest/routes/permission_groups/put_permission_group.rs b/api/server/src/routes/rest/routes/permission_groups/put_permission_group.rs similarity index 100% rename from api/src/routes/rest/routes/permission_groups/put_permission_group.rs rename to api/server/src/routes/rest/routes/permission_groups/put_permission_group.rs diff --git a/api/src/routes/rest/routes/search/mod.rs b/api/server/src/routes/rest/routes/search/mod.rs similarity index 100% rename from api/src/routes/rest/routes/search/mod.rs rename to api/server/src/routes/rest/routes/search/mod.rs diff --git a/api/src/routes/rest/routes/search/search.rs b/api/server/src/routes/rest/routes/search/search.rs similarity index 100% rename from api/src/routes/rest/routes/search/search.rs rename to api/server/src/routes/rest/routes/search/search.rs diff --git a/api/src/routes/rest/routes/sql/mod.rs b/api/server/src/routes/rest/routes/sql/mod.rs similarity index 100% rename from api/src/routes/rest/routes/sql/mod.rs rename to api/server/src/routes/rest/routes/sql/mod.rs diff --git a/api/src/routes/rest/routes/sql/run_sql.rs b/api/server/src/routes/rest/routes/sql/run_sql.rs similarity index 100% rename from api/src/routes/rest/routes/sql/run_sql.rs rename to api/server/src/routes/rest/routes/sql/run_sql.rs diff --git a/api/src/routes/rest/routes/users/assets/list_attributes.rs b/api/server/src/routes/rest/routes/users/assets/list_attributes.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/list_attributes.rs rename to api/server/src/routes/rest/routes/users/assets/list_attributes.rs diff --git a/api/src/routes/rest/routes/users/assets/list_dataset_groups.rs b/api/server/src/routes/rest/routes/users/assets/list_dataset_groups.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/list_dataset_groups.rs rename to api/server/src/routes/rest/routes/users/assets/list_dataset_groups.rs diff --git a/api/src/routes/rest/routes/users/assets/list_datasets.rs b/api/server/src/routes/rest/routes/users/assets/list_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/list_datasets.rs rename to api/server/src/routes/rest/routes/users/assets/list_datasets.rs diff --git a/api/src/routes/rest/routes/users/assets/list_permission_groups.rs b/api/server/src/routes/rest/routes/users/assets/list_permission_groups.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/list_permission_groups.rs rename to api/server/src/routes/rest/routes/users/assets/list_permission_groups.rs diff --git a/api/src/routes/rest/routes/users/assets/list_teams.rs b/api/server/src/routes/rest/routes/users/assets/list_teams.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/list_teams.rs rename to api/server/src/routes/rest/routes/users/assets/list_teams.rs diff --git a/api/src/routes/rest/routes/users/assets/mod.rs b/api/server/src/routes/rest/routes/users/assets/mod.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/mod.rs rename to api/server/src/routes/rest/routes/users/assets/mod.rs diff --git a/api/src/routes/rest/routes/users/assets/put_dataset_groups.rs b/api/server/src/routes/rest/routes/users/assets/put_dataset_groups.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/put_dataset_groups.rs rename to api/server/src/routes/rest/routes/users/assets/put_dataset_groups.rs diff --git a/api/src/routes/rest/routes/users/assets/put_datasets.rs b/api/server/src/routes/rest/routes/users/assets/put_datasets.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/put_datasets.rs rename to api/server/src/routes/rest/routes/users/assets/put_datasets.rs diff --git a/api/src/routes/rest/routes/users/assets/put_permission_groups.rs b/api/server/src/routes/rest/routes/users/assets/put_permission_groups.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/put_permission_groups.rs rename to api/server/src/routes/rest/routes/users/assets/put_permission_groups.rs diff --git a/api/src/routes/rest/routes/users/assets/put_teams.rs b/api/server/src/routes/rest/routes/users/assets/put_teams.rs similarity index 100% rename from api/src/routes/rest/routes/users/assets/put_teams.rs rename to api/server/src/routes/rest/routes/users/assets/put_teams.rs diff --git a/api/src/routes/rest/routes/users/favorites/create_favorite.rs b/api/server/src/routes/rest/routes/users/favorites/create_favorite.rs similarity index 100% rename from api/src/routes/rest/routes/users/favorites/create_favorite.rs rename to api/server/src/routes/rest/routes/users/favorites/create_favorite.rs diff --git a/api/src/routes/rest/routes/users/favorites/delete_favorite.rs b/api/server/src/routes/rest/routes/users/favorites/delete_favorite.rs similarity index 100% rename from api/src/routes/rest/routes/users/favorites/delete_favorite.rs rename to api/server/src/routes/rest/routes/users/favorites/delete_favorite.rs diff --git a/api/src/routes/rest/routes/users/favorites/list_favorites.rs b/api/server/src/routes/rest/routes/users/favorites/list_favorites.rs similarity index 100% rename from api/src/routes/rest/routes/users/favorites/list_favorites.rs rename to api/server/src/routes/rest/routes/users/favorites/list_favorites.rs diff --git a/api/src/routes/rest/routes/users/favorites/mod.rs b/api/server/src/routes/rest/routes/users/favorites/mod.rs similarity index 100% rename from api/src/routes/rest/routes/users/favorites/mod.rs rename to api/server/src/routes/rest/routes/users/favorites/mod.rs diff --git a/api/src/routes/rest/routes/users/favorites/update_favorites.rs b/api/server/src/routes/rest/routes/users/favorites/update_favorites.rs similarity index 100% rename from api/src/routes/rest/routes/users/favorites/update_favorites.rs rename to api/server/src/routes/rest/routes/users/favorites/update_favorites.rs diff --git a/api/src/routes/rest/routes/users/get_user.rs b/api/server/src/routes/rest/routes/users/get_user.rs similarity index 100% rename from api/src/routes/rest/routes/users/get_user.rs rename to api/server/src/routes/rest/routes/users/get_user.rs diff --git a/api/src/routes/rest/routes/users/get_user_by_id.rs b/api/server/src/routes/rest/routes/users/get_user_by_id.rs similarity index 100% rename from api/src/routes/rest/routes/users/get_user_by_id.rs rename to api/server/src/routes/rest/routes/users/get_user_by_id.rs diff --git a/api/src/routes/rest/routes/users/mod.rs b/api/server/src/routes/rest/routes/users/mod.rs similarity index 100% rename from api/src/routes/rest/routes/users/mod.rs rename to api/server/src/routes/rest/routes/users/mod.rs diff --git a/api/src/routes/rest/routes/users/update_user.rs b/api/server/src/routes/rest/routes/users/update_user.rs similarity index 100% rename from api/src/routes/rest/routes/users/update_user.rs rename to api/server/src/routes/rest/routes/users/update_user.rs diff --git a/api/src/routes/rest/webhooks/embeddings/create_embedding.rs b/api/server/src/routes/rest/webhooks/embeddings/create_embedding.rs similarity index 100% rename from api/src/routes/rest/webhooks/embeddings/create_embedding.rs rename to api/server/src/routes/rest/webhooks/embeddings/create_embedding.rs diff --git a/api/src/routes/rest/webhooks/embeddings/mod.rs b/api/server/src/routes/rest/webhooks/embeddings/mod.rs similarity index 100% rename from api/src/routes/rest/webhooks/embeddings/mod.rs rename to api/server/src/routes/rest/webhooks/embeddings/mod.rs diff --git a/api/src/routes/rest/webhooks/mod.rs b/api/server/src/routes/rest/webhooks/mod.rs similarity index 100% rename from api/src/routes/rest/webhooks/mod.rs rename to api/server/src/routes/rest/webhooks/mod.rs diff --git a/api/src/routes/ws/collections/collection_utils.rs b/api/server/src/routes/ws/collections/collection_utils.rs similarity index 100% rename from api/src/routes/ws/collections/collection_utils.rs rename to api/server/src/routes/ws/collections/collection_utils.rs diff --git a/api/src/routes/ws/collections/collections_router.rs b/api/server/src/routes/ws/collections/collections_router.rs similarity index 100% rename from api/src/routes/ws/collections/collections_router.rs rename to api/server/src/routes/ws/collections/collections_router.rs diff --git a/api/src/routes/ws/collections/delete_collection.rs b/api/server/src/routes/ws/collections/delete_collection.rs similarity index 100% rename from api/src/routes/ws/collections/delete_collection.rs rename to api/server/src/routes/ws/collections/delete_collection.rs diff --git a/api/src/routes/ws/collections/get_collection.rs b/api/server/src/routes/ws/collections/get_collection.rs similarity index 100% rename from api/src/routes/ws/collections/get_collection.rs rename to api/server/src/routes/ws/collections/get_collection.rs diff --git a/api/src/routes/ws/collections/list_collections.rs b/api/server/src/routes/ws/collections/list_collections.rs similarity index 100% rename from api/src/routes/ws/collections/list_collections.rs rename to api/server/src/routes/ws/collections/list_collections.rs diff --git a/api/src/routes/ws/collections/mod.rs b/api/server/src/routes/ws/collections/mod.rs similarity index 100% rename from api/src/routes/ws/collections/mod.rs rename to api/server/src/routes/ws/collections/mod.rs diff --git a/api/src/routes/ws/collections/post_collection.rs b/api/server/src/routes/ws/collections/post_collection.rs similarity index 100% rename from api/src/routes/ws/collections/post_collection.rs rename to api/server/src/routes/ws/collections/post_collection.rs diff --git a/api/src/routes/ws/collections/unsubscribe.rs b/api/server/src/routes/ws/collections/unsubscribe.rs similarity index 100% rename from api/src/routes/ws/collections/unsubscribe.rs rename to api/server/src/routes/ws/collections/unsubscribe.rs diff --git a/api/src/routes/ws/collections/update_collection.rs b/api/server/src/routes/ws/collections/update_collection.rs similarity index 100% rename from api/src/routes/ws/collections/update_collection.rs rename to api/server/src/routes/ws/collections/update_collection.rs diff --git a/api/src/routes/ws/dashboards/dashboard_utils.rs b/api/server/src/routes/ws/dashboards/dashboard_utils.rs similarity index 100% rename from api/src/routes/ws/dashboards/dashboard_utils.rs rename to api/server/src/routes/ws/dashboards/dashboard_utils.rs diff --git a/api/src/routes/ws/dashboards/dashboards_router.rs b/api/server/src/routes/ws/dashboards/dashboards_router.rs similarity index 100% rename from api/src/routes/ws/dashboards/dashboards_router.rs rename to api/server/src/routes/ws/dashboards/dashboards_router.rs diff --git a/api/src/routes/ws/dashboards/delete_dashboard.rs b/api/server/src/routes/ws/dashboards/delete_dashboard.rs similarity index 100% rename from api/src/routes/ws/dashboards/delete_dashboard.rs rename to api/server/src/routes/ws/dashboards/delete_dashboard.rs diff --git a/api/src/routes/ws/dashboards/get_dashboard.rs b/api/server/src/routes/ws/dashboards/get_dashboard.rs similarity index 100% rename from api/src/routes/ws/dashboards/get_dashboard.rs rename to api/server/src/routes/ws/dashboards/get_dashboard.rs diff --git a/api/src/routes/ws/dashboards/list_dashboards.rs b/api/server/src/routes/ws/dashboards/list_dashboards.rs similarity index 100% rename from api/src/routes/ws/dashboards/list_dashboards.rs rename to api/server/src/routes/ws/dashboards/list_dashboards.rs diff --git a/api/src/routes/ws/dashboards/mod.rs b/api/server/src/routes/ws/dashboards/mod.rs similarity index 100% rename from api/src/routes/ws/dashboards/mod.rs rename to api/server/src/routes/ws/dashboards/mod.rs diff --git a/api/src/routes/ws/dashboards/post_dashboard.rs b/api/server/src/routes/ws/dashboards/post_dashboard.rs similarity index 100% rename from api/src/routes/ws/dashboards/post_dashboard.rs rename to api/server/src/routes/ws/dashboards/post_dashboard.rs diff --git a/api/src/routes/ws/dashboards/unsubscribe.rs b/api/server/src/routes/ws/dashboards/unsubscribe.rs similarity index 100% rename from api/src/routes/ws/dashboards/unsubscribe.rs rename to api/server/src/routes/ws/dashboards/unsubscribe.rs diff --git a/api/src/routes/ws/dashboards/update_dashboard.rs b/api/server/src/routes/ws/dashboards/update_dashboard.rs similarity index 61% rename from api/src/routes/ws/dashboards/update_dashboard.rs rename to api/server/src/routes/ws/dashboards/update_dashboard.rs index 8ec0dde1b..ea6a7f921 100644 --- a/api/src/routes/ws/dashboards/update_dashboard.rs +++ b/api/server/src/routes/ws/dashboards/update_dashboard.rs @@ -28,7 +28,8 @@ use database::{ enums::{AssetPermissionRole, AssetType}, models::ThreadToDashboard, pool::get_pg_pool, - schema::{dashboards, threads_to_dashboards}, + schema::{dashboards, threads_to_dashboards, metric_files_to_dashboard_files}, + types::DashboardYml, vault::create_secret, }; @@ -41,6 +42,10 @@ pub struct UpdateDashboardRequest { pub name: Option, pub description: Option, pub config: Option, + /// YAML content of the dashboard + pub file_content: Option, + /// Whether to create a new version in the version history (defaults to true) + pub update_version: Option, pub threads: Option>, pub publicly_accessible: Option, #[serde(default)] @@ -112,6 +117,8 @@ pub async fn update_dashboard( req.publicly_accessible, req.public_password, req.public_expiry_date, + req.file_content, + req.update_version, ) .await { @@ -290,6 +297,8 @@ async fn update_dashboard_record( publicly_accessible: Option, public_password: Option>, public_expiry_date: Option>, + file_content: Option, + _update_version: Option, ) -> Result<()> { let _password_secret_id = match public_password { Some(Some(password)) => match create_secret(&dashboard_id, &password).await { @@ -319,12 +328,205 @@ async fn update_dashboard_record( None }; + // Fetch the current dashboard to check if we need to update it + let mut conn = match get_pg_pool().get().await { + Ok(conn) => conn, + Err(e) => { + tracing::error!("Unable to get connection from pool: {:?}", e); + return Err(anyhow!("Unable to get connection from pool: {}", e)); + } + }; + + // Handle file_content if provided (YAML validation) + let dashboard_yml_result = if let Some(content) = file_content.clone() { + // Validate YAML and convert to DashboardYml + match DashboardYml::new(content) { + Ok(yml) => { + // Validate metric references + let metric_ids: Vec = yml + .rows + .iter() + .flat_map(|row| row.items.iter()) + .map(|item| item.id) + .collect(); + + if !metric_ids.is_empty() { + // Validate that referenced metrics exist + match validate_dashboard_metric_ids(&metric_ids).await { + Ok(missing_ids) if !missing_ids.is_empty() => { + let error_msg = format!("Dashboard references non-existent metrics: {:?}", missing_ids); + tracing::error!("{}", error_msg); + return Err(anyhow!(error_msg)); + } + Err(e) => { + return Err(e); + } + Ok(_) => { + // Update metric associations - delete previous ones and create new ones + match update(metric_files_to_dashboard_files::table) + .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(*dashboard_id)) + .set(metric_files_to_dashboard_files::deleted_at.eq(Some(chrono::Utc::now()))) + .execute(&mut conn) + .await + { + Ok(_) => { + // Insert new metric associations + let metric_dashboard_values: Vec<_> = metric_ids + .iter() + .map(|metric_id| { + diesel::insert_into(metric_files_to_dashboard_files::table) + .values(( + metric_files_to_dashboard_files::metric_file_id.eq(*metric_id), + metric_files_to_dashboard_files::dashboard_file_id.eq(*dashboard_id), + metric_files_to_dashboard_files::created_at.eq(chrono::Utc::now()), + metric_files_to_dashboard_files::updated_at.eq(chrono::Utc::now()), + metric_files_to_dashboard_files::created_by.eq(*user_id), + )) + .on_conflict_do_nothing() + }) + .collect(); + + for insertion in metric_dashboard_values { + if let Err(e) = insertion.execute(&mut conn).await { + tracing::warn!( + "Failed to create metric-to-dashboard association: {}", + e + ); + } + } + } + Err(e) => { + tracing::warn!( + "Failed to clear existing metric associations: {}", + e + ); + } + } + } + } + } + + // Update config with the serialized YAML + Some(Ok(yml)) + } + Err(e) => { + let error_msg = format!("Invalid dashboard YAML: {}", e); + tracing::error!("{}", error_msg); + Some(Err(anyhow!(error_msg))) + } + } + } else { + None + }; + + // Process config if file_content is not provided but config is + let config_yml_result = if file_content.is_none() && config.is_some() { + let config_value = config.as_ref().unwrap(); + + // Try to convert the config to a DashboardYml + match serde_json::from_value::(config_value.clone()) { + Ok(yml) => { + // Validate the yml structure + if let Err(e) = yml.validate() { + let error_msg = format!("Invalid dashboard configuration: {}", e); + tracing::error!("{}", error_msg); + return Err(anyhow!(error_msg)); + } + + // Validate metric references + let metric_ids: Vec = yml + .rows + .iter() + .flat_map(|row| row.items.iter()) + .map(|item| item.id) + .collect(); + + if !metric_ids.is_empty() { + // Validate that referenced metrics exist + match validate_dashboard_metric_ids(&metric_ids).await { + Ok(missing_ids) if !missing_ids.is_empty() => { + let error_msg = format!("Dashboard references non-existent metrics: {:?}", missing_ids); + tracing::error!("{}", error_msg); + return Err(anyhow!(error_msg)); + } + Err(e) => { + return Err(e); + } + Ok(_) => { + // Update metric associations - delete previous ones and create new ones + match update(metric_files_to_dashboard_files::table) + .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(*dashboard_id)) + .set(metric_files_to_dashboard_files::deleted_at.eq(Some(chrono::Utc::now()))) + .execute(&mut conn) + .await + { + Ok(_) => { + // Insert new metric associations + let metric_dashboard_values: Vec<_> = metric_ids + .iter() + .map(|metric_id| { + diesel::insert_into(metric_files_to_dashboard_files::table) + .values(( + metric_files_to_dashboard_files::metric_file_id.eq(*metric_id), + metric_files_to_dashboard_files::dashboard_file_id.eq(*dashboard_id), + metric_files_to_dashboard_files::created_at.eq(chrono::Utc::now()), + metric_files_to_dashboard_files::updated_at.eq(chrono::Utc::now()), + metric_files_to_dashboard_files::created_by.eq(*user_id), + )) + .on_conflict_do_nothing() + }) + .collect(); + + for insertion in metric_dashboard_values { + if let Err(e) = insertion.execute(&mut conn).await { + tracing::warn!( + "Failed to create metric-to-dashboard association: {}", + e + ); + } + } + } + Err(e) => { + tracing::warn!( + "Failed to clear existing metric associations: {}", + e + ); + } + } + } + } + } + + Some(yml) + } + Err(e) => { + let error_msg = format!("Invalid dashboard configuration format: {}", e); + tracing::error!("{}", error_msg); + return Err(anyhow!(error_msg)); + } + } + } else { + None + }; + + // If YAML validation failed, return the error + if let Some(Err(e)) = dashboard_yml_result { + return Err(e); + } + + // Update dashboard record let changeset = DashboardChangeset { updated_at: Utc::now(), updated_by: *user_id, name: name.clone(), description, - config, + config: if let Some(Ok(ref yml)) = dashboard_yml_result { + Some(yml.to_value()?) + } else if let Some(ref yml) = config_yml_result { + Some(yml.to_value()?) + } else { + config + }, publicly_accessible, publicly_enabled_by, password_secret_id: None, @@ -361,7 +563,14 @@ async fn update_dashboard_record( let dashboard_search_handle = { let dashboard_id = dashboard_id.clone(); - let dashboard_name = name.unwrap_or_default(); + let dashboard_name = if let Some(Ok(ref yml)) = dashboard_yml_result { + yml.name.clone() + } else if let Some(ref yml) = config_yml_result { + yml.name.clone() + } else { + name.unwrap_or_default() + }; + tokio::spawn(async move { let mut conn = match get_pg_pool().get().await { Ok(conn) => conn, @@ -564,3 +773,48 @@ async fn update_dashboard_threads( Ok(()) } + +/// Validate that the metric IDs referenced in the dashboard exist +async fn validate_dashboard_metric_ids(metric_ids: &[Uuid]) -> Result> { + if metric_ids.is_empty() { + return Ok(Vec::new()); + } + + let mut conn = match get_pg_pool().get().await { + Ok(conn) => conn, + Err(e) => { + tracing::error!("Unable to get connection from pool: {:?}", e); + return Err(anyhow!("Unable to get connection from pool: {}", e)); + } + }; + + #[derive(Debug, diesel::QueryableByName)] + #[diesel(table_name = metric_files)] + struct MetricIdResult { + #[diesel(sql_type = diesel::sql_types::Uuid)] + id: Uuid, + } + + // Query to find which metric IDs exist + let query = diesel::sql_query( + "SELECT id FROM metric_files WHERE id = ANY($1) AND deleted_at IS NULL" + ) + .bind::, _>(metric_ids); + + let existing_metrics: Vec = match query.load::(&mut conn).await { + Ok(results) => results.into_iter().map(|r| r.id).collect(), + Err(e) => { + tracing::error!("Error validating metric IDs: {:?}", e); + return Err(anyhow!("Error validating metric IDs: {}", e)); + } + }; + + // Find missing metrics + let missing_ids: Vec = metric_ids + .iter() + .filter(|id| !existing_metrics.contains(id)) + .cloned() + .collect(); + + Ok(missing_ids) +} diff --git a/api/src/routes/ws/data_sources/data_source_utils/data_source_utils.rs b/api/server/src/routes/ws/data_sources/data_source_utils/data_source_utils.rs similarity index 100% rename from api/src/routes/ws/data_sources/data_source_utils/data_source_utils.rs rename to api/server/src/routes/ws/data_sources/data_source_utils/data_source_utils.rs diff --git a/api/src/routes/ws/data_sources/data_source_utils/mod.rs b/api/server/src/routes/ws/data_sources/data_source_utils/mod.rs similarity index 100% rename from api/src/routes/ws/data_sources/data_source_utils/mod.rs rename to api/server/src/routes/ws/data_sources/data_source_utils/mod.rs diff --git a/api/src/routes/ws/data_sources/data_sources_router.rs b/api/server/src/routes/ws/data_sources/data_sources_router.rs similarity index 100% rename from api/src/routes/ws/data_sources/data_sources_router.rs rename to api/server/src/routes/ws/data_sources/data_sources_router.rs diff --git a/api/src/routes/ws/data_sources/delete_data_source.rs b/api/server/src/routes/ws/data_sources/delete_data_source.rs similarity index 100% rename from api/src/routes/ws/data_sources/delete_data_source.rs rename to api/server/src/routes/ws/data_sources/delete_data_source.rs diff --git a/api/src/routes/ws/data_sources/get_data_source.rs b/api/server/src/routes/ws/data_sources/get_data_source.rs similarity index 100% rename from api/src/routes/ws/data_sources/get_data_source.rs rename to api/server/src/routes/ws/data_sources/get_data_source.rs diff --git a/api/src/routes/ws/data_sources/list_data_sources.rs b/api/server/src/routes/ws/data_sources/list_data_sources.rs similarity index 100% rename from api/src/routes/ws/data_sources/list_data_sources.rs rename to api/server/src/routes/ws/data_sources/list_data_sources.rs diff --git a/api/src/routes/ws/data_sources/mod.rs b/api/server/src/routes/ws/data_sources/mod.rs similarity index 100% rename from api/src/routes/ws/data_sources/mod.rs rename to api/server/src/routes/ws/data_sources/mod.rs diff --git a/api/src/routes/ws/data_sources/post_data_source.rs b/api/server/src/routes/ws/data_sources/post_data_source.rs similarity index 100% rename from api/src/routes/ws/data_sources/post_data_source.rs rename to api/server/src/routes/ws/data_sources/post_data_source.rs diff --git a/api/src/routes/ws/data_sources/update_data_source.rs b/api/server/src/routes/ws/data_sources/update_data_source.rs similarity index 100% rename from api/src/routes/ws/data_sources/update_data_source.rs rename to api/server/src/routes/ws/data_sources/update_data_source.rs diff --git a/api/src/routes/ws/datasets/dataset_utils.rs b/api/server/src/routes/ws/datasets/dataset_utils.rs similarity index 100% rename from api/src/routes/ws/datasets/dataset_utils.rs rename to api/server/src/routes/ws/datasets/dataset_utils.rs diff --git a/api/src/routes/ws/datasets/datasets_router.rs b/api/server/src/routes/ws/datasets/datasets_router.rs similarity index 100% rename from api/src/routes/ws/datasets/datasets_router.rs rename to api/server/src/routes/ws/datasets/datasets_router.rs diff --git a/api/src/routes/ws/datasets/delete_dataset.rs b/api/server/src/routes/ws/datasets/delete_dataset.rs similarity index 100% rename from api/src/routes/ws/datasets/delete_dataset.rs rename to api/server/src/routes/ws/datasets/delete_dataset.rs diff --git a/api/src/routes/ws/datasets/get_dataset.rs b/api/server/src/routes/ws/datasets/get_dataset.rs similarity index 100% rename from api/src/routes/ws/datasets/get_dataset.rs rename to api/server/src/routes/ws/datasets/get_dataset.rs diff --git a/api/src/routes/ws/datasets/list_datasets.rs b/api/server/src/routes/ws/datasets/list_datasets.rs similarity index 100% rename from api/src/routes/ws/datasets/list_datasets.rs rename to api/server/src/routes/ws/datasets/list_datasets.rs diff --git a/api/src/routes/ws/datasets/mod.rs b/api/server/src/routes/ws/datasets/mod.rs similarity index 100% rename from api/src/routes/ws/datasets/mod.rs rename to api/server/src/routes/ws/datasets/mod.rs diff --git a/api/src/routes/ws/datasets/post_dataset.rs b/api/server/src/routes/ws/datasets/post_dataset.rs similarity index 100% rename from api/src/routes/ws/datasets/post_dataset.rs rename to api/server/src/routes/ws/datasets/post_dataset.rs diff --git a/api/src/routes/ws/datasets/update_dataset.rs b/api/server/src/routes/ws/datasets/update_dataset.rs similarity index 100% rename from api/src/routes/ws/datasets/update_dataset.rs rename to api/server/src/routes/ws/datasets/update_dataset.rs diff --git a/api/src/routes/ws/datasets/updated_dataset_column.rs b/api/server/src/routes/ws/datasets/updated_dataset_column.rs similarity index 100% rename from api/src/routes/ws/datasets/updated_dataset_column.rs rename to api/server/src/routes/ws/datasets/updated_dataset_column.rs diff --git a/api/src/routes/ws/metrics/get_metric.rs b/api/server/src/routes/ws/metrics/get_metric.rs similarity index 100% rename from api/src/routes/ws/metrics/get_metric.rs rename to api/server/src/routes/ws/metrics/get_metric.rs diff --git a/api/src/routes/ws/metrics/get_metric_data.rs b/api/server/src/routes/ws/metrics/get_metric_data.rs similarity index 100% rename from api/src/routes/ws/metrics/get_metric_data.rs rename to api/server/src/routes/ws/metrics/get_metric_data.rs diff --git a/api/src/routes/ws/metrics/metrics_router.rs b/api/server/src/routes/ws/metrics/metrics_router.rs similarity index 100% rename from api/src/routes/ws/metrics/metrics_router.rs rename to api/server/src/routes/ws/metrics/metrics_router.rs diff --git a/api/src/routes/ws/metrics/mod.rs b/api/server/src/routes/ws/metrics/mod.rs similarity index 100% rename from api/src/routes/ws/metrics/mod.rs rename to api/server/src/routes/ws/metrics/mod.rs diff --git a/api/src/routes/ws/mod.rs b/api/server/src/routes/ws/mod.rs similarity index 100% rename from api/src/routes/ws/mod.rs rename to api/server/src/routes/ws/mod.rs diff --git a/api/src/routes/ws/organizations/mod.rs b/api/server/src/routes/ws/organizations/mod.rs similarity index 100% rename from api/src/routes/ws/organizations/mod.rs rename to api/server/src/routes/ws/organizations/mod.rs diff --git a/api/src/routes/ws/organizations/organization_router.rs b/api/server/src/routes/ws/organizations/organization_router.rs similarity index 100% rename from api/src/routes/ws/organizations/organization_router.rs rename to api/server/src/routes/ws/organizations/organization_router.rs diff --git a/api/src/routes/ws/organizations/post_organization.rs b/api/server/src/routes/ws/organizations/post_organization.rs similarity index 100% rename from api/src/routes/ws/organizations/post_organization.rs rename to api/server/src/routes/ws/organizations/post_organization.rs diff --git a/api/src/routes/ws/organizations/update_organization.rs b/api/server/src/routes/ws/organizations/update_organization.rs similarity index 100% rename from api/src/routes/ws/organizations/update_organization.rs rename to api/server/src/routes/ws/organizations/update_organization.rs diff --git a/api/src/routes/ws/permissions/delete_permission_group.rs b/api/server/src/routes/ws/permissions/delete_permission_group.rs similarity index 100% rename from api/src/routes/ws/permissions/delete_permission_group.rs rename to api/server/src/routes/ws/permissions/delete_permission_group.rs diff --git a/api/src/routes/ws/permissions/delete_team.rs b/api/server/src/routes/ws/permissions/delete_team.rs similarity index 100% rename from api/src/routes/ws/permissions/delete_team.rs rename to api/server/src/routes/ws/permissions/delete_team.rs diff --git a/api/src/routes/ws/permissions/get_permission_group.rs b/api/server/src/routes/ws/permissions/get_permission_group.rs similarity index 100% rename from api/src/routes/ws/permissions/get_permission_group.rs rename to api/server/src/routes/ws/permissions/get_permission_group.rs diff --git a/api/src/routes/ws/permissions/get_team_permissions.rs b/api/server/src/routes/ws/permissions/get_team_permissions.rs similarity index 100% rename from api/src/routes/ws/permissions/get_team_permissions.rs rename to api/server/src/routes/ws/permissions/get_team_permissions.rs diff --git a/api/src/routes/ws/permissions/get_user_permissions.rs b/api/server/src/routes/ws/permissions/get_user_permissions.rs similarity index 100% rename from api/src/routes/ws/permissions/get_user_permissions.rs rename to api/server/src/routes/ws/permissions/get_user_permissions.rs diff --git a/api/src/routes/ws/permissions/list_permission_groups.rs b/api/server/src/routes/ws/permissions/list_permission_groups.rs similarity index 100% rename from api/src/routes/ws/permissions/list_permission_groups.rs rename to api/server/src/routes/ws/permissions/list_permission_groups.rs diff --git a/api/src/routes/ws/permissions/list_teams.rs b/api/server/src/routes/ws/permissions/list_teams.rs similarity index 100% rename from api/src/routes/ws/permissions/list_teams.rs rename to api/server/src/routes/ws/permissions/list_teams.rs diff --git a/api/src/routes/ws/permissions/list_users.rs b/api/server/src/routes/ws/permissions/list_users.rs similarity index 100% rename from api/src/routes/ws/permissions/list_users.rs rename to api/server/src/routes/ws/permissions/list_users.rs diff --git a/api/src/routes/ws/permissions/mod.rs b/api/server/src/routes/ws/permissions/mod.rs similarity index 100% rename from api/src/routes/ws/permissions/mod.rs rename to api/server/src/routes/ws/permissions/mod.rs diff --git a/api/src/routes/ws/permissions/permissions_router.rs b/api/server/src/routes/ws/permissions/permissions_router.rs similarity index 100% rename from api/src/routes/ws/permissions/permissions_router.rs rename to api/server/src/routes/ws/permissions/permissions_router.rs diff --git a/api/src/routes/ws/permissions/permissions_utils.rs b/api/server/src/routes/ws/permissions/permissions_utils.rs similarity index 100% rename from api/src/routes/ws/permissions/permissions_utils.rs rename to api/server/src/routes/ws/permissions/permissions_utils.rs diff --git a/api/src/routes/ws/permissions/post_permission_group.rs b/api/server/src/routes/ws/permissions/post_permission_group.rs similarity index 100% rename from api/src/routes/ws/permissions/post_permission_group.rs rename to api/server/src/routes/ws/permissions/post_permission_group.rs diff --git a/api/src/routes/ws/permissions/post_team.rs b/api/server/src/routes/ws/permissions/post_team.rs similarity index 100% rename from api/src/routes/ws/permissions/post_team.rs rename to api/server/src/routes/ws/permissions/post_team.rs diff --git a/api/src/routes/ws/permissions/post_user.rs b/api/server/src/routes/ws/permissions/post_user.rs similarity index 100% rename from api/src/routes/ws/permissions/post_user.rs rename to api/server/src/routes/ws/permissions/post_user.rs diff --git a/api/src/routes/ws/permissions/update_permission_group.rs b/api/server/src/routes/ws/permissions/update_permission_group.rs similarity index 100% rename from api/src/routes/ws/permissions/update_permission_group.rs rename to api/server/src/routes/ws/permissions/update_permission_group.rs diff --git a/api/src/routes/ws/permissions/update_team_permission.rs b/api/server/src/routes/ws/permissions/update_team_permission.rs similarity index 100% rename from api/src/routes/ws/permissions/update_team_permission.rs rename to api/server/src/routes/ws/permissions/update_team_permission.rs diff --git a/api/src/routes/ws/permissions/update_user_permission.rs b/api/server/src/routes/ws/permissions/update_user_permission.rs similarity index 100% rename from api/src/routes/ws/permissions/update_user_permission.rs rename to api/server/src/routes/ws/permissions/update_user_permission.rs diff --git a/api/src/routes/ws/search/mod.rs b/api/server/src/routes/ws/search/mod.rs similarity index 100% rename from api/src/routes/ws/search/mod.rs rename to api/server/src/routes/ws/search/mod.rs diff --git a/api/src/routes/ws/search/search.rs b/api/server/src/routes/ws/search/search.rs similarity index 100% rename from api/src/routes/ws/search/search.rs rename to api/server/src/routes/ws/search/search.rs diff --git a/api/src/routes/ws/search/search_router.rs b/api/server/src/routes/ws/search/search_router.rs similarity index 100% rename from api/src/routes/ws/search/search_router.rs rename to api/server/src/routes/ws/search/search_router.rs diff --git a/api/src/routes/ws/sql/mod.rs b/api/server/src/routes/ws/sql/mod.rs similarity index 100% rename from api/src/routes/ws/sql/mod.rs rename to api/server/src/routes/ws/sql/mod.rs diff --git a/api/src/routes/ws/sql/run_sql.rs b/api/server/src/routes/ws/sql/run_sql.rs similarity index 100% rename from api/src/routes/ws/sql/run_sql.rs rename to api/server/src/routes/ws/sql/run_sql.rs diff --git a/api/src/routes/ws/sql/sql_router.rs b/api/server/src/routes/ws/sql/sql_router.rs similarity index 100% rename from api/src/routes/ws/sql/sql_router.rs rename to api/server/src/routes/ws/sql/sql_router.rs diff --git a/api/src/routes/ws/teams/list_teams.rs b/api/server/src/routes/ws/teams/list_teams.rs similarity index 100% rename from api/src/routes/ws/teams/list_teams.rs rename to api/server/src/routes/ws/teams/list_teams.rs diff --git a/api/src/routes/ws/teams/mod.rs b/api/server/src/routes/ws/teams/mod.rs similarity index 100% rename from api/src/routes/ws/teams/mod.rs rename to api/server/src/routes/ws/teams/mod.rs diff --git a/api/src/routes/ws/teams/teams_routes.rs b/api/server/src/routes/ws/teams/teams_routes.rs similarity index 100% rename from api/src/routes/ws/teams/teams_routes.rs rename to api/server/src/routes/ws/teams/teams_routes.rs diff --git a/api/src/routes/ws/terms/delete_term.rs b/api/server/src/routes/ws/terms/delete_term.rs similarity index 100% rename from api/src/routes/ws/terms/delete_term.rs rename to api/server/src/routes/ws/terms/delete_term.rs diff --git a/api/src/routes/ws/terms/get_term.rs b/api/server/src/routes/ws/terms/get_term.rs similarity index 100% rename from api/src/routes/ws/terms/get_term.rs rename to api/server/src/routes/ws/terms/get_term.rs diff --git a/api/src/routes/ws/terms/list_terms.rs b/api/server/src/routes/ws/terms/list_terms.rs similarity index 100% rename from api/src/routes/ws/terms/list_terms.rs rename to api/server/src/routes/ws/terms/list_terms.rs diff --git a/api/src/routes/ws/terms/mod.rs b/api/server/src/routes/ws/terms/mod.rs similarity index 100% rename from api/src/routes/ws/terms/mod.rs rename to api/server/src/routes/ws/terms/mod.rs diff --git a/api/src/routes/ws/terms/post_term.rs b/api/server/src/routes/ws/terms/post_term.rs similarity index 100% rename from api/src/routes/ws/terms/post_term.rs rename to api/server/src/routes/ws/terms/post_term.rs diff --git a/api/src/routes/ws/terms/terms_router.rs b/api/server/src/routes/ws/terms/terms_router.rs similarity index 100% rename from api/src/routes/ws/terms/terms_router.rs rename to api/server/src/routes/ws/terms/terms_router.rs diff --git a/api/src/routes/ws/terms/terms_utils.rs b/api/server/src/routes/ws/terms/terms_utils.rs similarity index 100% rename from api/src/routes/ws/terms/terms_utils.rs rename to api/server/src/routes/ws/terms/terms_utils.rs diff --git a/api/src/routes/ws/terms/update_term.rs b/api/server/src/routes/ws/terms/update_term.rs similarity index 100% rename from api/src/routes/ws/terms/update_term.rs rename to api/server/src/routes/ws/terms/update_term.rs diff --git a/api/src/routes/ws/threads_and_messages/README.md b/api/server/src/routes/ws/threads_and_messages/README.md similarity index 100% rename from api/src/routes/ws/threads_and_messages/README.md rename to api/server/src/routes/ws/threads_and_messages/README.md diff --git a/api/src/routes/ws/threads_and_messages/delete_thread.rs b/api/server/src/routes/ws/threads_and_messages/delete_thread.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/delete_thread.rs rename to api/server/src/routes/ws/threads_and_messages/delete_thread.rs diff --git a/api/src/routes/ws/threads_and_messages/duplicate_thread.rs b/api/server/src/routes/ws/threads_and_messages/duplicate_thread.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/duplicate_thread.rs rename to api/server/src/routes/ws/threads_and_messages/duplicate_thread.rs diff --git a/api/src/routes/ws/threads_and_messages/get_message_data.rs b/api/server/src/routes/ws/threads_and_messages/get_message_data.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/get_message_data.rs rename to api/server/src/routes/ws/threads_and_messages/get_message_data.rs diff --git a/api/src/routes/ws/threads_and_messages/get_thread.rs b/api/server/src/routes/ws/threads_and_messages/get_thread.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/get_thread.rs rename to api/server/src/routes/ws/threads_and_messages/get_thread.rs diff --git a/api/src/routes/ws/threads_and_messages/list_threads.rs b/api/server/src/routes/ws/threads_and_messages/list_threads.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/list_threads.rs rename to api/server/src/routes/ws/threads_and_messages/list_threads.rs diff --git a/api/src/routes/ws/threads_and_messages/messages_utils.rs b/api/server/src/routes/ws/threads_and_messages/messages_utils.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/messages_utils.rs rename to api/server/src/routes/ws/threads_and_messages/messages_utils.rs diff --git a/api/src/routes/ws/threads_and_messages/mod.rs b/api/server/src/routes/ws/threads_and_messages/mod.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/mod.rs rename to api/server/src/routes/ws/threads_and_messages/mod.rs diff --git a/api/src/routes/ws/threads_and_messages/post_thread.rs b/api/server/src/routes/ws/threads_and_messages/post_thread.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/post_thread.rs rename to api/server/src/routes/ws/threads_and_messages/post_thread.rs diff --git a/api/src/routes/ws/threads_and_messages/thread_utils.rs b/api/server/src/routes/ws/threads_and_messages/thread_utils.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/thread_utils.rs rename to api/server/src/routes/ws/threads_and_messages/thread_utils.rs diff --git a/api/src/routes/ws/threads_and_messages/threads_router.rs b/api/server/src/routes/ws/threads_and_messages/threads_router.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/threads_router.rs rename to api/server/src/routes/ws/threads_and_messages/threads_router.rs diff --git a/api/src/routes/ws/threads_and_messages/unsubscribe.rs b/api/server/src/routes/ws/threads_and_messages/unsubscribe.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/unsubscribe.rs rename to api/server/src/routes/ws/threads_and_messages/unsubscribe.rs diff --git a/api/src/routes/ws/threads_and_messages/update_message.rs b/api/server/src/routes/ws/threads_and_messages/update_message.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/update_message.rs rename to api/server/src/routes/ws/threads_and_messages/update_message.rs diff --git a/api/src/routes/ws/threads_and_messages/update_thread.rs b/api/server/src/routes/ws/threads_and_messages/update_thread.rs similarity index 100% rename from api/src/routes/ws/threads_and_messages/update_thread.rs rename to api/server/src/routes/ws/threads_and_messages/update_thread.rs diff --git a/api/src/routes/ws/users/favorites/create_favorite.rs b/api/server/src/routes/ws/users/favorites/create_favorite.rs similarity index 100% rename from api/src/routes/ws/users/favorites/create_favorite.rs rename to api/server/src/routes/ws/users/favorites/create_favorite.rs diff --git a/api/src/routes/ws/users/favorites/delete_favorite.rs b/api/server/src/routes/ws/users/favorites/delete_favorite.rs similarity index 100% rename from api/src/routes/ws/users/favorites/delete_favorite.rs rename to api/server/src/routes/ws/users/favorites/delete_favorite.rs diff --git a/api/src/routes/ws/users/favorites/favorites_utils.rs b/api/server/src/routes/ws/users/favorites/favorites_utils.rs similarity index 100% rename from api/src/routes/ws/users/favorites/favorites_utils.rs rename to api/server/src/routes/ws/users/favorites/favorites_utils.rs diff --git a/api/src/routes/ws/users/favorites/list_favorites.rs b/api/server/src/routes/ws/users/favorites/list_favorites.rs similarity index 100% rename from api/src/routes/ws/users/favorites/list_favorites.rs rename to api/server/src/routes/ws/users/favorites/list_favorites.rs diff --git a/api/src/routes/ws/users/favorites/mod.rs b/api/server/src/routes/ws/users/favorites/mod.rs similarity index 100% rename from api/src/routes/ws/users/favorites/mod.rs rename to api/server/src/routes/ws/users/favorites/mod.rs diff --git a/api/src/routes/ws/users/favorites/update_favorites.rs b/api/server/src/routes/ws/users/favorites/update_favorites.rs similarity index 100% rename from api/src/routes/ws/users/favorites/update_favorites.rs rename to api/server/src/routes/ws/users/favorites/update_favorites.rs diff --git a/api/src/routes/ws/users/invite_users.rs b/api/server/src/routes/ws/users/invite_users.rs similarity index 100% rename from api/src/routes/ws/users/invite_users.rs rename to api/server/src/routes/ws/users/invite_users.rs diff --git a/api/src/routes/ws/users/list_users.rs b/api/server/src/routes/ws/users/list_users.rs similarity index 100% rename from api/src/routes/ws/users/list_users.rs rename to api/server/src/routes/ws/users/list_users.rs diff --git a/api/src/routes/ws/users/mod.rs b/api/server/src/routes/ws/users/mod.rs similarity index 100% rename from api/src/routes/ws/users/mod.rs rename to api/server/src/routes/ws/users/mod.rs diff --git a/api/src/routes/ws/users/users_router.rs b/api/server/src/routes/ws/users/users_router.rs similarity index 100% rename from api/src/routes/ws/users/users_router.rs rename to api/server/src/routes/ws/users/users_router.rs diff --git a/api/src/routes/ws/ws.rs b/api/server/src/routes/ws/ws.rs similarity index 100% rename from api/src/routes/ws/ws.rs rename to api/server/src/routes/ws/ws.rs diff --git a/api/src/routes/ws/ws_router.rs b/api/server/src/routes/ws/ws_router.rs similarity index 100% rename from api/src/routes/ws/ws_router.rs rename to api/server/src/routes/ws/ws_router.rs diff --git a/api/src/routes/ws/ws_utils.rs b/api/server/src/routes/ws/ws_utils.rs similarity index 100% rename from api/src/routes/ws/ws_utils.rs rename to api/server/src/routes/ws/ws_utils.rs diff --git a/api/src/types/mod.rs b/api/server/src/types/mod.rs similarity index 100% rename from api/src/types/mod.rs rename to api/server/src/types/mod.rs diff --git a/api/src/utils/charting/mod.rs b/api/server/src/utils/charting/mod.rs similarity index 100% rename from api/src/utils/charting/mod.rs rename to api/server/src/utils/charting/mod.rs diff --git a/api/src/utils/charting/types.rs b/api/server/src/utils/charting/types.rs similarity index 100% rename from api/src/utils/charting/types.rs rename to api/server/src/utils/charting/types.rs diff --git a/api/src/utils/clients/ai/anthropic.rs b/api/server/src/utils/clients/ai/anthropic.rs similarity index 100% rename from api/src/utils/clients/ai/anthropic.rs rename to api/server/src/utils/clients/ai/anthropic.rs diff --git a/api/src/utils/clients/ai/embedding_router.rs b/api/server/src/utils/clients/ai/embedding_router.rs similarity index 100% rename from api/src/utils/clients/ai/embedding_router.rs rename to api/server/src/utils/clients/ai/embedding_router.rs diff --git a/api/src/utils/clients/ai/hugging_face.rs b/api/server/src/utils/clients/ai/hugging_face.rs similarity index 100% rename from api/src/utils/clients/ai/hugging_face.rs rename to api/server/src/utils/clients/ai/hugging_face.rs diff --git a/api/src/utils/clients/ai/langfuse.rs b/api/server/src/utils/clients/ai/langfuse.rs similarity index 100% rename from api/src/utils/clients/ai/langfuse.rs rename to api/server/src/utils/clients/ai/langfuse.rs diff --git a/api/src/utils/clients/ai/llm_router.rs b/api/server/src/utils/clients/ai/llm_router.rs similarity index 100% rename from api/src/utils/clients/ai/llm_router.rs rename to api/server/src/utils/clients/ai/llm_router.rs diff --git a/api/src/utils/clients/ai/mod.rs b/api/server/src/utils/clients/ai/mod.rs similarity index 100% rename from api/src/utils/clients/ai/mod.rs rename to api/server/src/utils/clients/ai/mod.rs diff --git a/api/src/utils/clients/ai/ollama.rs b/api/server/src/utils/clients/ai/ollama.rs similarity index 100% rename from api/src/utils/clients/ai/ollama.rs rename to api/server/src/utils/clients/ai/ollama.rs diff --git a/api/src/utils/clients/ai/openai.rs b/api/server/src/utils/clients/ai/openai.rs similarity index 100% rename from api/src/utils/clients/ai/openai.rs rename to api/server/src/utils/clients/ai/openai.rs diff --git a/api/src/utils/clients/aws.rs b/api/server/src/utils/clients/aws.rs similarity index 100% rename from api/src/utils/clients/aws.rs rename to api/server/src/utils/clients/aws.rs diff --git a/api/src/utils/clients/email/email_template.html b/api/server/src/utils/clients/email/email_template.html similarity index 100% rename from api/src/utils/clients/email/email_template.html rename to api/server/src/utils/clients/email/email_template.html diff --git a/api/src/utils/clients/email/mod.rs b/api/server/src/utils/clients/email/mod.rs similarity index 100% rename from api/src/utils/clients/email/mod.rs rename to api/server/src/utils/clients/email/mod.rs diff --git a/api/src/utils/clients/email/resend.rs b/api/server/src/utils/clients/email/resend.rs similarity index 100% rename from api/src/utils/clients/email/resend.rs rename to api/server/src/utils/clients/email/resend.rs diff --git a/api/src/utils/clients/mod.rs b/api/server/src/utils/clients/mod.rs similarity index 100% rename from api/src/utils/clients/mod.rs rename to api/server/src/utils/clients/mod.rs diff --git a/api/src/utils/clients/posthog.rs b/api/server/src/utils/clients/posthog.rs similarity index 100% rename from api/src/utils/clients/posthog.rs rename to api/server/src/utils/clients/posthog.rs diff --git a/api/src/utils/clients/sentry_utils.rs b/api/server/src/utils/clients/sentry_utils.rs similarity index 100% rename from api/src/utils/clients/sentry_utils.rs rename to api/server/src/utils/clients/sentry_utils.rs diff --git a/api/src/utils/clients/typesense.rs b/api/server/src/utils/clients/typesense.rs similarity index 100% rename from api/src/utils/clients/typesense.rs rename to api/server/src/utils/clients/typesense.rs diff --git a/api/src/utils/dataset/column_management.rs b/api/server/src/utils/dataset/column_management.rs similarity index 100% rename from api/src/utils/dataset/column_management.rs rename to api/server/src/utils/dataset/column_management.rs diff --git a/api/src/utils/dataset/mod.rs b/api/server/src/utils/dataset/mod.rs similarity index 100% rename from api/src/utils/dataset/mod.rs rename to api/server/src/utils/dataset/mod.rs diff --git a/api/src/utils/mod.rs b/api/server/src/utils/mod.rs similarity index 100% rename from api/src/utils/mod.rs rename to api/server/src/utils/mod.rs diff --git a/api/src/utils/prompts/analyst_chat_prompts/failed_to_fix_sql_prompts.rs b/api/server/src/utils/prompts/analyst_chat_prompts/failed_to_fix_sql_prompts.rs similarity index 100% rename from api/src/utils/prompts/analyst_chat_prompts/failed_to_fix_sql_prompts.rs rename to api/server/src/utils/prompts/analyst_chat_prompts/failed_to_fix_sql_prompts.rs diff --git a/api/src/utils/prompts/analyst_chat_prompts/master_response_prompt.rs b/api/server/src/utils/prompts/analyst_chat_prompts/master_response_prompt.rs similarity index 100% rename from api/src/utils/prompts/analyst_chat_prompts/master_response_prompt.rs rename to api/server/src/utils/prompts/analyst_chat_prompts/master_response_prompt.rs diff --git a/api/src/utils/prompts/analyst_chat_prompts/mod.rs b/api/server/src/utils/prompts/analyst_chat_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/analyst_chat_prompts/mod.rs rename to api/server/src/utils/prompts/analyst_chat_prompts/mod.rs diff --git a/api/src/utils/prompts/analyst_chat_prompts/orchestrator_prompt.rs b/api/server/src/utils/prompts/analyst_chat_prompts/orchestrator_prompt.rs similarity index 100% rename from api/src/utils/prompts/analyst_chat_prompts/orchestrator_prompt.rs rename to api/server/src/utils/prompts/analyst_chat_prompts/orchestrator_prompt.rs diff --git a/api/src/utils/prompts/custom_response_prompts/custom_response_prompt.rs b/api/server/src/utils/prompts/custom_response_prompts/custom_response_prompt.rs similarity index 100% rename from api/src/utils/prompts/custom_response_prompts/custom_response_prompt.rs rename to api/server/src/utils/prompts/custom_response_prompts/custom_response_prompt.rs diff --git a/api/src/utils/prompts/custom_response_prompts/mod.rs b/api/server/src/utils/prompts/custom_response_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/custom_response_prompts/mod.rs rename to api/server/src/utils/prompts/custom_response_prompts/mod.rs diff --git a/api/src/utils/prompts/generate_sql_prompts/dataset_selector_prompt.rs b/api/server/src/utils/prompts/generate_sql_prompts/dataset_selector_prompt.rs similarity index 100% rename from api/src/utils/prompts/generate_sql_prompts/dataset_selector_prompt.rs rename to api/server/src/utils/prompts/generate_sql_prompts/dataset_selector_prompt.rs diff --git a/api/src/utils/prompts/generate_sql_prompts/mod.rs b/api/server/src/utils/prompts/generate_sql_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/generate_sql_prompts/mod.rs rename to api/server/src/utils/prompts/generate_sql_prompts/mod.rs diff --git a/api/src/utils/prompts/generate_sql_prompts/multiple_datasets_prompt.rs b/api/server/src/utils/prompts/generate_sql_prompts/multiple_datasets_prompt.rs similarity index 100% rename from api/src/utils/prompts/generate_sql_prompts/multiple_datasets_prompt.rs rename to api/server/src/utils/prompts/generate_sql_prompts/multiple_datasets_prompt.rs diff --git a/api/src/utils/prompts/generate_sql_prompts/sql_gen_prompt.rs b/api/server/src/utils/prompts/generate_sql_prompts/sql_gen_prompt.rs similarity index 100% rename from api/src/utils/prompts/generate_sql_prompts/sql_gen_prompt.rs rename to api/server/src/utils/prompts/generate_sql_prompts/sql_gen_prompt.rs diff --git a/api/src/utils/prompts/generate_sql_prompts/sql_gen_thought_prompt.rs b/api/server/src/utils/prompts/generate_sql_prompts/sql_gen_thought_prompt.rs similarity index 100% rename from api/src/utils/prompts/generate_sql_prompts/sql_gen_thought_prompt.rs rename to api/server/src/utils/prompts/generate_sql_prompts/sql_gen_thought_prompt.rs diff --git a/api/src/utils/prompts/mod.rs b/api/server/src/utils/prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/mod.rs rename to api/server/src/utils/prompts/mod.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/bar_line_chart_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/bar_line_chart_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/bar_line_chart_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/bar_line_chart_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/combo_chart_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/combo_chart_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/combo_chart_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/combo_chart_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/metric_chart_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/metric_chart_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/metric_chart_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/metric_chart_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/mod.rs b/api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/mod.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/mod.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/pie_chart_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/pie_chart_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/pie_chart_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/pie_chart_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/scatter_chart_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/scatter_chart_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/scatter_chart_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/build_charts_prompts/scatter_chart_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/format_label_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/format_label_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/format_label_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/format_label_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/mod.rs b/api/server/src/utils/prompts/modify_visualization_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/mod.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/mod.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/modify_visualization_orchestrator_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/modify_visualization_orchestrator_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/modify_visualization_orchestrator_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/modify_visualization_orchestrator_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/select_visualization_prompt.rs b/api/server/src/utils/prompts/modify_visualization_prompts/select_visualization_prompt.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/select_visualization_prompt.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/select_visualization_prompt.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/styling_prompts/column_styling_prompts.rs b/api/server/src/utils/prompts/modify_visualization_prompts/styling_prompts/column_styling_prompts.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/styling_prompts/column_styling_prompts.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/styling_prompts/column_styling_prompts.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/styling_prompts/global_styling_prompts.rs b/api/server/src/utils/prompts/modify_visualization_prompts/styling_prompts/global_styling_prompts.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/styling_prompts/global_styling_prompts.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/styling_prompts/global_styling_prompts.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/styling_prompts/mod.rs b/api/server/src/utils/prompts/modify_visualization_prompts/styling_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/styling_prompts/mod.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/styling_prompts/mod.rs diff --git a/api/src/utils/prompts/modify_visualization_prompts/title_description_time_frame_prompts.rs b/api/server/src/utils/prompts/modify_visualization_prompts/title_description_time_frame_prompts.rs similarity index 100% rename from api/src/utils/prompts/modify_visualization_prompts/title_description_time_frame_prompts.rs rename to api/server/src/utils/prompts/modify_visualization_prompts/title_description_time_frame_prompts.rs diff --git a/api/src/utils/prompts/sql_evaluator_prompts/mod.rs b/api/server/src/utils/prompts/sql_evaluator_prompts/mod.rs similarity index 100% rename from api/src/utils/prompts/sql_evaluator_prompts/mod.rs rename to api/server/src/utils/prompts/sql_evaluator_prompts/mod.rs diff --git a/api/src/utils/prompts/sql_evaluator_prompts/sql_evaluation_summary_prompts.rs b/api/server/src/utils/prompts/sql_evaluator_prompts/sql_evaluation_summary_prompts.rs similarity index 100% rename from api/src/utils/prompts/sql_evaluator_prompts/sql_evaluation_summary_prompts.rs rename to api/server/src/utils/prompts/sql_evaluator_prompts/sql_evaluation_summary_prompts.rs diff --git a/api/src/utils/prompts/sql_evaluator_prompts/sql_evaluator_prompts.rs b/api/server/src/utils/prompts/sql_evaluator_prompts/sql_evaluator_prompts.rs similarity index 100% rename from api/src/utils/prompts/sql_evaluator_prompts/sql_evaluator_prompts.rs rename to api/server/src/utils/prompts/sql_evaluator_prompts/sql_evaluator_prompts.rs diff --git a/api/src/utils/query_engine/credentials.rs b/api/server/src/utils/query_engine/credentials.rs similarity index 100% rename from api/src/utils/query_engine/credentials.rs rename to api/server/src/utils/query_engine/credentials.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_bigquery_client.rs b/api/server/src/utils/query_engine/data_source_connections/get_bigquery_client.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_bigquery_client.rs rename to api/server/src/utils/query_engine/data_source_connections/get_bigquery_client.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_databricks_client.rs b/api/server/src/utils/query_engine/data_source_connections/get_databricks_client.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_databricks_client.rs rename to api/server/src/utils/query_engine/data_source_connections/get_databricks_client.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_mysql_connection.rs b/api/server/src/utils/query_engine/data_source_connections/get_mysql_connection.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_mysql_connection.rs rename to api/server/src/utils/query_engine/data_source_connections/get_mysql_connection.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_postgres_connection.rs b/api/server/src/utils/query_engine/data_source_connections/get_postgres_connection.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_postgres_connection.rs rename to api/server/src/utils/query_engine/data_source_connections/get_postgres_connection.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_redshift_connection.rs b/api/server/src/utils/query_engine/data_source_connections/get_redshift_connection.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_redshift_connection.rs rename to api/server/src/utils/query_engine/data_source_connections/get_redshift_connection.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_snowflake_client.rs b/api/server/src/utils/query_engine/data_source_connections/get_snowflake_client.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_snowflake_client.rs rename to api/server/src/utils/query_engine/data_source_connections/get_snowflake_client.rs diff --git a/api/src/utils/query_engine/data_source_connections/get_sql_server_connection.rs b/api/server/src/utils/query_engine/data_source_connections/get_sql_server_connection.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/get_sql_server_connection.rs rename to api/server/src/utils/query_engine/data_source_connections/get_sql_server_connection.rs diff --git a/api/src/utils/query_engine/data_source_connections/mod.rs b/api/server/src/utils/query_engine/data_source_connections/mod.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/mod.rs rename to api/server/src/utils/query_engine/data_source_connections/mod.rs diff --git a/api/src/utils/query_engine/data_source_connections/ssh_tunneling.rs b/api/server/src/utils/query_engine/data_source_connections/ssh_tunneling.rs similarity index 100% rename from api/src/utils/query_engine/data_source_connections/ssh_tunneling.rs rename to api/server/src/utils/query_engine/data_source_connections/ssh_tunneling.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/bigquery_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/bigquery_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/bigquery_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/bigquery_query.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/databricks_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/databricks_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/databricks_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/databricks_query.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/mod.rs b/api/server/src/utils/query_engine/data_source_query_routes/mod.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/mod.rs rename to api/server/src/utils/query_engine/data_source_query_routes/mod.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/mysql_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/mysql_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/mysql_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/mysql_query.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/postgres_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/postgres_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/postgres_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/postgres_query.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/query_router.rs b/api/server/src/utils/query_engine/data_source_query_routes/query_router.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/query_router.rs rename to api/server/src/utils/query_engine/data_source_query_routes/query_router.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/redshift_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/redshift_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/redshift_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/redshift_query.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/security_utils.rs b/api/server/src/utils/query_engine/data_source_query_routes/security_utils.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/security_utils.rs rename to api/server/src/utils/query_engine/data_source_query_routes/security_utils.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/snowflake_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/snowflake_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/snowflake_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/snowflake_query.rs diff --git a/api/src/utils/query_engine/data_source_query_routes/sql_server_query.rs b/api/server/src/utils/query_engine/data_source_query_routes/sql_server_query.rs similarity index 100% rename from api/src/utils/query_engine/data_source_query_routes/sql_server_query.rs rename to api/server/src/utils/query_engine/data_source_query_routes/sql_server_query.rs diff --git a/api/src/utils/query_engine/data_types.rs b/api/server/src/utils/query_engine/data_types.rs similarity index 100% rename from api/src/utils/query_engine/data_types.rs rename to api/server/src/utils/query_engine/data_types.rs diff --git a/api/src/utils/query_engine/import_dataset_columns.rs b/api/server/src/utils/query_engine/import_dataset_columns.rs similarity index 100% rename from api/src/utils/query_engine/import_dataset_columns.rs rename to api/server/src/utils/query_engine/import_dataset_columns.rs diff --git a/api/src/utils/query_engine/import_datasets.rs b/api/server/src/utils/query_engine/import_datasets.rs similarity index 100% rename from api/src/utils/query_engine/import_datasets.rs rename to api/server/src/utils/query_engine/import_datasets.rs diff --git a/api/src/utils/query_engine/mod.rs b/api/server/src/utils/query_engine/mod.rs similarity index 100% rename from api/src/utils/query_engine/mod.rs rename to api/server/src/utils/query_engine/mod.rs diff --git a/api/src/utils/query_engine/query_engine.rs b/api/server/src/utils/query_engine/query_engine.rs similarity index 100% rename from api/src/utils/query_engine/query_engine.rs rename to api/server/src/utils/query_engine/query_engine.rs diff --git a/api/src/utils/query_engine/test_data_source_connections.rs b/api/server/src/utils/query_engine/test_data_source_connections.rs similarity index 100% rename from api/src/utils/query_engine/test_data_source_connections.rs rename to api/server/src/utils/query_engine/test_data_source_connections.rs diff --git a/api/src/utils/query_engine/utils.rs b/api/server/src/utils/query_engine/utils.rs similarity index 100% rename from api/src/utils/query_engine/utils.rs rename to api/server/src/utils/query_engine/utils.rs diff --git a/api/src/utils/query_engine/values_index.rs b/api/server/src/utils/query_engine/values_index.rs similarity index 100% rename from api/src/utils/query_engine/values_index.rs rename to api/server/src/utils/query_engine/values_index.rs diff --git a/api/src/utils/query_engine/write_query_engine.rs b/api/server/src/utils/query_engine/write_query_engine.rs similarity index 100% rename from api/src/utils/query_engine/write_query_engine.rs rename to api/server/src/utils/query_engine/write_query_engine.rs diff --git a/api/src/utils/search_engine/mod.rs b/api/server/src/utils/search_engine/mod.rs similarity index 100% rename from api/src/utils/search_engine/mod.rs rename to api/server/src/utils/search_engine/mod.rs diff --git a/api/src/utils/search_engine/search_engine.rs b/api/server/src/utils/search_engine/search_engine.rs similarity index 100% rename from api/src/utils/search_engine/search_engine.rs rename to api/server/src/utils/search_engine/search_engine.rs diff --git a/api/src/utils/security/checks.rs b/api/server/src/utils/security/checks.rs similarity index 100% rename from api/src/utils/security/checks.rs rename to api/server/src/utils/security/checks.rs diff --git a/api/src/utils/security/dataset_security.rs b/api/server/src/utils/security/dataset_security.rs similarity index 100% rename from api/src/utils/security/dataset_security.rs rename to api/server/src/utils/security/dataset_security.rs diff --git a/api/src/utils/security/mod.rs b/api/server/src/utils/security/mod.rs similarity index 100% rename from api/src/utils/security/mod.rs rename to api/server/src/utils/security/mod.rs diff --git a/api/src/utils/serde_helpers/deserialization_helpers.rs b/api/server/src/utils/serde_helpers/deserialization_helpers.rs similarity index 100% rename from api/src/utils/serde_helpers/deserialization_helpers.rs rename to api/server/src/utils/serde_helpers/deserialization_helpers.rs diff --git a/api/src/utils/serde_helpers/mod.rs b/api/server/src/utils/serde_helpers/mod.rs similarity index 100% rename from api/src/utils/serde_helpers/mod.rs rename to api/server/src/utils/serde_helpers/mod.rs diff --git a/api/src/utils/sharing/asset_sharing.rs b/api/server/src/utils/sharing/asset_sharing.rs similarity index 100% rename from api/src/utils/sharing/asset_sharing.rs rename to api/server/src/utils/sharing/asset_sharing.rs diff --git a/api/src/utils/sharing/mod.rs b/api/server/src/utils/sharing/mod.rs similarity index 100% rename from api/src/utils/sharing/mod.rs rename to api/server/src/utils/sharing/mod.rs diff --git a/api/src/utils/stored_values/mod.rs b/api/server/src/utils/stored_values/mod.rs similarity index 100% rename from api/src/utils/stored_values/mod.rs rename to api/server/src/utils/stored_values/mod.rs diff --git a/api/src/utils/stored_values/search.rs b/api/server/src/utils/stored_values/search.rs similarity index 100% rename from api/src/utils/stored_values/search.rs rename to api/server/src/utils/stored_values/search.rs diff --git a/api/src/utils/user/mod.rs b/api/server/src/utils/user/mod.rs similarity index 100% rename from api/src/utils/user/mod.rs rename to api/server/src/utils/user/mod.rs diff --git a/api/src/utils/user/user_info.rs b/api/server/src/utils/user/user_info.rs similarity index 100% rename from api/src/utils/user/user_info.rs rename to api/server/src/utils/user/user_info.rs diff --git a/api/src/utils/validation/dataset_validation.rs b/api/server/src/utils/validation/dataset_validation.rs similarity index 100% rename from api/src/utils/validation/dataset_validation.rs rename to api/server/src/utils/validation/dataset_validation.rs diff --git a/api/src/utils/validation/mod.rs b/api/server/src/utils/validation/mod.rs similarity index 100% rename from api/src/utils/validation/mod.rs rename to api/server/src/utils/validation/mod.rs diff --git a/api/src/utils/validation/type_mapping.rs b/api/server/src/utils/validation/type_mapping.rs similarity index 100% rename from api/src/utils/validation/type_mapping.rs rename to api/server/src/utils/validation/type_mapping.rs diff --git a/api/src/utils/validation/types.rs b/api/server/src/utils/validation/types.rs similarity index 100% rename from api/src/utils/validation/types.rs rename to api/server/src/utils/validation/types.rs diff --git a/api/testkit/.env.test b/api/testkit/.env.test new file mode 100644 index 000000000..3ec8a8a83 --- /dev/null +++ b/api/testkit/.env.test @@ -0,0 +1,6 @@ +# Test Environment Configuration +TEST_ENV=test +TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres +TEST_REDIS_URL=redis://localhost:6379 +TEST_LOG=true +TEST_LOG_LEVEL=debug \ No newline at end of file diff --git a/api/testkit/Cargo.toml b/api/testkit/Cargo.toml new file mode 100644 index 000000000..feddb53c2 --- /dev/null +++ b/api/testkit/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "testkit" +version = "0.1.0" +edition = "2021" +description = "Test utilities for Buster API database pools" +build = "build.rs" + +[dependencies] +anyhow = { workspace = true } +tokio = { workspace = true } +diesel = { workspace = true } +diesel-async = { workspace = true } +sqlx = { workspace = true } +bb8-redis = { workspace = true } +uuid = { workspace = true } +dotenv = { workspace = true } +once_cell = { workspace = true } +futures = { workspace = true } +tracing = { workspace = true } +database = { path = "../libs/database" } + +[build-dependencies] +dotenv = { workspace = true } +tokio = { workspace = true, features = ["rt", "macros"] } +database = { path = "../libs/database" } + +[features] +default = [] +db_reset = [] # Use with caution - allows resetting the test database \ No newline at end of file diff --git a/api/testkit/README.md b/api/testkit/README.md new file mode 100644 index 000000000..8714c3fcd --- /dev/null +++ b/api/testkit/README.md @@ -0,0 +1,76 @@ +# Buster API Test Kit + +The `testkit` crate provides standardized database pool initialization for tests across the Buster API workspace, leveraging the existing database pools from the database library. + +## Features + +- **Automatic Database Pool Configuration**: Uses the database library's pools with test configurations +- **Environment Configuration**: Uses `.env.test` for test database configuration +- **Test ID Generation**: Provides unique IDs for test data isolation + +## Getting Started + +Add the testkit as a dev-dependency in your workspace crate: + +```toml +[dev-dependencies] +testkit = { path = "../../testkit" } +``` + +## Usage + +```rust +use anyhow::Result; + +#[tokio::test] +async fn my_test() -> Result<()> { + // Get database pools - these are initialized from the database library + let pg_pool = testkit::get_pg_pool(); + let redis_pool = testkit::get_redis_pool(); + + // Use the pools for testing + let conn = pg_pool.get().await?; + + // Generate a unique test ID for data isolation + let test_id = testkit::test_id(); + + // Use test_id to tag and later clean up test data + + Ok(()) +} +``` + +## Test Data Isolation + +Use the `test_id()` function to generate unique IDs for isolating test data: + +```rust +// Get a unique ID +let test_id = testkit::test_id(); + +// Tag test data with the ID +diesel::sql_query("INSERT INTO users (name, test_id) VALUES ($1, $2)") + .bind::("Test User") + .bind::(&test_id) + .execute(&mut conn) + .await?; + +// Clean up after test +diesel::sql_query("DELETE FROM users WHERE test_id = $1") + .bind::(&test_id) + .execute(&mut conn) + .await?; +``` + +## Environment Configuration + +By default, the testkit checks for and creates a `.env.test` file with: + +``` +TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres +TEST_REDIS_URL=redis://localhost:6379 +TEST_LOG=true +TEST_LOG_LEVEL=debug +``` + +When running tests, the testkit automatically sets `DATABASE_URL` to the value of `TEST_DATABASE_URL`, ensuring that tests use the test database. \ No newline at end of file diff --git a/api/testkit/build.rs b/api/testkit/build.rs new file mode 100644 index 000000000..2c8fd0220 --- /dev/null +++ b/api/testkit/build.rs @@ -0,0 +1,103 @@ +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + // Create default .env.test if it doesn't exist + ensure_test_env_exists(); + + // Load environment variables from .env + load_env_file(); + + // Try to initialize pools but don't fail the build if it fails + if let Err(e) = try_init_pools() { + println!("cargo:warning=Failed to initialize pools: {}", e); + println!("cargo:warning=This is not a build error - pools will be initialized when tests run"); + } else { + println!("cargo:warning=Successfully initialized database pools"); + } +} + +fn ensure_test_env_exists() { + let test_env_path = Path::new(".env.test"); + + // Only create if it doesn't exist + if !test_env_path.exists() { + println!("cargo:warning=Creating default .env.test file"); + + let default_content = r#" +# Test Environment Configuration +TEST_ENV=test +TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres +TEST_POOLER_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres +TEST_REDIS_URL=redis://localhost:6379 +TEST_DATABASE_POOL_SIZE=10 +TEST_SQLX_POOL_SIZE=10 +TEST_LOG=true +TEST_LOG_LEVEL=debug + "#.trim(); + + fs::write(test_env_path, default_content) + .expect("Failed to create default .env.test file"); + + println!("cargo:warning=Created default .env.test file"); + } +} + +fn load_env_file() { + // Try loading .env.test first, then fall back to .env + if Path::new(".env.test").exists() { + if let Ok(_) = dotenv::from_filename(".env.test") { + println!("cargo:warning=Loaded environment from .env.test"); + } + } else if let Ok(_) = dotenv::dotenv() { + println!("cargo:warning=Loaded environment from .env"); + } + + // Override DATABASE_URL to use TEST_DATABASE_URL for tests + if let Ok(test_db_url) = env::var("TEST_DATABASE_URL") { + env::set_var("DATABASE_URL", test_db_url); + println!("cargo:warning=Using TEST_DATABASE_URL for DATABASE_URL"); + } + + // Override POOLER_URL to use TEST_POOLER_URL for tests + if let Ok(test_pooler_url) = env::var("TEST_POOLER_URL") { + env::set_var("POOLER_URL", test_pooler_url); + println!("cargo:warning=Using TEST_POOLER_URL for POOLER_URL"); + } + + // Override REDIS_URL to use TEST_REDIS_URL for tests + if let Ok(test_redis_url) = env::var("TEST_REDIS_URL") { + env::set_var("REDIS_URL", test_redis_url); + println!("cargo:warning=Using TEST_REDIS_URL for REDIS_URL"); + } + + // Override pool sizes to prevent excessive connections in test environment + if let Ok(test_pool_size) = env::var("TEST_DATABASE_POOL_SIZE") { + env::set_var("DATABASE_POOL_SIZE", test_pool_size); + } + + if let Ok(test_sqlx_pool_size) = env::var("TEST_SQLX_POOL_SIZE") { + env::set_var("SQLX_POOL_SIZE", test_sqlx_pool_size); + } +} + +fn try_init_pools() -> Result<(), String> { + // Create a runtime for async operations + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() { + Ok(rt) => rt, + Err(e) => return Err(e.to_string()), + }; + + // Try to initialize pools but don't fail the build if it fails + runtime.block_on(async { + match database::pool::init_pools().await { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } + }) +} \ No newline at end of file diff --git a/api/testkit/src/lib.rs b/api/testkit/src/lib.rs new file mode 100644 index 000000000..4282465b3 --- /dev/null +++ b/api/testkit/src/lib.rs @@ -0,0 +1,156 @@ +use anyhow::Result; +use dotenv::dotenv; +use std::path::Path; +use std::sync::Once; +use once_cell::sync::OnceCell; +use database::pool::{PgPool, PgPoolSqlx, RedisPool}; + +static ENV_INIT: Once = Once::new(); +static POOL_INIT: OnceCell<()> = OnceCell::new(); + +/// Initialize the test environment by setting up .env.test +pub fn init_test_env() { + ENV_INIT.call_once(|| { + // Try loading .env.test first, then fall back to .env + if Path::new(".env.test").exists() { + dotenv::from_filename(".env.test").ok(); + } else { + dotenv().ok(); + } + + // Override DATABASE_URL to use TEST_DATABASE_URL for tests + if let Ok(test_db_url) = std::env::var("TEST_DATABASE_URL") { + std::env::set_var("DATABASE_URL", test_db_url); + } + + // Override POOLER_URL to use TEST_POOLER_URL for tests + if let Ok(test_pooler_url) = std::env::var("TEST_POOLER_URL") { + std::env::set_var("POOLER_URL", test_pooler_url); + } + + // Override REDIS_URL to use TEST_REDIS_URL for tests + if let Ok(test_redis_url) = std::env::var("TEST_REDIS_URL") { + std::env::set_var("REDIS_URL", test_redis_url); + } + + // Override pool sizes to prevent excessive connections in test environment + if let Ok(test_pool_size) = std::env::var("TEST_DATABASE_POOL_SIZE") { + std::env::set_var("DATABASE_POOL_SIZE", test_pool_size); + } + + if let Ok(test_sqlx_pool_size) = std::env::var("TEST_SQLX_POOL_SIZE") { + std::env::set_var("SQLX_POOL_SIZE", test_sqlx_pool_size); + } + }); +} + +/// Initialize database and Redis pools for testing +pub async fn init_pools() -> Result<()> { + // Setup the environment first + init_test_env(); + + // Only initialize pools once + if POOL_INIT.get().is_some() { + return Ok(()); + } + + // Use the init_test_pools function which is specifically designed for tests + let result = match database::pool::init_pools().await { + Ok(_) => { + // Success case - cache the result + let _ = POOL_INIT.set(()); + Ok(()) + }, + Err(e) => { + // Log the error but still cache the attempt to prevent repeated tries + tracing::error!("Failed to initialize test pools: {}", e); + let _ = POOL_INIT.set(()); + Err(e) + } + }; + + result +} + +/// Get the initialized PG pool, initializing it first if needed +pub fn get_pg_pool() -> &'static PgPool { + ensure_pools_initialized(); + database::pool::get_pg_pool() +} + +/// Get the initialized SQLX pool, initializing it first if needed +pub fn get_sqlx_pool() -> &'static PgPoolSqlx { + ensure_pools_initialized(); + database::pool::get_sqlx_pool() +} + +/// Get the initialized Redis pool, initializing it first if needed +pub fn get_redis_pool() -> &'static RedisPool { + ensure_pools_initialized(); + database::pool::get_redis_pool() +} + +/// Helper function to ensure pools are initialized +fn ensure_pools_initialized() { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + // Always initialize test environment + init_test_env(); + + // If pools aren't initialized yet, try to initialize them + if POOL_INIT.get().is_none() { + if let Err(e) = init_pools().await { + panic!("Failed to initialize database pools for tests: {}", e); + } + } + }); +} + +/// Generate a unique test ID - useful for creating test resources +pub fn test_id() -> String { + uuid::Uuid::new_v4().to_string() +} + +/// Clean up a test database connection - useful in test teardown +pub async fn cleanup_connection() -> Result<()> { + // This is a placeholder for any future cleanup that might be needed + // Currently the pools handle their own cleanup + Ok(()) +} + +/// Reset the test database to a clean state +/// Only use this in integration tests where you need a completely fresh DB +/// Most tests should isolate their data instead +#[cfg(feature = "db_reset")] +pub async fn reset_test_database() -> Result<()> { + let pool = get_pg_pool(); + let mut conn = pool.get().await?; + + // Execute a transaction that truncates all tables + // This code is only included when the db_reset feature is enabled + // as it's potentially destructive + + diesel::sql_query("BEGIN").execute(&mut conn).await?; + + // List of tables to truncate - add more as needed + let tables = vec![ + "users", "organizations", "users_to_organizations", + "api_keys", "teams", "teams_to_users", + "data_sources", "datasets", "dataset_columns", + "permission_groups", "datasets_to_permission_groups", + "terms", "collections", "dashboards", "threads", "messages" + ]; + + for table in tables { + diesel::sql_query(format!("TRUNCATE TABLE {} CASCADE", table)) + .execute(&mut conn) + .await?; + } + + diesel::sql_query("COMMIT").execute(&mut conn).await?; + + Ok(()) +} \ No newline at end of file diff --git a/api/tests/common/assertions/mod.rs b/api/tests/common/assertions/mod.rs deleted file mode 100644 index d79d89608..000000000 --- a/api/tests/common/assertions/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Re-export assertion modules -mod response; -mod model; - -pub use response::ResponseAssertions; -pub use model::ModelAssertions; \ No newline at end of file diff --git a/api/tests/common/assertions/model.rs b/api/tests/common/assertions/model.rs deleted file mode 100644 index 1a6a64a72..000000000 --- a/api/tests/common/assertions/model.rs +++ /dev/null @@ -1,141 +0,0 @@ -use anyhow::Result; -use std::fmt::Debug; - -/// Trait for making assertions on model objects -pub trait ModelAssertions { - /// Assert that a field has the expected value - fn assert_field(&self, field_name: &str, field_value: &T, expected: &T) -> &Self; - - /// Assert that a field is not null/none - fn assert_field_present(&self, field_name: &str, field_value: &Option) -> &Self; - - /// Assert that a string field contains expected content - fn assert_string_contains(&self, field_name: &str, field_value: &str, expected_content: &str) -> &Self; - - /// Assert that a numeric field is in range - fn assert_numeric_in_range( - &self, - field_name: &str, - field_value: &T, - min: &T, - max: &T - ) -> &Self; -} - -/// Implementation for any type -impl ModelAssertions for S { - fn assert_field(&self, field_name: &str, field_value: &T, expected: &T) -> &Self { - assert_eq!( - field_value, - expected, - "Expected field '{}' to be {:?}, but got {:?}", - field_name, - expected, - field_value - ); - self - } - - fn assert_field_present(&self, field_name: &str, field_value: &Option) -> &Self { - assert!( - field_value.is_some(), - "Expected field '{}' to be present, but it was None", - field_name - ); - self - } - - fn assert_string_contains(&self, field_name: &str, field_value: &str, expected_content: &str) -> &Self { - assert!( - field_value.contains(expected_content), - "Expected field '{}' to contain '{}', but got '{}'", - field_name, - expected_content, - field_value - ); - self - } - - fn assert_numeric_in_range( - &self, - field_name: &str, - field_value: &T, - min: &T, - max: &T - ) -> &Self { - assert!( - field_value >= min && field_value <= max, - "Expected field '{}' to be between {:?} and {:?}, but got {:?}", - field_name, - min, - max, - field_value - ); - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Debug)] - struct TestModel { - id: i32, - name: String, - age: u32, - email: Option, - } - - #[test] - fn test_assert_field() { - let model = TestModel { - id: 123, - name: "Test User".to_string(), - age: 30, - email: Some("test@example.com".to_string()), - }; - - // Test valid assertion - model.assert_field("id", &model.id, &123); - - // Test with string - model.assert_field("name", &model.name, &"Test User".to_string()); - } - - #[test] - fn test_assert_field_present() { - let model = TestModel { - id: 123, - name: "Test User".to_string(), - age: 30, - email: Some("test@example.com".to_string()), - }; - - model.assert_field_present("email", &model.email); - } - - #[test] - fn test_assert_string_contains() { - let model = TestModel { - id: 123, - name: "Test User".to_string(), - age: 30, - email: Some("test@example.com".to_string()), - }; - - model.assert_string_contains("name", &model.name, "User"); - } - - #[test] - fn test_assert_numeric_in_range() { - let model = TestModel { - id: 123, - name: "Test User".to_string(), - age: 30, - email: Some("test@example.com".to_string()), - }; - - model.assert_numeric_in_range("age", &model.age, &18, &60); - } -} \ No newline at end of file diff --git a/api/tests/common/assertions/response.rs b/api/tests/common/assertions/response.rs deleted file mode 100644 index 374cc2912..000000000 --- a/api/tests/common/assertions/response.rs +++ /dev/null @@ -1,179 +0,0 @@ -use anyhow::Result; -use reqwest::Response; -use serde::de::DeserializeOwned; -use async_trait::async_trait; - -/// Extension trait for reqwest::Response that adds common assertions -#[async_trait] -pub trait ResponseAssertions { - /// Assert response has expected status code - fn assert_status(&self, expected: reqwest::StatusCode) -> &Self; - - /// Parse response body as JSON and perform assertions on it - async fn assert_json(&self, assertions: F) -> Result - where - F: FnOnce(&T) -> bool + Send, - T: DeserializeOwned; - - /// Assert response contains expected header - fn assert_header(&self, name: &str, expected_value: &str) -> &Self; - - /// Assert response body contains a string - async fn assert_body_contains(&self, expected_content: &str) -> Result<&Self>; - - /// Assert response body matches exactly - async fn assert_body_text(&self, expected_text: &str) -> Result<&Self>; -} - -/// Helper for cloning responses so we can extract JSON and still keep the response -fn try_clone(response: &Response) -> Result { - response.try_clone() - .ok_or_else(|| anyhow::anyhow!("Failed to clone response")) -} - -#[async_trait] -impl ResponseAssertions for Response { - fn assert_status(&self, expected: reqwest::StatusCode) -> &Self { - assert_eq!( - self.status(), - expected, - "Expected status code {}, got {}", - expected, - self.status() - ); - self - } - - async fn assert_json(&self, assertions: F) -> Result - where - F: FnOnce(&T) -> bool + Send, - T: DeserializeOwned, - { - // Clone the response so we can return the parsed data - let mut response_copy = try_clone(self)?; - - // Parse JSON - let json_data: T = response_copy.json().await?; - - // Run assertions - assert!( - assertions(&json_data), - "JSON assertions failed for response" - ); - - Ok(json_data) - } - - fn assert_header(&self, name: &str, expected_value: &str) -> &Self { - let header_value = self.headers().get(name) - .unwrap_or_else(|| panic!("Header {} not found in response", name)) - .to_str() - .unwrap_or_else(|_| panic!("Header {} contains non-string value", name)); - - assert_eq!( - header_value, - expected_value, - "Expected header {} to have value {}, got {}", - name, - expected_value, - header_value - ); - - self - } - - async fn assert_body_contains(&self, expected_content: &str) -> Result<&Self> { - let response_copy = try_clone(self)?; - let body_text = response_copy.text().await?; - - assert!( - body_text.contains(expected_content), - "Expected response body to contain '{}', got '{}'", - expected_content, - body_text - ); - - Ok(self) - } - - async fn assert_body_text(&self, expected_text: &str) -> Result<&Self> { - let response_copy = try_clone(self)?; - let body_text = response_copy.text().await?; - - assert_eq!( - body_text, - expected_text, - "Expected response body to be '{}', got '{}'", - expected_text, - body_text - ); - - Ok(self) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use reqwest::{Client, StatusCode, Response}; - use serde::{Deserialize, Serialize}; - use mockito::Server; - - #[derive(Debug, Serialize, Deserialize, PartialEq)] - struct TestUser { - id: i32, - name: String, - } - - async fn get_mock_response() -> Result { - let server = Server::new_async().await; - - let _mock = server.mock("GET", "/test") - .with_status(200) - .with_header("X-Test", "test-value") - .with_body(r#"{"id": 123, "name": "Test User"}"#) - .create(); - - let client = Client::new(); - let response = client.get(&format!("{}/test", server.url())) - .send() - .await?; - - Ok(response) - } - - #[tokio::test] - async fn test_assert_status() -> Result<()> { - let response = get_mock_response().await?; - response.assert_status(StatusCode::OK); - Ok(()) - } - - #[tokio::test] - async fn test_assert_json() -> Result<()> { - let response = get_mock_response().await?; - - let user = response.assert_json::<_, TestUser>(|user| { - user.id == 123 && user.name == "Test User" - }).await?; - - assert_eq!(user.id, 123); - assert_eq!(user.name, "Test User"); - - Ok(()) - } - - #[tokio::test] - async fn test_assert_header() -> Result<()> { - let response = get_mock_response().await?; - response.assert_header("X-Test", "test-value"); - Ok(()) - } - - #[tokio::test] - async fn test_assert_body_contains() -> Result<()> { - let response = get_mock_response().await?; - response.assert_body_contains("Test User").await?; - Ok(()) - } -} \ No newline at end of file diff --git a/api/tests/common/db.rs b/api/tests/common/db.rs deleted file mode 100644 index 3ece72da8..000000000 --- a/api/tests/common/db.rs +++ /dev/null @@ -1,156 +0,0 @@ -use anyhow::Result; -use diesel::PgConnection; -use diesel::r2d2::{ConnectionManager, Pool}; -use uuid::Uuid; -use crate::tests::common::env::init_test_env; - -/// Represents a test database instance with utility functions -pub struct TestDb { - pub pool: Pool>, - pub test_id: String, // Unique identifier for this test run -} - -/// Trait for models that can be tagged with test_id -pub trait TestTaggable { - fn set_test_id(&mut self, test_id: &str); - fn get_test_id(&self) -> Option<&str>; -} - -impl TestDb { - /// Creates a new test database connection pool - pub async fn new() -> Result { - // Initialize the test environment - init_test_env(); - - // Generate unique test identifier - let test_id = Uuid::new_v4().to_string(); - - let database_url = std::env::var("TEST_DATABASE_URL") - .expect("TEST_DATABASE_URL must be set"); - - let manager = ConnectionManager::::new(database_url); - let pool = Pool::builder() - .max_size(5) - .build(manager)?; - - let db = Self { pool, test_id }; - - // Run any initial setup - db.setup_schema().await?; - - Ok(db) - } - - /// Sets up database schema and initial configuration - async fn setup_schema(&self) -> Result<()> { - // This would typically run migrations or setup test-specific tables - // For now, we're just ensuring we have a connection - let _conn = self.pool.get()?; - Ok(()) - } - - /// Sets up common test data that might be needed across multiple tests - pub async fn setup_test_data(&self) -> Result<()> { - // Add common test data setup here - // For example: - // - Create default test users - // - Set up required organization data - // - Initialize any required configuration - Ok(()) - } - - /// Cleans up test data after tests complete - pub async fn cleanup(&self) -> Result<()> { - // In a real implementation, we'd delete all data associated with this test_id - // Example: - // let conn = &mut self.pool.get()?; - // diesel::delete(users::table) - // .filter(users::test_id.eq(&self.test_id)) - // .execute(conn)?; - Ok(()) - } - - /// Gets a connection from the pool - pub fn get_conn(&self) -> Result>> { - Ok(self.pool.get()?) - } - - /// Add test_id to model to track test data - pub fn tag_model(&self, model: &mut T) - where - T: TestTaggable, - { - model.set_test_id(&self.test_id); - } - - /// Create a transaction for test operations - /// Note: Transactions are not recommended for certain operations, - /// but can be useful for tests to ensure isolation - pub fn transaction(&self, f: F) -> Result - where - F: FnOnce(&diesel::PgConnection) -> Result, - { - let conn = self.pool.get()?; - let result = diesel::connection::Connection::transaction(&*conn, |c| { - f(c) - })?; - Ok(result) - } - - /// Execute raw SQL, useful for test setup/teardown - pub fn execute_sql(&self, sql: &str) -> Result<()> { - use diesel::RunQueryDsl; - diesel::sql_query(sql).execute(&*self.pool.get()?)?; - Ok(()) - } -} - -/// Implement Drop to ensure cleanup runs even if tests panic -impl Drop for TestDb { - fn drop(&mut self) { - // Implement synchronous cleanup if needed - // Note: This runs on drop, so it should be quick and not fail - } -} - -/// Example implementation of TestTaggable for a User model -#[cfg(test)] -mod examples { - use super::*; - - // This is just an example - in your real code, you'd implement this for your actual models - pub struct User { - pub id: Uuid, - pub name: String, - pub test_id: Option, - } - - impl TestTaggable for User { - fn set_test_id(&mut self, test_id: &str) { - self.test_id = Some(test_id.to_string()); - } - - fn get_test_id(&self) -> Option<&str> { - self.test_id.as_deref() - } - } - - #[test] - fn test_tagging() { - let db = TestDb { - pool: Pool::builder() - .build(ConnectionManager::::new("dummy")) - .unwrap(), - test_id: "test-123".to_string(), - }; - - let mut user = User { - id: Uuid::new_v4(), - name: "Test User".to_string(), - test_id: None, - }; - - db.tag_model(&mut user); - assert_eq!(user.test_id, Some("test-123".to_string())); - } -} \ No newline at end of file diff --git a/api/tests/common/env.rs b/api/tests/common/env.rs deleted file mode 100644 index bd3ae3442..000000000 --- a/api/tests/common/env.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::sync::Once; -use dotenv::dotenv; -use std::path::Path; - -static ENV_SETUP: Once = Once::new(); - -/// Initialize test environment once per test process. -/// This ensures environment variables are loaded and configured properly. -/// This function is safe to call multiple times as it will only execute once. -pub fn init_test_env() { - ENV_SETUP.call_once(|| { - // Try loading .env.test first, then fall back to .env - if Path::new(".env.test").exists() { - dotenv::from_filename(".env.test").ok(); - } else { - dotenv().ok(); - } - - // Set test-specific environment variables if not already set - if std::env::var("TEST_ENV").is_err() { - std::env::set_var("TEST_ENV", "test"); - } - - // Initialize logger if TEST_LOG is enabled - if std::env::var("TEST_LOG").is_ok() { - init_test_logger(); - } - - // Check for required variables - let required_vars = [ - "TEST_DATABASE_URL", - "TEST_API_KEY", - ]; - - for var in required_vars { - if std::env::var(var).is_err() { - panic!("Required test environment variable {} is not set", var); - } - } - }); -} - -/// Keep the old function name for backward compatibility -pub fn setup_test_env() { - init_test_env(); -} - -/// Get a required environment variable with a helpful error message -pub fn get_test_env(key: &str) -> String { - std::env::var(key) - .unwrap_or_else(|_| panic!("Required test environment variable {} is missing. Check your .env.test file.", key)) -} - -/// Get an optional environment variable with a default value -/// Maintains old name for backward compatibility -pub fn get_test_config(key: &str, default: &str) -> String { - std::env::var(key).unwrap_or_else(|_| default.to_string()) -} - -/// Creates a temporary test directory and returns its path -pub fn setup_test_dir() -> std::path::PathBuf { - let test_dir = std::env::temp_dir().join("test_workspace"); - std::fs::create_dir_all(&test_dir).expect("Failed to create test directory"); - test_dir -} - -/// Cleans up the test directory -pub fn cleanup_test_dir(test_dir: &std::path::Path) { - if test_dir.exists() { - std::fs::remove_dir_all(test_dir).expect("Failed to clean up test directory"); - } -} - -/// Initialize test logger with appropriate filters -fn init_test_logger() { - let filter = std::env::var("TEST_LOG_LEVEL").unwrap_or_else(|_| "debug".to_string()); - - // This is a simplified version - in a real application, you would use - // a proper logging framework like tracing_subscriber - eprintln!("Test logger initialized with level: {}", filter); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_env_with_default() { - // Ensure the function works with non-existent vars - let result = get_test_config("NONEXISTENT_TEST_VAR", "default_value"); - assert_eq!(result, "default_value"); - - // Set an environment variable and test retrieval - std::env::set_var("TEST_EXISTING_VAR", "actual_value"); - let result = get_test_config("TEST_EXISTING_VAR", "default_value"); - assert_eq!(result, "actual_value"); - } -} \ No newline at end of file diff --git a/api/tests/common/fixtures/builder.rs b/api/tests/common/fixtures/builder.rs deleted file mode 100644 index 681b7cc5d..000000000 --- a/api/tests/common/fixtures/builder.rs +++ /dev/null @@ -1,176 +0,0 @@ -/// Generic builder pattern for test fixtures -pub trait FixtureBuilder { - /// Create a default instance of the fixture builder - fn default() -> Self; - - /// Build the final model from the builder (synchronous version) - #[allow(unused_variables)] - fn build(self) -> T where Self: Sized { - panic!("build() not implemented for this fixture - use async build() instead") - } - - /// Build the final model from the builder asynchronously - #[allow(unused_variables)] - async fn build(self) -> T where Self: Sized { - panic!("async build() not implemented for this fixture") - } - - /// Build and return a vector of `count` fixtures - fn build_many(self, count: usize) -> Vec where Self: Sized + Clone { - (0..count).map(|_| self.clone().build()).collect() - } -} - -/// Marker trait for a model that can be used as a fixture -pub trait TestFixture: Sized { - type Builder: FixtureBuilder; - - /// Create a builder with default values - fn builder() -> Self::Builder { - Self::Builder::default() - } -} - -use anyhow::Result; -use uuid::Uuid; -use crate::common::fixtures::users::create_test_user; -use diesel::result::Error as DieselError; -use database::{ - models::User, - pool::get_pg_pool, - schema::users, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -/// Simplified user structure for tests -#[derive(Debug, Clone)] -pub struct TestUser { - pub id: Uuid, - pub email: String, - pub organization_id: Uuid, -} - -/// Simple fixture builder for integration tests -pub struct TestFixtureBuilder; - -impl TestFixtureBuilder { - /// Create a new test fixture builder - pub fn new() -> Self { - Self - } - - /// Create a test user with proper database entry - pub async fn create_user(&mut self) -> Result { - // Create a user model - let model_user = create_test_user(); - - // Insert into database - let mut conn = get_pg_pool().get().await?; - diesel::insert_into(users::table) - .values(&model_user) - .execute(&mut conn) - .await?; - - // Return simplified test user - Ok(TestUser { - id: model_user.id, - email: model_user.email, - organization_id: Uuid::new_v4(), // In a real implementation, this would be properly set - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use uuid::Uuid; - - // Example model - #[derive(Debug, Clone, PartialEq)] - pub struct TestUser { - pub id: Uuid, - pub email: String, - pub name: Option, - } - - // Builder for the model - #[derive(Clone)] - pub struct TestUserBuilder { - id: Option, - email: Option, - name: Option, - } - - impl FixtureBuilder for TestUserBuilder { - fn default() -> Self { - Self { - id: None, - email: None, - name: None, - } - } - - fn build(self) -> TestUser { - TestUser { - id: self.id.unwrap_or_else(Uuid::new_v4), - email: self.email.unwrap_or_else(|| format!("user-{}@example.com", Uuid::new_v4())), - name: self.name, - } - } - } - - // Builder methods - impl TestUserBuilder { - pub fn id(mut self, id: Uuid) -> Self { - self.id = Some(id); - self - } - - pub fn email(mut self, email: impl Into) -> Self { - self.email = Some(email.into()); - self - } - - pub fn name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - } - - // Implement the TestFixture trait - impl TestFixture for TestUser { - type Builder = TestUserBuilder; - } - - #[test] - fn test_builder_pattern() { - // Create a user with default values - let user1 = TestUser::builder().build(); - assert!(user1.email.contains("@example.com")); - assert_eq!(user1.name, None); - - // Create a user with specific values - let user2 = TestUser::builder() - .email("test@example.com") - .name("Test User") - .build(); - - assert_eq!(user2.email, "test@example.com"); - assert_eq!(user2.name, Some("Test User".to_string())); - - // Create multiple users - let users = TestUser::builder() - .name("Same Name") - .build_many(3); - - assert_eq!(users.len(), 3); - assert_eq!(users[0].name, Some("Same Name".to_string())); - assert_eq!(users[1].name, Some("Same Name".to_string())); - assert_eq!(users[2].name, Some("Same Name".to_string())); - - // Each user should have a unique email - assert_ne!(users[0].email, users[1].email); - assert_ne!(users[1].email, users[2].email); - } -} \ No newline at end of file diff --git a/api/tests/common/fixtures/chats.rs b/api/tests/common/fixtures/chats.rs deleted file mode 100644 index f73772108..000000000 --- a/api/tests/common/fixtures/chats.rs +++ /dev/null @@ -1,226 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{Chat, MessageToFile, Message, MetricFile, DashboardFile, AssetPermission}, - pool::get_pg_pool, - schema::{chats, messages, messages_to_files, metric_files, dashboard_files, asset_permissions}, -}; -use diesel::insert_into; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -use super::builder::{FixtureBuilder, TestFixture, TestFixtureBuilder}; -use super::users::UserFixture; - -/// A fixture for testing chats -pub struct ChatFixture { - pub id: Uuid, - pub title: String, - pub organization_id: Uuid, - pub created_by: Uuid, - pub messages: Vec, -} - -/// Builder for chat fixtures -pub struct ChatFixtureBuilder { - title: Option, - user: Option, - message_count: usize, - add_file_references: bool, -} - -impl FixtureBuilder for ChatFixtureBuilder { - fn default() -> Self { - Self { - title: None, - user: None, - message_count: 0, - add_file_references: false, - } - } - - async fn build(self) -> ChatFixture { - let user = match self.user { - Some(user) => user, - None => UserFixture::default().build().await, - }; - - let title = self.title.unwrap_or_else(|| format!("Test Chat {}", Uuid::new_v4())); - - // Create chat - let mut conn = get_pg_pool().get().await.unwrap(); - - let chat = Chat { - id: Uuid::new_v4(), - title: title.clone(), - organization_id: user.organization_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - most_recent_file_id: None, - most_recent_file_type: None, - }; - - let chat = insert_into(chats::table) - .values(&chat) - .get_result::(&mut conn) - .await - .unwrap(); - - // Create permission for the user - let permission = AssetPermission { - identity_id: user.id, - identity_type: IdentityType::User, - asset_id: chat.id, - asset_type: AssetType::Chat, - role: AssetPermissionRole::Owner, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - }; - - insert_into(asset_permissions::table) - .values(&permission) - .execute(&mut conn) - .await - .unwrap(); - - // Create messages if requested - let mut message_ids = Vec::new(); - - if self.message_count > 0 { - for i in 0..self.message_count { - let message = Message { - id: Uuid::new_v4(), - request_message: Some(format!("Test message {}", i + 1)), - response_messages: json!([{"id": format!("resp_{}", i), "type": "text", "message": format!("Response {}", i + 1)}]), - reasoning: json!([{"id": format!("reason_{}", i), "type": "text", "message": format!("Reasoning {}", i + 1)}]), - title: format!("Message {}", i + 1), - raw_llm_messages: json!([]), - final_reasoning_message: Some(format!("Final reasoning {}", i + 1)), - chat_id: chat.id, - created_at: Utc::now() + chrono::Duration::seconds(i as i64 * 10), - updated_at: Utc::now() + chrono::Duration::seconds(i as i64 * 10), - deleted_at: None, - created_by: user.id, - feedback: None, - }; - - let created_message = insert_into(messages::table) - .values(&message) - .get_result::(&mut conn) - .await - .unwrap(); - - message_ids.push(created_message.id); - - // Add file reference if requested - if self.add_file_references { - let file_ref = MessageToFile { - id: Uuid::new_v4(), - message_id: created_message.id, - file_id: Uuid::new_v4(), // Mock file ID - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - is_duplicate: false, - }; - - insert_into(messages_to_files::table) - .values(&file_ref) - .execute(&mut conn) - .await - .unwrap(); - } - } - } - - ChatFixture { - id: chat.id, - title, - organization_id: chat.organization_id, - created_by: user.id, - messages: message_ids, - } - } -} - -impl ChatFixtureBuilder { - /// Set the chat title - pub fn with_title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Set the user who owns this chat - pub fn with_user(mut self, user: &UserFixture) -> Self { - self.user = Some(user.clone()); - self - } - - /// Set the number of messages to create - pub fn with_messages(mut self, count: usize) -> Self { - self.message_count = count; - self - } - - /// Add file references to messages - pub fn with_file_references(mut self, add: bool) -> Self { - self.add_file_references = add; - self - } -} - -impl TestFixture for ChatFixture { - type Builder = ChatFixtureBuilder; -} - -impl Clone for ChatFixture { - fn clone(&self) -> Self { - Self { - id: self.id, - title: self.title.clone(), - organization_id: self.organization_id, - created_by: self.created_by, - messages: self.messages.clone(), - } - } -} - -impl TestFixtureBuilder { - /// Create a test chat owned by a specific user - pub async fn create_chat(&mut self, user_id: &Uuid) -> Result { - let mut conn = get_pg_pool().get().await?; - - let chat = Chat { - id: Uuid::new_v4(), - title: format!("Test Chat {}", Uuid::new_v4()), - organization_id: Uuid::new_v4(), // In a real fixture, we'd use a proper organization id - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: *user_id, - updated_by: *user_id, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - most_recent_file_id: None, - most_recent_file_type: None, - }; - - insert_into(chats::table) - .values(&chat) - .get_result(&mut conn) - .await - .map_err(Into::into) - } -} \ No newline at end of file diff --git a/api/tests/common/fixtures/collections.rs b/api/tests/common/fixtures/collections.rs deleted file mode 100644 index 2c436936a..000000000 --- a/api/tests/common/fixtures/collections.rs +++ /dev/null @@ -1,40 +0,0 @@ -use chrono::Utc; -use database::models::Collection; -use uuid::Uuid; - -/// Creates a test collection with default values -pub async fn create_test_collection( - conn: &mut diesel_async::AsyncPgConnection, - user_id: Uuid, - org_id: Option, - name: Option, -) -> anyhow::Result { - use database::pool::get_pg_pool; - use database::schema::collections; - use diesel::ExpressionMethods; - use diesel_async::RunQueryDsl; - - let collection_name = name.unwrap_or_else(|| format!("Test Collection {}", Uuid::new_v4())); - let collection_id = Uuid::new_v4(); - let org_id = org_id.unwrap_or_else(Uuid::new_v4); - - let collection = Collection { - id: collection_id, - name: collection_name, - description: Some("Test collection description".to_string()), - created_by: user_id, - updated_by: user_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - organization_id: org_id, - }; - - // Insert the collection into the database - diesel::insert_into(collections::table) - .values(&collection) - .execute(conn) - .await?; - - Ok(collection) -} \ No newline at end of file diff --git a/api/tests/common/fixtures/dashboards.rs b/api/tests/common/fixtures/dashboards.rs deleted file mode 100644 index c009a4547..000000000 --- a/api/tests/common/fixtures/dashboards.rs +++ /dev/null @@ -1,47 +0,0 @@ -use chrono::Utc; -use database::{ - enums::Verification, - models::DashboardFile, - types::{DashboardYml, VersionHistory} -}; -use serde_json::Value; -use uuid::Uuid; - -/// Creates a test dashboard file model -pub fn create_test_dashboard_file( - user_id: &Uuid, - org_id: &Uuid, - name: Option, -) -> DashboardFile { - let dashboard_name = name.unwrap_or_else(|| format!("Test Dashboard {}", Uuid::new_v4())); - - // Create basic dashboard yaml content - let dashboard_yml = DashboardYml { - description: Some("Test dashboard description".to_string()), - layout: serde_json::json!({ - "rows": [], - "cols": [] - }), - }; - - // Create version history - let mut version_history = VersionHistory::default(); - version_history.add_version(1, dashboard_yml.clone()); - - // Convert to JSON for storage - let content = serde_json::to_value(dashboard_yml).unwrap(); - - DashboardFile { - id: Uuid::new_v4(), - name: dashboard_name, - content, - verification: Verification::Unverified, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: *user_id, - updated_by: *user_id, - organization_id: *org_id, - version_history, - } -} \ No newline at end of file diff --git a/api/tests/common/fixtures/metrics.rs b/api/tests/common/fixtures/metrics.rs deleted file mode 100644 index 0313871e5..000000000 --- a/api/tests/common/fixtures/metrics.rs +++ /dev/null @@ -1,95 +0,0 @@ -use chrono::Utc; -use database::{ - enums::Verification, - models::MetricFile, - types::{MetricYml, ChartConfig, VersionHistory} -}; -use serde_json::Value; -use uuid::Uuid; - -/// Creates a test metric file model -pub async fn create_test_metric_file( - conn: &mut diesel_async::AsyncPgConnection, - user_id: Uuid, - org_id: Option, - name: Option, -) -> anyhow::Result { - use database::schema::metric_files; - use diesel::ExpressionMethods; - use diesel_async::RunQueryDsl; - - let org_id = org_id.unwrap_or_else(Uuid::new_v4); - let metric_name = name.unwrap_or_else(|| format!("Test Metric {}", Uuid::new_v4())); - - // Create basic metric yaml content - let metric_yml = MetricYml { - description: Some("Test metric description".to_string()), - query: "SELECT * FROM test_table".to_string(), - chart_type: "bar".to_string(), - chart_config: ChartConfig::default(), - time_frame: "daily".to_string(), - dataset_ids: vec![Uuid::new_v4()], - }; - - // Create version history - let mut version_history = VersionHistory::default(); - version_history.add_version(1, metric_yml.clone()); - - // Convert to JSON for storage - let content = serde_json::to_value(metric_yml).unwrap(); - - let metric = MetricFile { - id: Uuid::new_v4(), - name: metric_name, - content, - verification: Verification::Unverified, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user_id, - updated_by: user_id, - organization_id: org_id, - version_history, - }; - - // Insert the metric into the database - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(conn) - .await?; - - Ok(metric) -} - -/// Creates update metric request data -pub fn create_update_metric_request() -> Value { - serde_json::json!({ - "title": "Updated Test Metric", - "description": "Updated test description", - "chart_config": { - "xAxis": { - "title": "Updated X Axis" - }, - "yAxis": { - "title": "Updated Y Axis" - } - }, - "time_frame": "weekly", - "dataset_ids": [Uuid::new_v4().to_string()], - "verification": "verified" - }) -} - -/// Creates a request to restore a metric to a specific version -pub fn create_restore_metric_request(version_number: i32) -> Value { - serde_json::json!({ - "restore_to_version": version_number - }) -} - -/// Creates dashboard association request data -pub fn create_metric_dashboard_association_request(dashboard_id: &Uuid) -> Value { - serde_json::json!({ - "dashboard_id": dashboard_id - }) -} \ No newline at end of file diff --git a/api/tests/common/fixtures/mod.rs b/api/tests/common/fixtures/mod.rs deleted file mode 100644 index 6d379130e..000000000 --- a/api/tests/common/fixtures/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod users; -pub mod threads; -pub mod metrics; -pub mod dashboards; -pub mod builder; -pub mod collections; -pub mod chats; - -// Re-export commonly used fixtures -pub use users::{create_test_user, UserFixture}; -pub use threads::create_test_thread; -pub use metrics::{create_test_metric_file, create_update_metric_request, create_metric_dashboard_association_request}; -pub use dashboards::create_test_dashboard_file; -pub use collections::create_test_collection; -pub use chats::ChatFixture; - -// Re-export builder traits -pub use builder::{FixtureBuilder, TestFixture}; \ No newline at end of file diff --git a/api/tests/common/fixtures/threads.rs b/api/tests/common/fixtures/threads.rs deleted file mode 100644 index 658c3ae0f..000000000 --- a/api/tests/common/fixtures/threads.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::database::models::Thread; -use chrono::Utc; -use uuid::Uuid; - -/// Creates a test thread with default values -pub fn create_test_thread(organization_id: Uuid, created_by: Uuid) -> Thread { - Thread { - id: Uuid::new_v4(), - title: "Test Thread".to_string(), - organization_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by, - } -} - -/// Creates a test thread with custom values -pub fn create_custom_test_thread( - title: &str, - organization_id: Uuid, - created_by: Uuid, -) -> Thread { - Thread { - id: Uuid::new_v4(), - title: title.to_string(), - organization_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by, - } -} - -/// Creates multiple test threads -pub fn create_test_threads( - count: usize, - organization_id: Uuid, - created_by: Uuid, -) -> Vec { - (0..count) - .map(|i| { - Thread { - id: Uuid::new_v4(), - title: format!("Test Thread {}", i), - organization_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by, - } - }) - .collect() -} \ No newline at end of file diff --git a/api/tests/common/fixtures/users.rs b/api/tests/common/fixtures/users.rs deleted file mode 100644 index 744271121..000000000 --- a/api/tests/common/fixtures/users.rs +++ /dev/null @@ -1,363 +0,0 @@ -use crate::common::http::test_app::TestApp; -use crate::common::fixtures::builder::{FixtureBuilder, TestFixture}; -use crate::database::{ - enums::{AssetType, UserOrganizationRole, SharingSetting, UserOrganizationStatus}, - models::{User, Chat, Message, MessageToFile, MetricFile, DashboardFile, UserToOrganization}, - pool::get_pg_pool, - schema::{users, chats, messages, messages_to_files, metric_files, dashboard_files, users_to_organizations}, -}; -use chrono::Utc; -use diesel::insert_into; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -/// Creates a test user with default values -pub fn create_test_user() -> User { - User { - id: Uuid::new_v4(), - email: "test@example.com".to_string(), - name: Some("Test User".to_string()), - config: json!({}), - created_at: Utc::now(), - updated_at: Utc::now(), - attributes: json!({}), - avatar_url: None, - } -} - -/// Creates a test user with custom values -pub fn create_custom_test_user( - email: &str, - name: Option<&str>, - attributes: serde_json::Value, -) -> User { - User { - id: Uuid::new_v4(), - email: email.to_string(), - name: name.map(String::from), - config: json!({}), - created_at: Utc::now(), - updated_at: Utc::now(), - attributes, - avatar_url: None, - } -} - -/// Creates multiple test users -pub fn create_test_users(count: usize) -> Vec { - (0..count) - .map(|i| { - User { - id: Uuid::new_v4(), - email: format!("test{}@example.com", i), - name: Some(format!("Test User {}", i)), - config: json!({}), - created_at: Utc::now(), - updated_at: Utc::now(), - attributes: json!({}), - avatar_url: None, - } - }) - .collect() -} - -/// Creates a test user in the database -pub async fn create_user(app: &TestApp) -> User { - let user = create_test_user(); - let mut conn = get_pg_pool().get().await.unwrap(); - - insert_into(users::table) - .values(&user) - .execute(&mut conn) - .await - .unwrap(); - - user -} - -/// Creates a chat for testing -pub async fn create_chat(app: &TestApp, user: &User, title: &str) -> Chat { - let chat = Chat { - id: Uuid::new_v4(), - title: title.to_string(), - organization_id: Uuid::new_v4(), - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - most_recent_file_id: None, - most_recent_file_type: None, - }; - - let mut conn = get_pg_pool().get().await.unwrap(); - - insert_into(chats::table) - .values(&chat) - .execute(&mut conn) - .await - .unwrap(); - - chat -} - -/// A user fixture for testing -#[derive(Clone)] -pub struct UserFixture { - pub id: Uuid, - pub email: String, - pub name: Option, - pub organization_id: Uuid, -} - -/// Builder for user fixtures -pub struct UserFixtureBuilder { - email: Option, - name: Option, - organization_id: Option, -} - -impl FixtureBuilder for UserFixtureBuilder { - fn default() -> Self { - Self { - email: None, - name: None, - organization_id: None, - } - } - - async fn build(self) -> UserFixture { - let email = self.email.unwrap_or_else(|| format!("user-{}@example.com", Uuid::new_v4())); - let name = self.name.unwrap_or_else(|| format!("Test User {}", Uuid::new_v4())); - let organization_id = self.organization_id.unwrap_or_else(Uuid::new_v4); - - // Create the user - let user = User { - id: Uuid::new_v4(), - email: email.clone(), - name: Some(name.clone()), - config: json!({}), - created_at: Utc::now(), - updated_at: Utc::now(), - attributes: json!({}), - avatar_url: None, - }; - - let mut conn = get_pg_pool().get().await.unwrap(); - - let user = insert_into(users::table) - .values(&user) - .get_result::(&mut conn) - .await - .unwrap(); - - // Create organization association - let user_to_org = UserToOrganization { - user_id: user.id, - organization_id, - role: UserOrganizationRole::Owner, - sharing_setting: SharingSetting::Private, - edit_sql: true, - upload_csv: true, - export_assets: true, - email_slack_enabled: true, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - deleted_by: None, - status: UserOrganizationStatus::Active, - }; - - insert_into(users_to_organizations::table) - .values(&user_to_org) - .execute(&mut conn) - .await - .unwrap(); - - UserFixture { - id: user.id, - email, - name: Some(name), - organization_id, - } - } -} - -impl UserFixtureBuilder { - pub fn with_email(mut self, email: impl Into) -> Self { - self.email = Some(email.into()); - self - } - - pub fn with_name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - - pub fn with_organization_id(mut self, org_id: Uuid) -> Self { - self.organization_id = Some(org_id); - self - } -} - -impl TestFixture for UserFixture { - type Builder = UserFixtureBuilder; -} - -/// Creates a chat with associated files for testing -pub async fn create_chat_with_files( - app: &TestApp, - user: &User, - file_type: AssetType, - title: &str, -) -> (Chat, Uuid) { - // First create the chat - let chat = Chat { - id: Uuid::new_v4(), - title: title.to_string(), - organization_id: Uuid::new_v4(), - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - most_recent_file_id: None, - most_recent_file_type: None, - }; - - let mut conn = get_pg_pool().get().await.unwrap(); - - insert_into(chats::table) - .values(&chat) - .execute(&mut conn) - .await - .unwrap(); - - // Create message - let message = Message { - id: Uuid::new_v4(), - request_message: Some("Test message".to_string()), - response_messages: json!([]), - reasoning: json!([]), - title: "Test message".to_string(), - raw_llm_messages: json!([]), - final_reasoning_message: None, - chat_id: chat.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: user.id, - feedback: None, - }; - - insert_into(messages::table) - .values(&message) - .execute(&mut conn) - .await - .unwrap(); - - // Create file based on type - let file_id = Uuid::new_v4(); - let file_type_str = match file_type { - AssetType::MetricFile => { - let metric_file = MetricFile { - id: file_id, - name: "Test Metric".to_string(), - file_name: "test_metric.yml".to_string(), - content: json!({}), - verification: database::enums::Verification::NotVerified, - evaluation_obj: None, - evaluation_summary: None, - evaluation_score: None, - organization_id: Uuid::new_v4(), - created_by: user.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - version_history: json!({}), - }; - - insert_into(metric_files::table) - .values(&metric_file) - .execute(&mut conn) - .await - .unwrap(); - - "metric" - }, - AssetType::DashboardFile => { - let dashboard_file = DashboardFile { - id: file_id, - name: "Test Dashboard".to_string(), - file_name: "test_dashboard.yml".to_string(), - content: json!({}), - filter: None, - organization_id: Uuid::new_v4(), - created_by: user.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - version_history: json!({}), - }; - - insert_into(dashboard_files::table) - .values(&dashboard_file) - .execute(&mut conn) - .await - .unwrap(); - - "dashboard" - }, - _ => panic!("Unsupported file type"), - }; - - // Create message-to-file association - let message_to_file = MessageToFile { - id: Uuid::new_v4(), - message_id: message.id, - file_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - is_duplicate: false, - }; - - insert_into(messages_to_files::table) - .values(&message_to_file) - .execute(&mut conn) - .await - .unwrap(); - - // Update chat with most recent file info - let updated_chat = Chat { - most_recent_file_id: Some(file_id), - most_recent_file_type: Some(file_type_str.to_string()), - ..chat - }; - - diesel::update(chats::table.find(chat.id)) - .set(( - chats::most_recent_file_id.eq(Some(file_id)), - chats::most_recent_file_type.eq(Some(file_type_str.to_string())), - )) - .execute(&mut conn) - .await - .unwrap(); - - (updated_chat, file_id) -} \ No newline at end of file diff --git a/api/tests/common/helpers.rs b/api/tests/common/helpers.rs deleted file mode 100644 index 8c5c032d1..000000000 --- a/api/tests/common/helpers.rs +++ /dev/null @@ -1,69 +0,0 @@ -use anyhow::Result; -use chrono::{DateTime, Utc}; -use uuid::Uuid; - -/// Generates a unique test identifier -pub fn generate_test_id() -> String { - format!("test_{}", Uuid::new_v4()) -} - -/// Gets the current timestamp for test data -pub fn get_test_timestamp() -> DateTime { - Utc::now() -} - -/// Creates a test error for error case testing -pub fn create_test_error(message: &str) -> anyhow::Error { - anyhow::anyhow!("Test error: {}", message) -} - -/// Waits for a condition with timeout -pub async fn wait_for_condition( - condition: F, - timeout_ms: u64, - check_interval_ms: u64, -) -> Result -where - F: Fn() -> Result, -{ - let start = std::time::Instant::now(); - let timeout = std::time::Duration::from_millis(timeout_ms); - let check_interval = std::time::Duration::from_millis(check_interval_ms); - - while start.elapsed() < timeout { - if condition()? { - return Ok(true); - } - tokio::time::sleep(check_interval).await; - } - - Ok(false) -} - -/// Runs a function with retry logic -pub async fn retry_with_backoff( - operation: F, - max_retries: u32, - initial_delay_ms: u64, -) -> Result -where - F: Fn() -> Result, - E: std::error::Error + Send + Sync + 'static, -{ - let mut current_retry = 0; - let mut delay = initial_delay_ms; - - loop { - match operation() { - Ok(value) => return Ok(value), - Err(e) => { - if current_retry >= max_retries { - return Err(anyhow::Error::new(e)); - } - tokio::time::sleep(std::time::Duration::from_millis(delay)).await; - current_retry += 1; - delay *= 2; // Exponential backoff - } - } - } -} \ No newline at end of file diff --git a/api/tests/common/helpers/mod.rs b/api/tests/common/helpers/mod.rs deleted file mode 100644 index a51517d46..000000000 --- a/api/tests/common/helpers/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod test_util; \ No newline at end of file diff --git a/api/tests/common/helpers/test_util.rs b/api/tests/common/helpers/test_util.rs deleted file mode 100644 index df30f8320..000000000 --- a/api/tests/common/helpers/test_util.rs +++ /dev/null @@ -1,24 +0,0 @@ -use anyhow::Result; -use database::pool::get_pg_pool; -use diesel::{Connection, ConnectionResult, PgConnection, QueryResult, RunQueryDsl, Table}; -use diesel_async::{AsyncPgConnection, RunQueryDsl as AsyncRunQueryDsl}; -use std::fmt::Debug; - -/// Helper function to temporarily drop and recreate tables for testing -pub async fn with_table_dropped(tables: &[&T]) -> Result<()> -where - T: Table + Debug, - T::AllColumns: diesel::QueryId, -{ - let mut conn = get_pg_pool().get().await?; - - // Delete all data from tables - for table in tables { - diesel::delete(table) - .execute(&mut conn) - .await - .unwrap_or_default(); - } - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/common/http/client.rs b/api/tests/common/http/client.rs deleted file mode 100644 index e6ea78f9b..000000000 --- a/api/tests/common/http/client.rs +++ /dev/null @@ -1,207 +0,0 @@ -use anyhow::Result; -use reqwest::{Client, RequestBuilder, Response}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use crate::tests::common::env::get_test_env_or; - -/// HTTP client for testing with convenient helper methods -pub struct TestHttpClient { - client: Client, - base_url: String, - default_headers: Vec<(String, String)>, -} - -impl TestHttpClient { - /// Create a new test HTTP client with default configuration - pub fn new() -> Result { - // Initialize client with reasonable defaults for testing - let client = Client::builder() - .timeout(Duration::from_secs(10)) - .build()?; - - // Get base URL from environment or use default - let base_url = get_test_env_or("TEST_API_URL", "http://localhost:8080"); - - Ok(Self { - client, - base_url, - default_headers: Vec::new(), - }) - } - - /// Create a client that uses a specific URL (like a mock server) - pub fn with_base_url(base_url: impl Into) -> Result { - let client = Client::builder() - .timeout(Duration::from_secs(10)) - .build()?; - - Ok(Self { - client, - base_url: base_url.into(), - default_headers: Vec::new(), - }) - } - - /// Add a default header to be sent with every request - pub fn with_default_header(mut self, name: impl Into, value: impl Into) -> Self { - self.default_headers.push((name.into(), value.into())); - self - } - - /// Add authorization header - pub fn with_auth(self, token: impl Into) -> Self { - self.with_default_header("Authorization", format!("Bearer {}", token.into())) - } - - /// Make a GET request to the specified path - pub fn get(&self, path: impl AsRef) -> RequestBuilder { - let url = self.build_url(path.as_ref()); - let mut builder = self.client.get(url); - builder = self.apply_default_headers(builder); - builder - } - - /// Make a POST request to the specified path with JSON body - pub fn post(&self, path: impl AsRef, body: &T) -> Result { - let url = self.build_url(path.as_ref()); - let mut builder = self.client.post(url).json(body); - builder = self.apply_default_headers(builder); - Ok(builder) - } - - /// Make a PUT request to the specified path with JSON body - pub fn put(&self, path: impl AsRef, body: &T) -> Result { - let url = self.build_url(path.as_ref()); - let mut builder = self.client.put(url).json(body); - builder = self.apply_default_headers(builder); - Ok(builder) - } - - /// Make a DELETE request to the specified path - pub fn delete(&self, path: impl AsRef) -> RequestBuilder { - let url = self.build_url(path.as_ref()); - let mut builder = self.client.delete(url); - builder = self.apply_default_headers(builder); - builder - } - - /// Send a request and parse the JSON response - pub async fn request_json Deserialize<'de>>( - &self, - builder: RequestBuilder - ) -> Result<(Response, T)> { - let response = builder.send().await?; - let status = response.status(); - - if !status.is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!( - "Request failed with status {}: {}", - status, - error_text - )); - } - - // Clone the response so we can return both the response and the parsed body - let response_copy = response.try_clone().ok_or_else(|| - anyhow::anyhow!("Failed to clone response") - )?; - - let body: T = response.json().await?; - - Ok((response_copy, body)) - } - - // Helper methods - - /// Build the full URL for a path - fn build_url(&self, path: &str) -> String { - let base = self.base_url.trim_end_matches('/'); - let path = path.trim_start_matches('/'); - format!("{}/{}", base, path) - } - - /// Apply default headers to a request builder - fn apply_default_headers(&self, mut builder: RequestBuilder) -> RequestBuilder { - for (name, value) in &self.default_headers { - builder = builder.header(name, value); - } - builder - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::common::http::MockServer; - use serde_json::json; - - #[derive(Debug, Deserialize, Serialize, PartialEq)] - struct TestUser { - id: i32, - name: String, - } - - #[tokio::test] - async fn test_request_with_mock() -> Result<()> { - // Create a mock server - let mut mock_server = MockServer::new().await?; - - // Setup mock response - let test_user = TestUser { id: 123, name: "Test User".to_string() }; - mock_server.mock_json("GET", "/users/123", 200, &test_user)?; - - // Create test client with mock server URL - let client = TestHttpClient::with_base_url(mock_server.url())? - .with_default_header("X-Test-Header", "test-value"); - - // Make a request - let (response, user) = client.request_json::( - client.get("/users/123") - ).await?; - - // Verify response - assert_eq!(response.status().as_u16(), 200); - assert_eq!(user, test_user); - - // Verify mock was called - mock_server.verify_all_mocks_called()?; - - Ok(()) - } - - #[tokio::test] - async fn test_post_request() -> Result<()> { - // Create a mock server - let mut mock_server = MockServer::new().await?; - - // Setup mock response for POST - let request_body = TestUser { id: 0, name: "New User".to_string() }; - let response_data = TestUser { id: 456, name: "New User".to_string() }; - - // Using a JSON matcher would be better, but for simplicity using any body - let mock = mock_server.server - .mock("POST", "/users") - .with_status(201) - .with_header("content-type", "application/json") - .with_body(json!(response_data).to_string()) - .create(); - - // Create test client - let client = TestHttpClient::with_base_url(mock_server.url())?; - - // Make POST request - let builder = client.post("/users", &request_body)?; - let (response, created_user) = client.request_json::(builder).await?; - - // Verify response - assert_eq!(response.status().as_u16(), 201); - assert_eq!(created_user.id, 456); - assert_eq!(created_user.name, "New User"); - - // Verify mock was called - mock.assert(); - - Ok(()) - } -} \ No newline at end of file diff --git a/api/tests/common/http/mock_server.rs b/api/tests/common/http/mock_server.rs deleted file mode 100644 index 06695002f..000000000 --- a/api/tests/common/http/mock_server.rs +++ /dev/null @@ -1,197 +0,0 @@ -use anyhow::Result; -use serde_json::Value; -use std::collections::HashMap; - -/// Wrapper around mockito server for easier mock creation and tracking -pub struct MockServer { - server: mockito::Server, - mock_responses: HashMap>, -} - -impl MockServer { - /// Create a new mock server - pub async fn new() -> Result { - Ok(Self { - server: mockito::Server::new_async().await, - mock_responses: HashMap::new(), - }) - } - - /// Get the base URL for the mock server - pub fn url(&self) -> String { - self.server.url() - } - - /// Mock a successful JSON response - pub fn mock_json( - &mut self, - method: &str, - path: &str, - status: usize, - body: &T - ) -> Result<&mockito::Mock> { - let body_str = serde_json::to_string(body)?; - let mock = self.server - .mock(method, path) - .with_status(status) - .with_header("content-type", "application/json") - .with_body(body_str) - .create(); - - let key = format!("{}:{}", method, path); - self.mock_responses.entry(key).or_insert_with(Vec::new).push(mock); - - // Return reference to the created mock - let mocks = self.mock_responses.get(key.as_str()).unwrap(); - Ok(&mocks[mocks.len() - 1]) - } - - /// Mock an error response - pub fn mock_error( - &mut self, - method: &str, - path: &str, - status: usize, - error_message: &str - ) -> Result<&mockito::Mock> { - let body = serde_json::json!({ - "error": error_message - }); - self.mock_json(method, path, status, &body) - } - - /// Mock a response with specific matcher for the request body - pub fn mock_with_body_match( - &mut self, - method: &str, - path: &str, - body_matcher: M, - response_body: &T, - status: usize, - ) -> Result<&mockito::Mock> - where - M: Into, - T: serde::Serialize, - { - let body_str = serde_json::to_string(response_body)?; - let mock = self.server - .mock(method, path) - .match_body(body_matcher) - .with_status(status) - .with_header("content-type", "application/json") - .with_body(body_str) - .create(); - - let key = format!("{}:{}", method, path); - self.mock_responses.entry(key).or_insert_with(Vec::new).push(mock); - - let mocks = self.mock_responses.get(key.as_str()).unwrap(); - Ok(&mocks[mocks.len() - 1]) - } - - /// Mock a response with custom headers - pub fn mock_with_headers( - &mut self, - method: &str, - path: &str, - status: usize, - headers: &[(&str, &str)], - body: &T, - ) -> Result<&mockito::Mock> { - let body_str = serde_json::to_string(body)?; - - // Create a mock builder - let mut mock_builder = self.server - .mock(method, path) - .with_status(status); - - // Add headers - for (name, value) in headers { - mock_builder = mock_builder.with_header(name, value); - } - - // Add body and create mock - let mock = mock_builder - .with_body(body_str) - .create(); - - let key = format!("{}:{}", method, path); - self.mock_responses.entry(key).or_insert_with(Vec::new).push(mock); - - let mocks = self.mock_responses.get(key.as_str()).unwrap(); - Ok(&mocks[mocks.len() - 1]) - } - - /// Verify all registered mocks have been called - pub fn verify_all_mocks_called(&self) -> Result<()> { - for (endpoint, mocks) in &self.mock_responses { - for (i, mock) in mocks.iter().enumerate() { - match mock.matched() { - true => {} - false => return Err(anyhow::anyhow!("Mock not matched for endpoint '{}' (index: {})", endpoint, i)), - } - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use reqwest::Client; - - #[tokio::test] - async fn test_mock_json() -> Result<()> { - let mut server = MockServer::new().await?; - - // Create a mock response - let response_data = serde_json::json!({ - "id": 123, - "name": "Test User" - }); - - let _mock = server.mock_json("GET", "/users/123", 200, &response_data)?; - - // Make a request to the mock server - let client = Client::new(); - let response = client.get(&format!("{}/users/123", server.url())) - .send() - .await?; - - assert_eq!(response.status().as_u16(), 200); - - let body: Value = response.json().await?; - assert_eq!(body["id"], 123); - assert_eq!(body["name"], "Test User"); - - // Verify the mock was called - server.verify_all_mocks_called()?; - - Ok(()) - } - - #[tokio::test] - async fn test_mock_error() -> Result<()> { - let mut server = MockServer::new().await?; - - // Create an error mock - let _mock = server.mock_error("GET", "/users/invalid", 404, "User not found")?; - - // Make a request to the mock server - let client = Client::new(); - let response = client.get(&format!("{}/users/invalid", server.url())) - .send() - .await?; - - assert_eq!(response.status().as_u16(), 404); - - let body: Value = response.json().await?; - assert_eq!(body["error"], "User not found"); - - // Verify the mock was called - server.verify_all_mocks_called()?; - - Ok(()) - } -} \ No newline at end of file diff --git a/api/tests/common/http/mod.rs b/api/tests/common/http/mod.rs deleted file mode 100644 index 5c9e17063..000000000 --- a/api/tests/common/http/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Re-export HTTP utilities -mod mock_server; -mod client; -mod test_app; - -pub use mock_server::MockServer; -pub use client::TestHttpClient; -pub use test_app::TestApp; \ No newline at end of file diff --git a/api/tests/common/http/test_app.rs b/api/tests/common/http/test_app.rs deleted file mode 100644 index 849271b07..000000000 --- a/api/tests/common/http/test_app.rs +++ /dev/null @@ -1,63 +0,0 @@ -use anyhow::Result; -use reqwest::{Client, ClientBuilder}; -use uuid::Uuid; -use std::sync::Arc; - -use crate::common::{db::TestDb, env::init_test_env}; - -#[derive(Debug, Clone)] -pub struct TestUser { - pub id: Uuid, - pub token: String, - pub email: String, -} - -pub struct TestApp { - pub client: Client, - pub test_user: TestUser, - pub db: Arc, -} - -impl TestApp { - pub async fn new() -> Result { - // Initialize test environment - init_test_env(); - - // Initialize database connection - let db = Arc::new(TestDb::new().await?); - - // Create test user ID - let user_id = Uuid::new_v4(); - - // Mock token for auth - let token = format!("test-token-{}", user_id); - - // Create test user - let test_user = TestUser { - id: user_id, - token, - email: format!("test-{}@example.com", user_id), - }; - - // Initialize HTTP client - // We're using localhost assuming the test server is running locally - let base_url = std::env::var("TEST_SERVER_URL") - .unwrap_or_else(|_| "http://localhost:3000".to_string()); - - let client = ClientBuilder::new() - .build()? - .to_owned(); - - // Create TestApp - let app = Self { - client, - test_user, - db, - }; - - // Set up initial test data if needed - app.db.setup_test_data().await?; - - Ok(app) - } -} \ No newline at end of file diff --git a/api/tests/common/matchers/headers.rs b/api/tests/common/matchers/headers.rs deleted file mode 100644 index fa31ca574..000000000 --- a/api/tests/common/matchers/headers.rs +++ /dev/null @@ -1,91 +0,0 @@ -use mockito::Matcher; -use std::collections::HashMap; - -/// Matcher for HTTP headers -/// -/// Creates a mockito matcher that checks for specific HTTP headers -/// in the request. This is useful for validating that requests are -/// properly sending required headers like authentication tokens. -/// -/// Example: -/// ``` -/// let headers = vec![ -/// ("Authorization", "Bearer token"), -/// ("Content-Type", "application/json") -/// ]; -/// let matcher = header_matcher(headers); -/// ``` -pub fn header_matcher<'a, I, K, V>(headers: I) -> HashMap<&'static str, Matcher> -where - I: IntoIterator, - K: AsRef, - V: AsRef, -{ - let mut matchers = HashMap::new(); - - for (key, value) in headers { - let header_key = key.as_ref(); - let header_value = value.as_ref(); - - // Create a matcher for this header - let matcher = Matcher::Exact(header_value.to_string()); - - // Convert the header name to a static string ref (required by mockito) - // This is a bit of a hack, but mockito requires static strings - let static_key = match header_key { - "authorization" | "Authorization" => "authorization", - "content-type" | "Content-Type" => "content-type", - "accept" | "Accept" => "accept", - "user-agent" | "User-Agent" => "user-agent", - "x-api-key" | "X-Api-Key" => "x-api-key", - _ => "custom-header", // Fallback for other headers - }; - - matchers.insert(static_key, matcher); - } - - matchers -} - -#[cfg(test)] -mod tests { - use super::*; - use mockito::Server; - use reqwest::Client; - - #[tokio::test] - async fn test_header_matcher() { - let server = Server::new_async().await; - - // Create a matcher for Authorization and Content-Type headers - let headers = vec![ - ("Authorization", "Bearer test-token"), - ("Content-Type", "application/json"), - ]; - - let header_matchers = header_matcher(headers); - - // Create a mock that requires these headers - let mock = server.mock("GET", "/test") - .match_header("authorization", header_matchers["authorization"].clone()) - .match_header("content-type", header_matchers["content-type"].clone()) - .with_status(200) - .with_body("success") - .create(); - - // Send request with the required headers - let client = Client::new(); - let response = client.get(&format!("{}/test", server.url())) - .header("Authorization", "Bearer test-token") - .header("Content-Type", "application/json") - .send() - .await - .expect("Request failed"); - - assert_eq!(response.status().as_u16(), 200); - assert_eq!(response.text().await.unwrap(), "success"); - - // Verify the mock was called - mock.assert(); - } -} \ No newline at end of file diff --git a/api/tests/common/matchers/json.rs b/api/tests/common/matchers/json.rs deleted file mode 100644 index f8a76d074..000000000 --- a/api/tests/common/matchers/json.rs +++ /dev/null @@ -1,218 +0,0 @@ -use serde_json::Value; -use mockito::Matcher; - -/// Create a matcher for JSON payloads that checks for subset matching -/// -/// This will match if the JSON payload contains all of the fields in the expected -/// value, but may also contain additional fields. Useful for asserting on the -/// essential parts of a JSON payload without requiring an exact match. -/// -/// Example: -/// ``` -/// let matcher = json_contains(json!({"name": "Test", "age": 30})); -/// // This will match a payload with name and age, plus other fields -/// ``` -pub fn json_contains(expected: T) -> Matcher { - let expected_json = serde_json::to_value(expected) - .expect("Failed to serialize expected value to JSON"); - - Matcher::Func(Box::new(move |body: &[u8]| { - if body.is_empty() { - return false; - } - - let body_str = String::from_utf8_lossy(body); - - match serde_json::from_str::(&body_str) { - Ok(actual_json) => { - json_contains_subset(&expected_json, &actual_json) - } - Err(_) => false, - } - })) -} - -/// Check if a JSON value contains another JSON value as a subset -/// -/// This is a recursive function that checks if the actual JSON value -/// contains all the fields from the expected JSON value. -fn json_contains_subset(expected: &Value, actual: &Value) -> bool { - match expected { - Value::Object(expected_obj) => { - match actual { - Value::Object(actual_obj) => { - for (k, v) in expected_obj { - match actual_obj.get(k) { - Some(actual_val) => { - if !json_contains_subset(v, actual_val) { - return false; - } - } - None => return false, - } - } - true - } - _ => false, - } - } - Value::Array(expected_arr) => { - match actual { - Value::Array(actual_arr) => { - if expected_arr.len() > actual_arr.len() { - return false; - } - - // Simple case: check each expected item exists in the actual array - for expected_item in expected_arr { - if !actual_arr.iter().any(|actual_item| { - json_contains_subset(expected_item, actual_item) - }) { - return false; - } - } - true - } - _ => false, - } - } - _ => expected == actual, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_json_contains_subset_exact_match() { - let expected = json!({ - "name": "Test User", - "age": 30 - }); - - let actual = json!({ - "name": "Test User", - "age": 30 - }); - - assert!(json_contains_subset(&expected, &actual)); - } - - #[test] - fn test_json_contains_subset_additional_fields() { - let expected = json!({ - "name": "Test User", - "age": 30 - }); - - let actual = json!({ - "id": 123, - "name": "Test User", - "age": 30, - "email": "test@example.com" - }); - - assert!(json_contains_subset(&expected, &actual)); - } - - #[test] - fn test_json_contains_subset_missing_field() { - let expected = json!({ - "name": "Test User", - "age": 30, - "email": "test@example.com" - }); - - let actual = json!({ - "name": "Test User", - "age": 30 - }); - - assert!(!json_contains_subset(&expected, &actual)); - } - - #[test] - fn test_json_contains_subset_mismatched_value() { - let expected = json!({ - "name": "Test User", - "age": 42 - }); - - let actual = json!({ - "name": "Test User", - "age": 30 - }); - - assert!(!json_contains_subset(&expected, &actual)); - } - - #[test] - fn test_json_contains_subset_nested_objects() { - let expected = json!({ - "user": { - "name": "Test User", - "address": { - "city": "Test City" - } - } - }); - - let actual = json!({ - "id": 123, - "user": { - "name": "Test User", - "age": 30, - "address": { - "city": "Test City", - "street": "Test Street" - } - }, - "status": "active" - }); - - assert!(json_contains_subset(&expected, &actual)); - } - - #[test] - fn test_json_contains_subset_arrays() { - let expected = json!({ - "items": [ - {"id": 1}, - {"id": 2} - ] - }); - - let actual = json!({ - "items": [ - {"id": 1, "name": "Item 1"}, - {"id": 2, "name": "Item 2"}, - {"id": 3, "name": "Item 3"} - ] - }); - - assert!(json_contains_subset(&expected, &actual)); - } - - #[test] - fn test_json_contains_subset_array_missing_item() { - let expected = json!({ - "items": [ - {"id": 1}, - {"id": 2}, - {"id": 4} // This doesn't exist in actual - ] - }); - - let actual = json!({ - "items": [ - {"id": 1, "name": "Item 1"}, - {"id": 2, "name": "Item 2"}, - {"id": 3, "name": "Item 3"} - ] - }); - - assert!(!json_contains_subset(&expected, &actual)); - } -} \ No newline at end of file diff --git a/api/tests/common/matchers/mod.rs b/api/tests/common/matchers/mod.rs deleted file mode 100644 index 996cffc91..000000000 --- a/api/tests/common/matchers/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Re-export matcher modules -mod json; -mod headers; - -pub use json::json_contains; -pub use headers::header_matcher; \ No newline at end of file diff --git a/api/tests/common/mod.rs b/api/tests/common/mod.rs deleted file mode 100644 index 9c32c3203..000000000 --- a/api/tests/common/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod db; -pub mod env; -pub mod fixtures; -pub mod helpers; -pub mod http; -pub mod assertions; -pub mod matchers; - -// Re-export commonly used utilities for easier access -pub use db::{TestDb, TestTaggable}; -pub use env::{init_test_env, setup_test_env, get_test_env, get_test_config}; -pub use fixtures::{TestFixture, FixtureBuilder}; -pub use assertions::{ResponseAssertions, ModelAssertions}; -pub use matchers::{json_contains, header_matcher}; -pub use http::TestApp; \ No newline at end of file diff --git a/api/tests/example_test.rs b/api/tests/example_test.rs deleted file mode 100644 index 621039c5f..000000000 --- a/api/tests/example_test.rs +++ /dev/null @@ -1,119 +0,0 @@ -use anyhow::Result; -use uuid::Uuid; - -// Import the TestInstance type -use crate::TestInstance; - -/// Example test that requires database access - environment automatically initialized -#[tokio::test] -async fn test_simple_database_connection() -> Result<()> { - // The environment is already initialized by the test framework - - // Just create a test instance to get a unique test ID and access to pools - let test = TestInstance::new().await?; - - // Now we can use the test database - let pool = test.get_diesel_pool(); - - // Verify connection works by getting a connection from the pool - let conn = pool.get().await?; - - // We've successfully connected! - Ok(()) -} - -/// Example test for working with test data -#[tokio::test] -async fn test_with_isolation() -> Result<()> { - // Create a test instance with a unique ID - let test = TestInstance::new().await?; - - // Use the test_id to tag test data - let test_id = &test.test_id; - - // Create a connection - let mut conn = test.get_diesel_pool().get().await?; - - // Example raw SQL for test data creation (using the test_id) - diesel::sql_query("INSERT INTO example_table (id, name, test_id) VALUES ($1, $2, $3)") - .bind::(Uuid::new_v4()) - .bind::("Test item") - .bind::(test_id) - .execute(&mut conn) - .await?; - - // Run your test logic... - - // Clean up after the test - diesel::sql_query("DELETE FROM example_table WHERE test_id = $1") - .bind::(test_id) - .execute(&mut conn) - .await?; - - Ok(()) -} - -/// Example test for third-party API integration -#[tokio::test] -async fn test_third_party_api() -> Result<()> { - // Skip test if third-party testing is disabled - if std::env::var("ENABLE_THIRD_PARTY_TESTS").is_err() { - println!("Skipping third-party API test"); - return Ok(()); - } - - // Get API credentials from the test environment - let api_key = std::env::var("THIRD_PARTY_API_KEY") - .expect("THIRD_PARTY_API_KEY must be set for this test"); - let api_url = std::env::var("THIRD_PARTY_API_URL") - .expect("THIRD_PARTY_API_URL must be set for this test"); - - // Create test instance for database access if needed - let test = TestInstance::new().await?; - - // Create API client with test credentials - // let client = ThirdPartyClient::new(&api_key, &api_url); - - // Run API tests... - - Ok(()) -} - -/// Example test that specifically uses Redis -#[tokio::test] -async fn test_redis_connection() -> Result<()> { - // Create test instance - let test = TestInstance::new().await?; - - // Get Redis pool - let redis_pool = test.get_redis_pool(); - - // Get Redis connection - let mut conn = redis_pool.get().await?; - - // Example Redis operations - // redis::cmd("SET").arg("test_key").arg("test_value").execute(&mut *conn); - // let value: String = redis::cmd("GET").arg("test_key").query_async(&mut *conn).await?; - // assert_eq!(value, "test_value"); - - Ok(()) -} - -/// Example of a test that runs with parallel services -#[tokio::test] -async fn test_multiple_services() -> Result<()> { - // Create test instance - let test = TestInstance::new().await?; - - // Access both SQL and Redis databases - let pg_pool = test.get_diesel_pool(); - let redis_pool = test.get_redis_pool(); - - // Example of parallel operations - let pg_conn = pg_pool.get().await?; - let redis_conn = redis_pool.get().await?; - - // Use both connections... - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/chats/duplicate_chat_test.rs b/api/tests/integration/chats/duplicate_chat_test.rs deleted file mode 100644 index 465981be6..000000000 --- a/api/tests/integration/chats/duplicate_chat_test.rs +++ /dev/null @@ -1,251 +0,0 @@ -use anyhow::Result; -use crate::common::fixtures::builder::BuildableFixture; -use crate::common::fixtures::{self, chats::ChatFixture, users::UserFixture}; -use crate::common::http::client::TestClient; -use crate::common::http::test_app; -use crate::common::env; -use crate::common::assertions::response::ResponseAssertions; -use axum::http::StatusCode; -use database::models::{Chat, MessageToFile}; -use diesel::prelude::*; -use diesel_async::RunQueryDsl; -use serde_json::{json, Value}; -use uuid::Uuid; - -/// Tests the full chat duplication functionality -/// -/// This test verifies: -/// - User can duplicate their own chat -/// - All messages are duplicated -/// - All file references are duplicated and marked with is_duplicate=true -/// - The new chat has the correct title with (Copy) suffix -#[tokio::test] -async fn test_duplicate_chat_integration() -> Result<()> { - // Initialize test environment - env::init_test_env(); - - // Setup test context - let app = test_app::create_test_app().await; - let client = TestClient::new(app); - let test_id = Uuid::new_v4().to_string(); - - // Create a test user - let user = UserFixture::default().build().await; - - // Create a test chat with messages and file references - let chat = ChatFixture::default() - .with_user(&user) - .with_title(format!("Test Chat for duplication {}", test_id)) - .with_messages(3) // Create 3 messages - .with_file_references(true) // Add file references to messages - .build() - .await; - - // Login as the user - client.login_as(&user).await; - - // Act: Send request to duplicate the chat - let response = client - .post("/chats/duplicate") - .json(&json!({ - "id": chat.id - })) - .send() - .await; - - // Assert: Check response status - response.assert_status(StatusCode::OK); - - // Parse response - let json_response: Value = response.json().await; - let chat_data = &json_response["data"]["chat"]; - - // Assert: Verify response contains expected fields - assert!(chat_data["id"].is_string(), "Chat id should be a string"); - assert_ne!( - chat_data["id"].as_str().unwrap(), - chat.id.to_string(), - "New chat should have a different ID than the original" - ); - assert_eq!( - chat_data["title"].as_str().unwrap(), - format!("{} (Copy)", chat.title), - "Chat title should have (Copy) suffix" - ); - - // Assert: Verify all messages were duplicated - let message_ids = chat_data["message_ids"].as_array().unwrap(); - assert_eq!(message_ids.len(), 3, "Chat should have 3 messages"); - - // Get the new chat ID - let new_chat_id = Uuid::parse_str(chat_data["id"].as_str().unwrap()).unwrap(); - - // Verify in the database that file references were duplicated with is_duplicate=true - let mut conn = fixtures::db::get_connection().await?; - - // Get all messages from the new chat - let sql = "SELECT id FROM messages WHERE chat_id = $1 AND deleted_at IS NULL"; - let message_ids: Vec<(Uuid,)> = diesel::sql_query(sql) - .bind::(new_chat_id) - .load(&mut conn) - .await?; - - // Check if file references were created correctly - let message_id_vec: Vec = message_ids.into_iter().map(|(id,)| id).collect(); - - let sql = "SELECT * FROM messages_to_files WHERE message_id = ANY($1) AND deleted_at IS NULL"; - let file_refs: Vec = diesel::sql_query(sql) - .bind::, _>(&message_id_vec) - .load(&mut conn) - .await?; - - // Assert: Verify all file references are marked as duplicates - assert!(!file_refs.is_empty(), "Should have at least one file reference"); - assert!( - file_refs.iter().all(|fr| fr.is_duplicate), - "All file references should be marked as duplicates" - ); - - Ok(()) -} - -/// Tests duplication of a chat from a specific message onwards -/// -/// This test verifies: -/// - User can duplicate a chat starting from a specific message -/// - Only messages from that point onward are included -/// - The new chat has the correct title with (Copy) suffix -#[tokio::test] -async fn test_duplicate_chat_with_message_id() -> Result<()> { - // Initialize test environment - env::init_test_env(); - - // Setup test context - let app = test_app::create_test_app().await; - let client = TestClient::new(app); - let test_id = Uuid::new_v4().to_string(); - - // Create a test user - let user = UserFixture::default().build().await; - - // Create a test chat with messages - let chat = ChatFixture::default() - .with_user(&user) - .with_title(format!("Test Chat for partial duplication {}", test_id)) - .with_messages(3) // Create 3 messages - .build() - .await; - - // Get the ID of the second message - let mut conn = fixtures::db::get_connection().await?; - let sql = "SELECT id FROM messages WHERE chat_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1 OFFSET 1"; - let second_message_id: (Uuid,) = diesel::sql_query(sql) - .bind::(chat.id) - .get_result(&mut conn) - .await?; - - // Login as the user - client.login_as(&user).await; - - // Act: Send request to duplicate the chat from the second message - let response = client - .post("/chats/duplicate") - .json(&json!({ - "id": chat.id, - "message_id": second_message_id.0 - })) - .send() - .await; - - // Assert: Check response status - response.assert_status(StatusCode::OK); - - // Parse response - let json_response: Value = response.json().await; - let chat_data = &json_response["data"]["chat"]; - - // Assert: Verify only 2 messages were duplicated (second and third) - let message_ids = chat_data["message_ids"].as_array().unwrap(); - assert_eq!(message_ids.len(), 2, "Chat should have 2 messages (from the second message)"); - - Ok(()) -} - -/// Tests error handling when attempting to duplicate a nonexistent chat -/// -/// This test verifies: -/// - API returns a 404 Not Found status when the chat ID doesn't exist -#[tokio::test] -async fn test_duplicate_nonexistent_chat() -> Result<()> { - // Initialize test environment - env::init_test_env(); - - // Setup test context - let app = test_app::create_test_app().await; - let client = TestClient::new(app); - - // Create a test user - let user = UserFixture::default().build().await; - - // Login as the user - client.login_as(&user).await; - - // Act: Send request to duplicate a nonexistent chat - let response = client - .post("/chats/duplicate") - .json(&json!({ - "id": Uuid::new_v4() - })) - .send() - .await; - - // Assert: Check response status - should be not found - response.assert_status(StatusCode::NOT_FOUND); - - Ok(()) -} - -/// Tests permission checks when duplicating chats -/// -/// This test verifies: -/// - User cannot duplicate a chat they don't have access to -/// - API returns a 403 Forbidden status in this case -#[tokio::test] -async fn test_duplicate_chat_with_no_permission() -> Result<()> { - // Initialize test environment - env::init_test_env(); - - // Setup test context - let app = test_app::create_test_app().await; - let client = TestClient::new(app); - let test_id = Uuid::new_v4().to_string(); - - // Create two users - let user1 = UserFixture::default().build().await; - let user2 = UserFixture::default().build().await; - - // Create a chat as user1 - let chat = ChatFixture::default() - .with_user(&user1) - .with_title(format!("Test Chat for permission check {}", test_id)) - .with_messages(2) - .build() - .await; - - // Act: Login as user2 who doesn't have access to the chat - client.login_as(&user2).await; - - // Try to duplicate the chat - let response = client - .post("/chats/duplicate") - .json(&json!({ - "id": chat.id - })) - .send() - .await; - - // Assert: Check response status - should be forbidden - response.assert_status(StatusCode::FORBIDDEN); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/chats/get_chat_test.rs b/api/tests/integration/chats/get_chat_test.rs deleted file mode 100644 index 2e0bf2eda..000000000 --- a/api/tests/integration/chats/get_chat_test.rs +++ /dev/null @@ -1,144 +0,0 @@ -use uuid::Uuid; -use crate::common::{ - env::{create_env, TestEnv}, - http::client::TestClient, - assertions::response::assert_api_ok, -}; -use chrono::Utc; -use database::enums::{AssetPermissionRole, AssetTypeEnum, IdentityTypeEnum}; -use diesel::sql_query; -use diesel_async::RunQueryDsl; - -#[tokio::test] -async fn test_get_chat_with_sharing_info() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and chat - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let chat_id = create_test_chat(&env, user_id).await; - - // Add sharing permissions - add_test_permissions(&env, chat_id, user_id).await; - - // Add public sharing - enable_public_sharing(&env, chat_id, user_id).await; - - // Test GET request - let response = client - .get(&format!("/api/v1/chats/{}", chat_id)) - .header("X-User-Id", user_id.to_string()) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Check fields - assert_eq!(data["id"], chat_id.to_string()); - - // Check sharing fields - assert_eq!(data["publicly_accessible"], true); - assert!(data["public_expiry_date"].is_string()); - assert_eq!(data["public_enabled_by"], "test@example.com"); - assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1); - - let permission = &data["individual_permissions"][0]; - assert_eq!(permission["email"], "test2@example.com"); - assert_eq!(permission["role"], "viewer"); - assert_eq!(permission["name"], "Test User 2"); -} - -// Helper functions to set up the test data -async fn create_test_chat(env: &TestEnv, user_id: Uuid) -> Uuid { - let mut conn = env.db_pool.get().await.unwrap(); - - // Insert test user - sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user_id) - .bind::("test@example.com") - .bind::("Test User") - .execute(&mut conn) - .await - .unwrap(); - - // Insert another test user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user2_id) - .bind::("test2@example.com") - .bind::("Test User 2") - .execute(&mut conn) - .await - .unwrap(); - - // Insert test chat - let chat_id = Uuid::parse_str("00000000-0000-0000-0000-000000000030").unwrap(); - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - - // Insert test organization if needed - sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") - .bind::(org_id) - .bind::("Test Organization") - .execute(&mut conn) - .await - .unwrap(); - - // Insert chat - sql_query(r#" - INSERT INTO chats (id, title, organization_id, created_by, updated_by, publicly_accessible) - VALUES ($1, 'Test Chat', $2, $3, $3, false) - ON CONFLICT DO NOTHING - "#) - .bind::(chat_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - chat_id -} - -async fn add_test_permissions(env: &TestEnv, chat_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Get the second user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - - // Add permission for user2 as viewer - sql_query(r#" - INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by) - VALUES ($1, $2, $3, $4, $5, $6, $6) - ON CONFLICT DO NOTHING - "#) - .bind::(user2_id) - .bind::(IdentityTypeEnum::User.to_string()) - .bind::(chat_id) - .bind::(AssetTypeEnum::Chat.to_string()) - .bind::(AssetPermissionRole::CanView.to_string()) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); -} - -async fn enable_public_sharing(env: &TestEnv, chat_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Set public access - let expiry_date = Utc::now() + chrono::Duration::days(7); - - sql_query(r#" - UPDATE chats - SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2 - WHERE id = $3 - "#) - .bind::(user_id) - .bind::(expiry_date) - .bind::(chat_id) - .execute(&mut conn) - .await - .unwrap(); -} \ No newline at end of file diff --git a/api/tests/integration/chats/list_chats_test.rs b/api/tests/integration/chats/list_chats_test.rs deleted file mode 100644 index 6c27ce57e..000000000 --- a/api/tests/integration/chats/list_chats_test.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::common::{ - fixtures::{ - chats::{create_chat_with_files, create_chat}, - users::create_user, - }, - http::test_app::TestApp, -}; -use axum::http::StatusCode; -use database::enums::AssetType; -use handlers::chats::list_chats_handler::ChatListItem; -use uuid::Uuid; - -#[tokio::test] -async fn test_list_chats_with_most_recent_file() { - // Arrange - let app = TestApp::new().await; - let user = create_user(&app).await; - - // Create a chat with a metric file - let (chat_with_file, file_id) = create_chat_with_files( - &app, - &user, - AssetType::MetricFile, - "Chat with metric file", - ).await; - - // Create a regular chat without a file - let chat_without_file = create_chat(&app, &user, "Chat without file").await; - - // Act - Get the list of chats - let response = app - .get(&format!("/api/chats?page=1&page_size=10")) - .with_auth(&user) - .send() - .await; - - // Assert - Check status and response structure - assert_eq!(response.status(), StatusCode::OK); - - let chats: Vec = response.json().await; - println!("Response: {:?}", chats); - - // Expect to find two chats in the response - assert!(chats.len() >= 2); - - // Find the chat with file - let chat_with_file_result = chats.iter() - .find(|c| c.id == chat_with_file.id.to_string()); - - assert!(chat_with_file_result.is_some()); - let chat = chat_with_file_result.unwrap(); - - // Check if the latest_file_id and latest_file_type are correctly set - assert_eq!(chat.latest_file_id, Some(file_id.to_string())); - assert_eq!(chat.latest_file_type, Some("metric".to_string())); - - // Find the chat without file - let chat_without_file_result = chats.iter() - .find(|c| c.id == chat_without_file.id.to_string()); - - assert!(chat_without_file_result.is_some()); - let chat = chat_without_file_result.unwrap(); - - // Check that latest_file_id and latest_file_type are None - assert_eq!(chat.latest_file_id, None); - assert_eq!(chat.latest_file_type, None); -} \ No newline at end of file diff --git a/api/tests/integration/chats/mod.rs b/api/tests/integration/chats/mod.rs deleted file mode 100644 index cbe9de69f..000000000 --- a/api/tests/integration/chats/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod sharing; -pub mod get_chat_test; -pub mod update_chat_test; -pub mod post_chat_test; -pub mod restore_chat_test; -pub mod list_chats_test; -pub mod duplicate_chat_test; \ No newline at end of file diff --git a/api/tests/integration/chats/post_chat_test.rs b/api/tests/integration/chats/post_chat_test.rs deleted file mode 100644 index bdd275e76..000000000 --- a/api/tests/integration/chats/post_chat_test.rs +++ /dev/null @@ -1,166 +0,0 @@ -use uuid::Uuid; -use serde_json::json; -use crate::common::{ - env::{create_env, TestEnv}, - http::client::TestClient, - assertions::response::assert_api_ok, -}; -use database::enums::AssetType; -use diesel::sql_query; -use diesel_async::RunQueryDsl; - -#[tokio::test] -async fn test_post_chat_with_asset_no_prompt() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and metric - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let metric_id = create_test_metric(&env, user_id).await; - - // Test POST request with asset_id but no prompt - let response = client - .post("/api/v1/chats") - .header("X-User-Id", user_id.to_string()) - .json(&json!({ - "asset_id": metric_id, - "asset_type": "metric" - })) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Verify chat was created - assert!(data["chat"]["id"].is_string()); - - // Verify messages were created (at least 2 messages should exist) - let messages = data["messages"].as_object().unwrap(); - assert!(messages.len() >= 2); - - // Verify file association in database - verify_file_association(&env, metric_id, data["chat"]["id"].as_str().unwrap()).await; -} - -#[tokio::test] -async fn test_post_chat_with_legacy_metric_id() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and metric - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let metric_id = create_test_metric(&env, user_id).await; - - // Test POST request with legacy metric_id - let response = client - .post("/api/v1/chats") - .header("X-User-Id", user_id.to_string()) - .json(&json!({ - "prompt": "Analyze this metric", - "metric_id": metric_id - })) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Verify chat was created - assert!(data["chat"]["id"].is_string()); - - // Verify file association in database - verify_file_association(&env, metric_id, data["chat"]["id"].as_str().unwrap()).await; -} - -#[tokio::test] -async fn test_post_chat_with_asset_id_but_no_asset_type() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and metric - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let metric_id = create_test_metric(&env, user_id).await; - - // Test POST request with asset_id but no asset_type (should fail) - let response = client - .post("/api/v1/chats") - .header("X-User-Id", user_id.to_string()) - .json(&json!({ - "asset_id": metric_id - })) - .send() - .await; - - // Assert error status code - assert_eq!(response.status().as_u16(), 400); -} - -// Helper functions to set up the test data -async fn create_test_metric(env: &TestEnv, user_id: Uuid) -> Uuid { - let mut conn = env.db_pool.get().await.unwrap(); - - // Insert test user - sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user_id) - .bind::("test@example.com") - .bind::("Test User") - .execute(&mut conn) - .await - .unwrap(); - - // Insert test organization - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") - .bind::(org_id) - .bind::("Test Organization") - .execute(&mut conn) - .await - .unwrap(); - - // Insert test metric - let metric_id = Uuid::parse_str("00000000-0000-0000-0000-000000000040").unwrap(); - - sql_query(r#" - INSERT INTO metric_files ( - id, name, file_name, content, organization_id, created_by, updated_by, - verification, version_history - ) - VALUES ( - $1, 'Test Metric', 'test_metric.yml', - '{"name":"Test Metric","description":"A test metric"}', - $2, $3, $3, 'PENDING', '{}' - ) - ON CONFLICT DO NOTHING - "#) - .bind::(metric_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - metric_id -} - -async fn verify_file_association(env: &TestEnv, metric_id: Uuid, chat_id: &str) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Check if there's a message associated with the metric file - let result: Vec<(i32,)> = sql_query(r#" - SELECT COUNT(*) FROM messages_to_files - WHERE metric_file_id = $1 AND message_id IN ( - SELECT id FROM messages WHERE thread_id = $2 - ) - "#) - .bind::(metric_id) - .bind::(Uuid::parse_str(chat_id).unwrap()) - .load(&mut conn) - .await - .unwrap(); - - assert!(result[0].0 > 0, "No file association found for the metric in messages"); -} \ No newline at end of file diff --git a/api/tests/integration/chats/restore_chat_test.rs b/api/tests/integration/chats/restore_chat_test.rs deleted file mode 100644 index 8fdcf389f..000000000 --- a/api/tests/integration/chats/restore_chat_test.rs +++ /dev/null @@ -1,352 +0,0 @@ -use anyhow::Result; -use database::enums::AssetType; -use uuid::Uuid; -use chrono::Utc; -use serde_json::json; - -use crate::common::{ - assertions::response::ResponseAssertion, - fixtures::chats::ChatFixtureBuilder, - fixtures::dashboards::DashboardFixtureBuilder, - fixtures::metrics::MetricFixtureBuilder, - fixtures::users::UserFixtureBuilder, - http::client::TestClient, -}; - -#[tokio::test] -async fn test_restore_metric_in_chat() -> Result<()> { - // Create test client - let client = TestClient::new().await?; - - // Create test user - let user = UserFixtureBuilder::new() - .with_name("Test User") - .with_email("test@example.com") - .create(&client) - .await?; - - // Create a metric with initial content - let metric = MetricFixtureBuilder::new() - .with_name("Test Metric") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .with_sql("SELECT * FROM test_table") - .create(&client) - .await?; - - // Create a chat - let chat = ChatFixtureBuilder::new() - .with_title("Test Chat") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .create(&client) - .await?; - - // Update the metric to create version 2 - let update_response = client - .put(&format!("/api/v1/metrics/{}", metric.id)) - .json(&json!({ - "name": "Updated Metric", - "sql": "SELECT * FROM updated_table" - })) - .with_auth(&user) - .send() - .await; - - update_response.assert_status_ok()?; - - // Now restore the metric to version 1 via the chat restoration endpoint - let restore_response = client - .put(&format!("/api/v1/chats/{}/restore", chat.id)) - .json(&json!({ - "asset_id": metric.id, - "asset_type": "metric_file", - "version_number": 1 - })) - .with_auth(&user) - .send() - .await; - - restore_response.assert_status_ok()?; - - // Extract the updated chat from the response - let chat_with_messages = restore_response.json::()?; - - // Verify that messages were created in the chat - let messages = chat_with_messages["data"]["messages"].as_object().unwrap(); - assert!(messages.len() >= 2, "Expected at least 2 messages in the chat"); - - // Verify that there's a message with restoration text in the response_messages - let has_restoration_message = messages.values().any(|msg| { - let response_messages = msg["response_messages"].as_array().unwrap_or(&Vec::new()); - response_messages.iter().any(|rm| { - rm["type"].as_str() == Some("text") && - rm["message"].as_str().unwrap_or("").contains("was created by restoring") - }) - }); - assert!(has_restoration_message, "Expected a restoration message in the chat"); - - // Verify that there's a file reference in the response_messages - let has_file_message = messages.values().any(|msg| { - let response_messages = msg["response_messages"].as_array().unwrap_or(&Vec::new()); - response_messages.iter().any(|rm| { - rm["type"].as_str() == Some("file") && - rm["file_type"].as_str() == Some("metric") - }) - }); - assert!(has_file_message, "Expected a file message referencing the metric"); - - // Get the metric to verify that a new version was created - let metric_response = client - .get(&format!("/api/v1/metrics/{}", metric.id)) - .with_auth(&user) - .send() - .await; - - metric_response.assert_status_ok()?; - let metric_data = metric_response.json::()?; - let version = metric_data["data"]["version"].as_i64().unwrap(); - - // The version should be 3 (initial + update + restore) - assert_eq!(version, 3, "Expected version to be 3 after restoration"); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_dashboard_in_chat() -> Result<()> { - // Create test client - let client = TestClient::new().await?; - - // Create test user - let user = UserFixtureBuilder::new() - .with_name("Test User") - .with_email("test2@example.com") - .create(&client) - .await?; - - // Create a dashboard with initial content - let dashboard = DashboardFixtureBuilder::new() - .with_name("Test Dashboard") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .create(&client) - .await?; - - // Create a chat - let chat = ChatFixtureBuilder::new() - .with_title("Test Chat for Dashboard") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .create(&client) - .await?; - - // Update the dashboard to create version 2 - let update_response = client - .put(&format!("/api/v1/dashboards/{}", dashboard.id)) - .json(&json!({ - "name": "Updated Dashboard" - })) - .with_auth(&user) - .send() - .await; - - update_response.assert_status_ok()?; - - // Now restore the dashboard to version 1 via the chat restoration endpoint - let restore_response = client - .put(&format!("/api/v1/chats/{}/restore", chat.id)) - .json(&json!({ - "asset_id": dashboard.id, - "asset_type": "dashboard_file", - "version_number": 1 - })) - .with_auth(&user) - .send() - .await; - - restore_response.assert_status_ok()?; - - // Extract the updated chat from the response - let chat_with_messages = restore_response.json::()?; - - // Verify that messages were created in the chat - let messages = chat_with_messages["data"]["messages"].as_object().unwrap(); - assert!(messages.len() >= 2, "Expected at least 2 messages in the chat"); - - // Verify that there's a message with restoration text in the response_messages - let has_restoration_message = messages.values().any(|msg| { - let response_messages = msg["response_messages"].as_array().unwrap_or(&Vec::new()); - response_messages.iter().any(|rm| { - rm["type"].as_str() == Some("text") && - rm["message"].as_str().unwrap_or("").contains("was created by restoring") - }) - }); - assert!(has_restoration_message, "Expected a restoration message in the chat"); - - // Verify that there's a file reference in the response_messages - let has_file_message = messages.values().any(|msg| { - let response_messages = msg["response_messages"].as_array().unwrap_or(&Vec::new()); - response_messages.iter().any(|rm| { - rm["type"].as_str() == Some("file") && - rm["file_type"].as_str() == Some("dashboard") - }) - }); - assert!(has_file_message, "Expected a file message referencing the dashboard"); - - // Get the dashboard to verify that a new version was created - let dashboard_response = client - .get(&format!("/api/v1/dashboards/{}", dashboard.id)) - .with_auth(&user) - .send() - .await; - - dashboard_response.assert_status_ok()?; - let dashboard_data = dashboard_response.json::()?; - let version = dashboard_data["data"]["dashboard"]["version"].as_i64().unwrap(); - - // The version should be 3 (initial + update + restore) - assert_eq!(version, 3, "Expected version to be 3 after restoration"); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_wrong_version_in_chat() -> Result<()> { - // Create test client - let client = TestClient::new().await?; - - // Create test user - let user = UserFixtureBuilder::new() - .with_name("Test User") - .with_email("test3@example.com") - .create(&client) - .await?; - - // Create a metric - let metric = MetricFixtureBuilder::new() - .with_name("Test Metric") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .with_sql("SELECT * FROM test_table") - .create(&client) - .await?; - - // Create a chat - let chat = ChatFixtureBuilder::new() - .with_title("Test Chat for Error Case") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .create(&client) - .await?; - - // Try to restore a non-existent version (version 999) - let restore_response = client - .put(&format!("/api/v1/chats/{}/restore", chat.id)) - .json(&json!({ - "asset_id": metric.id, - "asset_type": "metric_file", - "version_number": 999 - })) - .with_auth(&user) - .send() - .await; - - // This should fail with a 404 Not Found - assert_eq!(restore_response.status().as_u16(), 404); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_invalid_asset_type_in_chat() -> Result<()> { - // Create test client - let client = TestClient::new().await?; - - // Create test user - let user = UserFixtureBuilder::new() - .with_name("Test User") - .with_email("test4@example.com") - .create(&client) - .await?; - - // Create a chat - let chat = ChatFixtureBuilder::new() - .with_title("Test Chat for Invalid Asset Type") - .with_created_by(user.id) - .with_organization_id(user.organization_id) - .create(&client) - .await?; - - // Try to restore with an invalid asset type - let restore_response = client - .put(&format!("/api/v1/chats/{}/restore", chat.id)) - .json(&json!({ - "asset_id": Uuid::new_v4(), - "asset_type": "chat", // This is invalid for restoration - "version_number": 1 - })) - .with_auth(&user) - .send() - .await; - - // This should fail with a 400 Bad Request - assert_eq!(restore_response.status().as_u16(), 400); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_without_permission() -> Result<()> { - // Create test client - let client = TestClient::new().await?; - - // Create test users - let owner = UserFixtureBuilder::new() - .with_name("Owner User") - .with_email("owner@example.com") - .create(&client) - .await?; - - let other_user = UserFixtureBuilder::new() - .with_name("Other User") - .with_email("other@example.com") - .create(&client) - .await?; - - // Create a metric owned by the owner - let metric = MetricFixtureBuilder::new() - .with_name("Owner's Metric") - .with_created_by(owner.id) - .with_organization_id(owner.organization_id) - .with_sql("SELECT * FROM owner_table") - .create(&client) - .await?; - - // Create a chat owned by the owner - let chat = ChatFixtureBuilder::new() - .with_title("Owner's Chat") - .with_created_by(owner.id) - .with_organization_id(owner.organization_id) - .create(&client) - .await?; - - // Try to restore as the other user who doesn't have permission - let restore_response = client - .put(&format!("/api/v1/chats/{}/restore", chat.id)) - .json(&json!({ - "asset_id": metric.id, - "asset_type": "metric_file", - "version_number": 1 - })) - .with_auth(&other_user) - .send() - .await; - - // This should fail with a 403 Forbidden or 404 Not Found - // (Depending on implementation, it may be Not Found if the user can't see the resources at all) - let status = restore_response.status().as_u16(); - assert!(status == 403 || status == 404, "Expected status code 403 or 404, got {}", status); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/chats/sharing/create_sharing_test.rs b/api/tests/integration/chats/sharing/create_sharing_test.rs deleted file mode 100644 index c980b4656..000000000 --- a/api/tests/integration/chats/sharing/create_sharing_test.rs +++ /dev/null @@ -1,224 +0,0 @@ -use anyhow::Result; -use axum::{ - extract::Extension, - routing::post, - Router, -}; -use database::enums::{AssetPermissionRole, AssetType, IdentityType}; -use middleware::auth::AuthenticatedUser; -use serde_json::{json, Value}; -use sharing::create_share; -use src::routes::rest::routes::chats::create_chat_sharing_rest_handler; -use tests::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::FixtureBuilder, - http::client::TestClient, -}; -use uuid::Uuid; - -// Test for POST /chats/:id/sharing -// Creates a test server, adds test data, and makes a request to share a chat -#[tokio::test] -async fn test_create_chat_sharing_success() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the chat) - let user = fixture.create_user().await?; - - // Create a test chat owned by the user - let chat = fixture.create_chat(&user.id).await?; - - // Create another user to share with - let share_recipient = fixture.create_user().await?; - - // Create a manual permission so our test user has FullAccess to the chat - create_share( - chat.id, - AssetType::Chat, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", post(create_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request - let payload = json!([ - { - "email": share_recipient.email, - "role": "Viewer" - } - ]); - - let response = client - .post(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - Ok(()) -} - -#[tokio::test] -async fn test_create_chat_sharing_unauthorized() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create two test users - let owner = fixture.create_user().await?; - let non_owner = fixture.create_user().await?; - - // Create a test chat owned by the first user - let chat = fixture.create_chat(&owner.id).await?; - - // Create a manual permission so the owner has Owner access - create_share( - chat.id, - AssetType::Chat, - owner.id, - IdentityType::User, - AssetPermissionRole::Owner, - owner.id, - ) - .await?; - - // Set up the test server with our endpoint but authenticated as non-owner - let app = Router::new() - .route("/chats/:id/sharing", post(create_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: non_owner.id, // This user doesn't have permission to share the chat - email: non_owner.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request - let payload = json!([ - { - "email": "test@example.com", - "role": "Viewer" - } - ]); - - let response = client - .post(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is forbidden - response.assert_status_forbidden()?; - - Ok(()) -} - -#[tokio::test] -async fn test_create_chat_sharing_not_found() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user - let user = fixture.create_user().await?; - - // Generate a non-existent chat ID - let non_existent_chat_id = Uuid::new_v4(); - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", post(create_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request - let payload = json!([ - { - "email": "test@example.com", - "role": "Viewer" - } - ]); - - let response = client - .post(&format!("/chats/{}/sharing", non_existent_chat_id)) - .json(&payload) - .send() - .await?; - - // Assert the response is not found - response.assert_status_not_found()?; - - Ok(()) -} - -#[tokio::test] -async fn test_create_chat_sharing_invalid_email() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user - let user = fixture.create_user().await?; - - // Create a test chat owned by the user - let chat = fixture.create_chat(&user.id).await?; - - // Create a manual permission so our test user has FullAccess to the chat - create_share( - chat.id, - AssetType::Chat, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", post(create_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request with an invalid email format - let payload = json!([ - { - "email": "invalid-email", // Missing @ symbol - "role": "Viewer" - } - ]); - - let response = client - .post(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is bad request - response.assert_status_bad_request()?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/chats/sharing/delete_sharing_test.rs b/api/tests/integration/chats/sharing/delete_sharing_test.rs deleted file mode 100644 index cb2ab0768..000000000 --- a/api/tests/integration/chats/sharing/delete_sharing_test.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::common::{ - assertions::response::{assert_status, StatusCompare}, - db::MockDB, - fixtures::{ - chats::ChatFixtureBuilder, - users::UserFixtureBuilder, - }, - http::client::{ClientWithDatabase, RequestBuilderExt}, -}; -use database::enums::{AssetPermissionRole, AssetType, IdentityType}; -use sharing::create_asset_permission::create_share_by_email; -use reqwest::StatusCode; -use serde_json::json; -use uuid::Uuid; - -#[tokio::test] -async fn test_delete_chat_sharing_success() { - // Setup test database - let mock_db = MockDB::new().await; - let db_conn = mock_db.get_connection().await; - - // Create test user - let user = UserFixtureBuilder::new() - .with_email("test@example.com") - .build(&db_conn) - .await; - - // Create another user to share with - let shared_user = UserFixtureBuilder::new() - .with_email("shared@example.com") - .build(&db_conn) - .await; - - // Create a test chat owned by the user - let chat = ChatFixtureBuilder::new() - .with_created_by(user.id) - .build(&db_conn) - .await; - - // Create sharing permission for the chat - create_share_by_email( - &shared_user.email, - chat.id, - AssetType::Chat, - AssetPermissionRole::FullAccess, - user.id, - ).await.unwrap(); - - // Create client with test database - let client = ClientWithDatabase::new(mock_db); - - // Send DELETE request to remove sharing - let response = client - .delete(&format!("/chats/{}/sharing", chat.id)) - .with_authentication(&user.id.to_string()) - .json(&vec![shared_user.email.clone()]) - .send() - .await; - - // Assert success response - assert_status!(response, StatusCompare::Is(StatusCode::OK)); - - // Verify the permission no longer exists - // This would require checking the database or making a GET request to /chats/{id}/sharing - // In a real test, we would validate this properly -} - -#[tokio::test] -async fn test_delete_chat_sharing_not_found() { - // Setup test database - let mock_db = MockDB::new().await; - - // Create test user - let user = UserFixtureBuilder::new() - .with_email("test@example.com") - .build(&mock_db.get_connection().await) - .await; - - // Create client with test database - let client = ClientWithDatabase::new(mock_db); - - // Send DELETE request for a non-existent chat - let non_existent_chat_id = Uuid::new_v4(); - let response = client - .delete(&format!("/chats/{}/sharing", non_existent_chat_id)) - .with_authentication(&user.id.to_string()) - .json(&vec!["shared@example.com".to_string()]) - .send() - .await; - - // Assert not found response - assert_status!(response, StatusCompare::Is(StatusCode::NOT_FOUND)); -} - -#[tokio::test] -async fn test_delete_chat_sharing_invalid_email() { - // Setup test database - let mock_db = MockDB::new().await; - let db_conn = mock_db.get_connection().await; - - // Create test user - let user = UserFixtureBuilder::new() - .with_email("test@example.com") - .build(&db_conn) - .await; - - // Create a test chat owned by the user - let chat = ChatFixtureBuilder::new() - .with_created_by(user.id) - .build(&db_conn) - .await; - - // Create client with test database - let client = ClientWithDatabase::new(mock_db); - - // Send DELETE request with invalid email format - let response = client - .delete(&format!("/chats/{}/sharing", chat.id)) - .with_authentication(&user.id.to_string()) - .json(&vec!["invalid-email".to_string()]) - .send() - .await; - - // Assert bad request response - assert_status!(response, StatusCompare::Is(StatusCode::BAD_REQUEST)); -} - -#[tokio::test] -async fn test_delete_chat_sharing_unauthorized() { - // Setup test database - let mock_db = MockDB::new().await; - let db_conn = mock_db.get_connection().await; - - // Create test user (owner) - let owner = UserFixtureBuilder::new() - .with_email("owner@example.com") - .build(&db_conn) - .await; - - // Create another user (unauthorized) - let unauthorized_user = UserFixtureBuilder::new() - .with_email("unauthorized@example.com") - .build(&db_conn) - .await; - - // Create a shared user - let shared_user = UserFixtureBuilder::new() - .with_email("shared@example.com") - .build(&db_conn) - .await; - - // Create a test chat owned by the owner - let chat = ChatFixtureBuilder::new() - .with_created_by(owner.id) - .build(&db_conn) - .await; - - // Create client with test database - let client = ClientWithDatabase::new(mock_db); - - // Send DELETE request from unauthorized user - let response = client - .delete(&format!("/chats/{}/sharing", chat.id)) - .with_authentication(&unauthorized_user.id.to_string()) - .json(&vec![shared_user.email.clone()]) - .send() - .await; - - // Assert forbidden response - assert_status!(response, StatusCompare::Is(StatusCode::FORBIDDEN)); -} \ No newline at end of file diff --git a/api/tests/integration/chats/sharing/list_sharing_test.rs b/api/tests/integration/chats/sharing/list_sharing_test.rs deleted file mode 100644 index 134531b50..000000000 --- a/api/tests/integration/chats/sharing/list_sharing_test.rs +++ /dev/null @@ -1,170 +0,0 @@ -use std::sync::Arc; -use axum::{ - body::Body, - extract::connect_info::MockConnectInfo, - http::{Request, StatusCode}, -}; -use uuid::Uuid; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{AssetPermission, Chat, User}, -}; -use crate::common::{ - assertions::response::{assert_json_response, assert_status}, - db::setup_test_db, - fixtures::{ - chats::create_test_chat, - users::create_test_user, - }, - http::client::TestClient, -}; -use chrono::Utc; - -// Test listing sharing permissions for a chat -#[tokio::test] -async fn test_list_chat_sharing_permissions() { - // Setup test database and create test users and chat - let pool = setup_test_db().await; - let mut conn = pool.get().await.unwrap(); - - // Create test users - let owner = create_test_user(&mut conn).await; - let viewer = create_test_user(&mut conn).await; - - // Create test chat - let chat = create_test_chat(&mut conn, &owner.id).await; - - // Create sharing permission for the viewer - let permission = AssetPermission { - identity_id: viewer.id, - identity_type: IdentityType::User, - asset_id: chat.id, - asset_type: AssetType::Chat, - role: AssetPermissionRole::CanView, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - diesel::insert_into(database::schema::asset_permissions::table) - .values(&permission) - .execute(&mut conn) - .await - .unwrap(); - - // Make request as owner - let client = TestClient::new( - Arc::new(MockConnectInfo(([127, 0, 0, 1], 8080))), - owner, - ); - - let response = client - .get(&format!("/chats/{}/sharing", chat.id)) - .send() - .await; - - // Assert successful response - assert_status!(response, StatusCode::OK); - - // Verify response contains the sharing permission - let body = response.into_body(); - let json = hyper::body::to_bytes(body).await.unwrap(); - let response: serde_json::Value = serde_json::from_slice(&json).unwrap(); - - assert!(response["permissions"].is_array()); - assert_eq!(response["permissions"].as_array().unwrap().len(), 1); - assert_eq!(response["permissions"][0]["user_id"], viewer.id.to_string()); - assert_eq!(response["permissions"][0]["role"], "CanView"); -} - -// Test listing sharing permissions for a chat that doesn't exist -#[tokio::test] -async fn test_list_sharing_nonexistent_chat() { - // Setup test database and create test users - let pool = setup_test_db().await; - let mut conn = pool.get().await.unwrap(); - - // Create test user - let user = create_test_user(&mut conn).await; - - // Make request with non-existent chat ID - let client = TestClient::new( - Arc::new(MockConnectInfo(([127, 0, 0, 1], 8080))), - user, - ); - - let response = client - .get(&format!("/chats/{}/sharing", Uuid::new_v4())) - .send() - .await; - - // Assert not found response - assert_status!(response, StatusCode::NOT_FOUND); -} - -// Test listing sharing permissions for a chat without permission -#[tokio::test] -async fn test_list_sharing_without_permission() { - // Setup test database and create test users and chat - let pool = setup_test_db().await; - let mut conn = pool.get().await.unwrap(); - - // Create test users - let owner = create_test_user(&mut conn).await; - let unauthorized_user = create_test_user(&mut conn).await; - - // Create test chat - let chat = create_test_chat(&mut conn, &owner.id).await; - - // Make request as unauthorized user - let client = TestClient::new( - Arc::new(MockConnectInfo(([127, 0, 0, 1], 8080))), - unauthorized_user, - ); - - let response = client - .get(&format!("/chats/{}/sharing", chat.id)) - .send() - .await; - - // Assert forbidden response - assert_status!(response, StatusCode::FORBIDDEN); -} - -// Test listing sharing permissions for a chat with no sharing permissions -#[tokio::test] -async fn test_list_empty_sharing_permissions() { - // Setup test database and create test users and chat - let pool = setup_test_db().await; - let mut conn = pool.get().await.unwrap(); - - // Create test user - let user = create_test_user(&mut conn).await; - - // Create test chat - let chat = create_test_chat(&mut conn, &user.id).await; - - // Make request as owner - let client = TestClient::new( - Arc::new(MockConnectInfo(([127, 0, 0, 1], 8080))), - user, - ); - - let response = client - .get(&format!("/chats/{}/sharing", chat.id)) - .send() - .await; - - // Assert successful response - assert_status!(response, StatusCode::OK); - - // Verify response contains empty permissions array - let body = response.into_body(); - let json = hyper::body::to_bytes(body).await.unwrap(); - let response: serde_json::Value = serde_json::from_slice(&json).unwrap(); - - assert!(response["permissions"].is_array()); - assert_eq!(response["permissions"].as_array().unwrap().len(), 0); -} \ No newline at end of file diff --git a/api/tests/integration/chats/sharing/mod.rs b/api/tests/integration/chats/sharing/mod.rs deleted file mode 100644 index 084f53d5b..000000000 --- a/api/tests/integration/chats/sharing/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod list_sharing_test; -pub mod delete_sharing_test; -pub mod create_sharing_test; -pub mod update_sharing_test; diff --git a/api/tests/integration/chats/sharing/update_sharing_test.rs b/api/tests/integration/chats/sharing/update_sharing_test.rs deleted file mode 100644 index e789350bf..000000000 --- a/api/tests/integration/chats/sharing/update_sharing_test.rs +++ /dev/null @@ -1,390 +0,0 @@ -use anyhow::Result; -use axum::{ - extract::Extension, - routing::put, - Router, -}; -use chrono::{Duration, Utc}; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - get_pg_pool, - models::Chat, - schema::chats::dsl, -}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use diesel_async::RunQueryDsl as AsyncRunQueryDsl; -use middleware::auth::AuthenticatedUser; -use serde_json::{json, Value}; -use sharing::create_share; -use src::routes::rest::routes::chats::sharing::update_chat_sharing_rest_handler; -use tests::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::FixtureBuilder, - http::client::TestClient, -}; -use uuid::Uuid; - -// Test for PUT /chats/:id/sharing -// Creates a test server, adds test data, and makes a request to update chat sharing -#[tokio::test] -async fn test_update_chat_sharing_success() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the chat) - let user = fixture.create_user().await?; - - // Create a test chat owned by the user - let chat = fixture.create_chat(&user.id).await?; - - // Create another user to share with - let share_recipient = fixture.create_user().await?; - - // Create a manual permission so our test user has Owner access to the chat - create_share( - chat.id, - AssetType::Chat, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", put(update_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request to update sharing permissions with new format - let payload = json!({ - "users": [ - { - "email": share_recipient.email, - "role": "Editor" // Update to Editor role - } - ] - }); - - let response = client - .put(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - Ok(()) -} - -#[tokio::test] -async fn test_update_chat_public_sharing_success() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the chat) - let user = fixture.create_user().await?; - - // Create a test chat owned by the user - let chat = fixture.create_chat(&user.id).await?; - - // Create a manual permission so our test user has Owner access to the chat - create_share( - chat.id, - AssetType::Chat, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", put(update_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Set expiration date to 7 days from now - let expiration_date = Utc::now() + Duration::days(7); - - // Make the request to update public sharing settings - let payload = json!({ - "publicly_accessible": true, - "public_expiration": expiration_date.to_rfc3339() - }); - - let response = client - .put(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - // Verify database was updated correctly - let pool = get_pg_pool(); - let mut conn = pool.get().await?; - - let updated_chat: Chat = dsl::chats - .filter(dsl::id.eq(chat.id)) - .first(&mut conn) - .await?; - - assert!(updated_chat.publicly_accessible); - assert_eq!(updated_chat.publicly_enabled_by, Some(user.id)); - - // The public_expiry_date is stored as a timestamp, so we can't do an exact match. - // Instead, we'll check that it's within a minute of our expected value - let stored_expiry = updated_chat.public_expiry_date.unwrap(); - let diff = (stored_expiry - expiration_date).num_seconds().abs(); - assert!(diff < 60, "Expiry date should be within a minute of the requested date"); - - Ok(()) -} - -#[tokio::test] -async fn test_update_chat_sharing_unauthorized() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create two test users - let owner = fixture.create_user().await?; - let non_owner = fixture.create_user().await?; - - // Create a test chat owned by the first user - let chat = fixture.create_chat(&owner.id).await?; - - // Create a manual permission so the owner has Owner access - create_share( - chat.id, - AssetType::Chat, - owner.id, - IdentityType::User, - AssetPermissionRole::Owner, - owner.id, - ) - .await?; - - // Set up the test server with our endpoint but authenticated as non-owner - let app = Router::new() - .route("/chats/:id/sharing", put(update_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: non_owner.id, // This user doesn't have permission to update the chat sharing - email: non_owner.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request with new format - let payload = json!({ - "users": [ - { - "email": "test@example.com", - "role": "Editor" - } - ] - }); - - let response = client - .put(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is forbidden - response.assert_status_forbidden()?; - - Ok(()) -} - -#[tokio::test] -async fn test_update_chat_sharing_not_found() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user - let user = fixture.create_user().await?; - - // Generate a non-existent chat ID - let non_existent_chat_id = Uuid::new_v4(); - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", put(update_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request with new format - let payload = json!({ - "users": [ - { - "email": "test@example.com", - "role": "Editor" - } - ] - }); - - let response = client - .put(&format!("/chats/{}/sharing", non_existent_chat_id)) - .json(&payload) - .send() - .await?; - - // Assert the response is not found - response.assert_status_not_found()?; - - Ok(()) -} - -#[tokio::test] -async fn test_update_chat_sharing_invalid_email() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user - let user = fixture.create_user().await?; - - // Create a test chat owned by the user - let chat = fixture.create_chat(&user.id).await?; - - // Create a manual permission so our test user has Owner access to the chat - create_share( - chat.id, - AssetType::Chat, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", put(update_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request with an invalid email format using new request structure - let payload = json!({ - "users": [ - { - "email": "invalid-email", // Missing @ symbol - "role": "Editor" - } - ] - }); - - let response = client - .put(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is bad request - response.assert_status_bad_request()?; - - Ok(()) -} - -#[tokio::test] -async fn test_update_chat_sharing_mixed_updates() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the chat) - let user = fixture.create_user().await?; - - // Create a test chat owned by the user - let chat = fixture.create_chat(&user.id).await?; - - // Create another user to share with - let share_recipient = fixture.create_user().await?; - - // Create a manual permission so our test user has Owner access to the chat - create_share( - chat.id, - AssetType::Chat, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/chats/:id/sharing", put(update_chat_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Set expiration date to 7 days from now - let expiration_date = Utc::now() + Duration::days(7); - - // Make the request to update both user and public sharing settings - let payload = json!({ - "users": [ - { - "email": share_recipient.email, - "role": "Editor" - } - ], - "publicly_accessible": true, - "public_expiration": expiration_date.to_rfc3339() - }); - - let response = client - .put(&format!("/chats/{}/sharing", chat.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - // Verify database was updated correctly for public sharing - let pool = get_pg_pool(); - let mut conn = pool.get().await?; - - let updated_chat: Chat = dsl::chats - .filter(dsl::id.eq(chat.id)) - .first(&mut conn) - .await?; - - assert!(updated_chat.publicly_accessible); - assert_eq!(updated_chat.publicly_enabled_by, Some(user.id)); - - // Verify user permissions were updated too - // This would normally check the asset_permissions table in a real test - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/chats/update_chat_test.rs b/api/tests/integration/chats/update_chat_test.rs deleted file mode 100644 index fdfa5b2c7..000000000 --- a/api/tests/integration/chats/update_chat_test.rs +++ /dev/null @@ -1,139 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use database::models::Chat; -use diesel::prelude::*; -use std::str::FromStr; -use uuid::Uuid; - -use crate::common::{ - db::run_test_with_db, - fixtures::{builder::TestFixtureBuilder, chats::CreateChatParams}, - http::test_app::{TestApp, TestAppBuilder}, - http::client::ApiClient, -}; - -#[tokio::test] -async fn test_update_chat() -> Result<()> { - run_test_with_db(|db| async move { - // Create test user - let user_id = Uuid::new_v4(); - let mut fixture_builder = TestFixtureBuilder::new(db.clone()); - let user = fixture_builder.create_user(user_id).await?; - - // Create test chat - let chat_id = Uuid::new_v4(); - let chat = fixture_builder - .create_chat(CreateChatParams { - id: Some(chat_id), - title: Some("Original Title".to_string()), - created_by: Some(user_id), - ..Default::default() - }) - .await?; - - // Setup test app - let app = TestAppBuilder::new().with_db(db.clone()).build().await?; - let client = ApiClient::new(app.client, user.clone()); - - // Test updating the chat - let response = client - .put(&format!("/api/v1/chats/{}", chat_id)) - .json(&serde_json::json!({ - "title": "Updated Title" - })) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), 200); - let json = response.json::().await?; - assert_eq!(json["id"], chat_id.to_string()); - assert_eq!(json["success"], true); - assert_eq!(json["error"], serde_json::Value::Null); - - // Verify database update - let updated_chat = db.with_conn(|conn| async move { - use database::schema::chats; - - chats::table - .find(chat_id) - .first::(conn) - .await - .map_err(anyhow::Error::from) - }).await?; - - assert_eq!(updated_chat.title, "Updated Title"); - - Ok(()) - }).await -} - -#[tokio::test] -async fn test_update_chat_not_found() -> Result<()> { - run_test_with_db(|db| async move { - // Create test user - let user_id = Uuid::new_v4(); - let mut fixture_builder = TestFixtureBuilder::new(db.clone()); - let user = fixture_builder.create_user(user_id).await?; - - // Setup test app - let app = TestAppBuilder::new().with_db(db.clone()).build().await?; - let client = ApiClient::new(app.client, user.clone()); - - // Test updating a non-existent chat - let non_existent_id = Uuid::new_v4(); - let response = client - .put(&format!("/api/v1/chats/{}", non_existent_id)) - .json(&serde_json::json!({ - "title": "Updated Title" - })) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), 404); - - Ok(()) - }).await -} - -#[tokio::test] -async fn test_update_chat_unauthorized() -> Result<()> { - run_test_with_db(|db| async move { - // Create two test users - let user1_id = Uuid::new_v4(); - let user2_id = Uuid::new_v4(); - let mut fixture_builder = TestFixtureBuilder::new(db.clone()); - let user1 = fixture_builder.create_user(user1_id).await?; - let user2 = fixture_builder.create_user(user2_id).await?; - - // Create test chat owned by user1 - let chat_id = Uuid::new_v4(); - let chat = fixture_builder - .create_chat(CreateChatParams { - id: Some(chat_id), - title: Some("Original Title".to_string()), - created_by: Some(user1_id), - ..Default::default() - }) - .await?; - - // Setup test app with user2 (who doesn't own the chat) - let app = TestAppBuilder::new().with_db(db.clone()).build().await?; - let client = ApiClient::new(app.client, user2.clone()); - - // Test user2 trying to update user1's chat - let response = client - .put(&format!("/api/v1/chats/{}", chat_id)) - .json(&serde_json::json!({ - "title": "Updated Title" - })) - .send() - .await?; - - // Verify response (should be forbidden) - assert_eq!(response.status(), 403); - - Ok(()) - }).await -} \ No newline at end of file diff --git a/api/tests/integration/collections/add_assets_to_collection_test.rs b/api/tests/integration/collections/add_assets_to_collection_test.rs deleted file mode 100644 index 1466acec7..000000000 --- a/api/tests/integration/collections/add_assets_to_collection_test.rs +++ /dev/null @@ -1,370 +0,0 @@ -use crate::common::{ - fixtures::{collections::create_collection, dashboards::create_dashboard, metrics::create_metric, users::create_user}, - http::test_app::create_test_app, - matchers::json::json_eq, -}; -use axum::{body::Body, http::Request}; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - pool::get_pg_pool, - schema::{asset_permissions, collections, collections_to_assets}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use http::StatusCode; -use serde_json::{json, Value}; -use sharing::create_asset_permission::create_asset_permission; -use uuid::Uuid; - -#[tokio::test] -async fn test_add_assets_to_collection() { - // Create a test app and database connection - let app = create_test_app().await; - let pool = get_pg_pool(); - let mut conn = pool.get().await.unwrap(); - - // Create test user, collection, dashboard, and metric - let user = create_user(None).await; - let collection = create_collection(user.id, None).await; - let dashboard = create_dashboard(user.id, None).await; - let metric = create_metric(user.id, None).await; - - // Give the user permission to the collection as owner - create_asset_permission( - user.id, - IdentityType::User, - collection.id, - AssetType::Collection, - AssetPermissionRole::Owner, - user.id, - ) - .await - .unwrap(); - - // Give the user permission to the dashboard and metric - create_asset_permission( - user.id, - IdentityType::User, - dashboard.id, - AssetType::DashboardFile, - AssetPermissionRole::CanView, - user.id, - ) - .await - .unwrap(); - - create_asset_permission( - user.id, - IdentityType::User, - metric.id, - AssetType::MetricFile, - AssetPermissionRole::CanView, - user.id, - ) - .await - .unwrap(); - - // Build request to add assets to collection - let request = Request::builder() - .uri(format!("/collections/{}/assets", collection.id)) - .method("POST") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", user.id)) - .body(Body::from( - json!({ - "assets": [ - { - "id": dashboard.id, - "type": "dashboard" - }, - { - "id": metric.id, - "type": "metric" - } - ] - }) - .to_string(), - )) - .unwrap(); - - // Send the request - let response = app.oneshot(request).await.unwrap(); - - // Check the response - assert_eq!(response.status(), StatusCode::OK); - - // Parse the response body - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - let response_json: Value = serde_json::from_slice(&body).unwrap(); - - // Verify the response structure - assert!(json_eq( - &response_json, - &json!({ - "message": "Assets processed", - "added_count": 2, - "failed_count": 0, - "failed_assets": [] - }) - )); - - // Verify that the assets were added to the database - let dashboard_asset = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection.id)) - .filter(collections_to_assets::asset_id.eq(dashboard.id)) - .filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile)) - .filter(collections_to_assets::deleted_at.is_null()) - .first::(&mut conn) - .await; - - let metric_asset = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection.id)) - .filter(collections_to_assets::asset_id.eq(metric.id)) - .filter(collections_to_assets::asset_type.eq(AssetType::MetricFile)) - .filter(collections_to_assets::deleted_at.is_null()) - .first::(&mut conn) - .await; - - // Assert that both assets exist in the collection - assert!(dashboard_asset.is_ok()); - assert!(metric_asset.is_ok()); -} - -#[tokio::test] -async fn test_add_assets_to_collection_permission_denied() { - // Create a test app - let app = create_test_app().await; - - // Create test user, collection, dashboard, and metric - let user = create_user(None).await; - let other_user = create_user(None).await; - let collection = create_collection(other_user.id, None).await; - let dashboard = create_dashboard(user.id, None).await; - - // No permissions are given to the user for the collection - - // Build request to add assets to collection - let request = Request::builder() - .uri(format!("/collections/{}/assets", collection.id)) - .method("POST") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", user.id)) - .body(Body::from( - json!({ - "assets": [ - { - "id": dashboard.id, - "type": "dashboard" - } - ] - }) - .to_string(), - )) - .unwrap(); - - // Send the request - let response = app.oneshot(request).await.unwrap(); - - // Check that permission is denied - assert_eq!(response.status(), StatusCode::FORBIDDEN); -} - -#[tokio::test] -async fn test_add_assets_to_collection_not_found() { - // Create a test app - let app = create_test_app().await; - - // Create test user and dashboard - let user = create_user(None).await; - let dashboard = create_dashboard(user.id, None).await; - - // Create a non-existent collection ID - let non_existent_collection_id = Uuid::new_v4(); - - // Build request to add assets to a non-existent collection - let request = Request::builder() - .uri(format!("/collections/{}/assets", non_existent_collection_id)) - .method("POST") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", user.id)) - .body(Body::from( - json!({ - "assets": [ - { - "id": dashboard.id, - "type": "dashboard" - } - ] - }) - .to_string(), - )) - .unwrap(); - - // Send the request - let response = app.oneshot(request).await.unwrap(); - - // Check that collection not found - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[tokio::test] -async fn test_add_nonexistent_assets_to_collection() { - // Create a test app - let app = create_test_app().await; - - // Create test user and collection - let user = create_user(None).await; - let collection = create_collection(user.id, None).await; - - // Give the user permission to the collection - create_asset_permission( - user.id, - IdentityType::User, - collection.id, - AssetType::Collection, - AssetPermissionRole::Owner, - user.id, - ) - .await - .unwrap(); - - // Create a non-existent dashboard ID - let non_existent_dashboard_id = Uuid::new_v4(); - - // Build request to add non-existent assets to collection - let request = Request::builder() - .uri(format!("/collections/{}/assets", collection.id)) - .method("POST") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", user.id)) - .body(Body::from( - json!({ - "assets": [ - { - "id": non_existent_dashboard_id, - "type": "dashboard" - } - ] - }) - .to_string(), - )) - .unwrap(); - - // Send the request - let response = app.oneshot(request).await.unwrap(); - - // Check the response - should return 200 but with failed assets - assert_eq!(response.status(), StatusCode::OK); - - // Parse the response body - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - let response_json: Value = serde_json::from_slice(&body).unwrap(); - - // Verify the response contains the failed asset - assert!(json_eq( - &response_json, - &json!({ - "message": "Assets processed", - "added_count": 0, - "failed_count": 1, - "failed_assets": [ - { - "id": non_existent_dashboard_id, - "type": "dashboard", - "error": "Dashboard not found" - } - ] - }) - )); -} - -#[tokio::test] -async fn test_add_duplicate_assets_to_collection() { - // Create a test app and database connection - let app = create_test_app().await; - let pool = get_pg_pool(); - let mut conn = pool.get().await.unwrap(); - - // Create test user, collection, and dashboard - let user = create_user(None).await; - let collection = create_collection(user.id, None).await; - let dashboard = create_dashboard(user.id, None).await; - - // Give the user permission to the collection and dashboard - create_asset_permission( - user.id, - IdentityType::User, - collection.id, - AssetType::Collection, - AssetPermissionRole::Owner, - user.id, - ) - .await - .unwrap(); - - create_asset_permission( - user.id, - IdentityType::User, - dashboard.id, - AssetType::DashboardFile, - AssetPermissionRole::CanView, - user.id, - ) - .await - .unwrap(); - - // First, add the dashboard to the collection - diesel::insert_into(collections_to_assets::table) - .values(( - collections_to_assets::collection_id.eq(collection.id), - collections_to_assets::asset_id.eq(dashboard.id), - collections_to_assets::asset_type.eq(AssetType::DashboardFile), - collections_to_assets::created_at.eq(chrono::Utc::now()), - collections_to_assets::created_by.eq(user.id), - collections_to_assets::updated_at.eq(chrono::Utc::now()), - collections_to_assets::updated_by.eq(user.id), - )) - .execute(&mut conn) - .await - .unwrap(); - - // Build request to add the same dashboard again - let request = Request::builder() - .uri(format!("/collections/{}/assets", collection.id)) - .method("POST") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", user.id)) - .body(Body::from( - json!({ - "assets": [ - { - "id": dashboard.id, - "type": "dashboard" - } - ] - }) - .to_string(), - )) - .unwrap(); - - // Send the request - let response = app.oneshot(request).await.unwrap(); - - // Check the response - should return 200 as the asset is already in the collection - assert_eq!(response.status(), StatusCode::OK); - - // Parse the response body - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - let response_json: Value = serde_json::from_slice(&body).unwrap(); - - // Verify the response structure (added_count is 1 because we consider existing assets as "added") - assert!(json_eq( - &response_json, - &json!({ - "message": "Assets processed", - "added_count": 1, - "failed_count": 0, - "failed_assets": [] - }) - )); -} \ No newline at end of file diff --git a/api/tests/integration/collections/add_dashboards_to_collection_test.rs b/api/tests/integration/collections/add_dashboards_to_collection_test.rs deleted file mode 100644 index dbf06bea7..000000000 --- a/api/tests/integration/collections/add_dashboards_to_collection_test.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::common::{ - db::TestDb, - helpers::{create_authenticated_app, setup_user_with_permissions}, -}; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{CollectionToAsset, DashboardFile, Collection}, - pool::get_pg_pool, - schema::{collections_to_assets, collections, dashboard_files}, -}; -use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -#[tokio::test] -async fn test_add_dashboards_to_collection() { - // This is a placeholder test - would be implemented with actual test setup - // and verification in a real implementation - assert!(true); - - // The below code outlines how the test would be structured: - /* - // Setup test database - let test_db = TestDb::new().await.unwrap(); - - // Create test user with appropriate permissions - let (user, auth_token) = setup_user_with_permissions().await.unwrap(); - - // Create test collection - let collection = Collection { - id: Uuid::new_v4(), - name: "Test Collection".to_string(), - description: Some("Test Description".to_string()), - created_by: user.id, - updated_by: user.id, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - deleted_at: None, - organization_id: user.organization_id, - }; - - // Create test dashboards - let dashboard1 = DashboardFile { - id: Uuid::new_v4(), - name: "Test Dashboard 1".to_string(), - // ...other fields... - }; - - let dashboard2 = DashboardFile { - id: Uuid::new_v4(), - name: "Test Dashboard 2".to_string(), - // ...other fields... - }; - - // Insert test data into database - let mut conn = get_pg_pool().get().await.unwrap(); - diesel::insert_into(collections::table) - .values(&collection) - .execute(&mut conn) - .await - .unwrap(); - - diesel::insert_into(dashboard_files::table) - .values(&[&dashboard1, &dashboard2]) - .execute(&mut conn) - .await - .unwrap(); - - // Grant permissions to user for collection and dashboards - // (Implementation would vary based on your permission system) - - // Create app with auth middleware - let app = create_authenticated_app().await; - - // Make request to add dashboards to collection - let response = app - .oneshot( - axum::http::Request::builder() - .method(axum::http::Method::POST) - .uri(format!("/collections/{}/dashboards", collection.id)) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", auth_token)) - .body(axum::body::Body::from( - serde_json::to_string(&json!({ - "dashboard_ids": [dashboard1.id, dashboard2.id] - })) - .unwrap(), - )) - .unwrap(), - ) - .await - .unwrap(); - - // Assert response status - assert_eq!(response.status(), axum::http::StatusCode::OK); - - // Verify dashboards were added to collection - let collection_assets = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection.id)) - .filter(collections_to_assets::deleted_at.is_null()) - .select(CollectionToAsset::as_select()) - .load::(&mut conn) - .await - .unwrap(); - - assert_eq!(collection_assets.len(), 2); - - // Clean up test data - // (Implementation would depend on your test framework) - */ -} \ No newline at end of file diff --git a/api/tests/integration/collections/delete_collection_test.rs b/api/tests/integration/collections/delete_collection_test.rs deleted file mode 100644 index 6c5ec62fe..000000000 --- a/api/tests/integration/collections/delete_collection_test.rs +++ /dev/null @@ -1,59 +0,0 @@ -use uuid::Uuid; -use serde_json::json; - -use crate::common::{ - fixtures::users::create_test_user, - http::test_app::TestApp, -}; - -#[tokio::test] -async fn test_delete_collections_bulk() { - // Set up test app - let app = TestApp::new().await; - - // Create test user - let user = create_test_user(); - - // Test IDs - let id1 = Uuid::new_v4(); - let id2 = Uuid::new_v4(); - - // Call the API to delete collections in bulk - let response = app - .delete("/api/v1/collections") - .with_auth(&user) - .json(&json!({ - "ids": [id1, id2] - })) - .send() - .await; - - // Verify response status (we're not actually deleting real collections here) - // Since we're not creating test collections, it might fail with 404 or succeed with 200 - // We're mainly testing that the endpoint accepts the request format correctly - assert!(response.status().is_client_error() || response.status().is_success()); -} - -#[tokio::test] -async fn test_delete_collection_by_id() { - // Set up test app - let app = TestApp::new().await; - - // Create test user - let user = create_test_user(); - - // Test ID - let id = Uuid::new_v4(); - - // Call the API to delete a collection by ID - let response = app - .delete(&format!("/api/v1/collections/{}", id)) - .with_auth(&user) - .send() - .await; - - // Verify response status (we're not actually deleting a real collection here) - // Since we're not creating a test collection, it might fail with 404 or succeed with 200 - // We're mainly testing that the endpoint accepts the request format correctly - assert!(response.status().is_client_error() || response.status().is_success()); -} \ No newline at end of file diff --git a/api/tests/integration/collections/get_collection_test.rs b/api/tests/integration/collections/get_collection_test.rs deleted file mode 100644 index e21e605a9..000000000 --- a/api/tests/integration/collections/get_collection_test.rs +++ /dev/null @@ -1,208 +0,0 @@ -use uuid::Uuid; -use crate::common::{ - env::{create_env, TestEnv}, - http::client::TestClient, - assertions::response::assert_api_ok, -}; -use chrono::Utc; -use database::enums::{AssetPermissionRole, IdentityTypeEnum}; -use diesel::sql_query; -use diesel_async::RunQueryDsl; - -#[tokio::test] -async fn test_get_collection_with_sharing_info() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and collection - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let collection_id = create_test_collection(&env, user_id).await; - - // Add sharing permissions - add_test_permissions(&env, collection_id, user_id).await; - - // Add public sharing - enable_public_sharing(&env, collection_id, user_id).await; - - // Test GET request - let response = client - .get(&format!("/api/v1/collections/{}", collection_id)) - .header("X-User-Id", user_id.to_string()) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Check fields - assert_eq!(data["id"], collection_id.to_string()); - - // Check sharing fields - collections don't have public fields yet - assert_eq!(data["publicly_accessible"], false); - assert!(data["public_expiry_date"].is_null()); - assert!(data["public_enabled_by"].is_null()); - assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1); - - let permission = &data["individual_permissions"][0]; - assert_eq!(permission["email"], "test2@example.com"); - assert_eq!(permission["role"], "viewer"); - assert_eq!(permission["name"], "Test User 2"); - - // Check assets - assert!(data["assets"].is_array()); - assert_eq!(data["assets"].as_array().unwrap().len(), 2); - - // Verify metric asset - let metric_asset = data["assets"].as_array().unwrap().iter() - .find(|asset| asset["asset_type"] == "metric_file") - .expect("Should have a metric asset"); - - assert_eq!(metric_asset["name"], "Test Metric"); - assert_eq!(metric_asset["id"], "00000000-0000-0000-0000-000000000050"); - assert_eq!(metric_asset["created_by"]["email"], "test@example.com"); - assert_eq!(metric_asset["created_by"]["name"], "Test User"); - - // Verify dashboard asset - let dashboard_asset = data["assets"].as_array().unwrap().iter() - .find(|asset| asset["asset_type"] == "dashboard_file") - .expect("Should have a dashboard asset"); - - assert_eq!(dashboard_asset["name"], "Test Dashboard"); - assert_eq!(dashboard_asset["id"], "00000000-0000-0000-0000-000000000060"); - assert_eq!(dashboard_asset["created_by"]["email"], "test@example.com"); - assert_eq!(dashboard_asset["created_by"]["name"], "Test User"); -} - -// Helper functions to set up the test data -async fn create_test_collection(env: &TestEnv, user_id: Uuid) -> Uuid { - let mut conn = env.db_pool.get().await.unwrap(); - - // Insert test user - sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user_id) - .bind::("test@example.com") - .bind::("Test User") - .execute(&mut conn) - .await - .unwrap(); - - // Insert another test user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user2_id) - .bind::("test2@example.com") - .bind::("Test User 2") - .execute(&mut conn) - .await - .unwrap(); - - // Insert test collection - let collection_id = Uuid::parse_str("00000000-0000-0000-0000-000000000040").unwrap(); - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - - // Insert test organization if needed - sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") - .bind::(org_id) - .bind::("Test Organization") - .execute(&mut conn) - .await - .unwrap(); - - // Insert collection - sql_query(r#" - INSERT INTO collections (id, name, description, organization_id, created_by) - VALUES ($1, 'Test Collection', 'Test description', $2, $3) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - // Insert test metric file - let metric_id = Uuid::parse_str("00000000-0000-0000-0000-000000000050").unwrap(); - sql_query(r#" - INSERT INTO metric_files (id, name, file_name, content, organization_id, created_by, publicly_accessible, version_history, verification) - VALUES ($1, 'Test Metric', 'test_metric.yml', '{}', $2, $3, false, '{}', 'verified') - ON CONFLICT DO NOTHING - "#) - .bind::(metric_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - // Insert test dashboard file - let dashboard_id = Uuid::parse_str("00000000-0000-0000-0000-000000000060").unwrap(); - sql_query(r#" - INSERT INTO dashboard_files (id, name, file_name, content, organization_id, created_by, publicly_accessible, version_history) - VALUES ($1, 'Test Dashboard', 'test_dashboard.yml', '{}', $2, $3, false, '{}') - ON CONFLICT DO NOTHING - "#) - .bind::(dashboard_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - // Add assets to collection - sql_query(r#" - INSERT INTO collections_to_assets (collection_id, asset_id, asset_type, created_by, updated_by) - VALUES ($1, $2, $3, $4, $4) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::(metric_id) - .bind::("metric_file") - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - sql_query(r#" - INSERT INTO collections_to_assets (collection_id, asset_id, asset_type, created_by, updated_by) - VALUES ($1, $2, $3, $4, $4) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::(dashboard_id) - .bind::("dashboard_file") - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - collection_id -} - -async fn add_test_permissions(env: &TestEnv, collection_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Get the second user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - - // Add permission for user2 as viewer - sql_query(r#" - INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by) - VALUES ($1, $2, $3, $4, $5, $6, $6) - ON CONFLICT DO NOTHING - "#) - .bind::(user2_id) - .bind::(IdentityTypeEnum::User.to_string()) - .bind::(collection_id) - .bind::(AssetTypeEnum::Collection.to_string()) - .bind::(AssetPermissionRole::CanView.to_string()) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); -} - -async fn enable_public_sharing(_env: &TestEnv, _collection_id: Uuid, _user_id: Uuid) { - // Collections don't have public sharing fields yet, so this is a no-op -} \ No newline at end of file diff --git a/api/tests/integration/collections/mod.rs b/api/tests/integration/collections/mod.rs deleted file mode 100644 index 590fc930a..000000000 --- a/api/tests/integration/collections/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod sharing; -pub mod add_assets_to_collection_test; -pub mod add_dashboards_to_collection_test; -pub mod delete_collection_test; -pub mod get_collection_test; -pub mod remove_assets_from_collection_test; -pub mod remove_metrics_from_collection_test; \ No newline at end of file diff --git a/api/tests/integration/collections/remove_assets_from_collection_test.rs b/api/tests/integration/collections/remove_assets_from_collection_test.rs deleted file mode 100644 index a24adff1e..000000000 --- a/api/tests/integration/collections/remove_assets_from_collection_test.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::common::{ - assertions::{response::ResponseAssertions, model::ModelAssertions}, - fixtures::{self, collections::CollectionFixtureBuilder, dashboards::DashboardFileFixtureBuilder, metrics::MetricFileFixtureBuilder}, - http::client::TestClient, -}; -use database::{ - enums::AssetType, - models::CollectionToAsset, - pool::get_pg_pool, - schema::collections_to_assets, -}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use diesel_async::RunQueryDsl as AsyncRunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -#[tokio::test] -async fn test_remove_assets_from_collection() { - // Set up test data - let user = fixtures::users::create_test_user_with_organization().await; - let client = TestClient::new_authenticated(&user).await; - - // Create a collection and assets - let collection = CollectionFixtureBuilder::new() - .with_user(&user) - .with_organization_id(user.organization_id) - .build() - .create() - .await; - - let dashboard = DashboardFileFixtureBuilder::new() - .with_user(&user) - .with_organization_id(user.organization_id) - .build() - .create() - .await; - - let metric = MetricFileFixtureBuilder::new() - .with_user(&user) - .with_organization_id(user.organization_id) - .build() - .create() - .await; - - // Add assets to collection - let mut conn = get_pg_pool().get().await.unwrap(); - - // Create dashboard to collection relationship - let dashboard_to_collection = CollectionToAsset { - collection_id: collection.id, - asset_id: dashboard.id, - asset_type: AssetType::DashboardFile, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - }; - - diesel::insert_into(collections_to_assets::table) - .values(&dashboard_to_collection) - .execute(&mut conn) - .await - .unwrap(); - - // Create metric to collection relationship - let metric_to_collection = CollectionToAsset { - collection_id: collection.id, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - deleted_at: None, - created_by: user.id, - updated_by: user.id, - }; - - diesel::insert_into(collections_to_assets::table) - .values(&metric_to_collection) - .execute(&mut conn) - .await - .unwrap(); - - // Verify assets are in collection - let count = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection.id)) - .filter(collections_to_assets::deleted_at.is_null()) - .count() - .get_result::(&mut conn) - .await - .unwrap(); - - assert_eq!(count, 2, "Setup should have 2 assets in collection"); - - // Remove the assets from the collection - let response = client - .delete(&format!("/collections/{}/assets", collection.id)) - .json(&json!({ - "assets": [ - { - "id": dashboard.id, - "type": "dashboard" - }, - { - "id": metric.id, - "type": "metric" - } - ] - })) - .send() - .await; - - // Verify the response - response.assert_status_ok(); - let response_body = response.json_body().await; - - assert_eq!(response_body["message"], "Assets processed"); - assert_eq!(response_body["removed_count"], 2); - assert_eq!(response_body["failed_count"], 0); - assert!(response_body["failed_assets"].as_array().unwrap().is_empty()); - - // Verify assets are no longer in collection (soft deleted) - let count = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection.id)) - .filter(collections_to_assets::deleted_at.is_null()) - .count() - .get_result::(&mut conn) - .await - .unwrap(); - - assert_eq!(count, 0, "All assets should be removed from collection"); - - // Verify that the records were soft deleted, not hard deleted - let deleted_count = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection.id)) - .filter(collections_to_assets::deleted_at.is_not_null()) - .count() - .get_result::(&mut conn) - .await - .unwrap(); - - assert_eq!(deleted_count, 2, "Assets should be soft deleted"); -} - -#[tokio::test] -async fn test_remove_non_existent_asset_from_collection() { - // Set up test data - let user = fixtures::users::create_test_user_with_organization().await; - let client = TestClient::new_authenticated(&user).await; - - // Create a collection - let collection = CollectionFixtureBuilder::new() - .with_user(&user) - .with_organization_id(user.organization_id) - .build() - .create() - .await; - - // Try to remove a non-existent asset - let non_existent_id = Uuid::new_v4(); - let response = client - .delete(&format!("/collections/{}/assets", collection.id)) - .json(&json!({ - "assets": [ - { - "id": non_existent_id, - "type": "dashboard" - } - ] - })) - .send() - .await; - - // Verify the response - response.assert_status_ok(); - let response_body = response.json_body().await; - - assert_eq!(response_body["message"], "Assets processed"); - assert_eq!(response_body["removed_count"], 0); - assert_eq!(response_body["failed_count"], 1); - - let failed_assets = response_body["failed_assets"].as_array().unwrap(); - assert_eq!(failed_assets.len(), 1); - assert_eq!(failed_assets[0]["id"], non_existent_id.to_string()); - assert_eq!(failed_assets[0]["type"], "dashboard"); - assert!(failed_assets[0]["error"].as_str().unwrap().contains("not found")); -} - -#[tokio::test] -async fn test_collection_not_found() { - // Set up test data - let user = fixtures::users::create_test_user_with_organization().await; - let client = TestClient::new_authenticated(&user).await; - - // Try to remove assets from a non-existent collection - let non_existent_id = Uuid::new_v4(); - let response = client - .delete(&format!("/collections/{}/assets", non_existent_id)) - .json(&json!({ - "assets": [ - { - "id": Uuid::new_v4(), - "type": "dashboard" - } - ] - })) - .send() - .await; - - // Verify the response - response.assert_status_not_found(); - let error_text = response.text().await; - assert!(error_text.contains("Collection not found")); -} \ No newline at end of file diff --git a/api/tests/integration/collections/remove_metrics_from_collection_test.rs b/api/tests/integration/collections/remove_metrics_from_collection_test.rs deleted file mode 100644 index 914036350..000000000 --- a/api/tests/integration/collections/remove_metrics_from_collection_test.rs +++ /dev/null @@ -1,182 +0,0 @@ -use axum::{ - body::Body, - http::{Method, Request, StatusCode}, -}; -use database::enums::{AssetType, AssetPermissionRole, IdentityType}; -use diesel::prelude::*; -use diesel_async::RunQueryDsl; -use serde_json::json; -use tower::ServiceExt; -use uuid::Uuid; - -use crate::common::{ - fixtures::{collections::create_test_collection, metrics::create_test_metric_file, users::create_test_user}, - http::test_app::TestApp, -}; - -#[tokio::test] -async fn test_remove_metrics_from_collection() { - let test_app = TestApp::new().await; - let app = test_app.app(); - - // Create test user - let mut conn = test_app.get_db_conn().await; - let user = create_test_user(&mut conn).await; - - // Create test collection - let collection = create_test_collection(&mut conn, user.id, None, None).await.unwrap(); - - // Create test metrics - let metric1 = create_test_metric_file(&mut conn, user.id, None, None).await.unwrap(); - let metric2 = create_test_metric_file(&mut conn, user.id, None, None).await.unwrap(); - - // Add metrics to collection - let mut conn = test_app.get_db_conn().await; - diesel::insert_into(database::schema::collections_to_assets::table) - .values(&[ - ( - database::schema::collections_to_assets::collection_id.eq(collection.id), - database::schema::collections_to_assets::asset_id.eq(metric1.id), - database::schema::collections_to_assets::asset_type.eq(AssetType::MetricFile), - database::schema::collections_to_assets::created_by.eq(user.id), - database::schema::collections_to_assets::updated_by.eq(user.id), - ), - ( - database::schema::collections_to_assets::collection_id.eq(collection.id), - database::schema::collections_to_assets::asset_id.eq(metric2.id), - database::schema::collections_to_assets::asset_type.eq(AssetType::MetricFile), - database::schema::collections_to_assets::created_by.eq(user.id), - database::schema::collections_to_assets::updated_by.eq(user.id), - ), - ]) - .execute(&mut conn) - .await - .unwrap(); - - // Create permission for user - diesel::insert_into(database::schema::asset_permissions::table) - .values(( - database::schema::asset_permissions::asset_id.eq(collection.id), - database::schema::asset_permissions::asset_type.eq(AssetType::Collection), - database::schema::asset_permissions::identity_id.eq(user.id), - database::schema::asset_permissions::identity_type.eq(IdentityType::User), - database::schema::asset_permissions::role.eq(AssetPermissionRole::Owner), - database::schema::asset_permissions::created_by.eq(user.id), - database::schema::asset_permissions::updated_by.eq(user.id), - )) - .execute(&mut conn) - .await - .unwrap(); - - // Make the request to remove metrics - let request_body = json!({ - "metric_ids": [metric1.id] - }); - - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/collections/{}/metrics", collection.id)) - .header("content-type", "application/json") - .header("authorization", format!("Bearer {}", test_app.create_token_for_user(&user))) - .body(Body::from(serde_json::to_string(&request_body).unwrap())) - .unwrap(); - - let response = app.oneshot(request).await.unwrap(); - - // Check response status - assert_eq!(response.status(), StatusCode::OK); - - // Verify that metric1 is no longer in the collection - let metrics_in_collection = database::schema::collections_to_assets::table - .filter(database::schema::collections_to_assets::collection_id.eq(collection.id)) - .filter(database::schema::collections_to_assets::asset_type.eq(AssetType::MetricFile)) - .filter(database::schema::collections_to_assets::deleted_at.is_null()) - .select(database::schema::collections_to_assets::asset_id) - .get_results::(&mut conn) - .await - .unwrap(); - - // Should only have metric2 - assert_eq!(metrics_in_collection.len(), 1); - assert!(!metrics_in_collection.contains(&metric1.id)); - assert!(metrics_in_collection.contains(&metric2.id)); -} - -#[tokio::test] -async fn test_remove_metrics_from_collection_not_found() { - let test_app = TestApp::new().await; - let app = test_app.app(); - - // Create test user - let mut conn = test_app.get_db_conn().await; - let user = create_test_user(&mut conn).await; - - // Use a random collection ID that doesn't exist - let non_existent_collection_id = Uuid::new_v4(); - - // Make the request to remove metrics - let request_body = json!({ - "metric_ids": [Uuid::new_v4()] - }); - - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/collections/{}/metrics", non_existent_collection_id)) - .header("content-type", "application/json") - .header("authorization", format!("Bearer {}", test_app.create_token_for_user(&user))) - .body(Body::from(serde_json::to_string(&request_body).unwrap())) - .unwrap(); - - let response = app.oneshot(request).await.unwrap(); - - // Check response status - should be Not Found - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[tokio::test] -async fn test_remove_metrics_from_collection_unauthorized() { - let test_app = TestApp::new().await; - let app = test_app.app(); - - // Create test users - let mut conn = test_app.get_db_conn().await; - let owner = create_test_user(&mut conn).await; - let unauthorized_user = create_test_user(&mut conn).await; - - // Create test collection - let collection = create_test_collection(&mut conn, owner.id, None, None).await.unwrap(); - - // Create permission for owner only - let mut conn = test_app.get_db_conn().await; - diesel::insert_into(database::schema::asset_permissions::table) - .values(( - database::schema::asset_permissions::asset_id.eq(collection.id), - database::schema::asset_permissions::asset_type.eq(AssetType::Collection), - database::schema::asset_permissions::identity_id.eq(owner.id), - database::schema::asset_permissions::identity_type.eq(IdentityType::User), - database::schema::asset_permissions::role.eq(AssetPermissionRole::Owner), - database::schema::asset_permissions::created_by.eq(owner.id), - database::schema::asset_permissions::updated_by.eq(owner.id), - )) - .execute(&mut conn) - .await - .unwrap(); - - // Make the request to remove metrics with unauthorized user - let request_body = json!({ - "metric_ids": [Uuid::new_v4()] - }); - - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/collections/{}/metrics", collection.id)) - .header("content-type", "application/json") - .header("authorization", format!("Bearer {}", test_app.create_token_for_user(&unauthorized_user))) - .body(Body::from(serde_json::to_string(&request_body).unwrap())) - .unwrap(); - - let response = app.oneshot(request).await.unwrap(); - - // Check response status - should be Forbidden - assert_eq!(response.status(), StatusCode::FORBIDDEN); -} \ No newline at end of file diff --git a/api/tests/integration/collections/sharing/create_sharing_test.rs b/api/tests/integration/collections/sharing/create_sharing_test.rs deleted file mode 100644 index dfb76e7ac..000000000 --- a/api/tests/integration/collections/sharing/create_sharing_test.rs +++ /dev/null @@ -1,149 +0,0 @@ -use anyhow::Result; -use axum::http::StatusCode; -use database::enums::AssetPermissionRole; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - fixtures::{collections, users}, - http::client::TestClient, -}; - -/// Test successful sharing of a collection that belongs to the user -#[tokio::test] -async fn test_create_collection_sharing_success() -> Result<()> { - // Setup - let user = users::create_test_user().await?; - let collection = collections::create_test_collection_for_user(&user.id).await?; - - // Create test client with user auth - let client = TestClient::new_with_auth(&user.id); - - // Share the collection with a different user - let other_user = users::create_test_user().await?; - let response = client - .post(&format!("/collections/{}/sharing", collection.id)) - .json(&json!([ - { - "email": other_user.email, - "role": AssetPermissionRole::Viewer - } - ])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::OK); - let response_text = response.text().await?; - assert!(response_text.contains("Sharing permissions created successfully")); - - // Cleanup: Delete test data - users::delete_test_user(&user.id).await?; - users::delete_test_user(&other_user.id).await?; - collections::delete_test_collection(&collection.id).await?; - - Ok(()) -} - -/// Test attempting to share a collection that doesn't exist -#[tokio::test] -async fn test_create_collection_sharing_collection_not_found() -> Result<()> { - // Setup - let user = users::create_test_user().await?; - let non_existent_id = Uuid::new_v4(); - - // Create test client with user auth - let client = TestClient::new_with_auth(&user.id); - - // Attempt to share a non-existent collection - let response = client - .post(&format!("/collections/{}/sharing", non_existent_id)) - .json(&json!([ - { - "email": "test@example.com", - "role": AssetPermissionRole::Viewer - } - ])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::NOT_FOUND); - let response_text = response.text().await?; - assert!(response_text.contains("Collection not found")); - - // Cleanup - users::delete_test_user(&user.id).await?; - - Ok(()) -} - -/// Test attempting to share a collection without proper permissions -#[tokio::test] -async fn test_create_collection_sharing_insufficient_permissions() -> Result<()> { - // Setup - let owner = users::create_test_user().await?; - let collection = collections::create_test_collection_for_user(&owner.id).await?; - let unprivileged_user = users::create_test_user().await?; - - // Create test client with unprivileged user auth - let client = TestClient::new_with_auth(&unprivileged_user.id); - - // Attempt to share as unprivileged user - let response = client - .post(&format!("/collections/{}/sharing", collection.id)) - .json(&json!([ - { - "email": "test@example.com", - "role": AssetPermissionRole::Viewer - } - ])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::FORBIDDEN); - let response_text = response.text().await?; - assert!(response_text.contains("Insufficient permissions")); - - // Cleanup - users::delete_test_user(&owner.id).await?; - users::delete_test_user(&unprivileged_user.id).await?; - collections::delete_test_collection(&collection.id).await?; - - Ok(()) -} - -/// Test attempting to share with an invalid email -#[tokio::test] -async fn test_create_collection_sharing_invalid_email() -> Result<()> { - // Setup - let user = users::create_test_user().await?; - let collection = collections::create_test_collection_for_user(&user.id).await?; - - // Create test client with user auth - let client = TestClient::new_with_auth(&user.id); - - // Attempt to share with invalid email - let response = client - .post(&format!("/collections/{}/sharing", collection.id)) - .json(&json!([ - { - "email": "not-a-valid-email", - "role": AssetPermissionRole::Viewer - } - ])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let response_text = response.text().await?; - assert!(response_text.contains("Invalid email")); - - // Cleanup - users::delete_test_user(&user.id).await?; - collections::delete_test_collection(&collection.id).await?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/collections/sharing/delete_sharing_test.rs b/api/tests/integration/collections/sharing/delete_sharing_test.rs deleted file mode 100644 index 4553d1bbe..000000000 --- a/api/tests/integration/collections/sharing/delete_sharing_test.rs +++ /dev/null @@ -1,145 +0,0 @@ -use anyhow::Result; -use axum::http::StatusCode; -use database::enums::AssetPermissionRole; -use uuid::Uuid; -use serde_json::json; - -use crate::common::{ - fixtures::{collections, users}, - http::client::TestClient, -}; - -/// Test successfully deleting sharing permissions for a collection that belongs to the user -#[tokio::test] -async fn test_delete_collection_sharing_success() -> Result<()> { - // Setup - let user = users::create_test_user().await?; - let collection = collections::create_test_collection_for_user(&user.id).await?; - - // Create test client with user auth - let client = TestClient::new_with_auth(&user.id); - - // Create a test user to share with - let other_user = users::create_test_user().await?; - - // First share the collection with another user - let share_response = client - .post(&format!("/collections/{}/sharing", collection.id)) - .json(&json!([ - { - "email": other_user.email.clone(), - "role": AssetPermissionRole::Viewer - } - ])) - .send() - .await?; - - assert_eq!(share_response.status(), StatusCode::OK); - - // Now delete the sharing permission - let delete_response = client - .delete(&format!("/collections/{}/sharing", collection.id)) - .json(&json!([other_user.email.clone()])) - .send() - .await?; - - // Verify response - assert_eq!(delete_response.status(), StatusCode::OK); - let response_text = delete_response.text().await?; - assert!(response_text.contains("Sharing permissions deleted successfully")); - - // Cleanup: Delete test data - users::delete_test_user(&user.id).await?; - users::delete_test_user(&other_user.id).await?; - collections::delete_test_collection(&collection.id).await?; - - Ok(()) -} - -/// Test attempting to delete sharing permissions for a collection that doesn't exist -#[tokio::test] -async fn test_delete_collection_sharing_collection_not_found() -> Result<()> { - // Setup - let user = users::create_test_user().await?; - let non_existent_id = Uuid::new_v4(); - - // Create test client with user auth - let client = TestClient::new_with_auth(&user.id); - - // Attempt to delete sharing for a non-existent collection - let response = client - .delete(&format!("/collections/{}/sharing", non_existent_id)) - .json(&json!(["test@example.com"])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::NOT_FOUND); - let response_text = response.text().await?; - assert!(response_text.contains("Collection not found")); - - // Cleanup - users::delete_test_user(&user.id).await?; - - Ok(()) -} - -/// Test attempting to delete sharing permissions without proper authorization -#[tokio::test] -async fn test_delete_collection_sharing_insufficient_permissions() -> Result<()> { - // Setup - let owner = users::create_test_user().await?; - let collection = collections::create_test_collection_for_user(&owner.id).await?; - let unprivileged_user = users::create_test_user().await?; - - // Create test client with unprivileged user auth - let client = TestClient::new_with_auth(&unprivileged_user.id); - - // Attempt to delete sharing as unprivileged user - let response = client - .delete(&format!("/collections/{}/sharing", collection.id)) - .json(&json!(["test@example.com"])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::FORBIDDEN); - let response_text = response.text().await?; - assert!(response_text.contains("Insufficient permissions")); - - // Cleanup - users::delete_test_user(&owner.id).await?; - users::delete_test_user(&unprivileged_user.id).await?; - collections::delete_test_collection(&collection.id).await?; - - Ok(()) -} - -/// Test attempting to delete sharing with an invalid email format -#[tokio::test] -async fn test_delete_collection_sharing_invalid_email() -> Result<()> { - // Setup - let user = users::create_test_user().await?; - let collection = collections::create_test_collection_for_user(&user.id).await?; - - // Create test client with user auth - let client = TestClient::new_with_auth(&user.id); - - // Attempt to delete sharing with invalid email - let response = client - .delete(&format!("/collections/{}/sharing", collection.id)) - .json(&json!(["not-a-valid-email"])) - .send() - .await?; - - // Verify response - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let response_text = response.text().await?; - assert!(response_text.contains("Invalid email")); - - // Cleanup - users::delete_test_user(&user.id).await?; - collections::delete_test_collection(&collection.id).await?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/collections/sharing/list_sharing_test.rs b/api/tests/integration/collections/sharing/list_sharing_test.rs deleted file mode 100644 index 6d5a21527..000000000 --- a/api/tests/integration/collections/sharing/list_sharing_test.rs +++ /dev/null @@ -1,222 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{Collection, User}, - pool::get_pg_pool, - schema::{asset_permissions, collections, users}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use http::StatusCode; -use uuid::Uuid; - -use crate::common::{ - fixtures::users::create_test_user, - http::client::test_client, -}; - -#[tokio::test] -async fn test_list_collection_sharing() -> Result<()> { - // Create test users - let mut conn = get_pg_pool().get().await?; - - // Create owner user - let user = create_test_user(); - let user_id = user.id; - let user_email = user.email.clone(); - let org_id = Uuid::new_v4(); // For simplicity, we're just generating a UUID - - // Insert owner user - diesel::insert_into(users::table) - .values(&user) - .execute(&mut conn) - .await?; - - // Create another user - let other_user = create_test_user(); - let other_user_id = other_user.id; - - // Insert other user - diesel::insert_into(users::table) - .values(&other_user) - .execute(&mut conn) - .await?; - - // Create a test collection - let collection_id = Uuid::new_v4(); - let collection = Collection { - id: collection_id, - name: "Test Collection".to_string(), - description: Some("Test Description".to_string()), - created_by: user_id, - updated_by: user_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - organization_id: org_id, - }; - - // Insert the collection - diesel::insert_into(collections::table) - .values(&collection) - .execute(&mut conn) - .await?; - - // Create a sharing permission - create_test_permission(collection_id, other_user_id, AssetPermissionRole::CanView).await?; - - // Create a test client with the user's session - let client = test_client(&user_email).await?; - - // Make the request to list sharing permissions - let response = client - .get(&format!("/collections/{}/sharing", collection_id)) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::OK); - - // Parse the response - let response_json: serde_json::Value = response.json().await?; - let permissions = response_json.get("data") - .and_then(|d| d.get("permissions")) - .and_then(|p| p.as_array()) - .unwrap_or(&vec![]); - - // Assert there's at least one permission entry - assert!(!permissions.is_empty()); - - // Assert the permission entry has the expected structure - let permission = &permissions[0]; - assert!(permission.get("user_id").is_some()); - assert!(permission.get("email").is_some()); - assert!(permission.get("role").is_some()); - - Ok(()) -} - -#[tokio::test] -async fn test_list_collection_sharing_not_found() -> Result<()> { - // Create owner user - let mut conn = get_pg_pool().get().await?; - - let user = create_test_user(); - let user_email = user.email.clone(); - - // Insert owner user - diesel::insert_into(users::table) - .values(&user) - .execute(&mut conn) - .await?; - - // Create a test client with the user's session - let client = test_client(&user_email).await?; - - // Make the request with a random non-existent collection ID - let response = client - .get(&format!("/collections/{}/sharing", Uuid::new_v4())) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - Ok(()) -} - -#[tokio::test] -async fn test_list_collection_sharing_forbidden() -> Result<()> { - // Create test users - let mut conn = get_pg_pool().get().await?; - - // Create owner user - let owner = create_test_user(); - let owner_id = owner.id; - let org_id = Uuid::new_v4(); // For simplicity, we're just generating a UUID - - // Insert owner user - diesel::insert_into(users::table) - .values(&owner) - .execute(&mut conn) - .await?; - - // Create another user (that doesn't have access) - let other_user = create_test_user(); - let other_user_email = other_user.email.clone(); - - // Insert other user - diesel::insert_into(users::table) - .values(&other_user) - .execute(&mut conn) - .await?; - - // Create a test collection - let collection_id = Uuid::new_v4(); - let collection = Collection { - id: collection_id, - name: "Test Collection".to_string(), - description: Some("Test Description".to_string()), - created_by: owner_id, - updated_by: owner_id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - organization_id: org_id, - }; - - // Insert the collection - diesel::insert_into(collections::table) - .values(&collection) - .execute(&mut conn) - .await?; - - // Note: We don't create a permission for other_user, so they should be forbidden - - // Create a test client with the unauthorized user's session - let client = test_client(&other_user_email).await?; - - // Make the request with the collection ID - let response = client - .get(&format!("/collections/{}/sharing", collection_id)) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::FORBIDDEN); - - Ok(()) -} - - -// Helper function to create a test permission -async fn create_test_permission(asset_id: Uuid, user_id: Uuid, role: AssetPermissionRole) -> Result<()> { - let mut conn = get_pg_pool().get().await?; - - // Ensure the user_id exists - let user = users::table - .filter(users::id.eq(user_id)) - .first::(&mut conn) - .await?; - - // Create the permission - let permission_id = Uuid::new_v4(); - diesel::insert_into(asset_permissions::table) - .values(( - asset_permissions::id.eq(permission_id), - asset_permissions::asset_id.eq(asset_id), - asset_permissions::asset_type.eq(AssetType::Collection), - asset_permissions::identity_id.eq(user_id), - asset_permissions::identity_type.eq(IdentityType::User), - asset_permissions::role.eq(role), - asset_permissions::created_at.eq(Utc::now()), - asset_permissions::updated_at.eq(Utc::now()), - asset_permissions::created_by.eq(user_id), - asset_permissions::updated_by.eq(user_id), - )) - .execute(&mut conn) - .await?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/collections/sharing/mod.rs b/api/tests/integration/collections/sharing/mod.rs deleted file mode 100644 index 439ebe5e8..000000000 --- a/api/tests/integration/collections/sharing/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod list_sharing_test; -pub mod create_sharing_test; -pub mod delete_sharing_test; -pub mod update_sharing_test; \ No newline at end of file diff --git a/api/tests/integration/collections/sharing/update_sharing_test.rs b/api/tests/integration/collections/sharing/update_sharing_test.rs deleted file mode 100644 index 5e4f859e6..000000000 --- a/api/tests/integration/collections/sharing/update_sharing_test.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::common::{ - assertions::response::assert_status, - fixtures::{collections::create_test_collection, users::create_test_user}, - http::client::TestClient, -}; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::Collection, - pool::get_pg_pool, -}; -use diesel_async::RunQueryDsl; -use handlers::collections::sharing::ShareRecipient; -use sharing::check_asset_permission::check_access; -use sharing::create_asset_permission::create_share_by_email; -use uuid::Uuid; - -#[tokio::test] -async fn update_sharing_returns_success_for_authorized_user() { - // Setup test data - let user = create_test_user().await; - let user_id = user.id; - let collection = create_test_collection(&user_id).await; - let collection_id = collection.id; - - // Setup test client - let client = TestClient::new().await; - client.login_as(user).await; - - // Create a test user to share with - let test_user = create_test_user().await; - let test_email = test_user.email.clone(); - - // First share with test user as ReadOnly - create_share_by_email( - &test_email, - collection_id, - AssetType::Collection, - AssetPermissionRole::ReadOnly, - user_id, - ) - .await - .unwrap(); - - // Verify initial permission - let initial_role = check_access( - collection_id, - AssetType::Collection, - test_user.id, - IdentityType::User, - ) - .await - .unwrap(); - assert_eq!(initial_role, Some(AssetPermissionRole::ReadOnly)); - - // Create update request to change to FullAccess - let request = vec![ShareRecipient { - email: test_email.clone(), - role: AssetPermissionRole::FullAccess, - }]; - - // Send request - let response = client - .put(&format!("/collections/{}/sharing", collection_id)) - .json(&request) - .send() - .await; - - // Verify response - assert_status(&response, 200); - - // Verify permission was updated - let updated_role = check_access( - collection_id, - AssetType::Collection, - test_user.id, - IdentityType::User, - ) - .await - .unwrap(); - assert_eq!(updated_role, Some(AssetPermissionRole::FullAccess)); -} - -#[tokio::test] -async fn update_sharing_returns_forbidden_for_unauthorized_user() { - // Setup test data - owner and another user - let owner = create_test_user().await; - let user = create_test_user().await; - let collection = create_test_collection(&owner.id).await; - let collection_id = collection.id; - - // Share collection with user as ReadOnly - create_share_by_email( - &user.email, - collection_id, - AssetType::Collection, - AssetPermissionRole::ReadOnly, - owner.id, - ) - .await - .unwrap(); - - // Setup test client - login as non-owner with only ReadOnly access - let client = TestClient::new().await; - client.login_as(user).await; - - // Try to update permissions - let request = vec![ShareRecipient { - email: "test@example.com".to_string(), - role: AssetPermissionRole::ReadOnly, - }]; - - // Send request - let response = client - .put(&format!("/collections/{}/sharing", collection_id)) - .json(&request) - .send() - .await; - - // Verify forbidden response - assert_status(&response, 403); -} - -#[tokio::test] -async fn update_sharing_returns_not_found_for_nonexistent_collection() { - // Setup test data - let user = create_test_user().await; - let non_existent_id = Uuid::new_v4(); - - // Setup test client - let client = TestClient::new().await; - client.login_as(user).await; - - // Create request - let request = vec![ShareRecipient { - email: "test@example.com".to_string(), - role: AssetPermissionRole::ReadOnly, - }]; - - // Send request to non-existent collection - let response = client - .put(&format!("/collections/{}/sharing", non_existent_id)) - .json(&request) - .send() - .await; - - // Verify not found response - assert_status(&response, 404); -} - -#[tokio::test] -async fn update_sharing_returns_bad_request_for_invalid_email() { - // Setup test data - let user = create_test_user().await; - let collection = create_test_collection(&user.id).await; - let collection_id = collection.id; - - // Setup test client - let client = TestClient::new().await; - client.login_as(user).await; - - // Create request with invalid email - let request = vec![ShareRecipient { - email: "invalid-email".to_string(), // No @ symbol - role: AssetPermissionRole::ReadOnly, - }]; - - // Send request - let response = client - .put(&format!("/collections/{}/sharing", collection_id)) - .json(&request) - .send() - .await; - - // Verify bad request response - assert_status(&response, 400); -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/create_dashboard_test.rs b/api/tests/integration/dashboards/create_dashboard_test.rs deleted file mode 100644 index 30258bdb2..000000000 --- a/api/tests/integration/dashboards/create_dashboard_test.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anyhow::Result; -use axum::http::StatusCode; -use uuid::Uuid; - -#[tokio::test] -async fn test_create_dashboard_endpoint() -> Result<()> { - // This is a stub test for now - // In a real implementation, we would: - // 1. Setup a test app and database - // 2. Create a test user - // 3. Make a request to the endpoint - // 4. Verify the response - - // Mock the success case for now - let response_status = StatusCode::OK; - assert_eq!(response_status, StatusCode::OK); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/delete_dashboard_test.rs b/api/tests/integration/dashboards/delete_dashboard_test.rs deleted file mode 100644 index c2e79d8de..000000000 --- a/api/tests/integration/dashboards/delete_dashboard_test.rs +++ /dev/null @@ -1,291 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use database::{ - models::DashboardFile, - pool::get_pg_pool, - schema::dashboard_files, - types::VersionHistory, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use handlers::dashboards::{ - delete_dashboards_handler, - delete_dashboard_handler, - DeleteDashboardsRequest -}; -use serde_json::json; -use tokio; -use uuid::Uuid; - -use crate::common::{ - db::TestDb, - env::setup_test_env, - fixtures, -}; - -#[tokio::test] -async fn test_delete_dashboard_handler() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test dashboard - let test_dashboard = fixtures::dashboards::create_test_dashboard_file(&user_id, &org_id, Some("Test Dashboard For Deletion".to_string())); - let dashboard_id = test_dashboard.id; - - // Insert test dashboard into database - diesel::insert_into(dashboard_files::table) - .values(&test_dashboard) - .execute(&mut conn) - .await?; - - // Call the handler being tested - delete_dashboard_handler(dashboard_id, &user_id).await?; - - // Fetch the deleted dashboard from the database - let db_dashboard = dashboard_files::table - .filter(dashboard_files::id.eq(dashboard_id)) - .first::(&mut conn) - .await?; - - // Verify it has been soft deleted (deleted_at is set) - assert!(db_dashboard.deleted_at.is_some()); - - // Trying to delete it again should return an error - let result = delete_dashboard_handler(dashboard_id, &user_id).await; - assert!(result.is_err()); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_dashboard_handler_not_found() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let _test_db = TestDb::new().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Use a random UUID that doesn't exist - let nonexistent_dashboard_id = Uuid::new_v4(); - - // Call the handler being tested - should fail - let result = delete_dashboard_handler(nonexistent_dashboard_id, &user_id).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("not found")); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_already_deleted_dashboard() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test dashboard with deleted_at already set - let mut test_dashboard = fixtures::dashboards::create_test_dashboard_file(&user_id, &org_id, Some("Already Deleted Dashboard".to_string())); - test_dashboard.deleted_at = Some(Utc::now()); - let dashboard_id = test_dashboard.id; - - // Insert test dashboard into database - diesel::insert_into(dashboard_files::table) - .values(&test_dashboard) - .execute(&mut conn) - .await?; - - // Call the handler being tested - should fail because it's already deleted - let result = delete_dashboard_handler(dashboard_id, &user_id).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("not found") || error.contains("already deleted")); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_dashboards_handler_multiple_dashboards() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create multiple test dashboards - let dashboard1 = fixtures::dashboards::create_test_dashboard_file(&user_id, &org_id, Some("Test Dashboard 1".to_string())); - let dashboard2 = fixtures::dashboards::create_test_dashboard_file(&user_id, &org_id, Some("Test Dashboard 2".to_string())); - let dashboard3 = fixtures::dashboards::create_test_dashboard_file(&user_id, &org_id, Some("Test Dashboard 3".to_string())); - - let dashboard_ids = vec![dashboard1.id, dashboard2.id, dashboard3.id]; - - // Insert test dashboards into database - for dashboard in [&dashboard1, &dashboard2, &dashboard3] { - diesel::insert_into(dashboard_files::table) - .values(dashboard) - .execute(&mut conn) - .await?; - } - - // Create the request to delete all dashboards - let request = DeleteDashboardsRequest { - ids: dashboard_ids.clone(), - }; - - // Call the handler being tested - let response = delete_dashboards_handler(request, &user_id).await?; - - // Verify all dashboards were deleted successfully - assert_eq!(response.deleted_count, 3); - assert!(response.failed_ids.is_empty()); - assert!(response.success); - - // Verify each dashboard has been soft deleted in the database - for dashboard_id in dashboard_ids { - let db_dashboard = dashboard_files::table - .filter(dashboard_files::id.eq(dashboard_id)) - .first::(&mut conn) - .await?; - - assert!(db_dashboard.deleted_at.is_some()); - } - - Ok(()) -} - -#[tokio::test] -async fn test_delete_dashboards_handler_mixed_results() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create one valid dashboard - let dashboard = fixtures::dashboards::create_test_dashboard_file(&user_id, &org_id, Some("Test Dashboard".to_string())); - let valid_id = dashboard.id; - - // Generate a non-existent ID - let nonexistent_id = Uuid::new_v4(); - - // Insert the valid dashboard into database - diesel::insert_into(dashboard_files::table) - .values(&dashboard) - .execute(&mut conn) - .await?; - - // Create the request with one valid and one invalid ID - let request = DeleteDashboardsRequest { - ids: vec![valid_id, nonexistent_id], - }; - - // Call the handler being tested - let response = delete_dashboards_handler(request, &user_id).await?; - - // Verify partial success - assert_eq!(response.deleted_count, 1); - assert_eq!(response.failed_ids.len(), 1); - assert_eq!(response.failed_ids[0], nonexistent_id); - assert!(response.success); // Should still be true as at least one dashboard was deleted - - // Verify the valid dashboard has been soft deleted - let db_dashboard = dashboard_files::table - .filter(dashboard_files::id.eq(valid_id)) - .first::(&mut conn) - .await?; - - assert!(db_dashboard.deleted_at.is_some()); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_dashboards_handler_all_invalid() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let _test_db = TestDb::new().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Generate non-existent IDs - let nonexistent_id1 = Uuid::new_v4(); - let nonexistent_id2 = Uuid::new_v4(); - - // Create the request with all invalid IDs - let request = DeleteDashboardsRequest { - ids: vec![nonexistent_id1, nonexistent_id2], - }; - - // Call the handler being tested - let response = delete_dashboards_handler(request, &user_id).await?; - - // Verify complete failure - assert_eq!(response.deleted_count, 0); - assert_eq!(response.failed_ids.len(), 2); - assert!(response.failed_ids.contains(&nonexistent_id1)); - assert!(response.failed_ids.contains(&nonexistent_id2)); - assert!(!response.success); // Should be false as no dashboards were deleted - - Ok(()) -} - -#[tokio::test] -async fn test_delete_dashboards_handler_empty_ids() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let _test_db = TestDb::new().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Create the request with no IDs - let request = DeleteDashboardsRequest { - ids: vec![], - }; - - // Call the handler being tested - let response = delete_dashboards_handler(request, &user_id).await?; - - // Verify empty result - assert_eq!(response.deleted_count, 0); - assert_eq!(response.failed_ids.len(), 0); - assert!(!response.success); // Should be false as no dashboards were deleted - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/get_dashboard_test.rs b/api/tests/integration/dashboards/get_dashboard_test.rs deleted file mode 100644 index 1b6c03efd..000000000 --- a/api/tests/integration/dashboards/get_dashboard_test.rs +++ /dev/null @@ -1,193 +0,0 @@ -use uuid::Uuid; -use crate::common::{ - env::{create_env, TestEnv}, - http::client::TestClient, - assertions::response::assert_api_ok, -}; -use chrono::Utc; -use database::enums::{AssetPermissionRole, AssetType, AssetTypeEnum, IdentityTypeEnum}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -#[tokio::test] -async fn test_get_dashboard_with_sharing_info() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and dashboard - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let dashboard_id = create_test_dashboard(&env, user_id).await; - - // Add sharing permissions - add_test_permissions(&env, dashboard_id, user_id).await; - - // Add public sharing - enable_public_sharing(&env, dashboard_id, user_id).await; - - // Create a collection and add the dashboard to it - let collection_id = create_collection_and_add_dashboard(&env, dashboard_id, user_id).await; - - // Test GET request - let response = client - .get(&format!("/api/v1/dashboards/{}", dashboard_id)) - .header("X-User-Id", user_id.to_string()) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Check fields - assert_eq!(data["dashboard"]["id"], dashboard_id.to_string()); - - // Check sharing fields - assert_eq!(data["publicly_accessible"], true); - assert!(data["public_expiry_date"].is_string()); - assert_eq!(data["public_enabled_by"], "test@example.com"); - assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1); - - let permission = &data["individual_permissions"][0]; - assert_eq!(permission["email"], "test2@example.com"); - assert_eq!(permission["role"], "viewer"); - assert_eq!(permission["name"], "Test User 2"); - - // Check collections - assert_eq!(data["collections"].as_array().unwrap().len(), 1); - let collection = &data["collections"][0]; - assert_eq!(collection["id"], collection_id.to_string()); - assert_eq!(collection["name"], "Test Collection"); -} - -// Helper functions to set up the test data -async fn create_test_dashboard(env: &TestEnv, user_id: Uuid) -> Uuid { - let mut conn = env.db_pool.get().await.unwrap(); - - // Insert test user - diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user_id) - .bind::("test@example.com") - .bind::("Test User") - .execute(&mut conn) - .await - .unwrap(); - - // Insert another test user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user2_id) - .bind::("test2@example.com") - .bind::("Test User 2") - .execute(&mut conn) - .await - .unwrap(); - - // Insert test dashboard - let dashboard_id = Uuid::parse_str("00000000-0000-0000-0000-000000000020").unwrap(); - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - - // Insert test organization if needed - diesel::sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") - .bind::(org_id) - .bind::("Test Organization") - .execute(&mut conn) - .await - .unwrap(); - - // Insert dashboard - diesel::sql_query(r#" - INSERT INTO dashboard_files (id, name, file_name, content, organization_id, created_by, version_history) - VALUES ($1, 'Test Dashboard', 'test_dashboard.json', '{"rows": [{"items": [{"id": "00000000-0000-0000-0000-000000000010"}]}], "name": "Test Dashboard", "description": "Test description"}', $2, $3, '{}'::jsonb) - ON CONFLICT DO NOTHING - "#) - .bind::(dashboard_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - dashboard_id -} - -async fn add_test_permissions(env: &TestEnv, dashboard_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Get the second user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - - // Add permission for user2 as viewer - diesel::sql_query(r#" - INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by) - VALUES ($1, $2, $3, $4, $5, $6, $6) - ON CONFLICT DO NOTHING - "#) - .bind::(user2_id) - .bind::(IdentityTypeEnum::User.to_string()) - .bind::(dashboard_id) - .bind::(AssetTypeEnum::DashboardFile.to_string()) - .bind::(AssetPermissionRole::CanView.to_string()) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); -} - -async fn enable_public_sharing(env: &TestEnv, dashboard_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Set public access - let expiry_date = Utc::now() + chrono::Duration::days(7); - - diesel::sql_query(r#" - UPDATE dashboard_files - SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2 - WHERE id = $3 - "#) - .bind::(user_id) - .bind::(expiry_date) - .bind::(dashboard_id) - .execute(&mut conn) - .await - .unwrap(); -} - -async fn create_collection_and_add_dashboard(env: &TestEnv, dashboard_id: Uuid, user_id: Uuid) -> Uuid { - let mut conn = env.db_pool.get().await.unwrap(); - - // Get organization ID - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - - // Create a collection - let collection_id = Uuid::parse_str("00000000-0000-0000-0000-000000000030").unwrap(); - - diesel::sql_query(r#" - INSERT INTO collections (id, name, description, created_by, updated_by, organization_id) - VALUES ($1, $2, $3, $4, $4, $5) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::("Test Collection") - .bind::("Test Collection Description") - .bind::(user_id) - .bind::(org_id) - .execute(&mut conn) - .await - .unwrap(); - - // Add the dashboard to the collection - diesel::sql_query(r#" - INSERT INTO collections_to_assets (collection_id, asset_id, asset_type, created_by, updated_by) - VALUES ($1, $2, $3, $4, $4) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::(dashboard_id) - .bind::(AssetType::DashboardFile.to_string()) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - collection_id -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/metric_dashboard_association_test.rs b/api/tests/integration/dashboards/metric_dashboard_association_test.rs deleted file mode 100644 index 4529f17ee..000000000 --- a/api/tests/integration/dashboards/metric_dashboard_association_test.rs +++ /dev/null @@ -1,251 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::Utc; -use database::{ - models::{DashboardFile, MetricFile, MetricFileToDashboardFile}, - pool::get_pg_pool, - schema::{dashboard_files, metric_files, metric_files_to_dashboard_files}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - fixtures::{dashboards::create_test_dashboard_file, metrics::create_test_metric_file}, - helpers::setup_test_db, -}; - -#[tokio::test] -async fn test_metric_dashboard_association() -> Result<()> { - // Setup test database and user - let (test_db, user) = setup_test_db().await?; - let mut conn = test_db.pool.get().await?; - - // Create a test metric file - let metric_file = create_test_metric_file(&mut conn, &user, "Test Metric").await?; - - // Create a test dashboard file with the metric referenced - let dashboard_content = json!({ - "name": "Test Dashboard", - "description": "Dashboard for testing metric associations", - "rows": [{ - "items": [{ - "id": metric_file.id - }], - "rowHeight": 400, - "columnSizes": [12], - "id": 1 - }] - }); - - let dashboard_file = create_test_dashboard_file( - &mut conn, - &user, - "Test Dashboard", - serde_json::to_value(dashboard_content)?, - ) - .await?; - - // Verify the association was created - let associations = metric_files_to_dashboard_files::table - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_file.id)) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_file.id)) - .filter(metric_files_to_dashboard_files::deleted_at.is_null()) - .first::(&mut conn) - .await; - - // Assert the association exists - assert!(associations.is_ok(), "Metric dashboard association not found"); - - // Create a second test metric file for the update test - let metric_file2 = create_test_metric_file(&mut conn, &user, "Test Metric 2").await?; - - // Update the dashboard file to use the second metric - let updated_dashboard_content = json!({ - "name": "Test Dashboard Updated", - "description": "Dashboard for testing metric associations - updated", - "rows": [{ - "items": [{ - "id": metric_file2.id - }], - "rowHeight": 400, - "columnSizes": [12], - "id": 1 - }] - }); - - // Update the dashboard file with new content - diesel::update(dashboard_files::table) - .filter(dashboard_files::id.eq(dashboard_file.id)) - .set(( - dashboard_files::name.eq("Test Dashboard Updated"), - dashboard_files::content.eq(serde_json::to_value(updated_dashboard_content)?), - dashboard_files::updated_at.eq(chrono::Utc::now()), - )) - .execute(&mut conn) - .await?; - - // Now manually call the function to update the metric associations - use database::types::dashboard_yml::DashboardYml; - use database::enums::{IdentityType, AssetType, AssetPermissionRole}; - use diesel_async::RunQueryDsl; - use chrono::Utc; - - // Extract metric IDs from dashboard content (similar to the handler) - let updated_dashboard = diesel::dsl::select(dashboard_files::content) - .from(dashboard_files::table) - .filter(dashboard_files::id.eq(dashboard_file.id)) - .first::(&mut conn) - .await?; - - let dashboard_yml: DashboardYml = serde_json::from_value(updated_dashboard)?; - - // Extract metric IDs function - fn extract_metric_ids_from_dashboard(dashboard: &DashboardYml) -> Vec { - let mut metric_ids = Vec::new(); - - // Iterate through all rows and collect unique metric IDs - for row in &dashboard.rows { - for item in &row.items { - metric_ids.push(item.id); - } - } - - // Return unique metric IDs - metric_ids - } - - // Update metric associations function - async fn update_dashboard_metric_associations( - dashboard_id: Uuid, - metric_ids: Vec, - user_id: &Uuid, - conn: &mut diesel_async::AsyncPgConnection, - ) -> Result<()> { - // First, mark all existing associations as deleted - diesel::update( - metric_files_to_dashboard_files::table - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_id)) - .filter(metric_files_to_dashboard_files::deleted_at.is_null()) - ) - .set(metric_files_to_dashboard_files::deleted_at.eq(Utc::now())) - .execute(conn) - .await?; - - // For each metric ID, either create a new association or restore a previously deleted one - for metric_id in metric_ids { - // Check if the metric exists - let metric_exists = diesel::dsl::select( - diesel::dsl::exists( - metric_files::table - .filter(metric_files::id.eq(metric_id)) - .filter(metric_files::deleted_at.is_null()) - ) - ) - .get_result::(conn) - .await; - - // Skip if metric doesn't exist - if let Ok(exists) = metric_exists { - if !exists { - continue; - } - } else { - continue; - } - - // Check if there's a deleted association that can be restored - let existing = metric_files_to_dashboard_files::table - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_id)) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) - .first::(conn) - .await; - - match existing { - Ok(assoc) if assoc.deleted_at.is_some() => { - // Restore the deleted association - diesel::update( - metric_files_to_dashboard_files::table - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_id)) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) - ) - .set(( - metric_files_to_dashboard_files::deleted_at.eq::>>(None), - metric_files_to_dashboard_files::updated_at.eq(Utc::now()), - )) - .execute(conn) - .await?; - }, - Ok(_) => { - // Association already exists and is not deleted, do nothing - }, - Err(diesel::result::Error::NotFound) => { - // Create a new association - diesel::insert_into(metric_files_to_dashboard_files::table) - .values(( - metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_id), - metric_files_to_dashboard_files::metric_file_id.eq(metric_id), - metric_files_to_dashboard_files::created_at.eq(Utc::now()), - metric_files_to_dashboard_files::updated_at.eq(Utc::now()), - metric_files_to_dashboard_files::created_by.eq(user_id), - )) - .execute(conn) - .await?; - }, - Err(e) => return Err(anyhow::anyhow!("Database error: {}", e)), - } - } - - Ok(()) - } - - // Call the update function with the extracted metrics - update_dashboard_metric_associations( - dashboard_file.id, - extract_metric_ids_from_dashboard(&dashboard_yml), - &user.id, - &mut conn - ).await?; - - // Verify the first association is now marked as deleted - let old_association = metric_files_to_dashboard_files::table - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_file.id)) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_file.id)) - .first::(&mut conn) - .await?; - - assert!(old_association.deleted_at.is_some(), "Original metric association should be marked as deleted"); - - // Verify the new association exists - let new_association = metric_files_to_dashboard_files::table - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_file.id)) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_file2.id)) - .filter(metric_files_to_dashboard_files::deleted_at.is_null()) - .first::(&mut conn) - .await; - - assert!(new_association.is_ok(), "New metric dashboard association not found"); - - // Test cleanup - ensure we clean up our test data - diesel::delete(metric_files_to_dashboard_files::table) - .filter(metric_files_to_dashboard_files::dashboard_file_id.eq(dashboard_file.id)) - .execute(&mut conn) - .await?; - - diesel::delete(dashboard_files::table) - .filter(dashboard_files::id.eq(dashboard_file.id)) - .execute(&mut conn) - .await?; - - diesel::delete(metric_files::table) - .filter(metric_files::id.eq(metric_file.id)) - .execute(&mut conn) - .await?; - - diesel::delete(metric_files::table) - .filter(metric_files::id.eq(metric_file2.id)) - .execute(&mut conn) - .await?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/mod.rs b/api/tests/integration/dashboards/mod.rs deleted file mode 100644 index a92e747db..000000000 --- a/api/tests/integration/dashboards/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod create_dashboard_test; -pub mod delete_dashboard_test; -pub mod get_dashboard_test; -pub mod metric_dashboard_association_test; -pub mod sharing; -pub mod update_dashboard_test; diff --git a/api/tests/integration/dashboards/remove_dashboard_from_collections_test.rs b/api/tests/integration/dashboards/remove_dashboard_from_collections_test.rs deleted file mode 100644 index 8492156a1..000000000 --- a/api/tests/integration/dashboards/remove_dashboard_from_collections_test.rs +++ /dev/null @@ -1,130 +0,0 @@ -use anyhow::Result; -use axum::{ - body::Body, - extract::{Extension, Path}, - http::{Request, StatusCode}, - routing::delete, - Router, -}; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{CollectionToAsset, DashboardFile}, - pool::get_pg_pool, - schema::{collections, collections_to_assets, dashboard_files}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use handlers::dashboards::remove_dashboard_from_collections_handler; -use middleware::AuthenticatedUser; -use serde_json::{json, Value}; -use sharing::check_asset_permission::has_permission; -use std::str::FromStr; -use tokio::test; -use tower::ServiceExt; -use uuid::Uuid; - -use crate::common::{db::TestDb, helpers::create_test_user}; - -#[test] -async fn test_remove_dashboard_from_collections() -> Result<()> { - let test_db = TestDb::new().await?; - - // Create test user - let user = create_test_user(&test_db.pool).await?; - - // Create test dashboard - let dashboard_id = Uuid::new_v4(); - let dashboard = DashboardFile { - id: dashboard_id, - name: "Test Dashboard".to_string(), - created_by: user.id, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - deleted_at: None, - content: serde_json::from_str(r#"{"name": "Test Dashboard", "rows": []}"#)?, - filter: None, - version_history: None, - organization_id: user.organization_id, - status: None, - }; - - // Create test collection - let collection_id = Uuid::new_v4(); - diesel::insert_into(collections::table) - .values(( - collections::id.eq(collection_id), - collections::name.eq("Test Collection"), - collections::created_by.eq(user.id), - collections::created_at.eq(chrono::Utc::now()), - collections::updated_at.eq(chrono::Utc::now()), - collections::organization_id.eq(user.organization_id), - )) - .execute(&mut test_db.pool.get().await?) - .await?; - - // Add dashboard to collection - let collection_to_asset = CollectionToAsset { - id: Uuid::new_v4(), - collection_id, - asset_id: dashboard_id, - asset_type: AssetType::DashboardFile, - created_by: user.id, - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - updated_by: user.id, - deleted_at: None, - }; - - diesel::insert_into(collections_to_assets::table) - .values(&collection_to_asset) - .execute(&mut test_db.pool.get().await?) - .await?; - - // Test removing the dashboard from the collection - let result = remove_dashboard_from_collections_handler( - &dashboard_id, - vec![collection_id], - &user.id, - ) - .await?; - - assert_eq!(result.removed_count, 1); - assert_eq!(result.failed_count, 0); - assert!(result.failed_ids.is_empty()); - - // Verify the dashboard was removed from the collection - let removed = collections_to_assets::table - .filter(collections_to_assets::collection_id.eq(collection_id)) - .filter(collections_to_assets::asset_id.eq(dashboard_id)) - .filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile)) - .filter(collections_to_assets::deleted_at.is_not_null()) - .count() - .get_result::(&mut test_db.pool.get().await?) - .await?; - - assert_eq!(removed, 1); - - // Test the REST endpoint - let app = Router::new().route( - "/:id/collections", - delete(crate::src::routes::rest::routes::dashboards::remove_dashboard_from_collections::remove_dashboard_from_collections), - ); - - let request_body = json!({ - "collection_ids": [collection_id] - }); - - let request = Request::builder() - .uri(format!("/{}/collections", dashboard_id)) - .method("DELETE") - .header("Content-Type", "application/json") - .body(Body::from(serde_json::to_string(&request_body)?))?; - - let response = app - .oneshot(request) - .await?; - - assert_eq!(response.status(), StatusCode::OK); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/sharing/create_sharing_test.rs b/api/tests/integration/dashboards/sharing/create_sharing_test.rs deleted file mode 100644 index dd5b38b59..000000000 --- a/api/tests/integration/dashboards/sharing/create_sharing_test.rs +++ /dev/null @@ -1,68 +0,0 @@ -use database::enums::AssetPermissionRole; -use serde_json::json; -use tests::common::{ - db::TestDb, - fixtures::{dashboards::create_test_dashboard, users::create_test_user}, - http::client::TestClient, -}; -use uuid::Uuid; - -#[tokio::test] -async fn test_create_dashboard_sharing() { - // This test is a simplified version as we'd need a full test database setup for integration tests - // In a real test, we would: - // 1. Set up a test database - // 2. Create test users - // 3. Create a test dashboard - // 4. Set up initial permissions - // 5. Make the API request - // 6. Verify the response and database state - - // For now, we just assert that the test runs - // This would be replaced with real test logic - assert!(true); -} - -// Example test structure for reference: -// -// async fn test_create_dashboard_sharing_success() { -// // Set up test data -// let test_db = TestDb::new().await.unwrap(); -// let test_user = create_test_user().await.unwrap(); -// let test_dashboard = create_test_dashboard(&test_user.id).await.unwrap(); -// -// // Create test client -// let client = TestClient::new().with_auth(&test_user); -// -// // Make API request -// let response = client -// .post(&format!("/dashboards/{}/sharing", test_dashboard.id)) -// .json(&vec![ -// json!({ -// "email": "recipient@example.com", -// "role": "CanView" -// }) -// ]) -// .send() -// .await; -// -// // Verify response -// assert_eq!(response.status(), 200); -// -// // Verify database state -// let permissions = test_db.get_permissions_for_dashboard(test_dashboard.id).await.unwrap(); -// assert_eq!(permissions.len(), 1); -// assert_eq!(permissions[0].role, AssetPermissionRole::CanView); -// } -// -// async fn test_create_dashboard_sharing_unauthorized() { -// // Test the case where user doesn't have permission to share -// } -// -// async fn test_create_dashboard_sharing_not_found() { -// // Test the case where dashboard doesn't exist -// } -// -// async fn test_create_dashboard_sharing_invalid_email() { -// // Test the case where email is invalid -// } \ No newline at end of file diff --git a/api/tests/integration/dashboards/sharing/delete_sharing_test.rs b/api/tests/integration/dashboards/sharing/delete_sharing_test.rs deleted file mode 100644 index de2c5c582..000000000 --- a/api/tests/integration/dashboards/sharing/delete_sharing_test.rs +++ /dev/null @@ -1,155 +0,0 @@ -use axum::{ - body::Body, - http::{header, Method, Request, StatusCode}, -}; -use database::enums::{AssetPermissionRole, AssetType, IdentityType}; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - fixtures::{dashboards::create_test_dashboard, users::create_test_user}, - http::client::TestClient, -}; - -#[tokio::test] -async fn test_delete_dashboard_sharing() { - // Setup test data - let client = TestClient::new().await; - let test_user = create_test_user(); - let dashboard = create_test_dashboard(); - - // Create a user to share with - let shared_user = create_test_user(); - - // Setup sharing permission manually - client.setup_asset_permission( - dashboard.id, - AssetType::DashboardFile, - shared_user.id, - IdentityType::User, - AssetPermissionRole::CanView, - test_user.id, - ).await; - - // Create the delete request - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/dashboards/{}/sharing", dashboard.id)) - .header(header::CONTENT_TYPE, "application/json") - .header("X-User-ID", test_user.id.to_string()) - .body(Body::from( - serde_json::to_string(&vec![shared_user.email.clone()]).unwrap(), - )) - .unwrap(); - - // Send the request - let response = client.app.oneshot(request).await.unwrap(); - - // Verify response - assert_eq!(response.status(), StatusCode::OK); - - // Verify that the sharing permission is actually deleted - let permissions = client - .get_asset_permissions(dashboard.id, AssetType::DashboardFile) - .await; - - // Should only contain the owner's permission - assert_eq!(permissions.len(), 1); - assert_eq!(permissions[0].identity_id, test_user.id); -} - -#[tokio::test] -async fn test_delete_dashboard_sharing_not_found() { - // Setup - let client = TestClient::new().await; - let user = create_test_user(); - let non_existent_id = Uuid::new_v4(); - - // Create request with non-existent dashboard - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/dashboards/{}/sharing", non_existent_id)) - .header(header::CONTENT_TYPE, "application/json") - .header("X-User-ID", user.id.to_string()) - .body(Body::from( - serde_json::to_string(&vec!["test@example.com".to_string()]).unwrap(), - )) - .unwrap(); - - // Send request - let response = client.app.oneshot(request).await.unwrap(); - - // Verify 404 response - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[tokio::test] -async fn test_delete_dashboard_sharing_insufficient_permissions() { - // Setup - let client = TestClient::new().await; - let owner = create_test_user(); - let user_without_permission = create_test_user(); - let dashboard = create_test_dashboard(); - - // Set up the dashboard with owner - client.setup_asset_permission( - dashboard.id, - AssetType::DashboardFile, - owner.id, - IdentityType::User, - AssetPermissionRole::Owner, - owner.id, - ).await; - - // Create request from user without permission - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/dashboards/{}/sharing", dashboard.id)) - .header(header::CONTENT_TYPE, "application/json") - .header("X-User-ID", user_without_permission.id.to_string()) - .body(Body::from( - serde_json::to_string(&vec!["test@example.com".to_string()]).unwrap(), - )) - .unwrap(); - - // Send request - let response = client.app.oneshot(request).await.unwrap(); - - // Verify 403 Forbidden response - assert_eq!(response.status(), StatusCode::FORBIDDEN); -} - -#[tokio::test] -async fn test_delete_dashboard_sharing_invalid_email() { - // Setup - let client = TestClient::new().await; - let user = create_test_user(); - let dashboard = create_test_dashboard(); - - // Set up the dashboard with owner - client.setup_asset_permission( - dashboard.id, - AssetType::DashboardFile, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ).await; - - // Create request with invalid email - let request = Request::builder() - .method(Method::DELETE) - .uri(format!("/dashboards/{}/sharing", dashboard.id)) - .header(header::CONTENT_TYPE, "application/json") - .header("X-User-ID", user.id.to_string()) - .body(Body::from( - serde_json::to_string(&vec!["invalid-email-no-at-sign".to_string()]).unwrap(), - )) - .unwrap(); - - // Send request - let response = client.app.oneshot(request).await.unwrap(); - - // Verify 400 Bad Request response - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/sharing/list_sharing_test.rs b/api/tests/integration/dashboards/sharing/list_sharing_test.rs deleted file mode 100644 index 54a68e638..000000000 --- a/api/tests/integration/dashboards/sharing/list_sharing_test.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Placeholder test file until fixtures are set up - -#[tokio::test] -async fn test_placeholder() { - assert!(true); -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/sharing/mod.rs b/api/tests/integration/dashboards/sharing/mod.rs deleted file mode 100644 index a6e2475b0..000000000 --- a/api/tests/integration/dashboards/sharing/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod list_sharing_test; -mod create_sharing_test; -mod delete_sharing_test; -mod update_sharing_test; diff --git a/api/tests/integration/dashboards/sharing/update_sharing_test.rs b/api/tests/integration/dashboards/sharing/update_sharing_test.rs deleted file mode 100644 index fa99e5bff..000000000 --- a/api/tests/integration/dashboards/sharing/update_sharing_test.rs +++ /dev/null @@ -1,234 +0,0 @@ -use anyhow::Result; -use axum::{ - extract::Extension, - routing::put, - Router, -}; -use chrono::{Duration, Utc}; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - get_pg_pool, - models::DashboardFile, - schema::dashboard_files::dsl, -}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use diesel_async::RunQueryDsl as AsyncRunQueryDsl; -use handlers::dashboards::sharing::update_sharing_handler::UpdateDashboardSharingRequest; -use middleware::auth::AuthenticatedUser; -use serde_json::json; -use sharing::create_share; -use src::routes::rest::routes::dashboards::sharing::update_dashboard_sharing_rest_handler; -use tests::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::FixtureBuilder, - http::client::TestClient, -}; -use uuid::Uuid; - -#[tokio::test] -async fn test_update_dashboard_sharing_success() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the dashboard) - let user = fixture.create_user().await?; - - // Create a test dashboard owned by the user - let dashboard = fixture.create_dashboard(&user.id).await?; - - // Create another user to share with - let share_recipient = fixture.create_user().await?; - - // Create a manual permission so our test user has Owner access to the dashboard - create_share( - dashboard.id, - AssetType::DashboardFile, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/dashboards/:id/sharing", put(update_dashboard_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Make the request to update sharing permissions with new format - let payload = json!({ - "users": [ - { - "email": share_recipient.email, - "role": "Editor" - } - ] - }); - - let response = client - .put(&format!("/dashboards/{}/sharing", dashboard.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - Ok(()) -} - -#[tokio::test] -async fn test_update_dashboard_public_sharing_success() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the dashboard) - let user = fixture.create_user().await?; - - // Create a test dashboard owned by the user - let dashboard = fixture.create_dashboard(&user.id).await?; - - // Create a manual permission so our test user has Owner access to the dashboard - create_share( - dashboard.id, - AssetType::DashboardFile, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/dashboards/:id/sharing", put(update_dashboard_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Set expiration date to 7 days from now - let expiration_date = Utc::now() + Duration::days(7); - - // Make the request to update public sharing settings - let payload = json!({ - "publicly_accessible": true, - "public_expiration": expiration_date.to_rfc3339() - }); - - let response = client - .put(&format!("/dashboards/{}/sharing", dashboard.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - // Verify database was updated correctly - let pool = get_pg_pool(); - let mut conn = pool.get().await?; - - let updated_dashboard: DashboardFile = dsl::dashboard_files - .filter(dsl::id.eq(dashboard.id)) - .first(&mut conn) - .await?; - - assert!(updated_dashboard.publicly_accessible); - assert_eq!(updated_dashboard.publicly_enabled_by, Some(user.id)); - - // The public_expiry_date is stored as a timestamp, so we can't do an exact match. - // Instead, we'll check that it's within a minute of our expected value - let stored_expiry = updated_dashboard.public_expiry_date.unwrap(); - let diff = (stored_expiry - expiration_date).num_seconds().abs(); - assert!(diff < 60, "Expiry date should be within a minute of the requested date"); - - Ok(()) -} - -#[tokio::test] -async fn test_update_dashboard_sharing_mixed_updates() -> Result<()> { - // Set up test fixtures - let mut fixture = FixtureBuilder::new().await?; - - // Create a test user (this will be the owner of the dashboard) - let user = fixture.create_user().await?; - - // Create a test dashboard owned by the user - let dashboard = fixture.create_dashboard(&user.id).await?; - - // Create another user to share with - let share_recipient = fixture.create_user().await?; - - // Create a manual permission so our test user has Owner access to the dashboard - create_share( - dashboard.id, - AssetType::DashboardFile, - user.id, - IdentityType::User, - AssetPermissionRole::Owner, - user.id, - ) - .await?; - - // Set up the test server with our endpoint - let app = Router::new() - .route("/dashboards/:id/sharing", put(update_dashboard_sharing_rest_handler)) - .layer(Extension(AuthenticatedUser { - id: user.id, - email: user.email.clone(), - org_id: None, - })); - - let client = TestClient::new(app); - - // Set expiration date to 7 days from now - let expiration_date = Utc::now() + Duration::days(7); - - // Make the request to update both user and public sharing settings - let payload = json!({ - "users": [ - { - "email": share_recipient.email, - "role": "Editor" - } - ], - "publicly_accessible": true, - "public_expiration": expiration_date.to_rfc3339() - }); - - let response = client - .put(&format!("/dashboards/{}/sharing", dashboard.id)) - .json(&payload) - .send() - .await?; - - // Assert the response is successful - response.assert_status_ok()?; - - // Verify database was updated correctly for public sharing - let pool = get_pg_pool(); - let mut conn = pool.get().await?; - - let updated_dashboard: DashboardFile = dsl::dashboard_files - .filter(dsl::id.eq(dashboard.id)) - .first(&mut conn) - .await?; - - assert!(updated_dashboard.publicly_accessible); - assert_eq!(updated_dashboard.publicly_enabled_by, Some(user.id)); - - // Verify user permissions were updated too - // This would normally check the asset_permissions table in a real test - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/dashboards/update_dashboard_test.rs b/api/tests/integration/dashboards/update_dashboard_test.rs deleted file mode 100644 index c70c7f9dc..000000000 --- a/api/tests/integration/dashboards/update_dashboard_test.rs +++ /dev/null @@ -1,297 +0,0 @@ -use anyhow::Result; -use axum::http::StatusCode; -use serde_json::json; -use uuid::Uuid; - -use crate::common::TestApp; - -#[tokio::test] -async fn test_update_dashboard_endpoint() -> Result<()> { - // Setup test app - let app = TestApp::new().await?; - - // Create a test dashboard first - let create_response = app - .client - .post("/api/v1/dashboards") - .bearer_auth(&app.test_user.token) - .json(&json!({ - "name": "Test Dashboard", - "description": "Test Description", - "file": "name: Test Dashboard\ndescription: Test Description\nrows: []" - })) - .send() - .await?; - - let create_body: serde_json::Value = create_response.json().await?; - let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); - - // Make request to update dashboard - let update_response = app - .client - .put(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth(&app.test_user.token) - .json(&json!({ - "name": "Updated Dashboard Name", - "description": "Updated description" - })) - .send() - .await?; - - // Verify response - assert_eq!(update_response.status(), StatusCode::OK); - - // Parse response body - let update_body: serde_json::Value = update_response.json().await?; - - // Verify dashboard properties - assert_eq!(update_body["dashboard"]["name"], "Updated Dashboard Name"); - assert_eq!(update_body["dashboard"]["description"], "Updated description"); - - Ok(()) -} - -#[tokio::test] -async fn test_update_dashboard_with_file_endpoint() -> Result<()> { - // Setup test app - let app = TestApp::new().await?; - - // Create a test dashboard first - let create_response = app - .client - .post("/api/v1/dashboards") - .bearer_auth(&app.test_user.token) - .json(&json!({ - "name": "Test Dashboard", - "description": "Test Description", - "file": "name: Test Dashboard\ndescription: Test Description\nrows: []" - })) - .send() - .await?; - - let create_body: serde_json::Value = create_response.json().await?; - let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); - - // YAML content for update - let yaml_content = r#" - name: File Updated Dashboard - description: Updated from file - rows: [] - "#; - - // Make request to update dashboard with file - let update_response = app - .client - .put(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth(&app.test_user.token) - .json(&json!({ - "file": yaml_content - })) - .send() - .await?; - - // Verify response - assert_eq!(update_response.status(), StatusCode::OK); - - // Parse response body - let update_body: serde_json::Value = update_response.json().await?; - - // Verify dashboard properties - assert_eq!(update_body["dashboard"]["name"], "File Updated Dashboard"); - assert_eq!(update_body["dashboard"]["description"], "Updated from file"); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_dashboard_version() -> Result<()> { - // Setup test app - let app = TestApp::new().await?; - - // 1. Create a dashboard with initial content (version 1) - let v1_yaml_content = r#" - name: Original Dashboard - description: Original description - rows: - - items: - - id: "00000000-0000-0000-0000-000000000001" - row_height: 300 - column_sizes: [12] - id: 1 - "#; - - let create_response = app - .client - .post("/api/v1/dashboards") - .bearer_auth(&app.test_user.token) - .json(&json!({ - "file": v1_yaml_content - })) - .send() - .await?; - - let create_body: serde_json::Value = create_response.json().await?; - let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); - assert_eq!(create_body["dashboard"]["version"], 1); - - // 2. Update to create version 2 with different content - let v2_yaml_content = r#" - name: Updated Dashboard - description: Updated description - rows: - - items: - - id: "00000000-0000-0000-0000-000000000001" - - id: "00000000-0000-0000-0000-000000000002" - row_height: 400 - column_sizes: [6, 6] - id: 1 - - items: - - id: "00000000-0000-0000-0000-000000000003" - row_height: 300 - column_sizes: [12] - id: 2 - "#; - - let update_response = app - .client - .put(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth(&app.test_user.token) - .json(&json!({ - "file": v2_yaml_content - })) - .send() - .await?; - - let update_body: serde_json::Value = update_response.json().await?; - assert_eq!(update_body["dashboard"]["version"], 2); - assert_eq!(update_body["dashboard"]["name"], "Updated Dashboard"); - - // 3. Restore to version 1 - let restore_response = app - .client - .put(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth(&app.test_user.token) - .json(&json!({ - "restore_to_version": 1, - // Also add other fields to verify they're ignored - "name": "This Name Should Be Ignored", - "description": "This Description Should Be Ignored" - })) - .send() - .await?; - - // Verify response - assert_eq!(restore_response.status(), StatusCode::OK); - - // Parse response body - let restore_body: serde_json::Value = restore_response.json().await?; - - // 4. Verify a new version (3) is created with content from version 1 - assert_eq!(restore_body["dashboard"]["version"], 3); - assert_eq!(restore_body["dashboard"]["name"], "Original Dashboard"); - assert_eq!(restore_body["dashboard"]["description"], "Original description"); - - // 5. Verify by fetching the dashboard again - let get_response = app - .client - .get(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth(&app.test_user.token) - .send() - .await?; - - assert_eq!(get_response.status(), StatusCode::OK); - let fetched_dashboard: serde_json::Value = get_response.json().await?; - - // Verify the fetched dashboard matches the restored version - assert_eq!(fetched_dashboard["dashboard"]["name"], "Original Dashboard"); - assert_eq!(fetched_dashboard["dashboard"]["version"], 3); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_nonexistent_version() -> Result<()> { - // Setup test app - let app = TestApp::new().await?; - - // 1. Create a dashboard - let create_response = app - .client - .post("/api/v1/dashboards") - .bearer_auth(&app.test_user.token) - .json(&json!({ - "name": "Test Dashboard", - "description": "Test Description", - "file": "name: Test Dashboard\ndescription: Test Description\nrows: []" - })) - .send() - .await?; - - let create_body: serde_json::Value = create_response.json().await?; - let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); - - // 2. Attempt to restore to a non-existent version (999) - let restore_response = app - .client - .put(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth(&app.test_user.token) - .json(&json!({ - "restore_to_version": 999 - })) - .send() - .await?; - - // 3. Verify the request fails with an appropriate status code - assert_eq!(restore_response.status(), StatusCode::BAD_REQUEST); - - // 4. Verify error message contains information about the version not being found - let error_body: serde_json::Value = restore_response.json().await?; - let error_message = error_body["error"].as_str().unwrap_or(""); - assert!(error_message.contains("Version") && error_message.contains("not found"), - "Error message does not indicate version not found issue: {}", error_message); - - Ok(()) -} - -#[tokio::test] -async fn test_permission_checks_for_restoration() -> Result<()> { - // Setup test app - let app = TestApp::new().await?; - - // 1. Create a dashboard as first user - let create_response = app - .client - .post("/api/v1/dashboards") - .bearer_auth(&app.test_user.token) - .json(&json!({ - "name": "Test Dashboard", - "description": "Test Description", - "file": "name: Test Dashboard\ndescription: Test Description\nrows: []" - })) - .send() - .await?; - - let create_body: serde_json::Value = create_response.json().await?; - let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); - - // 2. Create a second user with no access to the dashboard - // Note: In a real test, you would create a second user and ensure they don't have access - // We'll simulate an unauthorized access attempt directly - - // 3. Attempt to restore as unauthorized user - let restore_response = app - .client - .put(&format!("/api/v1/dashboards/{}", dashboard_id)) - .bearer_auth("invalid-token") // Using invalid token to simulate unauthorized access - .json(&json!({ - "restore_to_version": 1 - })) - .send() - .await?; - - // 4. Verify the request fails with a 401 Unauthorized or 403 Forbidden - assert!(restore_response.status() == StatusCode::UNAUTHORIZED || - restore_response.status() == StatusCode::FORBIDDEN); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/data_sources/create_data_source_test.rs b/api/tests/integration/data_sources/create_data_source_test.rs deleted file mode 100644 index 9f21f7a5e..000000000 --- a/api/tests/integration/data_sources/create_data_source_test.rs +++ /dev/null @@ -1,126 +0,0 @@ -use axum::http::StatusCode; -use diesel_async::RunQueryDsl; -use middleware::types::AuthenticatedUser; -use serde_json::json; -use uuid::Uuid; -use database::enums::UserOrganizationRole; - -use crate::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::UserBuilder, - http::test_app::TestApp, -}; - -#[tokio::test] -async fn test_create_data_source() { - let app = TestApp::new().await.unwrap(); - - // Create a test user with organization and proper role - let user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::WorkspaceAdmin) // Ensure user has admin role - .build(&app.db.pool) - .await; - - // Prepare create request - let create_req = json!({ - "name": "New Data Source", - "env": "dev", - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "default_database": "test", - "default_schema": "public" - }); - - // Send create request - let response = app - .client - .post("/api/data_sources") - .header("Authorization", format!("Bearer {}", user.api_key)) - .json(&create_req) - .send() - .await - .unwrap(); - - // Assert response - assert_eq!(response.status(), StatusCode::OK); - - let body = response.json::().await.unwrap(); - assert!(body.get("id").is_some(), "Response should contain an ID"); - body.assert_has_key_with_value("name", "New Data Source"); - body.assert_has_key_with_value("db_type", "postgres"); - - let credentials = &body["credentials"]; - assert!(credentials.is_object()); - - // Test creating data source with same name (should fail) - let duplicate_req = json!({ - "name": "New Data Source", - "env": "dev", - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "default_database": "test", - "default_schema": "public" - }); - - let response = app - .client - .post("/api/data_sources") - .header("Authorization", format!("Bearer {}", user.api_key)) - .json(&duplicate_req) - .send() - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::CONFLICT); - - // Test creating data source with different environment - let diff_env_req = json!({ - "name": "New Data Source", - "env": "prod", - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "default_database": "test", - "default_schema": "public" - }); - - let response = app - .client - .post("/api/data_sources") - .header("Authorization", format!("Bearer {}", user.api_key)) - .json(&diff_env_req) - .send() - .await - .unwrap(); - - // Should succeed since it's a different environment - assert_eq!(response.status(), StatusCode::OK); - - // Test creating data source with insufficient permissions - let regular_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::User) // Regular user role - .build(&app.db.pool) - .await; - - let response = app - .client - .post("/api/data_sources") - .header("Authorization", format!("Bearer {}", regular_user.api_key)) - .json(&create_req) - .send() - .await - .unwrap(); - - // Should fail due to insufficient permissions - assert_eq!(response.status(), StatusCode::FORBIDDEN); -} \ No newline at end of file diff --git a/api/tests/integration/data_sources/delete_data_source_test.rs b/api/tests/integration/data_sources/delete_data_source_test.rs deleted file mode 100644 index b453b7f38..000000000 --- a/api/tests/integration/data_sources/delete_data_source_test.rs +++ /dev/null @@ -1,215 +0,0 @@ -use axum::http::StatusCode; -use diesel::sql_types; -use diesel_async::RunQueryDsl; -use middleware::types::AuthenticatedUser; -use serde_json::json; -use uuid::Uuid; -use database::enums::UserOrganizationRole; - -use crate::common::{ - fixtures::builder::UserBuilder, - http::test_app::TestApp, -}; - -// Mock DataSourceBuilder since we don't know the exact implementation -struct DataSourceBuilder { - name: String, - env: String, - organization_id: Uuid, - created_by: Uuid, - db_type: String, - credentials: serde_json::Value, - id: Uuid, -} - -impl DataSourceBuilder { - fn new() -> Self { - DataSourceBuilder { - name: "Test Data Source".to_string(), - env: "dev".to_string(), - organization_id: Uuid::new_v4(), - created_by: Uuid::new_v4(), - db_type: "postgres".to_string(), - credentials: json!({}), - id: Uuid::new_v4(), - } - } - - fn with_name(mut self, name: &str) -> Self { - self.name = name.to_string(); - self - } - - fn with_env(mut self, env: &str) -> Self { - self.env = env.to_string(); - self - } - - fn with_organization_id(mut self, organization_id: Uuid) -> Self { - self.organization_id = organization_id; - self - } - - fn with_created_by(mut self, created_by: Uuid) -> Self { - self.created_by = created_by; - self - } - - fn with_type(mut self, db_type: &str) -> Self { - self.db_type = db_type.to_string(); - self - } - - fn with_credentials(mut self, credentials: serde_json::Value) -> Self { - self.credentials = credentials; - self - } - - async fn build(self, pool: &diesel_async::pooled_connection::bb8::Pool) -> DataSourceResponse { - // Create data source directly in database using SQL - let mut conn = pool.get().await.unwrap(); - - // Insert the data source - diesel::sql_query("INSERT INTO data_sources (id, name, type, secret_id, organization_id, created_by, updated_by, created_at, updated_at, onboarding_status, env) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW(), 'notStarted', $8)") - .bind::(&self.id) - .bind::(&self.name) - .bind::(&self.db_type) - .bind::(&self.id) // Using the same UUID for both id and secret_id for simplicity - .bind::(&self.organization_id) - .bind::(&self.created_by) - .bind::(&self.created_by) - .bind::(&self.env) - .execute(&mut conn) - .await - .unwrap(); - - // Insert the secret - diesel::sql_query("INSERT INTO vault.secrets (id, secret) VALUES ($1, $2)") - .bind::(&self.id) - .bind::(&self.credentials.to_string()) - .execute(&mut conn) - .await - .unwrap(); - - // Construct response - DataSourceResponse { - id: self.id.to_string(), - } - } -} - -struct DataSourceResponse { - id: String, -} - -#[tokio::test] -async fn test_delete_data_source() { - let app = TestApp::new().await.unwrap(); - - // Create a test user with organization and proper role - let user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::WorkspaceAdmin) // Ensure user has admin role - .build(&app.db.pool) - .await; - - // Create a test data source - let data_source = DataSourceBuilder::new() - .with_name("Data Source To Delete") - .with_env("dev") - .with_organization_id(user.organization_id) - .with_created_by(user.id) - .with_type("postgres") - .with_credentials(json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "database": "test", - "schemas": ["public"] - })) - .build(&app.db.pool) - .await; - - // Send delete request - let response = app - .client - .delete(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", user.api_key)) - .send() - .await - .unwrap(); - - // Assert response - assert_eq!(response.status(), StatusCode::NO_CONTENT); - - // Try to get the deleted data source (should fail) - let response = app - .client - .get(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", user.api_key)) - .send() - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - // Try to delete a non-existent data source - let invalid_id = Uuid::new_v4(); - let response = app - .client - .delete(format!("/api/data_sources/{}", invalid_id)) - .header("Authorization", format!("Bearer {}", user.api_key)) - .send() - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); - - // Test deleting with insufficient permissions - let regular_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::User) // Regular user role - .build(&app.db.pool) - .await; - - // Create another data source - let another_data_source = DataSourceBuilder::new() - .with_name("Another Data Source") - .with_env("dev") - .with_organization_id(user.organization_id) - .with_created_by(user.id) - .with_type("postgres") - .with_credentials(json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password" - })) - .build(&app.db.pool) - .await; - - let response = app - .client - .delete(format!("/api/data_sources/{}", another_data_source.id)) - .header("Authorization", format!("Bearer {}", regular_user.api_key)) - .send() - .await - .unwrap(); - - // Should fail due to insufficient permissions - assert_eq!(response.status(), StatusCode::FORBIDDEN); - - // Check that the secret is actually deleted from the vault - let mut conn = app.db.pool.get().await.unwrap(); - let secret_exists: Option<(i64,)> = diesel::sql_query("SELECT 1 FROM vault.secrets WHERE id = $1") - .bind::(&Uuid::parse_str(&data_source.id).unwrap()) - .load(&mut conn) - .await - .unwrap() - .pop(); - - assert!(secret_exists.is_none(), "Secret should be deleted from the vault"); -} \ No newline at end of file diff --git a/api/tests/integration/data_sources/get_data_source_test.rs b/api/tests/integration/data_sources/get_data_source_test.rs deleted file mode 100644 index 0f735dfef..000000000 --- a/api/tests/integration/data_sources/get_data_source_test.rs +++ /dev/null @@ -1,234 +0,0 @@ -use axum::http::StatusCode; -use diesel::sql_types; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; -use database::enums::UserOrganizationRole; - -use crate::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::UserBuilder, - http::test_app::TestApp, -}; - -// DataSourceBuilder for setting up test data -struct DataSourceBuilder { - name: String, - env: String, - organization_id: Uuid, - created_by: Uuid, - db_type: String, - credentials: serde_json::Value, - id: Uuid, -} - -impl DataSourceBuilder { - fn new() -> Self { - DataSourceBuilder { - name: "Test Data Source".to_string(), - env: "dev".to_string(), - organization_id: Uuid::new_v4(), - created_by: Uuid::new_v4(), - db_type: "postgres".to_string(), - credentials: json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "default_database": "test_db", - "default_schema": "public" - }), - id: Uuid::new_v4(), - } - } - - fn with_name(mut self, name: &str) -> Self { - self.name = name.to_string(); - self - } - - fn with_env(mut self, env: &str) -> Self { - self.env = env.to_string(); - self - } - - fn with_organization_id(mut self, organization_id: Uuid) -> Self { - self.organization_id = organization_id; - self - } - - fn with_created_by(mut self, created_by: Uuid) -> Self { - self.created_by = created_by; - self - } - - fn with_type(mut self, db_type: &str) -> Self { - self.db_type = db_type.to_string(); - self - } - - fn with_credentials(mut self, credentials: serde_json::Value) -> Self { - self.credentials = credentials; - self - } - - async fn build(self, pool: &diesel_async::pooled_connection::bb8::Pool) -> DataSourceResponse { - // Create data source directly in database using SQL - let mut conn = pool.get().await.unwrap(); - - // Insert the data source - diesel::sql_query("INSERT INTO data_sources (id, name, type, secret_id, organization_id, created_by, updated_by, created_at, updated_at, onboarding_status, env) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW(), 'notStarted', $8)") - .bind::(&self.id) - .bind::(&self.name) - .bind::(&self.db_type) - .bind::(&self.id) // Using the same UUID for both id and secret_id for simplicity - .bind::(&self.organization_id) - .bind::(&self.created_by) - .bind::(&self.created_by) - .bind::(&self.env) - .execute(&mut conn) - .await - .unwrap(); - - // Insert the secret - diesel::sql_query("INSERT INTO vault.secrets (id, secret) VALUES ($1, $2)") - .bind::(&self.id) - .bind::(&self.credentials.to_string()) - .execute(&mut conn) - .await - .unwrap(); - - // Construct response - DataSourceResponse { - id: self.id.to_string(), - } - } -} - -struct DataSourceResponse { - id: String, -} - -#[tokio::test] -async fn test_get_data_source() { - let app = TestApp::new().await.unwrap(); - - // Create a test user with organization and proper role - let admin_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::WorkspaceAdmin) - .build(&app.db.pool) - .await; - - // Create a test data source - let postgres_credentials = json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "secure_password", - "default_database": "test_db", - "default_schema": "public" - }); - - let data_source = DataSourceBuilder::new() - .with_name("Test Postgres DB") - .with_env("dev") - .with_organization_id(admin_user.organization_id) - .with_created_by(admin_user.id) - .with_type("postgres") - .with_credentials(postgres_credentials) - .build(&app.db.pool) - .await; - - // Test successful get by admin - let response = app - .client - .get(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", admin_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::OK); - - let body = response.json::().await.unwrap(); - assert_eq!(body["id"], data_source.id); - assert_eq!(body["name"], "Test Postgres DB"); - assert_eq!(body["db_type"], "postgres"); - - // Verify credentials in response - let credentials = &body["credentials"]; - assert_eq!(credentials["type"], "postgres"); - assert_eq!(credentials["host"], "localhost"); - assert_eq!(credentials["port"], 5432); - assert_eq!(credentials["username"], "postgres"); - assert_eq!(credentials["password"], "secure_password"); // Credentials are returned in API - - // Create a data viewer user for testing - let viewer_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::DataViewer) - .build(&app.db.pool) - .await; - - // Test successful get by viewer - let response = app - .client - .get(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", viewer_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::OK); - - // Create a regular user for testing permissions - let regular_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::User) // Regular user with no data access - .build(&app.db.pool) - .await; - - // Test failed get by regular user (insufficient permissions) - let response = app - .client - .get(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", regular_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::FORBIDDEN); - - // Test with non-existent data source - let non_existent_id = Uuid::new_v4(); - let response = app - .client - .get(format!("/api/data_sources/{}", non_existent_id)) - .header("Authorization", format!("Bearer {}", admin_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::NOT_FOUND); - - // Create an organization for cross-org test - let another_org_user = UserBuilder::new() - .with_organization("Another Org") - .with_org_role(UserOrganizationRole::WorkspaceAdmin) - .build(&app.db.pool) - .await; - - // Test cross-organization access (should fail) - let response = app - .client - .get(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", another_org_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::NOT_FOUND); -} \ No newline at end of file diff --git a/api/tests/integration/data_sources/list_data_sources_test.rs b/api/tests/integration/data_sources/list_data_sources_test.rs deleted file mode 100644 index 6aaac3daa..000000000 --- a/api/tests/integration/data_sources/list_data_sources_test.rs +++ /dev/null @@ -1,260 +0,0 @@ -use axum::http::StatusCode; -use diesel::sql_types; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; -use database::enums::UserOrganizationRole; - -use crate::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::UserBuilder, - http::test_app::TestApp, -}; - -// DataSourceBuilder for setting up test data -struct DataSourceBuilder { - name: String, - env: String, - organization_id: Uuid, - created_by: Uuid, - db_type: String, - credentials: serde_json::Value, - id: Uuid, -} - -impl DataSourceBuilder { - fn new() -> Self { - DataSourceBuilder { - name: "Test Data Source".to_string(), - env: "dev".to_string(), - organization_id: Uuid::new_v4(), - created_by: Uuid::new_v4(), - db_type: "postgres".to_string(), - credentials: json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "default_database": "test_db", - "default_schema": "public" - }), - id: Uuid::new_v4(), - } - } - - fn with_name(mut self, name: &str) -> Self { - self.name = name.to_string(); - self - } - - fn with_env(mut self, env: &str) -> Self { - self.env = env.to_string(); - self - } - - fn with_organization_id(mut self, organization_id: Uuid) -> Self { - self.organization_id = organization_id; - self - } - - fn with_created_by(mut self, created_by: Uuid) -> Self { - self.created_by = created_by; - self - } - - fn with_type(mut self, db_type: &str) -> Self { - self.db_type = db_type.to_string(); - self - } - - fn with_credentials(mut self, credentials: serde_json::Value) -> Self { - self.credentials = credentials; - self - } - - async fn build(self, pool: &diesel_async::pooled_connection::bb8::Pool) -> DataSourceResponse { - // Create data source directly in database using SQL - let mut conn = pool.get().await.unwrap(); - - // Insert the data source - diesel::sql_query("INSERT INTO data_sources (id, name, type, secret_id, organization_id, created_by, updated_by, created_at, updated_at, onboarding_status, env) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW(), 'notStarted', $8)") - .bind::(&self.id) - .bind::(&self.name) - .bind::(&self.db_type) - .bind::(&self.id) // Using the same UUID for both id and secret_id for simplicity - .bind::(&self.organization_id) - .bind::(&self.created_by) - .bind::(&self.created_by) - .bind::(&self.env) - .execute(&mut conn) - .await - .unwrap(); - - // Insert the secret - diesel::sql_query("INSERT INTO vault.secrets (id, secret) VALUES ($1, $2)") - .bind::(&self.id) - .bind::(&self.credentials.to_string()) - .execute(&mut conn) - .await - .unwrap(); - - // Construct response - DataSourceResponse { - id: self.id.to_string(), - } - } -} - -struct DataSourceResponse { - id: String, -} - -#[tokio::test] -async fn test_list_data_sources() { - let app = TestApp::new().await.unwrap(); - - // Create a test user with organization and proper role - let admin_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::WorkspaceAdmin) - .build(&app.db.pool) - .await; - - // Create multiple test data sources for this organization - let postgres_credentials = json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "default_database": "test_db", - "default_schema": "public" - }); - - let mysql_credentials = json!({ - "type": "mysql", - "host": "mysql-server", - "port": 3306, - "username": "mysql_user", - "password": "mysql_pass", - "default_database": "mysql_db" - }); - - // Create first data source - let data_source1 = DataSourceBuilder::new() - .with_name("Postgres DB 1") - .with_env("dev") - .with_organization_id(admin_user.organization_id) - .with_created_by(admin_user.id) - .with_type("postgres") - .with_credentials(postgres_credentials.clone()) - .build(&app.db.pool) - .await; - - // Create second data source - let data_source2 = DataSourceBuilder::new() - .with_name("MySQL DB") - .with_env("dev") - .with_organization_id(admin_user.organization_id) - .with_created_by(admin_user.id) - .with_type("mysql") - .with_credentials(mysql_credentials) - .build(&app.db.pool) - .await; - - // Create a data source for another organization - let another_org_user = UserBuilder::new() - .with_organization("Another Org") - .with_org_role(UserOrganizationRole::WorkspaceAdmin) - .build(&app.db.pool) - .await; - - let data_source_other_org = DataSourceBuilder::new() - .with_name("Other Org DB") - .with_env("dev") - .with_organization_id(another_org_user.organization_id) - .with_created_by(another_org_user.id) - .with_type("postgres") - .with_credentials(postgres_credentials) - .build(&app.db.pool) - .await; - - // Test listing data sources - admin user should see both their organization's data sources - let response = app - .client - .get("/api/data_sources") - .header("Authorization", format!("Bearer {}", admin_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::OK); - - let body = response.json::().await.unwrap(); - let data_sources = body.as_array().unwrap(); - - // Should have exactly 2, not seeing the other organization's data source - assert_eq!(data_sources.len(), 2); - - // Verify the data sources belong to our organization - let ids: Vec<&str> = data_sources.iter() - .map(|ds| ds["id"].as_str().unwrap()) - .collect(); - - assert!(ids.contains(&data_source1.id.as_str())); - assert!(ids.contains(&data_source2.id.as_str())); - assert!(!ids.contains(&data_source_other_org.id.as_str())); - - // Create a data viewer role user - let viewer_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::DataViewer) - .build(&app.db.pool) - .await; - - // Test listing data sources with viewer role - should succeed - let response = app - .client - .get("/api/data_sources") - .header("Authorization", format!("Bearer {}", viewer_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::OK); - - // Create a regular user (no data access) - let regular_user = UserBuilder::new() - .with_organization("Test Org") - .with_org_role(UserOrganizationRole::User) - .build(&app.db.pool) - .await; - - // Test listing data sources with insufficient permissions - let response = app - .client - .get("/api/data_sources") - .header("Authorization", format!("Bearer {}", regular_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::FORBIDDEN); - - // Test pagination - let response = app - .client - .get("/api/data_sources?page=0&page_size=1") - .header("Authorization", format!("Bearer {}", admin_user.api_key)) - .send() - .await - .unwrap(); - - response.assert_status(StatusCode::OK); - - let body = response.json::().await.unwrap(); - let data_sources = body.as_array().unwrap(); - - assert_eq!(data_sources.len(), 1, "Pagination should limit to 1 result"); -} \ No newline at end of file diff --git a/api/tests/integration/data_sources/mod.rs b/api/tests/integration/data_sources/mod.rs deleted file mode 100644 index ac68ceb6c..000000000 --- a/api/tests/integration/data_sources/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod list_data_sources_test; -mod get_data_source_test; -mod update_data_source_test; -mod create_data_source_test; -mod delete_data_source_test; \ No newline at end of file diff --git a/api/tests/integration/data_sources/update_data_source_test.rs b/api/tests/integration/data_sources/update_data_source_test.rs deleted file mode 100644 index 2cd7f8c41..000000000 --- a/api/tests/integration/data_sources/update_data_source_test.rs +++ /dev/null @@ -1,197 +0,0 @@ -use axum::http::StatusCode; -use diesel::sql_types; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - assertions::response::ResponseAssertions, - fixtures::builder::UserBuilder, - http::test_app::TestApp, -}; - -// Mock DataSourceBuilder since we don't know the exact implementation -struct DataSourceBuilder { - name: String, - env: String, - organization_id: Uuid, - created_by: Uuid, - db_type: String, - credentials: serde_json::Value, - id: Uuid, -} - -impl DataSourceBuilder { - fn new() -> Self { - DataSourceBuilder { - name: "Test Data Source".to_string(), - env: "dev".to_string(), - organization_id: Uuid::new_v4(), - created_by: Uuid::new_v4(), - db_type: "postgres".to_string(), - credentials: json!({}), - id: Uuid::new_v4(), - } - } - - fn with_name(mut self, name: &str) -> Self { - self.name = name.to_string(); - self - } - - fn with_env(mut self, env: &str) -> Self { - self.env = env.to_string(); - self - } - - fn with_organization_id(mut self, organization_id: Uuid) -> Self { - self.organization_id = organization_id; - self - } - - fn with_created_by(mut self, created_by: Uuid) -> Self { - self.created_by = created_by; - self - } - - fn with_type(mut self, db_type: &str) -> Self { - self.db_type = db_type.to_string(); - self - } - - fn with_credentials(mut self, credentials: serde_json::Value) -> Self { - self.credentials = credentials; - self - } - - async fn build(self, pool: &diesel_async::pooled_connection::bb8::Pool) -> DataSourceResponse { - // Create data source directly in database using SQL - let mut conn = pool.get().await.unwrap(); - - // Insert the data source - diesel::sql_query("INSERT INTO data_sources (id, name, type, secret_id, organization_id, created_by, updated_by, created_at, updated_at, onboarding_status, env) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW(), 'notStarted', $8)") - .bind::(&self.id) - .bind::(&self.name) - .bind::(&self.db_type) - .bind::(&self.id) // Using the same UUID for both id and secret_id for simplicity - .bind::(&self.organization_id) - .bind::(&self.created_by) - .bind::(&self.created_by) - .bind::(&self.env) - .execute(&mut conn) - .await - .unwrap(); - - // Insert the secret - diesel::sql_query("INSERT INTO vault.secrets (id, secret) VALUES ($1, $2)") - .bind::(&self.id) - .bind::(&self.credentials.to_string()) - .execute(&mut conn) - .await - .unwrap(); - - // Construct response - DataSourceResponse { - id: self.id.to_string(), - } - } -} - -struct DataSourceResponse { - id: String, -} - -#[tokio::test] -async fn test_update_data_source() { - let app = TestApp::new().await.unwrap(); - - // Create a test user with organization - let user = UserBuilder::new() - .with_organization("Test Org") - .build(&app.db.pool) - .await; - - // Create a test data source - let data_source = DataSourceBuilder::new() - .with_name("Original DS Name") - .with_env("dev") - .with_organization_id(user.organization_id) - .with_created_by(user.id) - .with_type("postgres") - .with_credentials(json!({ - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "postgres", - "password": "password", - "database": "test", - "schemas": ["public"] - })) - .build(&app.db.pool) - .await; - - // Prepare update request - let update_req = json!({ - "name": "Updated DS Name", - "env": "prod", - "type": "postgres", - "host": "new-host", - "port": 5433, - "username": "new-user", - "password": "new-password", - "database": "new-db", - "schemas": ["public", "schema2"] - }); - - // Send update request - let response = app - .client - .put(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", user.api_key)) - .json(&update_req) - .send() - .await - .unwrap(); - - // Assert response - assert_eq!(response.status(), StatusCode::OK); - - let body = response.json::().await.unwrap(); - body.assert_has_key_with_value("id", data_source.id); - body.assert_has_key_with_value("name", "Updated DS Name"); - - let credentials = &body["credentials"]; - assert!(credentials.is_object()); - - // Test updating just the name - let name_only_update = json!({ - "name": "Name Only Update" - }); - - let response = app - .client - .put(format!("/api/data_sources/{}", data_source.id)) - .header("Authorization", format!("Bearer {}", user.api_key)) - .json(&name_only_update) - .send() - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.json::().await.unwrap(); - body.assert_has_key_with_value("name", "Name Only Update"); - - // Test updating with invalid UUID - let invalid_id = Uuid::new_v4(); - let response = app - .client - .put(format!("/api/data_sources/{}", invalid_id)) - .header("Authorization", format!("Bearer {}", user.api_key)) - .json(&name_only_update) - .send() - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); -} \ No newline at end of file diff --git a/api/tests/integration/favorites/create_favorites_bulk_test.rs b/api/tests/integration/favorites/create_favorites_bulk_test.rs deleted file mode 100644 index f27b7e976..000000000 --- a/api/tests/integration/favorites/create_favorites_bulk_test.rs +++ /dev/null @@ -1,114 +0,0 @@ -use anyhow::Result; -use serde_json::json; -use uuid::Uuid; - -use database::enums::AssetType; -use crate::common::{ - setup_test_env, TestApp, TestDb, TestTaggable, FixtureBuilder, - assertions::ResponseAssertions, -}; - -#[tokio::test] -async fn test_create_single_favorite() -> Result<()> { - // Setup - setup_test_env(); - let test_db = TestDb::new().await?; - test_db.setup_test_data().await?; - - let test_app = TestApp::new().await?; - let client = test_app.client(); - - // Create a test user and dashboard to favorite - let user = FixtureBuilder::create_user().build(); - let dashboard = FixtureBuilder::create_dashboard().with_user(&user).build(); - - // Save fixtures to database - let mut conn = test_db.pool.get().await?; - user.save(&mut conn).await?; - dashboard.save(&mut conn).await?; - - // Test single favorite creation - let response = client - .post(&format!("/api/users/me/favorites")) - .json(&json!({ - "id": dashboard.id, - "asset_type": "DashboardFile" - })) - .header("x-user-id", user.id.to_string()) - .send() - .await?; - - response.assert_status_ok()?; - let body = response.json::>().await?; - - // Assert that response contains the favorited dashboard - assert!(!body.is_empty()); - let favorited_item = body.iter().find(|item| { - item["id"] == dashboard.id.to_string() && item["asset_type"] == "DashboardFile" - }); - assert!(favorited_item.is_some()); - - Ok(()) -} - -#[tokio::test] -async fn test_create_multiple_favorites() -> Result<()> { - // Setup - setup_test_env(); - let test_db = TestDb::new().await?; - test_db.setup_test_data().await?; - - let test_app = TestApp::new().await?; - let client = test_app.client(); - - // Create a test user and multiple assets to favorite - let user = FixtureBuilder::create_user().build(); - let dashboard1 = FixtureBuilder::create_dashboard().with_user(&user).build(); - let dashboard2 = FixtureBuilder::create_dashboard().with_user(&user).build(); - let collection = FixtureBuilder::create_collection().with_user(&user).build(); - - // Save fixtures to database - let mut conn = test_db.pool.get().await?; - user.save(&mut conn).await?; - dashboard1.save(&mut conn).await?; - dashboard2.save(&mut conn).await?; - collection.save(&mut conn).await?; - - // Test bulk favorite creation - let response = client - .post(&format!("/api/users/me/favorites")) - .json(&json!([ - { - "id": dashboard1.id, - "asset_type": "DashboardFile" - }, - { - "id": dashboard2.id, - "asset_type": "DashboardFile" - }, - { - "id": collection.id, - "asset_type": "Collection" - } - ])) - .header("x-user-id", user.id.to_string()) - .send() - .await?; - - response.assert_status_ok()?; - let body = response.json::>().await?; - - // Assert that response contains all favorited items - assert!(body.len() >= 3); - - // Check if all three assets are in the favorites - let dashboard1_found = body.iter().any(|item| item["id"] == dashboard1.id.to_string()); - let dashboard2_found = body.iter().any(|item| item["id"] == dashboard2.id.to_string()); - let collection_found = body.iter().any(|item| item["id"] == collection.id.to_string()); - - assert!(dashboard1_found, "Dashboard 1 not found in favorites"); - assert!(dashboard2_found, "Dashboard 2 not found in favorites"); - assert!(collection_found, "Collection not found in favorites"); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/favorites/delete_favorites_bulk_test.rs b/api/tests/integration/favorites/delete_favorites_bulk_test.rs deleted file mode 100644 index 2dbe6a27b..000000000 --- a/api/tests/integration/favorites/delete_favorites_bulk_test.rs +++ /dev/null @@ -1,128 +0,0 @@ -use anyhow::Result; -use serde_json::json; -use uuid::Uuid; - -use database::enums::AssetType; -use handlers::favorites::{create_favorite, CreateFavoriteReq}; -use crate::common::{ - setup_test_env, TestApp, TestDb, TestTaggable, FixtureBuilder, - assertions::ResponseAssertions, -}; - -#[tokio::test] -async fn test_delete_single_favorite_by_id() -> Result<()> { - // Setup - setup_test_env(); - let test_db = TestDb::new().await?; - test_db.setup_test_data().await?; - - let test_app = TestApp::new().await?; - let client = test_app.client(); - - // Create a test user and dashboard to favorite - let user = FixtureBuilder::create_user().build(); - let dashboard = FixtureBuilder::create_dashboard().with_user(&user).build(); - - // Save fixtures to database - let mut conn = test_db.pool.get().await?; - user.save(&mut conn).await?; - dashboard.save(&mut conn).await?; - - // Create favorite first - let response = client - .post(&format!("/api/users/me/favorites")) - .json(&json!({ - "id": dashboard.id, - "asset_type": "DashboardFile" - })) - .header("x-user-id", user.id.to_string()) - .send() - .await?; - - response.assert_status_ok()?; - - // Now delete the favorite using path parameter - let response = client - .delete(&format!("/api/users/me/favorites/{}", dashboard.id)) - .header("x-user-id", user.id.to_string()) - .send() - .await?; - - response.assert_status_ok()?; - let body = response.json::>().await?; - - // Assert that the favorite is no longer in the list - let still_favorited = body.iter().any(|item| item["id"] == dashboard.id.to_string()); - assert!(!still_favorited, "Dashboard is still in favorites after deletion"); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_multiple_favorites_bulk() -> Result<()> { - // Setup - setup_test_env(); - let test_db = TestDb::new().await?; - test_db.setup_test_data().await?; - - let test_app = TestApp::new().await?; - let client = test_app.client(); - - // Create a test user and multiple assets to favorite - let user = FixtureBuilder::create_user().build(); - let dashboard1 = FixtureBuilder::create_dashboard().with_user(&user).build(); - let dashboard2 = FixtureBuilder::create_dashboard().with_user(&user).build(); - let collection = FixtureBuilder::create_collection().with_user(&user).build(); - - // Save fixtures to database - let mut conn = test_db.pool.get().await?; - user.save(&mut conn).await?; - dashboard1.save(&mut conn).await?; - dashboard2.save(&mut conn).await?; - collection.save(&mut conn).await?; - - // Create favorites first - let response = client - .post(&format!("/api/users/me/favorites")) - .json(&json!([ - { - "id": dashboard1.id, - "asset_type": "DashboardFile" - }, - { - "id": dashboard2.id, - "asset_type": "DashboardFile" - }, - { - "id": collection.id, - "asset_type": "Collection" - } - ])) - .header("x-user-id", user.id.to_string()) - .send() - .await?; - - response.assert_status_ok()?; - - // Now delete multiple favorites using bulk endpoint - let response = client - .delete(&format!("/api/users/me/favorites")) - .json(&json!([dashboard1.id, dashboard2.id])) - .header("x-user-id", user.id.to_string()) - .send() - .await?; - - response.assert_status_ok()?; - let body = response.json::>().await?; - - // Assert that the deleted favorites are no longer in the list - let dashboard1_found = body.iter().any(|item| item["id"] == dashboard1.id.to_string()); - let dashboard2_found = body.iter().any(|item| item["id"] == dashboard2.id.to_string()); - let collection_found = body.iter().any(|item| item["id"] == collection.id.to_string()); - - assert!(!dashboard1_found, "Dashboard 1 is still in favorites after deletion"); - assert!(!dashboard2_found, "Dashboard 2 is still in favorites after deletion"); - assert!(collection_found, "Collection should still be in favorites"); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/favorites/mod.rs b/api/tests/integration/favorites/mod.rs deleted file mode 100644 index 20a4d250a..000000000 --- a/api/tests/integration/favorites/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod create_favorites_bulk_test; -pub mod delete_favorites_bulk_test; \ No newline at end of file diff --git a/api/tests/integration/metrics/delete_metric_test.rs b/api/tests/integration/metrics/delete_metric_test.rs deleted file mode 100644 index 2bc0718c8..000000000 --- a/api/tests/integration/metrics/delete_metric_test.rs +++ /dev/null @@ -1,358 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use database::{ - models::MetricFile, - pool::get_pg_pool, - schema::metric_files, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use handlers::metrics::{delete_metric_handler, delete_metrics_handler, DeleteMetricsRequest}; -use std::collections::HashSet; -use tokio; -use uuid::Uuid; - -use crate::common::{ - db::TestDb, - env::setup_test_env, - fixtures::metrics::create_test_metric_file, - http::{client::TestClient, test_app::create_test_app}, -}; - -#[tokio::test] -async fn test_delete_metric_handler() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test metric - let test_metric = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric For Deletion".to_string())).await?; - let metric_id = test_metric.id; - - // Call the handler being tested - delete_metric_handler(&metric_id, &user_id).await?; - - // Fetch the deleted metric from the database - let db_metric = metric_files::table - .filter(metric_files::id.eq(metric_id)) - .first::(&mut conn) - .await?; - - // Verify it has been soft deleted (deleted_at is set) - assert!(db_metric.deleted_at.is_some()); - - // Trying to delete it again should return an error - let result = delete_metric_handler(&metric_id, &user_id).await; - assert!(result.is_err()); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metric_handler_not_found() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let _test_db = TestDb::new().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Use a random UUID that doesn't exist - let nonexistent_metric_id = Uuid::new_v4(); - - // Call the handler being tested - should fail - let result = delete_metric_handler(&nonexistent_metric_id, &user_id).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("not found")); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_already_deleted_metric() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test metric - let mut test_metric = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Already Deleted Metric".to_string())).await?; - - // Mark as deleted - test_metric.deleted_at = Some(Utc::now()); - - // Update the metric in the database - diesel::update(metric_files::table) - .filter(metric_files::id.eq(test_metric.id)) - .set(metric_files::deleted_at.eq(test_metric.deleted_at)) - .execute(&mut conn) - .await?; - - // Call the handler being tested - should fail because it's already deleted - let result = delete_metric_handler(&test_metric.id, &user_id).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("not found") || error.contains("already deleted")); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metrics_bulk_handler() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create and insert multiple test metrics - let test_metric1 = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric 1".to_string())).await?; - let test_metric2 = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric 2".to_string())).await?; - let test_metric3 = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric 3".to_string())).await?; - - let metric_ids = vec![test_metric1.id, test_metric2.id, test_metric3.id]; - - // Create the bulk delete request - let request = DeleteMetricsRequest { - ids: metric_ids.clone(), - }; - - // Call the bulk delete handler - let result = delete_metrics_handler(request, &user_id).await?; - - // Verify all metrics were successfully deleted - assert_eq!(result.successful_ids.len(), 3); - assert_eq!(result.failed_ids.len(), 0); - - // Convert to a set for easier lookup - let successful_ids: HashSet<_> = result.successful_ids.into_iter().collect(); - - // Verify all expected IDs are in the successful list - assert!(successful_ids.contains(&test_metric1.id)); - assert!(successful_ids.contains(&test_metric2.id)); - assert!(successful_ids.contains(&test_metric3.id)); - - // Verify each metric has been soft deleted in the database - for id in &metric_ids { - let db_metric = metric_files::table - .filter(metric_files::id.eq(id)) - .first::(&mut conn) - .await?; - - assert!(db_metric.deleted_at.is_some()); - } - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metrics_bulk_partial_success() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create and insert a test metric - let test_metric = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric".to_string())).await?; - - // Generate two non-existent metric IDs - let nonexistent_id1 = Uuid::new_v4(); - let nonexistent_id2 = Uuid::new_v4(); - - // Create the bulk delete request with mix of real and non-existent IDs - let request = DeleteMetricsRequest { - ids: vec![test_metric.id, nonexistent_id1, nonexistent_id2], - }; - - // Call the bulk delete handler - let result = delete_metrics_handler(request, &user_id).await?; - - // Verify partial success (1 success, 2 failures) - assert_eq!(result.successful_ids.len(), 1); - assert_eq!(result.failed_ids.len(), 2); - - // Verify the successful ID matches our real metric - assert_eq!(result.successful_ids[0], test_metric.id); - - // Create a set of failed IDs for easier lookup - let failed_ids: HashSet<_> = result.failed_ids.iter().map(|f| f.id).collect(); - - // Verify the failed IDs match our non-existent IDs - assert!(failed_ids.contains(&nonexistent_id1)); - assert!(failed_ids.contains(&nonexistent_id2)); - - // Verify the real metric has been soft deleted in the database - let db_metric = metric_files::table - .filter(metric_files::id.eq(test_metric.id)) - .first::(&mut conn) - .await?; - - assert!(db_metric.deleted_at.is_some()); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metrics_bulk_empty_list() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - - // Create test user ID - let user_id = Uuid::new_v4(); - - // Create a bulk delete request with empty IDs list - let request = DeleteMetricsRequest { - ids: vec![], - }; - - // Call the bulk delete handler - let result = delete_metrics_handler(request, &user_id).await?; - - // Verify empty results - assert_eq!(result.successful_ids.len(), 0); - assert_eq!(result.failed_ids.len(), 0); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metrics_rest_bulk_endpoint() -> Result<()> { - // Create test app - let app = create_test_app().await?; - let client = TestClient::new()? - .with_auth("test-token"); // Use test auth token - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create and insert multiple test metrics - let test_metric1 = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric 1".to_string())).await?; - let test_metric2 = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric 2".to_string())).await?; - let test_metric3 = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric 3".to_string())).await?; - - // Create request payload - let request_body = serde_json::json!({ - "ids": [test_metric1.id, test_metric2.id, test_metric3.id] - }); - - // Send bulk delete request - let builder = client.delete("/api/v1/metrics") - .json(&request_body) - .send() - .await?; - - // Verify response status - assert_eq!(builder.status().as_u16(), 204); // No Content for successful deletion - - // Verify metrics are deleted in database - for id in [test_metric1.id, test_metric2.id, test_metric3.id] { - let db_metric = metric_files::table - .filter(metric_files::id.eq(id)) - .first::(&mut conn) - .await?; - - assert!(db_metric.deleted_at.is_some(), "Metric with ID {} should be marked as deleted", id); - } - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metrics_rest_bulk_partial_success() -> Result<()> { - // Create test app - let app = create_test_app().await?; - let client = TestClient::new()? - .with_auth("test-token"); // Use test auth token - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create and insert one test metric - let test_metric = create_test_metric_file(&mut conn, user_id, Some(org_id), Some("Test Metric".to_string())).await?; - - // Generate non-existent IDs - let nonexistent_id1 = Uuid::new_v4(); - let nonexistent_id2 = Uuid::new_v4(); - - // Create request payload - let request_body = serde_json::json!({ - "ids": [test_metric.id, nonexistent_id1, nonexistent_id2] - }); - - // Send bulk delete request - let builder = client.delete("/api/v1/metrics") - .json(&request_body) - .send() - .await?; - - // Verify response status - should be 207 Multi-Status - assert_eq!(builder.status().as_u16(), 207); - - // Parse response - let response: serde_json::Value = builder.json().await?; - - // Check response structure - assert!(response["successful_ids"].is_array()); - assert!(response["failed_ids"].is_array()); - - // Check counts - let successful_ids = response["successful_ids"].as_array().unwrap(); - let failed_ids = response["failed_ids"].as_array().unwrap(); - - assert_eq!(successful_ids.len(), 1); - assert_eq!(failed_ids.len(), 2); - - // Verify existing metric was deleted - let db_metric = metric_files::table - .filter(metric_files::id.eq(test_metric.id)) - .first::(&mut conn) - .await?; - - assert!(db_metric.deleted_at.is_some(), "Existing metric should be marked as deleted"); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/metrics/get_metric_test.rs b/api/tests/integration/metrics/get_metric_test.rs deleted file mode 100644 index c33f2f873..000000000 --- a/api/tests/integration/metrics/get_metric_test.rs +++ /dev/null @@ -1,261 +0,0 @@ -use uuid::Uuid; -use crate::common::{ - env::{create_env, TestEnv}, - http::client::TestClient, - assertions::response::assert_api_ok, -}; -use chrono::Utc; -use database::enums::{AssetPermissionRole, AssetType, AssetTypeEnum, IdentityTypeEnum}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -#[tokio::test] -async fn test_get_metric_with_sharing_info() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and metric - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let metric_id = create_test_metric(&env, user_id).await; - - // Add sharing permissions - add_test_permissions(&env, metric_id, user_id).await; - - // Add public sharing - enable_public_sharing(&env, metric_id, user_id).await; - - // Test GET request - let response = client - .get(&format!("/api/v1/metrics/{}", metric_id)) - .header("X-User-Id", user_id.to_string()) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Check fields - assert_eq!(data["id"], metric_id.to_string()); - assert_eq!(data["type"], "metric"); - - // Check sharing fields - assert_eq!(data["publicly_accessible"], true); - assert!(data["public_expiry_date"].is_string()); - assert_eq!(data["public_enabled_by"], "test@example.com"); - assert_eq!(data["individual_permissions"].as_array().unwrap().len(), 1); - - let permission = &data["individual_permissions"][0]; - assert_eq!(permission["email"], "test2@example.com"); - assert_eq!(permission["role"], "viewer"); - assert_eq!(permission["name"], "Test User 2"); - - // Check that dashboards and collections arrays exist but are empty - // since we haven't associated any in this test - assert!(data["dashboards"].is_array()); - assert!(data["collections"].is_array()); - assert_eq!(data["dashboards"].as_array().unwrap().len(), 0); - assert_eq!(data["collections"].as_array().unwrap().len(), 0); -} - -#[tokio::test] -async fn test_get_metric_with_associations() { - // Setup test environment - let env = create_env().await; - let client = TestClient::new(&env); - - // Create test user and metric - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let metric_id = create_test_metric(&env, user_id).await; - - // Create dashboard and collection and associate them with the metric - let (dashboard_id, collection_id) = create_associations(&env, metric_id, user_id).await; - - // Test GET request - let response = client - .get(&format!("/api/v1/metrics/{}", metric_id)) - .header("X-User-Id", user_id.to_string()) - .send() - .await; - - // Assert success and verify response - let data = assert_api_ok(response).await; - - // Check fields - assert_eq!(data["id"], metric_id.to_string()); - - // Check that dashboards array contains our dashboard - assert!(data["dashboards"].is_array()); - assert_eq!(data["dashboards"].as_array().unwrap().len(), 1); - assert_eq!(data["dashboards"][0]["id"], dashboard_id.to_string()); - assert_eq!(data["dashboards"][0]["name"], "Test Dashboard"); - - // Check that collections array contains our collection - assert!(data["collections"].is_array()); - assert_eq!(data["collections"].as_array().unwrap().len(), 1); - assert_eq!(data["collections"][0]["id"], collection_id.to_string()); - assert_eq!(data["collections"][0]["name"], "Test Collection"); -} - -// Helper functions to set up the test data -async fn create_test_metric(env: &TestEnv, user_id: Uuid) -> Uuid { - let mut conn = env.db_pool.get().await.unwrap(); - - // Insert test user - diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user_id) - .bind::("test@example.com") - .bind::("Test User") - .execute(&mut conn) - .await - .unwrap(); - - // Insert another test user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - diesel::sql_query("INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING") - .bind::(user2_id) - .bind::("test2@example.com") - .bind::("Test User 2") - .execute(&mut conn) - .await - .unwrap(); - - // Insert test metric - let metric_id = Uuid::parse_str("00000000-0000-0000-0000-000000000010").unwrap(); - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - - // Insert test organization if needed - diesel::sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") - .bind::(org_id) - .bind::("Test Organization") - .execute(&mut conn) - .await - .unwrap(); - - // Insert metric - diesel::sql_query(r#" - INSERT INTO metric_files (id, name, file_name, content, verification, organization_id, created_by, version_history) - VALUES ($1, 'Test Metric', 'test.yml', '{"description": "Test description", "time_frame": "daily", "dataset_ids": [], "chart_config": {}, "sql": "SELECT 1;"}', 'notRequested', $2, $3, '{}'::jsonb) - ON CONFLICT DO NOTHING - "#) - .bind::(metric_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - metric_id -} - -async fn add_test_permissions(env: &TestEnv, metric_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Get the second user - let user2_id = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(); - - // Add permission for user2 as viewer - diesel::sql_query(r#" - INSERT INTO asset_permissions (identity_id, identity_type, asset_id, asset_type, role, created_by, updated_by) - VALUES ($1, $2, $3, $4, $5, $6, $6) - ON CONFLICT DO NOTHING - "#) - .bind::(user2_id) - .bind::(IdentityTypeEnum::User.to_string()) - .bind::(metric_id) - .bind::(AssetTypeEnum::MetricFile.to_string()) - .bind::(AssetPermissionRole::CanView.to_string()) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); -} - -async fn enable_public_sharing(env: &TestEnv, metric_id: Uuid, user_id: Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Set public access - let expiry_date = Utc::now() + chrono::Duration::days(7); - - diesel::sql_query(r#" - UPDATE metric_files - SET publicly_accessible = true, publicly_enabled_by = $1, public_expiry_date = $2 - WHERE id = $3 - "#) - .bind::(user_id) - .bind::(expiry_date) - .bind::(metric_id) - .execute(&mut conn) - .await - .unwrap(); -} - -async fn create_associations(env: &TestEnv, metric_id: Uuid, user_id: Uuid) -> (Uuid, Uuid) { - let mut conn = env.db_pool.get().await.unwrap(); - - // Create test organization if not already created - let org_id = Uuid::parse_str("00000000-0000-0000-0000-000000000100").unwrap(); - diesel::sql_query("INSERT INTO organizations (id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING") - .bind::(org_id) - .bind::("Test Organization") - .execute(&mut conn) - .await - .unwrap(); - - // Create a dashboard - let dashboard_id = Uuid::parse_str("00000000-0000-0000-0000-000000000020").unwrap(); - diesel::sql_query(r#" - INSERT INTO dashboard_files (id, name, file_name, content, organization_id, created_by, version_history) - VALUES ($1, 'Test Dashboard', 'test-dashboard.yml', '{}', $2, $3, '{}'::jsonb) - ON CONFLICT DO NOTHING - "#) - .bind::(dashboard_id) - .bind::(org_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - // Create a collection - let collection_id = Uuid::parse_str("00000000-0000-0000-0000-000000000030").unwrap(); - diesel::sql_query(r#" - INSERT INTO collections (id, name, description, created_by, updated_by, organization_id) - VALUES ($1, 'Test Collection', 'Test collection description', $2, $2, $3) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::(user_id) - .bind::(org_id) - .execute(&mut conn) - .await - .unwrap(); - - // Associate metric with dashboard - diesel::sql_query(r#" - INSERT INTO metric_files_to_dashboard_files (metric_file_id, dashboard_file_id, created_by) - VALUES ($1, $2, $3) - ON CONFLICT DO NOTHING - "#) - .bind::(metric_id) - .bind::(dashboard_id) - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - // Associate metric with collection - diesel::sql_query(r#" - INSERT INTO collections_to_assets (collection_id, asset_id, asset_type, created_by, updated_by) - VALUES ($1, $2, $3, $4, $4) - ON CONFLICT DO NOTHING - "#) - .bind::(collection_id) - .bind::(metric_id) - .bind::("metric_file") - .bind::(user_id) - .execute(&mut conn) - .await - .unwrap(); - - (dashboard_id, collection_id) -} \ No newline at end of file diff --git a/api/tests/integration/metrics/mod.rs b/api/tests/integration/metrics/mod.rs deleted file mode 100644 index fb9b7b1ef..000000000 --- a/api/tests/integration/metrics/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Export test modules -pub mod update_metric_test; -pub mod delete_metric_test; -pub mod post_metric_dashboard_test; -pub mod get_metric_test; -pub mod sharing; \ No newline at end of file diff --git a/api/tests/integration/metrics/post_metric_dashboard_test.rs b/api/tests/integration/metrics/post_metric_dashboard_test.rs deleted file mode 100644 index a905c10e4..000000000 --- a/api/tests/integration/metrics/post_metric_dashboard_test.rs +++ /dev/null @@ -1,177 +0,0 @@ -use anyhow::Result; -use database::{ - enums::AssetType, - models::{MetricFile, DashboardFile}, - pool::get_pg_pool, - schema::{collections_to_assets, dashboard_files, metric_files}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use handlers::metrics::{post_metric_dashboard_handler, PostMetricDashboardRequest}; -use tokio; -use uuid::Uuid; - -use crate::common::{ - db::TestDb, - env::setup_test_env, - fixtures::{create_test_metric_file, create_test_dashboard_file}, -}; - -#[tokio::test] -async fn test_post_metric_dashboard_handler() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test metric and dashboard - let test_metric = create_test_metric_file(&user_id, &org_id, Some("Test Metric".to_string())); - let metric_id = test_metric.id; - - let test_dashboard = create_test_dashboard_file(&user_id, &org_id, Some("Test Dashboard".to_string())); - let dashboard_id = test_dashboard.id; - - // Insert test metric and dashboard into database - diesel::insert_into(metric_files::table) - .values(&test_metric) - .execute(&mut conn) - .await?; - - diesel::insert_into(dashboard_files::table) - .values(&test_dashboard) - .execute(&mut conn) - .await?; - - // Create the request - let request = PostMetricDashboardRequest { - dashboard_id, - }; - - // Call the handler being tested - let response = post_metric_dashboard_handler(&metric_id, &user_id, request).await?; - - // Verify the response - assert_eq!(response.metric_id, metric_id); - assert_eq!(response.dashboard_id, dashboard_id); - - // Check the database to ensure the association was created - let association_exists = collections_to_assets::table - .filter(collections_to_assets::asset_id.eq(metric_id)) - .filter(collections_to_assets::collection_id.eq(dashboard_id)) - .filter(collections_to_assets::asset_type.eq(AssetType::MetricFile)) - .filter(collections_to_assets::deleted_at.is_null()) - .count() - .first::(&mut conn) - .await?; - - assert_eq!(association_exists, 1); - - // Test idempotency - calling it again should not create a duplicate - let request2 = PostMetricDashboardRequest { - dashboard_id, - }; - - let _ = post_metric_dashboard_handler(&metric_id, &user_id, request2).await?; - - let association_count = collections_to_assets::table - .filter(collections_to_assets::asset_id.eq(metric_id)) - .filter(collections_to_assets::collection_id.eq(dashboard_id)) - .filter(collections_to_assets::asset_type.eq(AssetType::MetricFile)) - .filter(collections_to_assets::deleted_at.is_null()) - .count() - .first::(&mut conn) - .await?; - - // Should still be only 1 association - assert_eq!(association_count, 1); - - Ok(()) -} - -#[tokio::test] -async fn test_post_metric_dashboard_handler_different_orgs() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Create two different organization IDs - let org_id1 = Uuid::new_v4(); - let org_id2 = Uuid::new_v4(); - - // Create test metric in org1 - let test_metric = create_test_metric_file(&user_id, &org_id1, Some("Org1 Metric".to_string())); - let metric_id = test_metric.id; - - // Create test dashboard in org2 - let test_dashboard = create_test_dashboard_file(&user_id, &org_id2, Some("Org2 Dashboard".to_string())); - let dashboard_id = test_dashboard.id; - - // Insert test metric and dashboard into database - diesel::insert_into(metric_files::table) - .values(&test_metric) - .execute(&mut conn) - .await?; - - diesel::insert_into(dashboard_files::table) - .values(&test_dashboard) - .execute(&mut conn) - .await?; - - // Create the request - let request = PostMetricDashboardRequest { - dashboard_id, - }; - - // Call the handler being tested - should fail because they're in different orgs - let result = post_metric_dashboard_handler(&metric_id, &user_id, request).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("same organization")); - - Ok(()) -} - -#[tokio::test] -async fn test_post_metric_dashboard_handler_not_found() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let _test_db = TestDb::new().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Use random UUIDs that don't exist - let nonexistent_metric_id = Uuid::new_v4(); - let nonexistent_dashboard_id = Uuid::new_v4(); - - // Create the request - let request = PostMetricDashboardRequest { - dashboard_id: nonexistent_dashboard_id, - }; - - // Call the handler being tested - should fail - let result = post_metric_dashboard_handler(&nonexistent_metric_id, &user_id, request).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("not found") || error.contains("unauthorized")); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/metrics/sharing/create_sharing_test.rs b/api/tests/integration/metrics/sharing/create_sharing_test.rs deleted file mode 100644 index 312c5d0d4..000000000 --- a/api/tests/integration/metrics/sharing/create_sharing_test.rs +++ /dev/null @@ -1,103 +0,0 @@ -use axum::http::StatusCode; -use chrono::Utc; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{AssetPermission}, - pool::get_pg_pool, - schema::{users, metric_files, asset_permissions}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - http::client::TestClient, - fixtures::{users::create_test_user, metrics::create_test_metric_file}, -}; - -#[tokio::test] -async fn test_create_metric_sharing_success() { - // Setup test database connection - let mut conn = get_pg_pool().get().await.unwrap(); - - // 1. Create test owner and shared user - let owner = create_test_user("owner@example.com"); - let org_id = Uuid::new_v4(); // Need org id for fixture - diesel::insert_into(users::table) - .values(&owner) - .execute(&mut conn) - .await - .unwrap(); - - let shared_user = create_test_user("shared@example.com"); - diesel::insert_into(users::table) - .values(&shared_user) - .execute(&mut conn) - .await - .unwrap(); - - // 2. Create test metric - let metric = create_test_metric_file(&owner.id, &org_id, None); - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await - .unwrap(); - - // 3. Create owner permission for test user - let now = Utc::now(); - let permission = AssetPermission { - identity_id: owner.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::Owner, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - diesel::insert_into(asset_permissions::table) - .values(&permission) - .execute(&mut conn) - .await - .unwrap(); - - // 4. Create test client - let client = TestClient::new().await; - - // 5. Send create sharing request - let response = client - .post(&format!("/metrics/{}/sharing", metric.id)) - .with_auth(&owner.id.to_string()) - .json(&json!({ - "recipients": [ - { - "email": "shared@example.com", - "role": "CanView" - } - ] - })) - .send() - .await; - - // 6. Assert response - assert_eq!(response.status(), StatusCode::OK); - - // 7. Check that permission was created - let permissions = asset_permissions::table - .filter(asset_permissions::asset_id.eq(metric.id)) - .filter(asset_permissions::identity_id.eq(shared_user.id)) - .filter(asset_permissions::asset_type.eq(AssetType::MetricFile)) - .filter(asset_permissions::identity_type.eq(IdentityType::User)) - .filter(asset_permissions::deleted_at.is_null()) - .load::(&mut conn) - .await - .unwrap(); - - assert_eq!(permissions.len(), 1); - assert_eq!(permissions[0].role, AssetPermissionRole::CanView); -} \ No newline at end of file diff --git a/api/tests/integration/metrics/sharing/delete_sharing_test.rs b/api/tests/integration/metrics/sharing/delete_sharing_test.rs deleted file mode 100644 index 579ce2611..000000000 --- a/api/tests/integration/metrics/sharing/delete_sharing_test.rs +++ /dev/null @@ -1,207 +0,0 @@ -use chrono::Utc; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{AssetPermission, MetricFile, User}, - pool::get_pg_pool, - schema::{asset_permissions, metric_files, users}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use http::StatusCode; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - http::client::test_client, - fixtures::builder::{TestFixtureBuilder, TestUser}, -}; - -#[tokio::test] -async fn test_delete_metric_sharing_success() -> anyhow::Result<()> { - // Create a test fixture with a metric and users - let mut builder = TestFixtureBuilder::new(); - let user = builder.create_user().await?; - let other_user = builder.create_user().await?; - - // Create a test metric - let metric_id = Uuid::new_v4(); - let metric = create_test_metric(&user, metric_id).await?; - - // Create a sharing permission - create_test_permission(metric_id, other_user.id, AssetPermissionRole::CanView).await?; - - // Create a test client with the owner's session - let client = test_client(&user.email).await?; - - // Make the request to delete sharing permissions - let response = client - .delete(&format!("/metrics/{}/sharing", metric_id)) - .json(&json!({ - "emails": [other_user.email] - })) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::OK); - - // Verify the permission is soft-deleted in the database - let mut conn = get_pg_pool().get().await?; - let deleted_permissions = asset_permissions::table - .filter(asset_permissions::asset_id.eq(metric_id)) - .filter(asset_permissions::asset_type.eq(AssetType::MetricFile)) - .filter(asset_permissions::identity_id.eq(other_user.id)) - .filter(asset_permissions::deleted_at.is_not_null()) - .load::(&mut conn) - .await?; - - assert_eq!(deleted_permissions.len(), 1); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metric_sharing_not_found() -> anyhow::Result<()> { - // Create a test fixture with a user - let mut builder = TestFixtureBuilder::new(); - let user = builder.create_user().await?; - - // Create a test client with the user's session - let client = test_client(&user.email).await?; - - // Make the request with a random non-existent metric ID - let response = client - .delete(&format!("/metrics/{}/sharing", Uuid::new_v4())) - .json(&json!({ - "emails": ["test@example.com"] - })) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metric_sharing_forbidden() -> anyhow::Result<()> { - // Create a test fixture with users - let mut builder = TestFixtureBuilder::new(); - let owner = builder.create_user().await?; - let other_user = builder.create_user().await?; // User without permission to modify sharing - let third_user = builder.create_user().await?; // User with view permission - - // Create a test metric - let metric_id = Uuid::new_v4(); - let metric = create_test_metric(&owner, metric_id).await?; - - // Create a sharing permission for the third user - create_test_permission(metric_id, third_user.id, AssetPermissionRole::CanView).await?; - - // Create a test client with the unauthorized user's session - let client = test_client(&other_user.email).await?; - - // Make the request with the metric ID - let response = client - .delete(&format!("/metrics/{}/sharing", metric_id)) - .json(&json!({ - "emails": [third_user.email] - })) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::FORBIDDEN); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_metric_sharing_non_existent_permission() -> anyhow::Result<()> { - // Create a test fixture with users - let mut builder = TestFixtureBuilder::new(); - let user = builder.create_user().await?; - let other_user = builder.create_user().await?; // User who doesn't have a share - - // Create a test metric - let metric_id = Uuid::new_v4(); - let metric = create_test_metric(&user, metric_id).await?; - - // Create a test client with the owner's session - let client = test_client(&user.email).await?; - - // Make the request to delete a non-existent sharing permission - let response = client - .delete(&format!("/metrics/{}/sharing", metric_id)) - .json(&json!({ - "emails": [other_user.email] - })) - .send() - .await?; - - // The API should still return 200 OK even if the permission doesn't exist - assert_eq!(response.status(), StatusCode::OK); - - Ok(()) -} - -// Helper function to create a test metric -async fn create_test_metric(user: &TestUser, id: Uuid) -> anyhow::Result { - let mut conn = get_pg_pool().get().await?; - - let metric = MetricFile { - id, - organization_id: user.organization_id, - created_by: user.id, - file_name: "test_metric.yml".to_string(), - file_content: Some(json!({ - "title": "Test Metric", - "description": "Test Description", - "time_frame": "last 30 days", - "dataset_ids": [], - "chart_config": {} - }).to_string()), - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; - - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await?; - - Ok(metric) -} - -// Helper function to create a test permission -async fn create_test_permission(asset_id: Uuid, user_id: Uuid, role: AssetPermissionRole) -> anyhow::Result<()> { - let mut conn = get_pg_pool().get().await?; - - // Ensure the user_id exists - let user = users::table - .filter(users::id.eq(user_id)) - .first::(&mut conn) - .await?; - - // Create the permission - let permission_id = Uuid::new_v4(); - diesel::insert_into(asset_permissions::table) - .values(( - asset_permissions::id.eq(permission_id), - asset_permissions::asset_id.eq(asset_id), - asset_permissions::asset_type.eq(AssetType::MetricFile), - asset_permissions::identity_id.eq(user_id), - asset_permissions::identity_type.eq(IdentityType::User), - asset_permissions::role.eq(role), - asset_permissions::created_at.eq(Utc::now()), - asset_permissions::updated_at.eq(Utc::now()), - asset_permissions::created_by.eq(user_id), - asset_permissions::updated_by.eq(user_id), - )) - .execute(&mut conn) - .await?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/metrics/sharing/list_sharing_test.rs b/api/tests/integration/metrics/sharing/list_sharing_test.rs deleted file mode 100644 index 1e4dff05b..000000000 --- a/api/tests/integration/metrics/sharing/list_sharing_test.rs +++ /dev/null @@ -1,181 +0,0 @@ -use chrono::Utc; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType, Verification}, - models::{MetricFile, User}, - pool::get_pg_pool, - schema::{asset_permissions, metric_files, users}, - types::{MetricYml, VersionHistory}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use http::StatusCode; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - http::client::test_client, - fixtures::builder::{TestFixtureBuilder, TestUser}, -}; - -#[tokio::test] -async fn test_list_metric_sharing() -> anyhow::Result<()> { - // Create a test fixture with a metric and users - let mut builder = TestFixtureBuilder::new(); - let user = builder.create_user().await?; - let other_user = builder.create_user().await?; - - // Create a test metric - let metric_id = Uuid::new_v4(); - let metric = create_test_metric(&user, metric_id).await?; - - // Create a sharing permission - create_test_permission(metric_id, other_user.id, AssetPermissionRole::CanView).await?; - - // Create a test client with the user's session - let client = test_client(&user.email).await?; - - // Make the request to list sharing permissions - let response = client - .get(&format!("/metrics/{}/sharing", metric_id)) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::OK); - - // Parse the response - let response_json: serde_json::Value = response.json().await?; - let permissions = response_json.get("data") - .and_then(|d| d.get("permissions")) - .and_then(|p| p.as_array()) - .unwrap_or(&vec![]); - - // Assert there's at least one permission entry - assert!(!permissions.is_empty()); - - // Assert the permission entry has the expected structure - let permission = &permissions[0]; - assert!(permission.get("user_id").is_some()); - assert!(permission.get("email").is_some()); - assert!(permission.get("role").is_some()); - - Ok(()) -} - -#[tokio::test] -async fn test_list_metric_sharing_not_found() -> anyhow::Result<()> { - // Create a test fixture with a user - let mut builder = TestFixtureBuilder::new(); - let user = builder.create_user().await?; - - // Create a test client with the user's session - let client = test_client(&user.email).await?; - - // Make the request with a random non-existent metric ID - let response = client - .get(&format!("/metrics/{}/sharing", Uuid::new_v4())) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - Ok(()) -} - -#[tokio::test] -async fn test_list_metric_sharing_forbidden() -> anyhow::Result<()> { - // Create a test fixture with users - let mut builder = TestFixtureBuilder::new(); - let owner = builder.create_user().await?; - let other_user = builder.create_user().await?; // User without access - - // Create a test metric - let metric_id = Uuid::new_v4(); - let metric = create_test_metric(&owner, metric_id).await?; - - // Create a test client with the unauthorized user's session - let client = test_client(&other_user.email).await?; - - // Make the request with the metric ID - let response = client - .get(&format!("/metrics/{}/sharing", metric_id)) - .send() - .await?; - - // Assert the response status - assert_eq!(response.status(), StatusCode::FORBIDDEN); - - Ok(()) -} - -// Helper function to create a test metric -async fn create_test_metric(user: &TestUser, id: Uuid) -> Result { - let mut conn = get_pg_pool().get().await?; - - let metric = MetricFile { - id, - name: "Test Metric".to_string(), - file_name: "test_metric.yml".to_string(), - content: MetricYml { - title: "Test Metric".to_string(), - description: Some("Test Description".to_string()), - filter: None, - time_frame: "last 30 days".to_string(), - dataset_ids: vec![], - chart_config: json!({}), - data_metadata: None, - }, - verification: Verification::Verified, - evaluation_obj: None, - evaluation_summary: None, - evaluation_score: None, - organization_id: user.organization_id, - created_by: user.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - publicly_accessible: false, - publicly_enabled_by: None, - public_expiry_date: None, - version_history: VersionHistory::default(), - }; - - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await?; - - Ok(metric) -} - -// Helper function to create a test permission -async fn create_test_permission(asset_id: Uuid, user_id: Uuid, role: AssetPermissionRole) -> Result<()> { - let mut conn = get_pg_pool().get().await?; - - // Ensure the user_id exists - let user = users::table - .filter(users::id.eq(user_id)) - .first::(&mut conn) - .await?; - - // Create the permission - let permission_id = Uuid::new_v4(); - diesel::insert_into(asset_permissions::table) - .values(( - asset_permissions::id.eq(permission_id), - asset_permissions::asset_id.eq(asset_id), - asset_permissions::asset_type.eq(AssetType::MetricFile), - asset_permissions::identity_id.eq(user_id), - asset_permissions::identity_type.eq(IdentityType::User), - asset_permissions::role.eq(role), - asset_permissions::created_at.eq(Utc::now()), - asset_permissions::updated_at.eq(Utc::now()), - asset_permissions::created_by.eq(user_id), - asset_permissions::updated_by.eq(user_id), - )) - .execute(&mut conn) - .await?; - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/metrics/sharing/mod.rs b/api/tests/integration/metrics/sharing/mod.rs deleted file mode 100644 index bd317d6e2..000000000 --- a/api/tests/integration/metrics/sharing/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod list_sharing_test; -pub mod create_sharing_test; -pub mod delete_sharing_test; -pub mod update_sharing_test; diff --git a/api/tests/integration/metrics/sharing/update_sharing_test.rs b/api/tests/integration/metrics/sharing/update_sharing_test.rs deleted file mode 100644 index e75a943c5..000000000 --- a/api/tests/integration/metrics/sharing/update_sharing_test.rs +++ /dev/null @@ -1,400 +0,0 @@ -use axum::http::StatusCode; -use chrono::Utc; -use database::{ - enums::{AssetPermissionRole, AssetType, IdentityType}, - models::{AssetPermission}, - pool::get_pg_pool, - schema::{users, metric_files, asset_permissions}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use serde_json::json; -use uuid::Uuid; - -use crate::common::{ - http::client::TestClient, - fixtures::{users::create_test_user, metrics::create_test_metric_file}, -}; - -#[tokio::test] -async fn test_update_metric_sharing_success() { - // Setup test database connection - let mut conn = get_pg_pool().get().await.unwrap(); - - // 1. Create test owner and shared user - let owner = create_test_user("owner@example.com"); - let org_id = Uuid::new_v4(); // Need org id for fixture - diesel::insert_into(users::table) - .values(&owner) - .execute(&mut conn) - .await - .unwrap(); - - let shared_user = create_test_user("shared@example.com"); - diesel::insert_into(users::table) - .values(&shared_user) - .execute(&mut conn) - .await - .unwrap(); - - // 2. Create test metric - let metric = create_test_metric_file(&owner.id, &org_id, None); - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await - .unwrap(); - - // 3. Create owner permission for test user - let now = Utc::now(); - let owner_permission = AssetPermission { - identity_id: owner.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::Owner, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - // 4. Create existing CanView permission for shared user - let shared_permission = AssetPermission { - identity_id: shared_user.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::CanView, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - // Insert both permissions - diesel::insert_into(asset_permissions::table) - .values(&vec![owner_permission, shared_permission]) - .execute(&mut conn) - .await - .unwrap(); - - // 5. Create test client - let client = TestClient::new().await; - - // 6. Send update sharing request to change role from CanView to CanEdit - let response = client - .put(&format!("/metrics/{}/sharing", metric.id)) - .with_auth(&owner.id.to_string()) - .json(&json!({ - "users": [ - { - "email": "shared@example.com", - "role": "CanEdit" - } - ] - })) - .send() - .await; - - // 7. Assert response - assert_eq!(response.status(), StatusCode::OK); - - // 8. Check that permission was updated - let permissions = asset_permissions::table - .filter(asset_permissions::asset_id.eq(metric.id)) - .filter(asset_permissions::identity_id.eq(shared_user.id)) - .filter(asset_permissions::asset_type.eq(AssetType::MetricFile)) - .filter(asset_permissions::identity_type.eq(IdentityType::User)) - .filter(asset_permissions::deleted_at.is_null()) - .load::(&mut conn) - .await - .unwrap(); - - assert_eq!(permissions.len(), 1); - assert_eq!(permissions[0].role, AssetPermissionRole::CanEdit); // Role should be updated -} - -#[tokio::test] -async fn test_update_metric_sharing_unauthorized() { - // Setup test database connection - let mut conn = get_pg_pool().get().await.unwrap(); - - // 1. Create test owner, shared user, and unauthorized user - let owner = create_test_user("owner@example.com"); - let org_id = Uuid::new_v4(); - diesel::insert_into(users::table) - .values(&owner) - .execute(&mut conn) - .await - .unwrap(); - - let shared_user = create_test_user("shared@example.com"); - diesel::insert_into(users::table) - .values(&shared_user) - .execute(&mut conn) - .await - .unwrap(); - - let unauthorized_user = create_test_user("unauthorized@example.com"); - diesel::insert_into(users::table) - .values(&unauthorized_user) - .execute(&mut conn) - .await - .unwrap(); - - // 2. Create test metric - let metric = create_test_metric_file(&owner.id, &org_id, None); - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await - .unwrap(); - - // 3. Create owner permission for owner and CanView for shared user - let now = Utc::now(); - let owner_permission = AssetPermission { - identity_id: owner.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::Owner, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - let shared_permission = AssetPermission { - identity_id: shared_user.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::CanView, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - diesel::insert_into(asset_permissions::table) - .values(&vec![owner_permission, shared_permission]) - .execute(&mut conn) - .await - .unwrap(); - - // 4. Create test client - let client = TestClient::new().await; - - // 5. Send update sharing request as unauthorized user - let response = client - .put(&format!("/metrics/{}/sharing", metric.id)) - .with_auth(&unauthorized_user.id.to_string()) - .json(&json!({ - "users": [ - { - "email": "shared@example.com", - "role": "CanEdit" - } - ] - })) - .send() - .await; - - // 6. Assert response is forbidden - assert_eq!(response.status(), StatusCode::FORBIDDEN); - - // 7. Check that permission was not updated - let permissions = asset_permissions::table - .filter(asset_permissions::asset_id.eq(metric.id)) - .filter(asset_permissions::identity_id.eq(shared_user.id)) - .filter(asset_permissions::asset_type.eq(AssetType::MetricFile)) - .filter(asset_permissions::identity_type.eq(IdentityType::User)) - .filter(asset_permissions::deleted_at.is_null()) - .load::(&mut conn) - .await - .unwrap(); - - assert_eq!(permissions.len(), 1); - assert_eq!(permissions[0].role, AssetPermissionRole::CanView); // Role should not change -} - -#[tokio::test] -async fn test_update_metric_sharing_invalid_email() { - // Setup test database connection - let mut conn = get_pg_pool().get().await.unwrap(); - - // 1. Create test owner - let owner = create_test_user("owner@example.com"); - let org_id = Uuid::new_v4(); - diesel::insert_into(users::table) - .values(&owner) - .execute(&mut conn) - .await - .unwrap(); - - // 2. Create test metric - let metric = create_test_metric_file(&owner.id, &org_id, None); - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await - .unwrap(); - - // 3. Create owner permission - let now = Utc::now(); - let owner_permission = AssetPermission { - identity_id: owner.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::Owner, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - diesel::insert_into(asset_permissions::table) - .values(&owner_permission) - .execute(&mut conn) - .await - .unwrap(); - - // 4. Create test client - let client = TestClient::new().await; - - // 5. Send update sharing request with invalid email - let response = client - .put(&format!("/metrics/{}/sharing", metric.id)) - .with_auth(&owner.id.to_string()) - .json(&json!({ - "users": [ - { - "email": "invalid-email-format", - "role": "CanView" - } - ] - })) - .send() - .await; - - // 6. Assert response is bad request - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -async fn test_update_metric_sharing_nonexistent_metric() { - // Setup test database connection - let mut conn = get_pg_pool().get().await.unwrap(); - - // 1. Create test user - let user = create_test_user("user@example.com"); - diesel::insert_into(users::table) - .values(&user) - .execute(&mut conn) - .await - .unwrap(); - - // 2. Create test client - let client = TestClient::new().await; - - // 3. Send update sharing request with non-existent metric id - let nonexistent_id = Uuid::new_v4(); - let response = client - .put(&format!("/metrics/{}/sharing", nonexistent_id)) - .with_auth(&user.id.to_string()) - .json(&json!({ - "users": [ - { - "email": "share@example.com", - "role": "CanView" - } - ] - })) - .send() - .await; - - // 4. Assert response is not found - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[tokio::test] -async fn test_update_metric_public_sharing() { - // Setup test database connection - let mut conn = get_pg_pool().get().await.unwrap(); - - // 1. Create test owner - let owner = create_test_user("owner@example.com"); - let org_id = Uuid::new_v4(); - diesel::insert_into(users::table) - .values(&owner) - .execute(&mut conn) - .await - .unwrap(); - - // 2. Create test metric - let metric = create_test_metric_file(&owner.id, &org_id, None); - diesel::insert_into(metric_files::table) - .values(&metric) - .execute(&mut conn) - .await - .unwrap(); - - // 3. Create owner permission - let now = Utc::now(); - let owner_permission = AssetPermission { - identity_id: owner.id, - identity_type: IdentityType::User, - asset_id: metric.id, - asset_type: AssetType::MetricFile, - role: AssetPermissionRole::Owner, - created_at: now, - updated_at: now, - deleted_at: None, - created_by: owner.id, - updated_by: owner.id, - }; - - diesel::insert_into(asset_permissions::table) - .values(&owner_permission) - .execute(&mut conn) - .await - .unwrap(); - - // 4. Create test client - let client = TestClient::new().await; - - // Set expiration date to 7 days from now - let expiration_date = (Utc::now() + chrono::Duration::days(7)).to_rfc3339(); - - // 5. Send update sharing request with public access settings - let response = client - .put(&format!("/metrics/{}/sharing", metric.id)) - .with_auth(&owner.id.to_string()) - .json(&json!({ - "publicly_accessible": true, - "public_expiration": expiration_date - })) - .send() - .await; - - // 6. Assert response is successful - assert_eq!(response.status(), StatusCode::OK); - - // 7. Check that the metric was updated with public access settings - let updated_metric = metric_files::table - .find(metric.id) - .first::(&mut conn) - .await - .unwrap(); - - assert!(updated_metric.publicly_accessible); - assert_eq!(updated_metric.publicly_enabled_by, Some(owner.id)); - assert!(updated_metric.public_expiry_date.is_some()); -} \ No newline at end of file diff --git a/api/tests/integration/metrics/update_metric_test.rs b/api/tests/integration/metrics/update_metric_test.rs deleted file mode 100644 index 2c27f0a80..000000000 --- a/api/tests/integration/metrics/update_metric_test.rs +++ /dev/null @@ -1,205 +0,0 @@ -use anyhow::Result; -use database::{ - enums::Verification, - models::MetricFile, - pool::get_pg_pool, - schema::metric_files, - types::{MetricYml, VersionHistory}, -}; -use diesel::{ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; -use handlers::metrics::{update_metric_handler, UpdateMetricRequest}; -use serde_json::Value; -use tokio; -use uuid::Uuid; - -use crate::common::{ - db::TestDb, - env::setup_test_env, - fixtures::{create_test_metric_file, create_test_user, create_update_metric_request, create_restore_metric_request}, -}; - -#[tokio::test] -async fn test_update_metric_handler() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test metric - let test_metric = create_test_metric_file(&user_id, &org_id, Some("Test Metric".to_string())); - let metric_id = test_metric.id; - - // Insert test metric into database - diesel::insert_into(metric_files::table) - .values(&test_metric) - .execute(&mut conn) - .await?; - - // Create update request - let update_json = create_update_metric_request(); - let update_request: UpdateMetricRequest = serde_json::from_value(update_json)?; - - // Call the handler being tested - let updated_metric = update_metric_handler(&metric_id, &user_id, update_request).await?; - - // Fetch the updated metric from the database - let db_metric = metric_files::table - .filter(metric_files::id.eq(metric_id)) - .first::(&mut conn) - .await?; - - // Verify the results - assert_eq!(updated_metric.id, metric_id); - assert_eq!(updated_metric.name, "Updated Test Metric"); - assert_eq!(db_metric.name, "Updated Test Metric"); - assert_eq!(db_metric.verification, Verification::Verified); - - // Verify content updates (time_frame and description) - let content: Value = db_metric.content; - assert_eq!(content["time_frame"].as_str().unwrap(), "weekly"); - assert_eq!(content["description"].as_str().unwrap(), "Updated test description"); - - // Verify version history has been updated - assert!(db_metric.version_history.versions.len() > 1); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_metric_version() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test metric with initial version - let test_metric = create_test_metric_file(&user_id, &org_id, Some("Original Metric".to_string())); - let metric_id = test_metric.id; - - // Insert test metric into database - diesel::insert_into(metric_files::table) - .values(&test_metric) - .execute(&mut conn) - .await?; - - // Update the metric to create version 2 - let update_json = create_update_metric_request(); - let update_request: UpdateMetricRequest = serde_json::from_value(update_json)?; - update_metric_handler(&metric_id, &user_id, update_request).await?; - - // Fetch the metric to verify we have 2 versions - let db_metric = metric_files::table - .filter(metric_files::id.eq(metric_id)) - .first::(&mut conn) - .await?; - - assert_eq!(db_metric.version_history.versions.len(), 2); - assert_eq!(db_metric.name, "Updated Test Metric"); - - // Create restore request to restore to version 1 - let restore_json = create_restore_metric_request(1); - let restore_request: UpdateMetricRequest = serde_json::from_value(restore_json)?; - - // Restore to version 1 - let restored_metric = update_metric_handler(&metric_id, &user_id, restore_request).await?; - - // Fetch the restored metric from the database - let db_metric_after_restore = metric_files::table - .filter(metric_files::id.eq(metric_id)) - .first::(&mut conn) - .await?; - - // Verify we now have 3 versions - assert_eq!(db_metric_after_restore.version_history.versions.len(), 3); - - // Verify the restored content matches the original version - let content: Value = db_metric_after_restore.content; - assert_eq!(content["name"].as_str().unwrap(), "Original Metric"); - assert_eq!(content["time_frame"].as_str().unwrap(), "daily"); - - // Verify the restored metric in response matches as well - assert_eq!(restored_metric.name, "Original Metric"); - assert_eq!(restored_metric.versions.len(), 3); - - Ok(()) -} - -#[tokio::test] -async fn test_restore_metric_nonexistent_version() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let test_db = TestDb::new().await?; - let mut conn = test_db.get_conn().await?; - - // Create test user and organization - let user_id = Uuid::new_v4(); - let org_id = Uuid::new_v4(); - - // Create test metric - let test_metric = create_test_metric_file(&user_id, &org_id, Some("Test Metric".to_string())); - let metric_id = test_metric.id; - - // Insert test metric into database - diesel::insert_into(metric_files::table) - .values(&test_metric) - .execute(&mut conn) - .await?; - - // Create restore request with a non-existent version number - let restore_json = create_restore_metric_request(999); - let restore_request: UpdateMetricRequest = serde_json::from_value(restore_json)?; - - // Attempt to restore to non-existent version - let result = update_metric_handler(&metric_id, &user_id, restore_request).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("Version 999 not found")); - - Ok(()) -} - -#[tokio::test] -async fn test_update_metric_handler_not_found() -> Result<()> { - // Setup test environment - setup_test_env(); - - // Initialize test database - let _test_db = TestDb::new().await?; - - // Create test user - let user_id = Uuid::new_v4(); - - // Use a random UUID that doesn't exist - let nonexistent_metric_id = Uuid::new_v4(); - - // Create update request - let update_json = create_update_metric_request(); - let update_request: UpdateMetricRequest = serde_json::from_value(update_json)?; - - // Call the handler being tested - should fail - let result = update_metric_handler(&nonexistent_metric_id, &user_id, update_request).await; - - // Verify the error - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - assert!(error.contains("not found") || error.contains("NotFound")); - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/mod.rs b/api/tests/integration/mod.rs deleted file mode 100644 index 558839caf..000000000 --- a/api/tests/integration/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Export test modules -pub mod chats; -pub mod dashboards; -pub mod collections; -pub mod data_sources; -pub mod favorites; -pub mod metrics; -pub mod organizations; -pub mod routes; -pub mod threads_and_messages; \ No newline at end of file diff --git a/api/tests/integration/organizations/mod.rs b/api/tests/integration/organizations/mod.rs deleted file mode 100644 index d48716019..000000000 --- a/api/tests/integration/organizations/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod update_organization_test; \ No newline at end of file diff --git a/api/tests/integration/organizations/update_organization_test.rs b/api/tests/integration/organizations/update_organization_test.rs deleted file mode 100644 index 6a9f2ac7a..000000000 --- a/api/tests/integration/organizations/update_organization_test.rs +++ /dev/null @@ -1,165 +0,0 @@ -#[cfg(test)] -mod tests { - use axum::{ - http::{Request, StatusCode}, - routing::put, - Router, - }; - use axum_test::{TestServer, TestServerConfig}; - use serde_json::json; - use uuid::Uuid; - - use database::enums::UserOrganizationRole; - use middleware::{AuthenticatedUser, OrganizationMembership}; - use crate::routes::rest::routes::organizations::update_organization::update_organization; - - #[tokio::test] - async fn test_update_organization_success() { - // Skip this test in CI because it requires database access - if std::env::var("CI").is_ok() { - return; - } - - // Setup mock user with WorkspaceAdmin role - let org_id = Uuid::new_v4(); - let user_id = Uuid::new_v4(); - let user = AuthenticatedUser { - id: user_id, - email: "test@example.com".to_string(), - name: Some("Test User".to_string()), - config: json!({}), - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - attributes: json!({}), - avatar_url: None, - organizations: vec![OrganizationMembership { - id: org_id, - role: UserOrganizationRole::WorkspaceAdmin, - }], - teams: vec![], - }; - - // Create router with the update_organization endpoint - let app = Router::new() - .route("/:id", put(update_organization)) - .layer(axum::middleware::map_extension(move |_: ()| user.clone())); - - // Create test server - let config = TestServerConfig::builder() - .default_content_type("application/json") - .build(); - let server = TestServer::new_with_config(app, config).unwrap(); - - // Make request to update organization name - let response = server - .put(&format!("/{}", org_id)) - .json(&json!({ - "name": "Updated Organization Name" - })) - .await; - - // Assertions - assert_eq!(response.status_code(), StatusCode::OK); - - // Verify response contains updated name - let response_json = response.json::(); - assert_eq!(response_json["data"]["name"], "Updated Organization Name"); - assert_eq!(response_json["data"]["id"], org_id.to_string()); - } - - #[tokio::test] - async fn test_update_organization_not_admin() { - // Skip this test in CI because it requires database access - if std::env::var("CI").is_ok() { - return; - } - - // Setup mock user with non-admin role - let org_id = Uuid::new_v4(); - let user_id = Uuid::new_v4(); - let user = AuthenticatedUser { - id: user_id, - email: "test@example.com".to_string(), - name: Some("Test User".to_string()), - config: json!({}), - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - attributes: json!({}), - avatar_url: None, - organizations: vec![OrganizationMembership { - id: org_id, - role: UserOrganizationRole::Viewer, // Non-admin role - }], - teams: vec![], - }; - - // Create router with the update_organization endpoint - let app = Router::new() - .route("/:id", put(update_organization)) - .layer(axum::middleware::map_extension(move |_: ()| user.clone())); - - // Create test server - let config = TestServerConfig::builder() - .default_content_type("application/json") - .build(); - let server = TestServer::new_with_config(app, config).unwrap(); - - // Make request to update organization name - let response = server - .put(&format!("/{}", org_id)) - .json(&json!({ - "name": "Updated Organization Name" - })) - .await; - - // Assert forbidden status - assert_eq!(response.status_code(), StatusCode::FORBIDDEN); - } - - #[tokio::test] - async fn test_update_organization_no_fields() { - // Skip this test in CI because it requires database access - if std::env::var("CI").is_ok() { - return; - } - - // Setup mock user - let org_id = Uuid::new_v4(); - let user_id = Uuid::new_v4(); - let user = AuthenticatedUser { - id: user_id, - email: "test@example.com".to_string(), - name: Some("Test User".to_string()), - config: json!({}), - created_at: chrono::Utc::now(), - updated_at: chrono::Utc::now(), - attributes: json!({}), - avatar_url: None, - organizations: vec![OrganizationMembership { - id: org_id, - role: UserOrganizationRole::WorkspaceAdmin, - }], - teams: vec![], - }; - - // Create router with the update_organization endpoint - let app = Router::new() - .route("/:id", put(update_organization)) - .layer(axum::middleware::map_extension(move |_: ()| user.clone())); - - // Create test server - let config = TestServerConfig::builder() - .default_content_type("application/json") - .build(); - let server = TestServer::new_with_config(app, config).unwrap(); - - // Make request with empty payload - let response = server - .put(&format!("/{}", org_id)) - .json(&json!({})) - .await; - - // Assert bad request status - assert_eq!(response.status_code(), StatusCode::BAD_REQUEST); - } -} \ No newline at end of file diff --git a/api/tests/integration/routes/mod.rs b/api/tests/integration/routes/mod.rs deleted file mode 100644 index 9715f2c60..000000000 --- a/api/tests/integration/routes/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod rest; \ No newline at end of file diff --git a/api/tests/integration/routes/rest/mod.rs b/api/tests/integration/routes/rest/mod.rs deleted file mode 100644 index e092c2402..000000000 --- a/api/tests/integration/routes/rest/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod search; \ No newline at end of file diff --git a/api/tests/integration/routes/rest/search/mod.rs b/api/tests/integration/routes/rest/search/mod.rs deleted file mode 100644 index cc0958fd0..000000000 --- a/api/tests/integration/routes/rest/search/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod search_test; \ No newline at end of file diff --git a/api/tests/integration/routes/rest/search/search_test.rs b/api/tests/integration/routes/rest/search/search_test.rs deleted file mode 100644 index 8f25b8b40..000000000 --- a/api/tests/integration/routes/rest/search/search_test.rs +++ /dev/null @@ -1,89 +0,0 @@ -use axum::{ - body::Body, - http::{Request, StatusCode}, - Router, -}; -use serde_json::json; -use tower::ServiceExt; -use uuid::Uuid; - -use middleware::{AuthenticatedUser, User}; -use search::SearchObjectType; - -// This is a basic test structure that would need to be extended with -// proper mocking of the database and search functionality -async fn setup_test_app() -> Router { - // In a real test, we would initialize the database and set up the - // necessary data for the test. For this example, we just set up the router. - crate::routes::rest::router() -} - -#[tokio::test] -async fn test_search_endpoint_unauthorized() { - // We need a more comprehensive test setup with proper mocking - // This is a skeleton for when we have the full environment - - let app = setup_test_app().await; - - // Create a request without authentication - let request = Request::builder() - .uri("/search?query=test") - .body(Body::empty()) - .unwrap(); - - // In a real test with proper environment, we would expect: - // let response = app.oneshot(request).await.unwrap(); - // assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - - // In this test, we just check that the app can be created - assert!(true, "App setup successful"); -} - -#[tokio::test] -async fn test_search_endpoint_with_query() { - // We need a more comprehensive test setup with proper mocking - // This is a skeleton for when we have the full environment - - let app = setup_test_app().await; - - // In a real test, we would mock the auth middleware and create a request - // with proper authentication like this: - // - // let user = User { - // id: Uuid::new_v4(), - // email: "test@example.com".to_string(), - // // Include other required fields - // }; - // - // let auth_user = AuthenticatedUser { - // id: user.id, - // email: user.email.clone(), - // // Include other required fields - // }; - // - // let request = Request::builder() - // .uri("/search?query=test&asset_types[]=thread") - // .extension(auth_user) - // .body(Body::empty()) - // .unwrap(); - // - // let response = app.oneshot(request).await.unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - - // In this test, we just check that the app can be created - assert!(true, "App setup successful"); -} - -#[tokio::test] -async fn test_search_endpoint_empty_query() { - // We need a more comprehensive test setup with proper mocking - // This is a skeleton for when we have the full environment - - let app = setup_test_app().await; - - // In a real test, we'd set up auth and make a request as above - // but with an empty query to test that it returns recent items - - // In this test, we just check that the app can be created - assert!(true, "App setup successful"); -} \ No newline at end of file diff --git a/api/tests/integration/threads_and_messages/agent_thread_test.rs b/api/tests/integration/threads_and_messages/agent_thread_test.rs deleted file mode 100644 index 886315590..000000000 --- a/api/tests/integration/threads_and_messages/agent_thread_test.rs +++ /dev/null @@ -1,163 +0,0 @@ -use anyhow::Result; -use chrono::Utc; -use serde_json::json; -use uuid::Uuid; - -use crate::database::{ - models::{Message, Thread, User}, - schema::{messages, messages_to_files, metric_files, threads}, -}; -use crate::routes::ws::threads_and_messages::post_thread::{ - agent_message_transformer::{BusterContainer, ReasoningMessage}, - agent_thread::AgentThreadHandler, -}; -use crate::tests::common::{db::TestDb, env::setup_test_env}; -use crate::utils::clients::ai::litellm::Message as AgentMessage; - -async fn setup_test_thread(test_db: &TestDb, user: &AuthenticatedUser) -> Result<(Thread, Message)> { - let thread_id = Uuid::new_v4(); - let message_id = Uuid::new_v4(); - - // Create thread - let thread = Thread { - id: thread_id, - title: "Test Thread".to_string(), - organization_id: Uuid::parse_str(&user.attributes["organization_id"].as_str().unwrap())?, - created_by: user.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; - - diesel::insert_into(threads::table) - .values(&thread) - .execute(&mut test_db.pool.get().await?) - .await?; - - // Create initial message - let message = Message { - id: message_id, - request: "test request".to_string(), - response: json!({}), - thread_id, - created_by: user.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; - - diesel::insert_into(messages::table) - .values(&message) - .execute(&mut test_db.pool.get().await?) - .await?; - - Ok((thread, message)) -} - -#[tokio::test] -async fn test_end_to_end_agent_thread_flow() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Setup test thread and message - let (thread, message) = setup_test_thread(&test_db, &user).await?; - - // Create agent handler - let handler = AgentThreadHandler::new()?; - - // Create test request - let request = ChatCreateNewChat { - prompt: "Test prompt".to_string(), - chat_id: Some(thread.id), - message_id: Some(message.id), - }; - - // Process request - handler.handle_request(request, user.clone()).await?; - - // Verify final state - let stored_message = messages::table - .filter(messages::id.eq(message.id)) - .first::(&mut test_db.pool.get().await?) - .await?; - - // Message should be updated with final state - assert!(!stored_message.response.as_array().unwrap().is_empty()); - - Ok(()) -} - -#[tokio::test] -async fn test_file_creation_and_linking() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Setup test thread and message - let (thread, message) = setup_test_thread(&test_db, &user).await?; - - // Create test messages with file creation - let transformed_messages = vec![ - BusterContainer::ReasoningMessage(ReasoningMessage::File(/* create test file message */)), - ]; - - // Store final state - AgentThreadHandler::store_final_message_state( - &message, - transformed_messages, - &thread.organization_id, - &user.id, - ) - .await?; - - // Verify file was created and linked - let file_links = messages_to_files::table - .filter(messages_to_files::message_id.eq(message.id)) - .count() - .get_result::(&mut test_db.pool.get().await?) - .await?; - - assert!(file_links > 0); - - Ok(()) -} - -#[tokio::test] -async fn test_concurrent_agent_threads() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Create multiple threads - let mut handles = vec![]; - let handler = AgentThreadHandler::new()?; - - for i in 0..3 { - let (thread, message) = setup_test_thread(&test_db, &user).await?; - let handler = handler.clone(); - let user = user.clone(); - - let request = ChatCreateNewChat { - prompt: format!("Test prompt {}", i), - chat_id: Some(thread.id), - message_id: Some(message.id), - }; - - let handle = tokio::spawn(async move { - handler.handle_request(request, user).await - }); - - handles.push(handle); - } - - // Wait for all threads to complete - for handle in handles { - handle.await??; - } - - Ok(()) -} \ No newline at end of file diff --git a/api/tests/integration/threads_and_messages/mod.rs b/api/tests/integration/threads_and_messages/mod.rs deleted file mode 100644 index 292ce65b4..000000000 --- a/api/tests/integration/threads_and_messages/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod agent_thread_test; -pub mod post_thread_test; \ No newline at end of file diff --git a/api/tests/integration/threads_and_messages/post_thread_test.rs b/api/tests/integration/threads_and_messages/post_thread_test.rs deleted file mode 100644 index 43b463c4b..000000000 --- a/api/tests/integration/threads_and_messages/post_thread_test.rs +++ /dev/null @@ -1,205 +0,0 @@ -use anyhow::Result; -use database::enums::AssetType; -use handlers::chats::post_chat_handler::ChatCreateNewChat; -use middleware::AuthenticatedUser; -use mockito::{mock, server::MockServer}; -use std::sync::Arc; -use tokio::sync::mpsc; -use uuid::Uuid; - -use crate::{ - routes::ws::{ - threads_and_messages::post_thread, - ws::{WsErrorCode, WsEvent, WsResponseMessage}, - ws_utils::{send_error_message, send_ws_message}, - }, - tests::common::{db::TestDb, env::setup_test_env, fixtures::metrics::create_test_metric_file, fixtures::dashboards::create_test_dashboard_file}, -}; - -/// Mock function to test the error handling in our WebSocket endpoint -async fn mock_send_error_message( - _subscription: &String, - _route: crate::routes::ws::ws_router::WsRoutes, - _event: WsEvent, - _code: WsErrorCode, - _message: String, - _user: &AuthenticatedUser, -) -> Result<()> { - // In a real implementation, this would send an error message - // For testing, we just return Ok - Ok(()) -} - -/// Mock function to test the streaming in our WebSocket endpoint -async fn mock_send_ws_message(_subscription: &String, _message: &WsResponseMessage) -> Result<()> { - // In a real implementation, this would send a WebSocket message - // For testing, we just return Ok - Ok(()) -} - -// Helper to create test chat request with asset -fn create_test_chat_request_with_asset( - asset_id: Uuid, - asset_type: Option, - prompt: Option -) -> ChatCreateNewChat { - ChatCreateNewChat { - prompt, - chat_id: None, - message_id: None, - asset_id: Some(asset_id), - asset_type, - metric_id: None, - dashboard_id: None, - } -} - -// Helper to create test chat request with legacy asset fields -fn create_test_chat_request_with_legacy_fields( - metric_id: Option, - dashboard_id: Option, - prompt: Option -) -> ChatCreateNewChat { - ChatCreateNewChat { - prompt, - chat_id: None, - message_id: None, - asset_id: None, - asset_type: None, - metric_id, - dashboard_id, - } -} - -#[tokio::test] -async fn test_validation_rejects_asset_id_without_type() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Create request with asset_id but no asset_type - let request = create_test_chat_request_with_asset( - Uuid::new_v4(), // Random asset ID - None, // Missing asset_type - None, // No prompt - ); - - // Mock the send_error_message function - we expect validation to fail - // and trigger an error message - let send_error_result = post_thread(&user, request).await; - - // Validation should reject the request - assert!(send_error_result.is_ok(), "Expected validation to reject the request and return OK from sending error"); - - Ok(()) -} - -#[tokio::test] -async fn test_prompt_less_flow_with_asset() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Create a test metric file - let metric_file = create_test_metric_file(&test_db, &user).await?; - - // Create request with asset but no prompt - let request = create_test_chat_request_with_asset( - metric_file.id, - Some(AssetType::MetricFile), - None, // No prompt - ); - - // Process request - let result = post_thread(&user, request).await; - - // No errors should occur - assert!(result.is_ok(), "Expected prompt-less flow to succeed"); - - Ok(()) -} - -#[tokio::test] -async fn test_legacy_asset_fields_support() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Create a test dashboard file - let dashboard_file = create_test_dashboard_file(&test_db, &user).await?; - - // Create request with legacy dashboard_id field - let request = create_test_chat_request_with_legacy_fields( - None, // No metric_id - Some(dashboard_file.id), // Use dashboard_id - Some("Test prompt".to_string()), // With prompt - ); - - // Process request - let result = post_thread(&user, request).await; - - // No errors should occur - assert!(result.is_ok(), "Expected legacy field support to work"); - - Ok(()) -} - -#[tokio::test] -async fn test_with_both_prompt_and_asset() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Create a test metric file - let metric_file = create_test_metric_file(&test_db, &user).await?; - - // Create request with both asset and prompt - let request = create_test_chat_request_with_asset( - metric_file.id, - Some(AssetType::MetricFile), - Some("Test prompt with asset".to_string()), // With prompt - ); - - // Process request - let result = post_thread(&user, request).await; - - // No errors should occur - assert!(result.is_ok(), "Expected prompt + asset flow to succeed"); - - Ok(()) -} - -#[tokio::test] -async fn test_error_handling_during_streaming() -> Result<()> { - // Setup test environment - setup_test_env(); - let test_db = TestDb::new().await?; - let user = test_db.create_test_user().await?; - - // Create a mock server to simulate external dependencies - let mock_server = MockServer::start().await; - - // Create a test chat request - let request = ChatCreateNewChat { - prompt: Some("Test prompt that will cause an error".to_string()), - chat_id: None, - message_id: None, - asset_id: None, - asset_type: None, - metric_id: None, - dashboard_id: None, - }; - - // Process request - assuming our test is set up to trigger an error - // during processing - let result = post_thread(&user, request).await; - - // We still expect the function to return Ok() since errors are handled within - assert!(result.is_ok(), "Expected error handling to contain errors"); - - Ok(()) -} \ No newline at end of file