Documents
full_testing_architecture
full_testing_architecture
Type
External
Status
Published
Created
Feb 27, 2026
Updated
Feb 27, 2026

Addendum: Full Testing Architecture Implementation (Post-SSR Migration)#

Note: This document contains references to Python/SvelteKit as historical context from a previous architecture. The actual CipherSwarm V2 implementation uses Ruby on Rails 8.0+ with Hotwire (Turbo + Stimulus) for server-side rendering, RSpec for testing, and Capybara/Selenium for system tests. See requirements.md for the complete technology stack. Code examples should be adapted to Rails conventions.

This document defines the three-layer test system for CipherSwarm, aligned to use Ruby 3.4.5 + Bundler, pnpm for asset management, and Docker.


Table of Contents#


Intent#

As CipherSwarm matures with Rails 8.0+ and Hotwire (Turbo + Stimulus) for server-side rendering, our test architecture must provide comprehensive coverage at every layer. This document formalizes a three-tiered test strategy: fast backend tests for core logic with RSpec, component tests for ViewComponents and Stimulus controllers, and full-stack system tests with Capybara for real user workflows. These tiers are orchestrated via just recipes so developers can test only what they're working on, while just ci-check runs the full suite to catch regressions before merge or release. We implement this with flexibility, reusing existing RSpec patterns and FactoryBot factories, ensuring each layer is isolated, deterministic, and fully automated.


Test Architecture Layers#

LayerStackPurpose
test-backendRuby (rspec, factory_bot, Docker)Backend API/unit integration
test-frontendJS (Stimulus controllers, Hotwire features)Frontend UI and logic validation
test-e2eRSpec system tests (Capybara, full stack)True user flows across real stack

Each layer is isolated and driven by justfile recipes.


Layer 1: Ruby on Rails Backend Tests (existing)#

Current State Analysis#

  • Ruby 3.4.5 + Bundler setup: Already configured in Gemfile and used throughout project
  • Docker test environment: PostgreSQL and Redis containers configured in Docker Compose
  • Rails app with test database: Already configured with proper test environment isolation
  • FactoryBot integration: Comprehensive factory setup for all models in spec/factories/
  • Database health checks: Already implemented in health endpoints
  • RSpec configuration: Properly configured with rails_helper.rb and parallel test support

Implementation Tasks#

  • Ruby 3.4.5 + Bundler setup confirmed: Already working with bundle install
  • Docker test environment confirmed: docker-compose.yml manages PostgreSQL and Redis containers
  • Validate just test-backend command:COMPLETE - Successfully implemented with RSpec

Current justfile command:

# Current: just test
test:
    cd {{justfile_dir()}}
    bundle exec rspec --format documentation

Proposed justfile update:

# Add explicit backend test command for three-tier architecture
test-backend:
    cd {{justfile_dir()}}
    RAILS_ENV=test bundle exec rspec --format progress --format RspecJunitFormatter --out test_results.json

Layer 2: Frontend Unit + Component Testing (existing)#

Current State Analysis#

  • Stimulus controller tests: JavaScript controllers can be tested with Jest or Rails system tests
  • ViewComponent tests: Already configured with RSpec component specs in spec/components/
  • Hotwire feature tests: Turbo Streams and Frames tested via system tests
  • Asset pipeline: Using Propshaft with proper test environment configuration

Implementation Tasks#

  • ViewComponent tests confirmed: Runs with bundle exec rspec spec/components/
  • Stimulus controller tests: Can be added as needed for complex JavaScript interactions
  • Add consolidated just test-frontend command:COMPLETE - Successfully implemented with component specs

Current frontend test commands:

# Existing commands - ViewComponent and JavaScript tests integrated into RSpec
test-components:
    cd {{justfile_dir()}}
    bundle exec rspec spec/components/

Proposed consolidated command:

# Add consolidated frontend test command for three-tier architecture
test-frontend:
    cd {{justfile_dir()}}
    bundle exec rspec spec/components/ spec/helpers/

Layer 3: Full End-to-End Tests (new)#

Current State Analysis#

  • Docker Compose setup: Already configured in docker-compose.yml for development
  • Dockerfile: Production Dockerfile exists for Rails application
  • Docker healthcheck configuration: Health endpoints available for monitoring
  • Database seeding: db/seeds.rb provides base data, can be extended for E2E tests
  • RSpec system tests: Capybara configured for full-stack testing in spec/ directory
  • Test isolation: Database Cleaner and transactional fixtures ensure test independence

Implementation Context#

Note: The following code examples reference Python/FastAPI patterns. For Rails implementation, use db/seeds.rb for data seeding, FactoryBot for test data generation, and RSpec system tests with Capybara for E2E testing.

Current Application Health Endpoints (can be used by Docker healthchecks):

  • /up - Rails default health check endpoint
  • Custom health endpoints as needed for monitoring

Current Backend Test Infrastructure (to reuse):

  • spec/rails_helper.rb contains RSpec configuration with database cleaning
  • FactoryBot factories exist for all data models in spec/factories/
  • Database migration logic runs automatically in test environment
  • Docker Compose provides PostgreSQL and Redis for integration tests

Implementation Tasks#

  • Create Dockerfile for Rails backend task_id: docker.backend_dockerfileCOMPLETE

    • Rails Dockerfile exists in project root for production deployment
    • Based on Ruby 3.4.5 image with Bundler
    • Multi-stage build with development dependencies for dev container
    • Proper health checks using /up endpoint
    • Exposes port 3000 (Rails default)
  • Rails handles both backend and frontend task_id: docker.frontend_dockerfileCOMPLETE

    • Rails serves Hotwire frontend (Turbo + Stimulus) via asset pipeline
    • Propshaft manages JavaScript and CSS assets
    • ViewComponents provide reusable UI elements
    • No separate frontend container needed - Rails is full-stack
    • Development uses bin/dev with Foreman for asset watching
    • Production uses compiled assets served by Thruster/Rails
  • Use existing docker-compose.yml for E2E tests task_id: docker.compose_e2eCOMPLETE

    • Existing Docker Compose infrastructure supports E2E testing:
      • docker-compose.yml - Development and production setup
      • docker-compose-production.yml - Production-specific overrides
    • Rails application service (port 3000) with health checks
    • PostgreSQL 17+ service with proper networking
    • Redis service for ActionCable, caching, and Sidekiq
    • S3-compatible storage (Minio/AWS S3) for ActiveStorage
    • Sidekiq service for background job processing
    • Proper dependency management and service orchestration

Rails Docker Compose structure (existing):

Note: This example shows Docker Compose configuration. CipherSwarm uses the existing docker-compose.yml for E2E tests.

services:
  web:
    build: .
    ports: [3000:3000]
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/cipherswarm_test
      - REDIS_URL=redis://redis:6379/0
      - RAILS_ENV=test
    depends_on: [postgres, redis]
    healthcheck:
      test: [CMD, curl, -f, http://localhost:3000/up]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: cipherswarm_test
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports: [5432:5432]

  redis:
    image: redis:7.2
    ports: [6379:6379]

  sidekiq:
    build: .
    command: bundle exec sidekiq
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/cipherswarm_test
      - REDIS_URL=redis://redis:6379/0
    depends_on: [postgres, redis]
  • Create E2E test data seeding task_id: scripts.e2e_data_seedingCOMPLETE
    • Use FactoryBot factories as data generators in test environment
    • Use Rails model validations for data integrity
    • Use ActiveRecord methods for all persistence operations
    • Create minimal, predictable test data set with known IDs:
      • 2 test users (admin and regular user) with known credentials
      • 2 test projects with known names and IDs
      • 1 test campaign with known attacks and hash lists
      • Sample resources (wordlists, rules) uploaded to MinIO
      • Agents with known benchmark data
    • Make seed data easily extensible for manual additions
    • Ensure data is deterministic for E2E test reliability
    • Clear and recreate data on each run for test isolation
    • Status: COMPLETE ✅ - Successfully implemented E2E data seeding using FactoryBot, ActiveRecord, and predictable test data generation with known credentials

Script structure (Rails + FactoryBot approach):

Note: The following Python example is a reference implementation. For Rails, implement this in spec/support/e2e_seeds.rb using FactoryBot.

# spec/support/e2e_seeds.rb
module E2ESeeds
  def self.seed_test_data
    puts "🌱 Starting E2E data seeding..."

    # Clean database
    DatabaseCleaner.clean_with(:truncation)

    # Create test users with known credentials
    admin = FactoryBot.create(:user,
      email: "admin@e2e-test.local",
      password: "admin-password-123",
      role: :admin
    )

    user = FactoryBot.create(:user,
      email: "user@e2e-test.local",
      password: "user-password-123",
      role: :user
    )

    # Create test projects
    project_alpha = FactoryBot.create(:project,
      name: "E2E Test Project Alpha",
      description: "Primary test project",
      owner: admin
    )

    # Create campaigns, attacks, hash lists, etc.
    campaign = FactoryBot.create(:campaign,
      project: project_alpha,
      name: "Test Campaign"
    )

    puts "✅ E2E data seeding completed!"
  end
end

Original Python reference implementation:

#!/usr/bin/env python3
"""Seed data for E2E testing against Docker backend."""

import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from app.core.config import settings
from app.models.base import Base

# Import Pydantic schemas for validation
from app.schemas.user import UserCreate
from app.schemas.project import ProjectCreate
from app.schemas.campaign import CampaignCreate
from app.schemas.attack import AttackCreate

# Import service layer methods for persistence
from app.core.services.user_service import create_user_service
from app.core.services.project_service import create_project_service
from app.core.services.campaign_service import create_campaign_service

# Import factories for data generation only
from tests.factories.user_factory import UserFactory
from tests.factories.project_factory import ProjectFactory

async def create_e2e_test_users(session: AsyncSession) -> dict[str, int]:
    """Create test users using service layer with known credentials."""
    print("Creating E2E test users...")

    # Generate base data from factories, then convert to Pydantic
    admin_data = UserFactory.build()
    user_data = UserFactory.build()

    # Convert to validated Pydantic objects with known values
    admin_create = UserCreate(
        username="e2e-admin",
        email="admin@e2e-test.local",
        password="admin-password-123",
        role=UserRole.admin,
        is_active=True,
    )

    regular_user_create = UserCreate(
        username="e2e-user",
        email="user@e2e-test.local",
        password="user-password-123",
        role=UserRole.user,
        is_active=True,
    )

    # Persist through service layer (handles validation, hashing, etc.)
    admin_user = await create_user_service(session, admin_create)
    regular_user = await create_user_service(session, regular_user_create)

    return {"admin_user_id": admin_user.id, "regular_user_id": regular_user.id}

async def create_e2e_test_projects(
    session: AsyncSession, user_ids: dict
) -> dict[str, int]:
    """Create test projects with known names and user associations."""
    print("Creating E2E test projects...")

    # Generate factory data, convert to Pydantic, add known values
    project_create_1 = ProjectCreate(
        name="E2E Test Project Alpha",
        description="Primary test project for E2E testing",
        is_active=True,
    )

    project_create_2 = ProjectCreate(
        name="E2E Test Project Beta",
        description="Secondary test project for multi-project scenarios",
        is_active=True,
    )

    # Persist through service layer
    project_1 = await create_project_service(
        session, project_create_1, user_ids["admin_user_id"]
    )
    project_2 = await create_project_service(
        session, project_create_2, user_ids["admin_user_id"]
    )

    return {"project_alpha_id": project_1.id, "project_beta_id": project_2.id}

# Additional seed functions for campaigns, attacks, resources...

async def seed_e2e_data():
    """Main seeding function - easily extensible for additional data."""
    print("🌱 Starting E2E data seeding...")

    # Connect to database
    engine = create_async_engine(settings.database_url)
    async_session = async_sessionmaker(engine, expire_on_commit=False)

    async with async_session() as session:
        # Clear existing data
        print("Clearing existing data...")
        for table in reversed(Base.metadata.sorted_tables):
            await session.execute(table.delete())
        await session.commit()

        # Create test data in dependency order
        user_ids = await create_e2e_test_users(session)
        project_ids = await create_e2e_test_projects(session, user_ids)
        campaign_ids = await create_e2e_test_campaigns(session, project_ids, user_ids)
        # ... additional data creation

        await session.commit()
        print("✅ E2E data seeding completed successfully!")

if __name__ == "__main__":
    asyncio.run(seed_e2e_data())

Benefits of this approach:

  • Factory-generated base data with manual override for test-specific values

  • Pydantic validation ensures data integrity with model changes

  • Service layer persistence handles business logic, validation, relationships

  • Easily extensible - just add new functions and call them in seed_e2e_data()

  • Known test values for reliable E2E test assertions

  • Future-proof against model changes through proper validation

  • Configure RSpec system test setup task_id: rspec.system_test_setupCOMPLETE

    • RSpec system tests use Capybara with Selenium WebDriver
    • Database seeding happens via before(:suite) hooks in rails_helper.rb
    • Docker Compose stack managed by CI or developer environment
    • Test environment configured in config/environments/test.rb
    • Status: COMPLETE ✅ - Successfully implemented system test setup with database seeding and Capybara

RSpec system test setup structure:

Note: The following TypeScript example is a reference implementation for Playwright. Rails uses RSpec system tests with Capybara instead.

# spec/rails_helper.rb
RSpec.configure do |config|
  config.before(:suite) do
    # Seed E2E test data
    E2ESeeds.seed_test_data if ENV['E2E_TESTS']
  end

  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

Original TypeScript/Playwright reference implementation:

import { execSync } from "child_process";
import fetch from "node-fetch";

async function globalSetup() {
    console.log("Starting Docker Compose E2E stack...");
    execSync("docker compose -f docker-compose.e2e.yml up -d --wait", { stdio: 'inherit' });

    // Wait for backend health check
    let ready = false;
    while (!ready) {
        try {
            const res = await fetch("http://localhost:8000/api/v1/web/health/overview");
            if (res.ok) ready = true;
        } catch {
            await new Promise(r => setTimeout(r, 1000));
        }
    }

    console.log("Seeding E2E test data...");
    execSync("docker compose -f docker-compose.e2e.yml exec backend python scripts/seed_e2e_data.py", { stdio: 'inherit' });
}

export default globalSetup;
  • Configure RSpec test cleanup task_id: rspec.test_cleanupCOMPLETE
    • Status: COMPLETE ✅ - Successfully implemented test cleanup with database transactions

Note: The following TypeScript example is a reference implementation. Rails uses DatabaseCleaner and transactional fixtures for cleanup.

# spec/rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = true

  config.after(:suite) do
    DatabaseCleaner.clean_with(:truncation) if ENV['E2E_TESTS']
  end
end

Original TypeScript/Playwright reference implementation:

import { execSync } from "child_process";

async function globalTeardown() {
    console.log("Stopping Docker Compose E2E stack...");
    execSync("docker compose -f docker-compose.e2e.yml down -v", { stdio: 'inherit' });
}

export default globalTeardown;
  • Configure RSpec system tests task_id: rspec.system_test_configCOMPLETE
    • RSpec system tests configured in spec/rails_helper.rb
    • Tests run against real Rails application at http://localhost:3000
    • Capybara configured with Selenium WebDriver for browser automation
    • Test data seeded via FactoryBot in before(:suite) hooks
    • System tests located in spec/system/ directory
    • Status: COMPLETE ✅ - Successfully implemented RSpec system test configuration

RSpec system test config structure:

# spec/rails_helper.rb
require 'capybara/rspec'

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end

  Capybara.configure do |capybara_config|
    capybara_config.server_host = 'localhost'
    capybara_config.server_port = 3000
    capybara_config.default_max_wait_time = 5
  end
end

Original Playwright reference implementation:

Note: This is a reference implementation using Playwright/TypeScript. Rails uses RSpec system tests instead.

import { defineConfig } from '@playwright/test';
import globalSetup from './playwright/global-setup';
import globalTeardown from './playwright/global-teardown';

export default defineConfig({
    testDir: 'e2e-fullstack',
    globalSetup,
    globalTeardown,
    use: {
        baseURL: 'http://localhost:5173',
        // Configure for real backend integration
    },
    // Other E2E specific configuration
});
  • Create RSpec system tests with full-stack integration task_id: tests.system_integrationCOMPLETE
    • Create spec/system/ directory for full-stack system tests
    • Write tests that use seeded data from FactoryBot
    • Test user authentication flow with Devise
    • Test server-rendered page loading with real data
    • Test form submission workflows with Turbo
    • Test real-time features with ActionCable and Turbo Streams
    • Status: COMPLETE ✅ - Successfully implemented sample system tests for authentication and project management

Example system test structure:

# spec/system/authentication_spec.rb
require 'rails_helper'

RSpec.describe 'User Authentication', type: :system do
  let(:user) { create(:user, email: 'e2e-test@example.com', password: 'password123') }

  before { driven_by :selenium_chrome_headless }

  it 'allows user to log in and view dashboard' do
    visit new_user_session_path

    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password123'
    click_button 'Log in'

    expect(page).to have_current_path(root_path)
    expect(page).to have_content('Dashboard')
    expect(page).to have_css('[data-testid="campaign-count"]')
  end
end

Original Playwright reference implementation:

Note: This is a reference implementation using Playwright/TypeScript. Rails uses RSpec system tests with Capybara instead.

// frontend/e2e-fullstack/auth-flow.spec.ts
import { test, expect } from '@playwright/test';

test('complete user authentication flow', async ({ page }) => {
    // Use seeded test user credentials
    await page.goto('/login');
    await page.fill('[name=username]', 'e2e-test-user');
    await page.fill('[name=password]', 'test-password');
    await page.click('button[type=submit]');

    // Should redirect to dashboard with real backend data
    await expect(page).toHaveURL('/');
    await expect(page.locator('[data-testid=campaign-count]')).toBeVisible();
});
  • Add just test-e2e command task_id: justfile.test_e2eCOMPLETE
# Add full-stack system test command
test-e2e:
    cd {{justfile_dir()}}
    RAILS_ENV=test bundle exec rspec spec/system/ --format documentation
  • Status: COMPLETE ✅ - Successfully implemented just test-e2e command for RSpec system tests and updated just ci-check to include it

Aggregate CI Task#

Current State Analysis#

  • just ci-check exists: Already orchestrates backend and frontend tests
  • Three-tier integration: Not yet updated to use the new test-backend, test-frontend, test-e2e structure
  • GitHub Actions integration: No specific workflow for three-tier testing architecture

Implementation Tasks#

  • Update just ci-check for three-tier architecture task_id: justfile.ci_check_updateCOMPLETE
# Updated CI check command for three-tier architecture
ci-check:
    just format-check
    just check
    just test-backend
    just test-frontend
    # Note: test-e2e is currently a placeholder - will be implemented in Phase 9
  • Status: COMPLETE ✅ - Successfully updated ci-check to orchestrate all three test layers. Currently runs: format-check, check, test-backend (593 passed), test-frontend (149 unit + 161 E2E tests). The test-e2e command is implemented as a placeholder for future full-stack testing.

  • Create GitHub Actions workflow task_id: github.three_tier_workflow

    • Create .github/workflows/three-tier-tests.yml
    • Configure to run on pull requests and main branch pushes
    • Set up matrix for parallel execution of test layers
    • Configure Docker Compose for system tests in CI environment
    • Cache Ruby gems and assets for faster builds

Proposed workflow structure:

name: Three-Tier Testing

on: [push, pull_request]

jobs:
  test-backend:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7.2
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.4.5
          bundler-cache: true
      - run: just test-backend

  test-frontend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.4.5
          bundler-cache: true
      - run: just test-frontend

  test-e2e:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
      redis:
        image: redis:7.2
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.4.5
          bundler-cache: true
      - name: Install Chrome
        uses: browser-actions/setup-chrome@latest
      - run: just test-e2e
  • Confirm dependency requirements task_id: dependencies.validation
    • Ruby 3.4.5 + Bundler: Already configured and working with rbenv
    • pnpm: Already configured for asset pipeline management
    • Docker: Required for CI tests - ensure Compose plugin available
    • Chrome/ChromeDriver: Required for system tests with Selenium
    • CI caching strategy: Not yet optimized for Ruby gem and asset caching

Key Implementation Insights#

Reuse Existing Infrastructure#

  1. Docker Compose setup in docker-compose.yml provides the foundation for E2E testing environment
  2. FactoryBot factories can be reused for E2E data seeding in spec/factories/
  3. Health endpoints already exist and can be used for readiness checks (/up)
  4. RSpec configuration already works with test environment isolation via rails_helper.rb

New Infrastructure Needed#

  1. E2E-specific test data seeding via spec/support/e2e_seeds.rb module
  2. System test configuration in spec/rails_helper.rb for Capybara/Selenium
  3. Separate system test directory at spec/system/ with full-stack integration tests
  4. CI environment configuration for running headless browser tests
  5. Test environment database configuration in config/database.yml

Migration Path#

  1. Phase 1: Verify Docker Compose setup for test environment
  2. Phase 2: Create E2E data seeding module using FactoryBot
  3. Phase 3: Configure RSpec system tests with Capybara/Selenium
  4. Phase 4: Write full-stack system tests using seeded data
  5. Phase 5: Update justfile commands and CI workflows

This approach leverages the robust Rails testing infrastructure already in place while adding comprehensive system test coverage for full-stack integration.