diff --git a/.github/actions/setup-test-environment/action.yml b/.github/actions/setup-test-environment/action.yml new file mode 100644 index 000000000..1ca6fa15d --- /dev/null +++ b/.github/actions/setup-test-environment/action.yml @@ -0,0 +1,59 @@ +name: 'Setup Test Environment' +description: 'Installs tools, starts Supabase, runs migrations, and seeds the database.' + +runs: + using: "composite" + steps: + - name: Setup Supabase CLI + uses: supabase/setup-cli@v1 + with: + version: latest # Or pin to a specific version + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Cache Rust dependencies + uses: useblacksmith/rust-cache@v3 + + - name: Install Diesel CLI + shell: bash + run: cargo install diesel_cli --no-default-features --features postgres + + - name: Start Supabase + shell: bash + run: | + echo "Starting Supabase..." + supabase start --exclude postgrest,studio + + echo "Waiting a bit for services to stabilize after start..." + sleep 15 # Adjust if needed, Supabase start should block but sometimes a small delay helps + + echo "Checking Supabase status..." + supabase status + if [ $? -ne 0 ]; then + echo "::error::Supabase failed to start correctly." + # Attempt to fetch logs if possible (might not be available easily with setup-cli) + # supabase logs --project-ref local # This might need project-ref + exit 1 + fi + + echo "Supabase started." + + - name: Run Migrations + working-directory: ./api # Assuming migrations are always relative to api + shell: bash + run: diesel migration run + env: + DATABASE_URL: postgres://postgres:postgres@127.0.0.1:54322/postgres + + - name: Seed Database + shell: bash + run: | + # Use hardcoded default credentials for local Supabase + PGPASSWORD=postgres psql -h 127.0.0.1 -p 54322 -U postgres -d postgres -f ./api/libs/database/seed.sql + env: + DATABASE_URL: postgres://postgres:postgres@127.0.0.1:54322/postgres # Also set here just in case seed script needs it \ No newline at end of file diff --git a/.github/actions/stop-supabase/action.yml b/.github/actions/stop-supabase/action.yml new file mode 100644 index 000000000..6888c5a03 --- /dev/null +++ b/.github/actions/stop-supabase/action.yml @@ -0,0 +1,9 @@ +name: 'Stop Supabase' +description: 'Stops the Supabase local environment.' + +runs: + using: "composite" + steps: + - name: Stop Supabase instance + shell: bash + run: supabase stop --no-backup \ No newline at end of file diff --git a/.github/workflows/api-testing.yml b/.github/workflows/api-testing.yml new file mode 100644 index 000000000..9ee4aff8c --- /dev/null +++ b/.github/workflows/api-testing.yml @@ -0,0 +1,73 @@ +name: API Testing + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: blacksmith-32vcpu-ubuntu-2204 + environment: testing + + # Service container for Redis (needed by the setup action) + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Node.js setup removed + + - name: Setup Test Environment + uses: ./.github/actions/setup-test-environment + + - name: Mount Cargo Home Cache (Sticky Disk) + uses: useblacksmith/stickydisk@v1 + with: + key: ${{ github.repository }}-cargo-home # Unique key per repository + path: ~/.cargo # Cache cargo registry/git data + + - name: Mount API Target Cache (Sticky Disk) + uses: useblacksmith/stickydisk@v1 + with: + key: ${{ github.repository }}-api-target # Unique key per repository + path: ./api/target # Cache build artifacts + + - name: Set up Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable # Or specify a version + + - name: Run API Tests + working-directory: ./api # Tests run from the api directory + run: cargo test --workspace # Run tests for all packages in the api workspace + env: + RUST_TEST_THREADS: 24 + # Use hardcoded default values and secrets + DATABASE_URL: postgres://postgres:postgres@127.0.0.1:54322/postgres + REDIS_URL: redis://localhost:6379 # Connect to the Redis service container + JWT_SECRET: 'super-secret-jwt-token-with-at-least-32-characters-long' # Use default local value + SUPABASE_URL: http://127.0.0.1:54321 # Default local URL + SUPABASE_SERVICE_ROLE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MjM0MzA5Nn0.EGgMpd9zvvHPCOq4DJRLwzJ1iS3GV4AEyzguXGcbEIY' # Use default local value + RUST_LOG: debug # Or adjust as needed + + # Sensitive values from Secrets + OPENAI_API_KEY: ${{ secrets.GH_ACTIONS_OPENAI_API_KEY }} + RESEND_API_KEY: ${{ secrets.GH_ACTIONS_RESEND_API_KEY }} + COHERE_API_KEY: ${{ secrets.GH_ACTIONS_COHERE_API_KEY }} + LLM_API_KEY: ${{ secrets.GH_ACTIONS_LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.GH_ACTIONS_LLM_BASE_URL }} + + - name: Stop Supabase # Use the cleanup action + uses: ./.github/actions/stop-supabase + if: always() # Ensure Supabase is stopped even if tests fail diff --git a/.github/workflows/cli-testing.yml b/.github/workflows/cli-testing.yml new file mode 100644 index 000000000..12fb2f0cb --- /dev/null +++ b/.github/workflows/cli-testing.yml @@ -0,0 +1,101 @@ +name: CLI Testing + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: blacksmith-32vcpu-ubuntu-2204 # Using a powerful runner as requested + environment: testing + + # Service containers + services: + # Redis needed by API + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # --- Build API Docker Image with Blacksmith Caching --- + - name: Build and Load API Docker Image + uses: useblacksmith/build-push-action@v1 + with: + context: ./api + file: ./api/Dockerfile + push: false # Do not push, load locally for service container + load: true # Load the image into the runner's Docker daemon + tags: local-api-test:latest # Tag for the service definition + + # --- Setup Supabase Environment on Host --- + - name: Setup Test Environment # Runs Supabase, migrations, seeding on host + uses: ./.github/actions/setup-test-environment + + # --- Start API Container Manually --- + - name: Start API Container + run: | + docker run -d --name local-api -p 3001:3001 \ + --network=host \ + -e DATABASE_URL='postgres://postgres:postgres@127.0.0.1:54322/postgres' \ + -e POOLER_URL='postgres://postgres:postgres@127.0.0.1:54322/postgres' \ + -e REDIS_URL='redis://127.0.0.1:6379' \ + -e JWT_SECRET='super-secret-jwt-token-with-at-least-32-characters-long' \ + -e SUPABASE_URL='http://127.0.0.1:54321' \ + -e SUPABASE_SERVICE_ROLE_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MjM0MzA5Nn0.EGgMpd9zvvHPCOq4DJRLwzJ1iS3GV4AEyzguXGcbEIY' \ + -e ENVIRONMENT='development' \ + -e LOG_LEVEL='debug' \ + -e PORT='3001' \ + -e RUST_LOG='debug' \ + -e OPENAI_API_KEY='${{ secrets.GH_ACTIONS_OPENAI_API_KEY }}' \ + -e RESEND_API_KEY='${{ secrets.GH_ACTIONS_RESEND_API_KEY }}' \ + -e COHERE_API_KEY='${{ secrets.GH_ACTIONS_COHERE_API_KEY }}' \ + -e LLM_API_KEY='${{ secrets.GH_ACTIONS_LLM_API_KEY }}' \ + -e LLM_BASE_URL='${{ secrets.GH_ACTIONS_LLM_BASE_URL }}' \ + local-api-test:latest + + - name: Wait for API Health Check + run: | + echo "Waiting for API to be healthy..." + for i in {1..30}; do # Wait up to 30 seconds + if curl -f http://localhost:3001/health; then + echo "API is healthy!" + exit 0 + fi + sleep 1 + done + echo "API did not become healthy in time." + exit 1 + + - name: Run CLI Tests + working-directory: ./cli # Tests run from the cli directory + run: cargo test --workspace # Run tests for all packages in the cli workspace + env: + RUST_TEST_THREADS: 24 + # Point to services on host (DB/Supabase/Redis) and API container + DATABASE_URL: postgres://postgres:postgres@127.0.0.1:54322/postgres + REDIS_URL: redis://localhost:6379 # Tests run on host, connect to exposed Redis port + JWT_SECRET: 'super-secret-jwt-token-with-at-least-32-characters-long' # Use default local value + SUPABASE_URL: http://127.0.0.1:54321 # Tests run on host + SUPABASE_SERVICE_ROLE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MjM0MzA5Nn0.EGgMpd9zvvHPCOq4DJRLwzJ1iS3GV4AEyzguXGcbEIY' # Use default local value + RUST_LOG: debug # Or adjust as needed + BUSTER_API_URL: http://localhost:3001 # Point CLI tests to the manually started API container + + # Secrets are passed to the API container, not needed directly by CLI tests + + - name: Stop Supabase # Use the cleanup action + uses: ./.github/actions/stop-supabase + if: always() # Ensure Supabase is stopped even if tests fail + + - name: Stop API Container # Cleanup manually started container + if: always() + run: docker stop local-api && docker rm local-api diff --git a/.github/workflows/web-testing.yml b/.github/workflows/web-testing.yml new file mode 100644 index 000000000..76b635f78 --- /dev/null +++ b/.github/workflows/web-testing.yml @@ -0,0 +1,123 @@ +name: Web App E2E Testing + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: blacksmith-32vcpu-ubuntu-2204 + environment: testing + + # Service containers + services: + # Redis needed by API + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js (Blacksmith Cache) + uses: useblacksmith/setup-node@v5 + with: + node-version: '20' + + - name: Mount NPM Cache (Sticky Disk) + uses: useblacksmith/stickydisk@v1 + with: + key: ${{ github.repository }}-npm-cache # Unique key per repository + path: ~/.npm + + - name: Mount node_modules (Sticky Disk) + uses: useblacksmith/stickydisk@v1 + with: + key: ${{ github.repository }}-node-modules # Unique key per repository + path: ./web/node_modules # Mount directly into the web directory's node_modules + + # --- Setup Supabase Environment --- + - name: Setup Test Environment # Runs Supabase, migrations, seeding on host + uses: ./.github/actions/setup-test-environment + + # --- Build API Docker Image with Blacksmith Caching --- + - name: Build and Load API Docker Image + uses: useblacksmith/build-push-action@v1 + with: + context: ./api + file: ./api/Dockerfile + push: false # Do not push, load locally for service container + load: true # Load the image into the runner's Docker daemon + tags: local-api-test:latest # Tag for the service definition + + # --- Start API Container Manually --- + - name: Start API Container + run: | + docker run -d --name local-api -p 3001:3001 \ + --network=host \ + -e DATABASE_URL='postgres://postgres:postgres@127.0.0.1:54322/postgres' \ + -e POOLER_URL='postgres://postgres:postgres@127.0.0.1:54322/postgres' \ + -e REDIS_URL='redis://127.0.0.1:6379' \ + -e JWT_SECRET='super-secret-jwt-token-with-at-least-32-characters-long' \ + -e SUPABASE_URL='http://127.0.0.1:54321' \ + -e SUPABASE_SERVICE_ROLE_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MjM0MzA5Nn0.EGgMpd9zvvHPCOq4DJRLwzJ1iS3GV4AEyzguXGcbEIY' \ + -e ENVIRONMENT='development' \ + -e BUSTER_URL='http://localhost:3000' \ + -e BUSTER_WH_TOKEN='buster-wh-token' \ + -e LOG_LEVEL='debug' \ + -e PORT='3001' \ + -e RUST_LOG='debug' \ + -e OPENAI_API_KEY='${{ secrets.GH_ACTIONS_OPENAI_API_KEY }}' \ + -e RESEND_API_KEY='${{ secrets.GH_ACTIONS_RESEND_API_KEY }}' \ + -e COHERE_API_KEY='${{ secrets.GH_ACTIONS_COHERE_API_KEY }}' \ + -e LLM_API_KEY='${{ secrets.GH_ACTIONS_LLM_API_KEY }}' \ + -e LLM_BASE_URL='${{ secrets.GH_ACTIONS_LLM_BASE_URL }}' \ + local-api-test:latest + + - name: Wait for API Health Check + run: | + echo "Waiting for API to be healthy..." + for i in {1..30}; do # Wait up to 30 seconds + if curl -f http://localhost:3001/health; then + echo "API is healthy!" + exit 0 + fi + sleep 1 + done + echo "API did not become healthy in time." + exit 1 + + - name: Run Frontend E2E Tests + working-directory: ./web + run: | + echo "Running web E2E tests..." + # Add your actual test command here, e.g.: + npm install # Or yarn install, pnpm install + npm run build # If needed + npm run test:e2e + env: + # Point to the API service container + NEXT_PUBLIC_API_URL: http://localhost:3001 + NEXT_PUBLIC_URL: http://localhost:3000 # Assuming default URL for the app itself + # Use Supabase details - pointing to Supabase running on the HOST (127.0.0.1) + NEXT_PUBLIC_SUPABASE_URL: http://127.0.0.1:54321 # Default local URL on host + NEXT_PUBLIC_SUPABASE_ANON_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODIzNDMwOTZ9.7UIsMFfHYKxH7bUJCRfxd6lr7CSXGF7UxtZQO10FMMo' # Use default local value + NEXT_PUBLIC_WEB_SOCKET_URL: ws://localhost:3001 # Point WS to API service container + + # Pass any other required NEXT_PUBLIC_ variables + + - name: Stop Supabase # Use the cleanup action + uses: ./.github/actions/stop-supabase + if: always() # Ensure Supabase is stopped even if tests fail + + - name: Stop API Container # Cleanup manually started container + if: always() + run: docker stop local-api && docker rm local-api diff --git a/api/libs/database/tests/common/assets.rs b/api/libs/database/tests/common/assets.rs index 46f6dfee1..ce740efcc 100644 --- a/api/libs/database/tests/common/assets.rs +++ b/api/libs/database/tests/common/assets.rs @@ -3,12 +3,14 @@ use chrono::Utc; use database::enums::{AssetPermissionRole, AssetType, Verification}; use database::models::{Chat, Collection, DashboardFile, MetricFile}; use database::schema::{chats, collections, dashboard_files, metric_files}; +use database::types::metric_yml::{ + BarAndLineAxis, BarLineChartConfig, BaseChartConfig, ChartConfig, +}; use database::types::{DashboardYml, MetricYml, VersionHistory}; -use database::types::metric_yml::{BaseChartConfig, BarAndLineAxis, BarLineChartConfig, ChartConfig}; use diesel_async::RunQueryDsl; use indexmap; -use uuid::Uuid; use std::collections::HashMap; +use uuid::Uuid; use crate::common::db::TestDb; use crate::common::permissions::PermissionTestHelpers; @@ -18,13 +20,10 @@ pub struct AssetTestHelpers; impl AssetTestHelpers { /// Creates a test metric file - pub async fn create_test_metric( - test_db: &TestDb, - name: &str, - ) -> Result { + pub async fn create_test_metric(test_db: &TestDb, name: &str) -> Result { let mut conn = test_db.diesel_conn().await?; let metric_id = Uuid::new_v4(); - + // Create a simple metric content let content = MetricYml { name: name.to_string(), @@ -34,11 +33,15 @@ impl AssetTestHelpers { chart_config: create_default_chart_config(), dataset_ids: Vec::new(), }; - + let metric_file = MetricFile { id: metric_id, name: format!("{}-{}", test_db.test_id, name), - file_name: format!("{}-{}.yml", test_db.test_id, name.to_lowercase().replace(" ", "_")), + file_name: format!( + "{}-{}.yml", + test_db.test_id, + name.to_lowercase().replace(" ", "_") + ), content, verification: Verification::Verified, evaluation_obj: None, @@ -56,34 +59,35 @@ impl AssetTestHelpers { data_metadata: None, public_password: None, }; - + diesel::insert_into(metric_files::table) .values(&metric_file) .execute(&mut conn) .await?; - + Ok(metric_file) } - + /// Creates a test dashboard file - pub async fn create_test_dashboard( - test_db: &TestDb, - name: &str, - ) -> Result { + pub async fn create_test_dashboard(test_db: &TestDb, name: &str) -> Result { let mut conn = test_db.diesel_conn().await?; let dashboard_id = Uuid::new_v4(); - + // Create a simple dashboard content let content = DashboardYml { name: name.to_string(), description: Some(format!("Test dashboard description for {}", name)), rows: Vec::new(), }; - + let dashboard_file = DashboardFile { id: dashboard_id, name: format!("{}-{}", test_db.test_id, name), - file_name: format!("{}-{}.yml", test_db.test_id, name.to_lowercase().replace(" ", "_")), + file_name: format!( + "{}-{}.yml", + test_db.test_id, + name.to_lowercase().replace(" ", "_") + ), content, filter: None, organization_id: test_db.organization_id, @@ -97,23 +101,20 @@ impl AssetTestHelpers { version_history: VersionHistory(HashMap::new()), public_password: None, }; - + diesel::insert_into(dashboard_files::table) .values(&dashboard_file) .execute(&mut conn) .await?; - + Ok(dashboard_file) } - + /// Creates a test collection - pub async fn create_test_collection( - test_db: &TestDb, - name: &str, - ) -> Result { + pub async fn create_test_collection(test_db: &TestDb, name: &str) -> Result { let mut conn = test_db.diesel_conn().await?; let collection_id = Uuid::new_v4(); - + let collection = Collection { id: collection_id, name: format!("{}-{}", test_db.test_id, name), @@ -125,23 +126,20 @@ impl AssetTestHelpers { deleted_at: None, organization_id: test_db.organization_id, }; - + diesel::insert_into(collections::table) .values(&collection) .execute(&mut conn) .await?; - + Ok(collection) } - + /// Creates a test chat - pub async fn create_test_chat( - test_db: &TestDb, - title: &str, - ) -> Result { + pub async fn create_test_chat(test_db: &TestDb, title: &str) -> Result { let mut conn = test_db.diesel_conn().await?; let chat_id = Uuid::new_v4(); - + let chat = Chat { id: chat_id, title: format!("{}-{}", test_db.test_id, title), @@ -156,16 +154,17 @@ impl AssetTestHelpers { public_expiry_date: None, most_recent_file_id: None, most_recent_file_type: None, + most_recent_version_number: None, }; - + diesel::insert_into(chats::table) .values(&chat) .execute(&mut conn) .await?; - + Ok(chat) } - + /// Creates a test metric with owner permission pub async fn create_test_metric_with_permission( test_db: &TestDb, @@ -174,7 +173,7 @@ impl AssetTestHelpers { role: AssetPermissionRole, ) -> Result { let metric = Self::create_test_metric(test_db, name).await?; - + // Add permission PermissionTestHelpers::create_user_permission( test_db, @@ -182,11 +181,12 @@ impl AssetTestHelpers { AssetType::MetricFile, user_id, role, - ).await?; - + ) + .await?; + Ok(metric) } - + /// Creates a test dashboard with owner permission pub async fn create_test_dashboard_with_permission( test_db: &TestDb, @@ -195,7 +195,7 @@ impl AssetTestHelpers { role: AssetPermissionRole, ) -> Result { let dashboard = Self::create_test_dashboard(test_db, name).await?; - + // Add permission PermissionTestHelpers::create_user_permission( test_db, @@ -203,11 +203,12 @@ impl AssetTestHelpers { AssetType::DashboardFile, user_id, role, - ).await?; - + ) + .await?; + Ok(dashboard) } - + /// Creates a test collection with owner permission pub async fn create_test_collection_with_permission( test_db: &TestDb, @@ -216,7 +217,7 @@ impl AssetTestHelpers { role: AssetPermissionRole, ) -> Result { let collection = Self::create_test_collection(test_db, name).await?; - + // Add permission PermissionTestHelpers::create_user_permission( test_db, @@ -224,11 +225,12 @@ impl AssetTestHelpers { AssetType::Collection, user_id, role, - ).await?; - + ) + .await?; + Ok(collection) } - + /// Creates a test chat with owner permission pub async fn create_test_chat_with_permission( test_db: &TestDb, @@ -237,7 +239,7 @@ impl AssetTestHelpers { role: AssetPermissionRole, ) -> Result { let chat = Self::create_test_chat(test_db, title).await?; - + // Add permission PermissionTestHelpers::create_user_permission( test_db, @@ -245,8 +247,9 @@ impl AssetTestHelpers { AssetType::Chat, user_id, role, - ).await?; - + ) + .await?; + Ok(chat) } } @@ -281,4 +284,4 @@ fn create_default_chart_config() -> ChartConfig { bar_show_total_at_top: None, line_group_type: None, }) -} \ No newline at end of file +} diff --git a/api/libs/database/tests/common/db.rs b/api/libs/database/tests/common/db.rs index c623a8a2c..aab8714bd 100644 --- a/api/libs/database/tests/common/db.rs +++ b/api/libs/database/tests/common/db.rs @@ -93,6 +93,7 @@ impl TestDb { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, + payment_required: false, }; let mut conn = self.diesel_conn().await?; diff --git a/api/libs/handlers/tests/metrics/update_metric_test.rs b/api/libs/handlers/tests/metrics/update_metric_test.rs index 438593de6..9aecf8705 100644 --- a/api/libs/handlers/tests/metrics/update_metric_test.rs +++ b/api/libs/handlers/tests/metrics/update_metric_test.rs @@ -41,6 +41,7 @@ impl TestSetup { created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), deleted_at: None, + payment_required: false, }; // Create user with specified role