Fix code quality violations and enhance ROUTE-EXISTS-01 rule

Implement JQHTML function cache ID system and fix bundle compilation
Implement underscore prefix for system tables
Fix JS syntax linter to support decorators and grant exception to Task system
SPA: Update planning docs and wishlists with remaining features
SPA: Document Navigation API abandonment and future enhancements
Implement SPA browser integration with History API (Phase 1)
Convert contacts view page to SPA action
Convert clients pages to SPA actions and document conversion procedure
SPA: Merge GET parameters and update documentation
Implement SPA route URL generation in JavaScript and PHP
Implement SPA bootstrap controller architecture
Add SPA routing manual page (rsx:man spa)
Add SPA routing documentation to CLAUDE.md
Phase 4 Complete: Client-side SPA routing implementation
Update get_routes() consumers for unified route structure
Complete SPA Phase 3: PHP-side route type detection and is_spa flag
Restore unified routes structure and Manifest_Query class
Refactor route indexing and add SPA infrastructure
Phase 3 Complete: SPA route registration in manifest
Implement SPA Phase 2: Extract router code and test decorators
Rename Jqhtml_Component to Component and complete SPA foundation setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-19 17:48:15 +00:00
parent 77b4d10af8
commit 9ebcc359ae
4360 changed files with 37751 additions and 18578 deletions

720
app/RSpade/tests/CLAUDE.md Executable file
View File

@@ -0,0 +1,720 @@
# RSpade Testing Framework - AI Agent Instructions
**PURPOSE**: Comprehensive guidelines for AI agents working with RSpade framework tests.
## TESTING PHILOSOPHY
### The "Press Button, Light Turns Green" Philosophy
**Analogy**: You have a button that should turn a light green when pressed. If the button doesn't turn the light green, you **DO NOT**:
- Paint the light green with a brush
- Manually flip the light switch
- Bypass the button with a workaround
- Mark the test as passing
Instead, you **FIX THE BUTTON** so pressing it turns the light green.
**Application to Testing**:
- Tests must be **fully automated** - one command to run, no manual intervention
- Tests must be **deterministic** - same input always produces same output
- Tests must be **repeatable** - can run multiple times with identical results
- Tests must **require zero human assistance** to complete
**Manual intervention to help a test complete is FORBIDDEN.**
If you find yourself:
- Running commands manually to fix test database state
- Switching to production database because test database has issues
- Modifying files between test runs to make tests pass
- Skipping test assertions because they're "too hard to fix"
**STOP IMMEDIATELY** and report the issue to the user. These are symptoms that the test infrastructure is broken and needs fixing, NOT that you should work around the problem.
## CRITICAL TESTING MANDATES
**ABSOLUTE REQUIREMENTS - NO EXCEPTIONS:**
### 1. NEVER Switch to Production Database
Tests **MUST** pass on the test database (`rspade_test`).
**Why**: Switching databases to avoid errors completely defeats the purpose of testing. The test database IS the controlled environment where tests should work. If they don't work there, there's a real problem to fix.
**Forbidden**:
```bash
# ❌ WRONG - Bypassing test database
mysql -urspade -prspadepass rspade -e "SELECT * FROM users" # Using production DB
# ❌ WRONG - Modifying test to use production
DB_DATABASE=rspade ./run_test.sh
```
**Correct**:
```bash
# ✅ CORRECT - Tests use test database exclusively
test_db_query "SELECT * FROM users" # Helper function for test DB only
```
### 2. NEVER Mark a Test as Passing If It's Failing
Do not write code to circumvent test failures or skip over difficult errors.
**Why**: Failing tests indicate real problems that must be fixed. A bypassed test is worse than no test - it creates false confidence.
**Forbidden**:
```bash
# ❌ WRONG - Hiding failures
if ! some_test_command; then
echo "PASS: Test (ignored failure)" # Lying about pass
exit 0
fi
# ❌ WRONG - Skipping hard assertions
# if [ "$count" -ne 5 ]; then
# echo "FAIL" # Commented out because "too hard to fix"
# fi
```
**Correct**:
```bash
# ✅ CORRECT - Fail loud and clear
if [ "$count" -ne 5 ]; then
echo "FAIL: Expected 5 records, found $count"
exit 1
fi
```
### 3. When a Test Fails: Fix the Error OR Ask the User
**Priority order**:
1. **Analyze the error** - Understand what's actually wrong
2. **Attempt to fix the underlying issue** - Fix the button, not paint the light
3. **If unclear or complex, STOP and ask user for direction** - Better to ask than guess
4. **NEVER work around the failure** - Workarounds defeat the purpose
**Why**: Unexpected test errors are valuable - they reveal real issues. Simply skipping over them completely defeats the point of doing tests. The entire purpose of testing is to uncover unforeseen issues.
**Example - Wrong Approach**:
```bash
# Test fails because migrations didn't run
# ❌ WRONG - Manually run migrations
php artisan migrate --force
# ❌ WRONG - Skip the test
exit 0
# ❌ WRONG - Switch to production database where migrations exist
```
**Example - Correct Approach**:
```bash
# Test fails because migrations didn't run
# ✅ CORRECT - Fix db_reset.sh to run migrations automatically
# Update the db_reset.sh script to include migration step
# Now test runs migrations automatically and passes
```
**When to ask user**:
- Error is cryptic or unclear
- Multiple potential solutions exist
- Fix would require architectural changes
- Uncertain if behavior is intentional or a bug
**When NOT to ask user** (fix it yourself):
- Simple typos or syntax errors
- Missing files that should obviously exist
- Clear logic errors in test code
- Standard debugging (add logging, check return values, etc.)
## TEST STRUCTURE
### Test Location
**All framework tests**: `/system/app/RSpade/tests/`
```
/system/app/RSpade/tests/
├── CLAUDE.md # This file - AI agent instructions
├── README.md # Human-readable test documentation
├── _lib/ # Test utilities and helpers
│ ├── test_env.sh # Test mode management functions
│ ├── rsx_test_config.php # Manifest config for test mode
│ ├── db_reset.sh # Database reset utility
│ ├── db_snapshot_create.sh # Create database snapshot
│ └── db_snapshot_restore.sh # Restore database snapshot
├── services/ # Test service classes
│ ├── service_test_service.php # Example test service
│ └── scheduled_test_service.php # Scheduled task tests
├── basic/ # Basic framework tests
│ ├── 01_framework_verification/
│ │ ├── run_test.sh # Test script
│ │ └── README.md # Test documentation
│ └── 02_database_connection/
│ ├── run_test.sh
│ └── README.md
└── tasks/ # Task system tests
└── 01_task_dispatch_and_execution/
├── run_test.sh
└── README.md
```
### Test Script Pattern
Every test follows this pattern:
```bash
#!/bin/bash
set -e # Exit on any error
TEST_NAME="Descriptive Test Name"
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source test environment helpers
source "$TEST_DIR/../../_lib/test_env.sh"
# Parse arguments (for test composition)
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset)
SKIP_RESET=true
shift
;;
esac
done
# Ensure test mode exits on script exit (success or failure)
trap test_trap_exit EXIT
# SETUP
echo "[SETUP] Preparing test..." >&2
# Enter test mode (switches to rspade_test database)
test_mode_enter
# Reset database to known state (unless called as sub-test)
if [ "$SKIP_RESET" = false ]; then
echo "[SETUP] Resetting test database..." >&2
"$TEST_DIR/../../_lib/db_reset.sh"
fi
# TEST LOGIC
echo "[TEST] Testing feature..." >&2
# Test assertions here
# Use test_db_query, test_db_count helpers
# Fail loud with clear error messages
# All tests passed
echo "PASS: $TEST_NAME"
exit 0
# TEARDOWN happens automatically via trap
```
**Key elements**:
- `set -e` - Any command failure fails the entire test
- `trap test_trap_exit EXIT` - Ensures cleanup even on failure
- `test_mode_enter` - Switches to test database
- `db_reset.sh` - Ensures known database state
- `--skip-reset` - For test composition (calling tests from other tests)
- Clear `[SETUP]` and `[TEST]` prefixes in output
- `echo ... >&2` - Test output goes to stderr, only final result to stdout
## TEST ENVIRONMENT MANAGEMENT
### Test Mode Functions
**Source the helpers** (required in every test script):
```bash
source /system/app/RSpade/tests/_lib/test_env.sh
```
**Available functions**:
#### test_mode_enter()
Switches environment to test mode.
**What it does**:
1. Checks if already in test mode (fails if `DB_DATABASE=rspade_test`)
2. Backs up current `.env` to `/var/www/html/system/.env.testbackup`
3. Modifies `.env` to set `DB_DATABASE=rspade_test`
4. Sets `RSX_ADDITIONAL_CONFIG=/var/www/html/system/app/RSpade/tests/_lib/rsx_test_config.php`
5. Clears Laravel config cache
6. Rebuilds manifest to include test directory
**Safety**: Refuses to create backup if already in test mode (prevents backing up test config as production).
#### test_mode_exit()
Restores production environment.
**What it does**:
1. Restores original `.env` from backup
2. Clears Laravel config cache
3. Rebuilds manifest without test directory
**Note**: Automatically called by `test_trap_exit` trap handler.
#### test_trap_exit()
Cleanup handler for test scripts.
**Usage**:
```bash
trap test_trap_exit EXIT
```
**What it does**:
- Calls `test_mode_exit()` to restore environment
- Does NOT delete backup file (contains production credentials)
#### test_db_query(sql)
Execute SQL query on test database, return single value.
**Usage**:
```bash
name=$(test_db_query "SELECT name FROM users WHERE id=1")
status=$(test_db_query "SELECT status FROM _tasks WHERE id=$task_id")
```
**Returns**: Single value from first column of first row (or empty string if no results).
#### test_db_count(table [WHERE clause])
Count rows in test database table.
**Usage**:
```bash
count=$(test_db_count "users")
count=$(test_db_count "users WHERE active=1")
count=$(test_db_count "_tasks WHERE status='pending'")
```
**Returns**: Integer count of matching rows.
### Test Database Isolation
**CRITICAL**: Tests NEVER touch the production database (`rspade`). They use a completely separate database (`rspade_test`).
**Environment Variables During Test Mode**:
- `DB_DATABASE=rspade_test` - Points to test database
- `RSX_ADDITIONAL_CONFIG=.../rsx_test_config.php` - Loads test manifest config
**Test Manifest Config** (`/system/app/RSpade/tests/_lib/rsx_test_config.php`):
```php
return [
'manifest' => [
'scan_directories' => [
'app/RSpade/tests', // Include framework tests in manifest
],
],
];
```
This allows test services, tasks, and other test code to be discovered by the manifest system **only when in test mode**, without polluting the production manifest.
### Database Reset Process
**Script**: `/system/app/RSpade/tests/_lib/db_reset.sh`
**What it does**:
1. **Safety checks** - Verifies we're in test mode, fails loud if not
2. **Drop/recreate** - `DROP DATABASE rspade_test; CREATE DATABASE rspade_test;`
3. **Restore snapshot** - If snapshot exists, restore it (fast)
4. **Run migrations** - `php artisan migrate --production` (runs any new migrations not in snapshot)
**Safety checks**:
```bash
# Ensures DB_DATABASE=rspade_test (fails if not)
if ! grep -q "^DB_DATABASE=rspade_test" /var/www/html/.env; then
echo "FATAL ERROR: db_reset.sh must be called from within test mode!"
exit 1
fi
# Ensures test config is loaded (fails if not)
if ! grep -q "^RSX_ADDITIONAL_CONFIG=.*rsx_test_config.php" /var/www/html/.env; then
echo "FATAL ERROR: Test config not loaded!"
exit 1
fi
```
**When to call**:
- At the beginning of every test (unless using `--skip-reset` for test composition)
- Ensures known database state for deterministic tests
**When NOT to call manually**:
- Tests call it automatically - no manual intervention needed
## TEST COMPOSITION
### The --skip-reset Flag
**Purpose**: Allows tests to call other tests without resetting database state between them.
**Use case**: Building complex state by chaining simpler tests.
**Example**:
```bash
# Test A creates a user
./tests/users/create_user.sh
# Test B needs a user to exist, calls Test A with --skip-reset
./tests/users/create_user.sh --skip-reset
# Now test database has a user from Test A
# Test B can proceed with its logic without losing that state
```
**Pattern**:
```bash
# In your test script
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset)
SKIP_RESET=true
shift
;;
esac
done
# Later in setup
if [ "$SKIP_RESET" = false ]; then
"$TEST_DIR/../../_lib/db_reset.sh"
fi
```
**When to use**:
- Calling prerequisite tests to set up state
- Composing complex tests from simpler building blocks
- Chaining tests that depend on each other's state
**When NOT to use**:
- Running tests independently (always reset for determinism)
- Tests should be runnable in isolation without `--skip-reset` by default
## WRITING NEW TESTS
### Step 1: Create Test Directory
```bash
mkdir -p /system/app/RSpade/tests/category/NN_test_name
cd /system/app/RSpade/tests/category/NN_test_name
```
**Naming**:
- `category/` - Group of related tests (basic, tasks, database, etc.)
- `NN_` - Two-digit prefix for ordering (01_, 02_, etc.)
- `test_name` - Descriptive name with underscores
### Step 2: Create run_test.sh
```bash
#!/bin/bash
set -e
TEST_NAME="Descriptive Test Name"
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$TEST_DIR/../../_lib/test_env.sh"
# --skip-reset support
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset) SKIP_RESET=true; shift ;;
esac
done
trap test_trap_exit EXIT
# SETUP
echo "[SETUP] Preparing test..." >&2
test_mode_enter
if [ "$SKIP_RESET" = false ]; then
echo "[SETUP] Resetting test database..." >&2
"$TEST_DIR/../../_lib/db_reset.sh"
fi
# TEST LOGIC
echo "[TEST] Testing feature..." >&2
# Your test code here
# Use test_db_query and test_db_count helpers
# Example assertion
result=$(test_db_query "SELECT COUNT(*) FROM users")
if [ "$result" != "0" ]; then
echo "FAIL: $TEST_NAME - Expected 0 users, found $result"
exit 1
fi
# All tests passed
echo "PASS: $TEST_NAME"
exit 0
```
Make it executable:
```bash
chmod +x run_test.sh
```
### Step 3: Create README.md
Document what the test verifies, prerequisites, expected output, etc. See existing test READMEs for examples.
### Step 4: Test Your Test
**Critical**: Run your test multiple times to verify it's deterministic and repeatable.
```bash
# Run once
./run_test.sh
# Run again immediately - should pass again
./run_test.sh
# Run a third time - still should pass
./run_test.sh
```
**If it fails on subsequent runs**:
- Test is not properly resetting state
- Test is leaving artifacts in database
- Test has race conditions or timing issues
- **FIX THE TEST** - don't work around it
### Writing Good Test Assertions
**Fail loud with clear error messages**:
```bash
# ❌ BAD - Cryptic failure
if [ "$count" != "5" ]; then
exit 1
fi
# ✅ GOOD - Clear failure message
if [ "$count" != "5" ]; then
echo "FAIL: $TEST_NAME - Expected 5 records, found $count"
exit 1
fi
```
**Test one thing per assertion**:
```bash
# ✅ GOOD - Separate assertions
if [ "$status" != "completed" ]; then
echo "FAIL: $TEST_NAME - Expected status 'completed', got '$status'"
exit 1
fi
if [ -z "$result" ]; then
echo "FAIL: $TEST_NAME - Expected result, got empty string"
exit 1
fi
```
**Use helper functions**:
```bash
# ✅ GOOD - Using test_db_count helper
count=$(test_db_count "_tasks WHERE status='pending'")
if [ "$count" -ne 1 ]; then
echo "FAIL: $TEST_NAME - Expected 1 pending task, found $count"
exit 1
fi
```
## DEBUGGING FAILED TESTS
### Step 1: Read the Error Message
Failed tests should output clear error messages. Read them carefully.
```bash
[TEST] ✓ Task dispatched with ID: 1
FAIL: Task Dispatch and Execution - Expected status 'completed', got 'pending'
```
**Analysis**: Task was dispatched but never completed. Issue is in task processing, not dispatch.
### Step 2: Add Debugging Output
Add temporary debugging to understand what's happening:
```bash
# Add debugging
echo "[DEBUG] Task ID: $task_id" >&2
echo "[DEBUG] Status: $status" >&2
echo "[DEBUG] Result: $result" >&2
# Run test
./run_test.sh
```
### Step 3: Query Database Directly
Use test helpers to inspect database state:
```bash
# In your test script
test_db_query "SELECT * FROM _tasks WHERE id=$task_id" >&2
```
### Step 4: Verify Test Database State
Check that test database is properly reset:
```bash
source _lib/test_env.sh
test_mode_enter
test_db_query "SHOW TABLES"
test_mode_exit
```
### Step 5: Fix Root Cause
**DO NOT**:
- Skip the failing assertion
- Work around the failure with try/catch or || true
- Switch to production database
- Modify the test to expect wrong behavior
**DO**:
- Fix the underlying code being tested
- Fix the test infrastructure (db_reset, test_env, etc.)
- Ask user for guidance if unclear
## COMMON MISTAKES
### Mistake 1: Manual Database Setup
**Wrong**:
```bash
# Running commands manually before test
mysql -urspade -prspadepass rspade_test -e "INSERT INTO users ..."
./run_test.sh
```
**Correct**:
```bash
# Test sets up its own state automatically
./run_test.sh
# Test calls db_reset.sh internally, sets up state, runs assertions
```
### Mistake 2: Using Production Database
**Wrong**:
```bash
# Test fails on rspade_test, so switch to rspade
sed -i 's/rspade_test/rspade/' .env
./run_test.sh
```
**Correct**:
```bash
# Fix the underlying issue so test passes on rspade_test
# Maybe db_reset.sh isn't running migrations?
# Maybe test service isn't in manifest?
# FIX THE PROBLEM, don't bypass it
```
### Mistake 3: Hiding Failures
**Wrong**:
```bash
# Test assertion fails, so comment it out
# if [ "$count" -ne 5 ]; then
# echo "FAIL"
# exit 1
# fi
```
**Correct**:
```bash
# Fix why count isn't 5
# Add debugging to understand what's happening
echo "[DEBUG] Count: $count" >&2
test_db_query "SELECT * FROM users" >&2
# Then fix the underlying issue
```
### Mistake 4: Non-Deterministic Tests
**Wrong**:
```bash
# Test depends on timestamp or random values
expected=$(date +%s)
actual=$(test_db_query "SELECT created_at FROM users WHERE id=1")
if [ "$actual" != "$expected" ]; then
echo "FAIL"
fi
```
**Correct**:
```bash
# Test for existence, not exact timestamp
created_at=$(test_db_query "SELECT created_at FROM users WHERE id=1")
if [ -z "$created_at" ]; then
echo "FAIL: $TEST_NAME - created_at is empty"
exit 1
fi
echo "[TEST] ✓ created_at is set" >&2
```
## AI AGENT RESPONSIBILITIES
### When Writing Tests
1. **Follow the standard pattern** - Use existing tests as templates
2. **Make tests deterministic** - Same input = same output, always
3. **Make tests repeatable** - Run 3+ times to verify
4. **Fail loud with clear messages** - Future debugging will thank you
5. **Document what's being tested** - README.md for each test
### When Debugging Failed Tests
1. **Read the error message carefully** - It's telling you something
2. **Analyze the root cause** - Don't jump to solutions
3. **Fix the underlying issue** - Not the test, the actual problem
4. **If unclear, ask the user** - Better to ask than guess wrong
5. **Never work around failures** - Defeats the entire purpose
### When NOT to Create Tests
Tests should verify framework functionality, not individual features of user applications.
**Create tests for**:
- Core framework systems (manifest, bundles, routing, auth)
- Framework utilities (task system, file uploads, caching)
- Critical infrastructure (database connections, migrations)
**Do NOT create tests for**:
- User application features (unless explicitly requested)
- One-off debugging (use artisan commands instead)
- Features that aren't production-ready yet
### Red Flags - Stop and Ask User
If you encounter any of these, STOP and ask user for guidance:
1. **Test fails only on subsequent runs** - State isn't properly reset
2. **Test requires manual setup steps** - Test infrastructure is broken
3. **Test passes but output shows errors** - Something is being silently caught
4. **Can't reproduce failure** - Race condition or non-deterministic test
5. **Unclear what test should verify** - Specification ambiguity
## SUMMARY
**Remember these core principles**:
1. **Tests are fully automated** - One command, no manual steps
2. **Tests are deterministic** - Same input = same output, always
3. **Tests are repeatable** - Can run 100 times with identical results
4. **Tests require zero human assistance** - Complete autonomously
5. **Test failures are valuable** - They reveal real problems
6. **Never work around test failures** - Fix the root cause or ask user
7. **Manual intervention is forbidden** - Fix the infrastructure instead
**The "Press Button, Light Turns Green" Philosophy**: If the button doesn't turn the light green, fix the button. Don't paint the light green and declare success.
When in doubt, ask the user. It's better to raise a concern than silently compromise test integrity.

422
app/RSpade/tests/README.md Executable file
View File

@@ -0,0 +1,422 @@
# RSpade Testing Framework
## Philosophy
These tests verify RSpade framework functionality using real integration tests:
- **Real database** (`rspade_test` - completely isolated from production)
- **Real browser** (Playwright with Chromium for UI tests)
- **Real HTTP requests** (curl, Playwright)
- **No mocks** - test actual behavior, not simulations
Tests are designed to be:
1. **Self-contained** - Each test is a standalone directory with `run_test.sh`
2. **Explicit** - Verbose over DRY, clear over clever
3. **LLM-friendly** - Easy for AI to read, write, and maintain
4. **Evolutionary** - Patterns emerge organically as tests are written
## Running Tests
**Run all automated tests:**
```bash
./run_all_tests.sh
```
Automatically creates fresh database snapshot before running tests.
**Run all tests with existing snapshot (faster):**
```bash
./run_all_tests.sh --use-existing-snapshot
```
**Run all tests with full migration output:**
```bash
./run_all_tests.sh --full-output
```
**Run specific test:**
```bash
./basic/01_framework_verification/run_test.sh
```
**Run specific test without database reset:**
```bash
./basic/01_framework_verification/run_test.sh --skip-reset
```
Use with caution - only for manual testing or when composing tests.
## Test Database
All tests use the `rspade_test` database:
- Completely separate from `rspade` production database
- Created fresh: `rspade_test` (utf8mb4_unicode_ci)
- Full access granted to `rspade` user
- Reset before each top-level test (ensures deterministic behavior)
**Database reset process:**
1. Drop `rspade_test` database
2. Create `rspade_test` database
3. Either:
- **With snapshot**: Restore snapshot + run new migrations (~1 second)
- **Without snapshot**: Run all migrations (~5-10 seconds)
**Create snapshot** (first time or after adding migrations):
```bash
./_lib/db_snapshot_create.sh # Quiet mode
./_lib/db_snapshot_create.sh --full-output # Show all migration output
```
This creates `_lib/test_db_snapshot.sql` which speeds up all future test runs.
If snapshot creation fails, run with `--full-output` to see detailed error messages.
## Test Contract
Every test directory MUST have:
**`run_test.sh`** - Executable script that:
- Accepts `--skip-reset` flag to skip database reset (for test composition)
- Sets up test prerequisites
- Runs the actual test
- Cleans up after itself (trap EXIT)
- Exits with code 0 (pass) or 1 (fail)
- Outputs EXACTLY ONE LINE to stdout:
- `PASS: Test description`
- `FAIL: Test description - reason`
- `SKIP: Test description - reason`
### Test Composition with --skip-reset
Tests can call other tests to set up complex state:
```bash
# Complex test that needs attachments and users
echo "[SETUP] Creating prerequisite state..." >&2
"$TEST_DIR/../01_create_user/run_test.sh" --skip-reset
"$TEST_DIR/../02_add_attachment/run_test.sh" --skip-reset
# Now test the feature that requires both users and attachments
echo "[TEST] Testing feature with complex state..." >&2
# ... your test logic ...
```
**Important**: Only the top-level test (called without --skip-reset) resets the database. Sub-tests called with --skip-reset build upon the existing state.
**`README.md`** - Documents:
- What this test verifies
- Prerequisites/assumptions
- How to run standalone
- Known limitations
## Standard Test Pattern
### Basic Test Template
```bash
#!/bin/bash
set -e
TEST_NAME="Example Test"
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source test environment helpers
source "$TEST_DIR/../../_lib/test_env.sh"
# Parse arguments
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset)
SKIP_RESET=true
shift
;;
esac
done
# Ensure test mode exits on script exit (success or failure)
trap test_trap_exit EXIT
# SETUP
echo "[SETUP] Preparing test environment..." >&2
# Reset database unless --skip-reset
if [ "$SKIP_RESET" = false ]; then
"$TEST_DIR/../../_lib/db_reset.sh"
fi
# Enter test mode (switches Laravel to rspade_test database)
test_mode_enter
# TEST LOGIC
echo "[TEST] Running test logic..." >&2
# Your test code here
count=$(test_db_count users)
if [ "$count" -eq 0 ]; then
echo "PASS: $TEST_NAME"
exit 0
else
echo "FAIL: $TEST_NAME - Expected 0 users, found $count"
exit 1
fi
# TEARDOWN happens automatically via trap
```
### Key Patterns
- `set -e` - Exit on first error
- `trap test_trap_exit EXIT` - Ensures `.env` always restored
- `--skip-reset` flag - Enables test composition (calling other tests to build state)
- `test_mode_enter` - Switches Laravel to test database
- Log to stderr `>&2`, only PASS/FAIL to stdout
- Single exit point with clear status
## Shared Helpers
Located in `_lib/`, source in your test:
### test_env.sh
```bash
source "$TEST_DIR/../../_lib/test_env.sh"
test_mode_enter # Switch Laravel to rspade_test + run rsx:clean
test_mode_exit # Restore Laravel to rspade + run rsx:clean
test_trap_exit # Cleanup (call in trap EXIT)
test_db_query "SQL" # Execute SQL on test database
test_db_count "table" # Count rows in table
```
**Important**: `test_mode_enter` and `test_mode_exit` automatically run `rsx:clean` after switching the database in `.env` to ensure the manifest is rebuilt for the correct database environment.
### db_reset.sh
```bash
"$TEST_DIR/../../_lib/db_reset.sh" # Drop, create, migrate rspade_test
```
**Note**: Always check `--skip-reset` flag before calling db_reset.sh
## Test Organization
```
tests/
├── _lib/ Shared utilities (grow organically)
├── basic/ Template and fundamental tests
├── core/ Framework core functionality tests
└── features/ Feature-specific tests
```
## Writing New Tests
1. **Create test directory:**
```bash
mkdir -p basic/my_new_test
```
2. **Copy template:**
```bash
cp basic/01_framework_verification/run_test.sh basic/my_new_test/
```
3. **Edit test logic:**
- Update `TEST_NAME`
- Modify test logic section
- Update assertions
4. **Create README.md:**
```markdown
# My New Test
Tests that [feature] works correctly.
## What it verifies
- Thing 1
- Thing 2
## How to run
```bash
./basic/my_new_test/run_test.sh
```
5. **Make executable:**
```bash
chmod +x basic/my_new_test/run_test.sh
```
6. **Run standalone:**
```bash
./basic/my_new_test/run_test.sh
```
## Interactive Tests
Some tests require human verification and use `run_interactive_test.sh`:
- Not run by default in `run_all_tests.sh`
- Set up environment, provide instructions, wait for user input
- Useful for visual testing, complex flows, accessibility
**Example:**
```bash
#!/bin/bash
set -e
# ... setup code ...
echo "Instructions:"
echo "1. Visit http://localhost/login"
echo "2. Verify form displays correctly"
echo "3. Did test pass? (yes/no/skip): "
read -r response
case "$response" in
yes|y) echo "PASS: $TEST_NAME"; exit 0 ;;
no|n) echo "FAIL: $TEST_NAME"; exit 1 ;;
skip|s) echo "SKIP: $TEST_NAME"; exit 0 ;;
esac
```
Run interactive tests:
```bash
./run_interactive_tests.sh
```
## Best Practices
### Database
- Always use `--skip-reset` flag in test contract
- Default: reset database (safe but slow)
- Only skip reset when you know database state is correct
- Future: Snapshot/restore will make this fast
### Output
- Log progress to stderr: `echo "..." >&2`
- Only PASS/FAIL/SKIP to stdout
- Include useful context in failures: `FAIL: Test - Expected X got Y`
### Error Handling
- Use `set -e` to exit on first error
- Add cleanup trap: `trap test_trap_exit EXIT`
- Test edge cases (empty results, missing data)
### Test Independence
- Each test should work standalone
- Don't rely on other tests running first
- Reset database or seed required data
- Clean up temp files
## Common Patterns
### Pattern: Database Query Test
```bash
# Query test database
result=$(test_db_query "SELECT name FROM users WHERE id=1")
if [ "$result" = "Test User" ]; then
echo "PASS: User name correct"
else
echo "FAIL: Expected 'Test User', got '$result'"
exit 1
fi
```
### Pattern: Count Test
```bash
count=$(test_db_count users)
if [ "$count" -eq 5 ]; then
echo "PASS: Correct user count"
else
echo "FAIL: Expected 5 users, found $count"
exit 1
fi
```
### Pattern: API Test
```bash
response=$(curl -s http://localhost/_ajax/Test/method)
if echo "$response" | jq -e '.success == true' > /dev/null 2>&1; then
echo "PASS: API returns success"
else
echo "FAIL: API failed - $response"
exit 1
fi
```
## Database Snapshot System
The testing framework uses database snapshots to speed up test runs.
**How it works:**
1. First time: Run `_lib/db_snapshot_create.sh` to create pristine snapshot
2. Tests restore from snapshot (~1 second) instead of running all migrations (~5-10 seconds)
3. After snapshot restore, any new migrations run automatically
4. When you add new migrations, recreate snapshot for maximum speed
**When to recreate snapshot:**
- After adding new migration files
- After modifying existing migration files
- If you notice tests taking longer than expected
**Snapshot file:**
- Location: `_lib/test_db_snapshot.sql`
- Not committed to git (add to .gitignore if needed)
- Each developer creates their own snapshot locally
## Troubleshooting
**Test hangs:**
- Check for missing cleanup trap
- Look for processes not cleaned up
- Add timeout to long operations
**Database errors:**
- Verify `rspade_test` database exists
- Check credentials work: `mysql -urspade -prspadepass rspade_test`
- Ensure migrations ran: `mysql -urspade -prspadepass rspade_test -e "SHOW TABLES"`
**Environment not restored:**
- Check trap is set: `trap test_trap_exit EXIT`
- Verify `.env.backup` doesn't exist after test
- Manually restore: `test_mode_exit`
**Test passes alone, fails in suite:**
- Database state contamination
- Missing `--skip-reset` flag handling
- Shared resource conflict
## Evolution
This framework is intentionally minimal. As you write tests:
- Extract common patterns into `_lib/` helpers
- Update templates with better patterns
- Document new best practices here
- Don't over-abstract - keep tests explicit
When in doubt, favor:
- **Explicit** over implicit
- **Verbose** over DRY
- **Working** over elegant
## Testing the Test Framework
The first test is a meta-test that verifies the framework itself works:
```bash
./basic/01_framework_verification/run_test.sh
```
This test verifies:
- Test environment helpers load correctly
- Test mode enter/exit works
- Database reset works
- Migrations run successfully
- Test database is accessible

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# RSpade Test Database Reset Script
#
# Drops and recreates rspade_test database, then either:
# 1. Restores from snapshot (fast) + runs any new migrations
# 2. Runs all migrations from scratch (slow, if no snapshot exists)
#
# MUST be called from within test mode (after test_mode_enter)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SNAPSHOT_FILE="$SCRIPT_DIR/test_db_snapshot.sql"
# SAFETY CHECK: Ensure we're in test mode
if ! grep -q "^DB_DATABASE=rspade_test" /var/www/html/.env; then
echo "FATAL ERROR: db_reset.sh must be called from within test mode!" >&2
echo "" >&2
echo "Current database: $(grep "^DB_DATABASE=" /var/www/html/.env | cut -d= -f2)" >&2
echo "" >&2
echo "This is a safety check to prevent accidentally resetting the production database." >&2
echo "" >&2
echo "Correct usage:" >&2
echo " source /system/app/RSpade/tests/_lib/test_env.sh" >&2
echo " test_mode_enter" >&2
echo " _lib/db_reset.sh" >&2
echo " # ... run tests ..." >&2
echo " test_mode_exit" >&2
exit 1
fi
if ! grep -q "^RSX_ADDITIONAL_CONFIG=.*rsx_test_config.php" /var/www/html/.env; then
echo "FATAL ERROR: Test config not loaded!" >&2
echo "" >&2
echo "RSX_ADDITIONAL_CONFIG must point to rsx_test_config.php when running tests." >&2
echo "This should be set automatically by test_mode_enter." >&2
echo "" >&2
echo "Current RSX_ADDITIONAL_CONFIG: $(grep "^RSX_ADDITIONAL_CONFIG=" /var/www/html/.env || echo 'NOT SET')" >&2
exit 1
fi
echo "[DB RESET] Dropping rspade_test database..." >&2
mysql -h127.0.0.1 -urspade -prspadepass -e "DROP DATABASE IF EXISTS rspade_test" 2>/dev/null
echo "[DB RESET] Creating rspade_test database..." >&2
mysql -h127.0.0.1 -urspade -prspadepass -e "CREATE DATABASE rspade_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" 2>/dev/null
cd /var/www/html
if [ -f "$SNAPSHOT_FILE" ]; then
# Fast path: Restore from snapshot
echo "[DB RESET] Restoring from snapshot..." >&2
mysql -h127.0.0.1 -urspade -prspadepass rspade_test < "$SNAPSHOT_FILE" 2>/dev/null
# Run any new migrations that were added after snapshot
echo "[DB RESET] Running new migrations..." >&2
php artisan migrate --production 2>&1 | grep -v "Nothing to migrate" >&2
else
# Slow path: Run all migrations from scratch
echo "[DB RESET] No snapshot found, running all migrations..." >&2
echo "[DB RESET] (Create snapshot with: _lib/db_snapshot_create.sh)" >&2
php artisan migrate --production 2>&1 | grep -v "Nothing to migrate" >&2
fi
echo "[DB RESET] Database reset complete" >&2

View File

@@ -0,0 +1,103 @@
#!/bin/bash
# RSpade Test Database Snapshot Creator
#
# Creates a pristine snapshot of the test database after running all migrations.
# This snapshot can be used to speed up test database resets.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SNAPSHOT_FILE="$SCRIPT_DIR/test_db_snapshot.sql"
FULL_OUTPUT=false
# Parse arguments
for arg in "$@"; do
case $arg in
--full-output)
FULL_OUTPUT=true
shift
;;
esac
done
echo "[SNAPSHOT] Creating pristine test database snapshot..." >&2
# Source test environment helpers
source "$SCRIPT_DIR/test_env.sh"
# Ensure test mode exits on script exit (success or failure)
trap test_trap_exit EXIT
# Reset database and run ALL migrations from scratch
echo "[SNAPSHOT] Resetting test database..." >&2
if [ "$FULL_OUTPUT" = true ]; then
# Show full migration output
"$SCRIPT_DIR/db_reset.sh"
else
# Suppress output, capture errors
TEMP_OUTPUT="/tmp/snapshot_output_$$"
if ! "$SCRIPT_DIR/db_reset.sh" > "$TEMP_OUTPUT" 2>&1; then
echo "" >&2
echo "[ERROR] Database provisioning failed" >&2
echo "[ERROR] Run with --full-output to see details:" >&2
echo "[ERROR] $0 --full-output" >&2
echo "" >&2
cat "$TEMP_OUTPUT" >&2
rm -f "$TEMP_OUTPUT"
exit 1
fi
rm -f "$TEMP_OUTPUT"
fi
# Enter test mode to verify database is accessible
test_mode_enter > /dev/null 2>&1
# Verify migrations ran
migration_count=$(test_db_query "SELECT COUNT(*) FROM migrations")
if [ -z "$migration_count" ] || [ "$migration_count" -eq 0 ]; then
echo "[ERROR] No migrations found in database" >&2
echo "[ERROR] Run with --full-output to see details:" >&2
echo "[ERROR] $0 --full-output" >&2
exit 1
fi
echo "[SNAPSHOT] Found $migration_count migrations" >&2
# Exit test mode before dump
test_mode_exit > /dev/null 2>&1
# Create snapshot
echo "[SNAPSHOT] Creating snapshot file..." >&2
# Backup current .env
SNAPSHOT_ENV_BACKUP="/tmp/rspade_snapshot_backup_$$"
cp /var/www/html/.env "$SNAPSHOT_ENV_BACKUP"
# Switch to test database for dump
sed -i 's/^DB_DATABASE=.*$/DB_DATABASE=rspade_test/' /var/www/html/.env
php artisan config:clear > /dev/null 2>&1
# Create dump
if ! mysqldump -h127.0.0.1 -urspade -prspadepass rspade_test \
--no-tablespaces \
--single-transaction \
--quick \
--lock-tables=false \
> "$SNAPSHOT_FILE" 2>/dev/null; then
echo "[ERROR] Failed to create database dump" >&2
mv "$SNAPSHOT_ENV_BACKUP" /var/www/html/.env
php artisan config:clear > /dev/null 2>&1
exit 1
fi
# Restore original .env
mv "$SNAPSHOT_ENV_BACKUP" /var/www/html/.env
php artisan config:clear > /dev/null 2>&1
# Get snapshot size
snapshot_size=$(du -h "$SNAPSHOT_FILE" | cut -f1)
echo "[SNAPSHOT] Snapshot created successfully ($snapshot_size)" >&2
echo "[SNAPSHOT] Tests will now use this snapshot for faster database resets" >&2

View File

@@ -0,0 +1,18 @@
<?php
/**
* RSpade Test Configuration
*
* Additional configuration loaded when RSX_ADDITIONAL_CONFIG env variable is set.
* Used by test runner to include tests directory in manifest scanning.
*
* Loaded by: test_mode_enter.sh
* Cleared by: test_mode_exit.sh
*/
return [
'manifest' => [
'scan_directories' => [
'app/RSpade/tests', // Include framework tests directory in manifest
],
],
];

View File

@@ -0,0 +1,928 @@
-- MySQL dump 10.13 Distrib 8.0.43, for Linux (x86_64)
--
-- Host: 127.0.0.1 Database: rspade_test
-- ------------------------------------------------------
-- Server version 8.0.43-0ubuntu0.22.04.2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `client_departments`
--
DROP TABLE IF EXISTS `client_departments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `client_departments` (
`id` bigint NOT NULL AUTO_INCREMENT,
`site_id` bigint NOT NULL,
`client_id` bigint NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`created_by` bigint DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_site_id` (`site_id`),
KEY `idx_client_id` (`client_id`),
KEY `idx_name` (`name`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
CONSTRAINT `fk_client_departments_client` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `client_departments`
--
LOCK TABLES `client_departments` WRITE;
/*!40000 ALTER TABLE `client_departments` DISABLE KEYS */;
/*!40000 ALTER TABLE `client_departments` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `clients`
--
DROP TABLE IF EXISTS `clients`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `clients` (
`id` bigint NOT NULL AUTO_INCREMENT,
`site_id` bigint NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`address` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`city` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`state` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`zip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fax` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone_secondary` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`billing_contact_id` bigint DEFAULT NULL,
`priority` bigint NOT NULL DEFAULT '2',
`notes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`created_by` bigint DEFAULT NULL,
`owner_user_id` bigint DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint DEFAULT NULL,
`address_street` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'renamed from address',
`address_country` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'USA',
`industry` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`company_size` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '1-10, 11-50, 51-200, 201-500, 501-1000, 1000+',
`established_year` bigint DEFAULT NULL,
`revenue_range` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`facebook_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`twitter_handle` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`linkedin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`instagram_handle` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`tags` json DEFAULT NULL,
`status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active' COMMENT 'active, inactive, prospect, archived',
`preferred_contact_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'email' COMMENT 'email, phone, LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, any',
`newsletter_opt_in` tinyint(1) NOT NULL DEFAULT '0',
`deleted_at` timestamp(3) NULL DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_site_id` (`site_id`),
KEY `idx_name` (`name`),
KEY `idx_created_by_user_id` (`created_by`),
KEY `idx_owner_user_id` (`owner_user_id`),
KEY `idx_billing_contact_id` (`billing_contact_id`),
KEY `idx_priority` (`priority`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_email` (`email`),
KEY `idx_status` (`status`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_clients_billing_contact` FOREIGN KEY (`billing_contact_id`) REFERENCES `contacts` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `clients`
--
LOCK TABLES `clients` WRITE;
/*!40000 ALTER TABLE `clients` DISABLE KEYS */;
/*!40000 ALTER TABLE `clients` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `contacts`
--
DROP TABLE IF EXISTS `contacts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `contacts` (
`id` bigint NOT NULL AUTO_INCREMENT,
`site_id` bigint NOT NULL,
`client_id` bigint NOT NULL,
`first_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`client_department_id` bigint DEFAULT NULL,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`email_secondary` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone_work` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone_cell` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone_other` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`address` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`city` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`state` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`zip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`reports_to_contact_id` bigint DEFAULT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`priority` bigint NOT NULL DEFAULT '2',
`notes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`created_by` bigint DEFAULT NULL,
`owner_user_id` bigint DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint DEFAULT NULL,
`deleted_at` timestamp(3) NULL DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_site_id` (`site_id`),
KEY `idx_client_id` (`client_id`),
KEY `idx_first_name` (`first_name`),
KEY `idx_last_name` (`last_name`),
KEY `idx_email` (`email`),
KEY `idx_client_department_id` (`client_department_id`),
KEY `idx_reports_to_contact_id` (`reports_to_contact_id`),
KEY `idx_is_active` (`is_active`),
KEY `idx_priority` (`priority`),
KEY `idx_created_by_user_id` (`created_by`),
KEY `idx_owner_user_id` (`owner_user_id`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_contacts_client` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_contacts_department` FOREIGN KEY (`client_department_id`) REFERENCES `client_departments` (`id`) ON DELETE SET NULL,
CONSTRAINT `fk_contacts_reports_to` FOREIGN KEY (`reports_to_contact_id`) REFERENCES `contacts` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `contacts`
--
LOCK TABLES `contacts` WRITE;
/*!40000 ALTER TABLE `contacts` DISABLE KEYS */;
/*!40000 ALTER TABLE `contacts` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `countries`
--
DROP TABLE IF EXISTS `countries`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `countries` (
`id` bigint NOT NULL AUTO_INCREMENT,
`alpha2` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ISO 3166-1 alpha-2 code (US, CA, GB)',
`alpha3` varchar(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ISO 3166-1 alpha-3 code (USA, CAN, GBR)',
`numeric` varchar(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ISO 3166-1 numeric code (840, 124, 826)',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Official country name',
`common_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Common name if different from official',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Active status - disable instead of delete to preserve FKs',
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `alpha2` (`alpha2`),
KEY `idx_alpha2` (`alpha2`),
KEY `idx_alpha3` (`alpha3`),
KEY `idx_enabled` (`enabled`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `countries`
--
LOCK TABLES `countries` WRITE;
/*!40000 ALTER TABLE `countries` DISABLE KEYS */;
/*!40000 ALTER TABLE `countries` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `demo_products`
--
DROP TABLE IF EXISTS `demo_products`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `demo_products` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`price` decimal(10,2) NOT NULL,
`status_id` bigint NOT NULL DEFAULT '1',
`category_id` bigint NOT NULL DEFAULT '1',
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_status_id` (`status_id`),
KEY `idx_category_id` (`category_id`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `demo_products`
--
LOCK TABLES `demo_products` WRITE;
/*!40000 ALTER TABLE `demo_products` DISABLE KEYS */;
INSERT INTO `demo_products` VALUES (1,'Wireless Mouse','Ergonomic wireless mouse with 2.4GHz connectivity',29.99,1,1,'2025-11-16 19:37:32.241','2025-11-16 19:37:32.241',NULL,NULL),(2,'Vintage T-Shirt','Classic cotton t-shirt with retro design',19.99,1,2,'2025-11-16 19:37:32.241','2025-11-16 19:37:32.241',NULL,NULL),(3,'JavaScript: The Good Parts','Essential guide to JavaScript programming',39.99,2,3,'2025-11-16 19:37:32.241','2025-11-16 19:37:32.241',NULL,NULL),(4,'Premium Coffee Beans','Single-origin Arabica beans from Colombia',24.99,1,4,'2025-11-16 19:37:32.241','2025-11-16 19:37:32.241',NULL,NULL),(5,'Gaming Laptop','High-performance laptop with RTX graphics',1299.99,3,1,'2025-11-16 19:37:32.241','2025-11-16 19:37:32.241',NULL,NULL);
/*!40000 ALTER TABLE `demo_products` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `file_attachments`
--
DROP TABLE IF EXISTS `file_attachments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `file_attachments` (
`id` bigint NOT NULL AUTO_INCREMENT,
`key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`file_storage_id` bigint NOT NULL,
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`file_extension` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`file_type_id` bigint DEFAULT NULL,
`width` bigint DEFAULT NULL COMMENT 'Width in pixels for images/videos',
`height` bigint DEFAULT NULL COMMENT 'Height in pixels for images/videos',
`duration` bigint DEFAULT NULL COMMENT 'Duration in seconds for videos/animated images',
`is_animated` tinyint(1) DEFAULT '0' COMMENT 'Whether this is an animated image (GIF, WebP, APNG)',
`frame_count` bigint DEFAULT NULL COMMENT 'Number of frames in animated images',
`fileable_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fileable_id` bigint DEFAULT NULL,
`fileable_category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fileable_type_meta` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fileable_order` bigint DEFAULT NULL,
`fileable_meta` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`site_id` bigint NOT NULL,
`session_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_files_key` (`key`),
KEY `idx_files_file_hash_id` (`file_storage_id`),
KEY `idx_files_site_id` (`site_id`),
KEY `idx_files_fileable` (`fileable_type`,`fileable_id`),
KEY `idx_files_file_type_id` (`file_type_id`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_file_attachments_type_meta` (`fileable_type_meta`),
KEY `idx_session_id` (`session_id`),
CONSTRAINT `file_attachments_ibfk_1` FOREIGN KEY (`file_storage_id`) REFERENCES `file_storage` (`id`) ON DELETE RESTRICT,
CONSTRAINT `file_attachments_ibfk_2` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `file_attachments`
--
LOCK TABLES `file_attachments` WRITE;
/*!40000 ALTER TABLE `file_attachments` DISABLE KEYS */;
/*!40000 ALTER TABLE `file_attachments` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `file_storage`
--
DROP TABLE IF EXISTS `file_storage`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `file_storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`hash` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`size` bigint NOT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_file_hashes_hash` (`hash`),
KEY `idx_file_hashes_size` (`size`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `file_storage`
--
LOCK TABLES `file_storage` WRITE;
/*!40000 ALTER TABLE `file_storage` DISABLE KEYS */;
/*!40000 ALTER TABLE `file_storage` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `flash_alerts`
--
DROP TABLE IF EXISTS `flash_alerts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `flash_alerts` (
`id` bigint NOT NULL AUTO_INCREMENT,
`session_id` bigint DEFAULT NULL,
`type_id` bigint NOT NULL,
`message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
KEY `idx_session_id` (`session_id`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
CONSTRAINT `flash_alerts_session_fk` FOREIGN KEY (`session_id`) REFERENCES `sessions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `flash_alerts`
--
LOCK TABLES `flash_alerts` WRITE;
/*!40000 ALTER TABLE `flash_alerts` DISABLE KEYS */;
/*!40000 ALTER TABLE `flash_alerts` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `ip_addresses`
--
DROP TABLE IF EXISTS `ip_addresses`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `ip_addresses` (
`id` bigint NOT NULL AUTO_INCREMENT,
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`city` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`state` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`country` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`lat` double DEFAULT NULL,
`lng` double DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ip_addresses_ip` (`ip_address`),
KEY `idx_ip_addresses_country` (`country`),
KEY `idx_ip_addresses_location` (`lat`,`lng`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `ip_addresses`
--
LOCK TABLES `ip_addresses` WRITE;
/*!40000 ALTER TABLE `ip_addresses` DISABLE KEYS */;
/*!40000 ALTER TABLE `ip_addresses` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `login_users`
--
DROP TABLE IF EXISTS `login_users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `login_users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`is_activated` tinyint(1) NOT NULL DEFAULT '0',
`is_verified` tinyint(1) NOT NULL DEFAULT '0',
`status_id` bigint NOT NULL DEFAULT '1',
`remember_token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`last_login` timestamp(3) NULL DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_login_users_email` (`email`),
KEY `idx_login_users_is_activated` (`is_activated`),
KEY `idx_login_users_is_verified` (`is_verified`),
KEY `idx_login_users_status_id` (`status_id`),
KEY `idx_login_users_created_at` (`created_at`),
KEY `idx_login_users_updated_at` (`updated_at`),
KEY `idx_login_users_last_login` (`last_login`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `login_users`
--
LOCK TABLES `login_users` WRITE;
/*!40000 ALTER TABLE `login_users` DISABLE KEYS */;
INSERT INTO `login_users` VALUES (1,'admin@test.com','$2y$10$jROh4YFvV3tNRCPLrNgmC.aIJfP.yLkUZCY39fNuKcWJ16h9xlhe.',1,1,1,NULL,NULL,'2025-11-16 19:37:30.000','2025-11-16 19:37:30.000',NULL,NULL);
/*!40000 ALTER TABLE `login_users` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `migrations`
--
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` bigint NOT NULL AUTO_INCREMENT,
`migration` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` bigint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `migrations`
--
LOCK TABLES `migrations` WRITE;
/*!40000 ALTER TABLE `migrations` DISABLE KEYS */;
INSERT INTO `migrations` VALUES (1,'2024_01_01_000000_create_sessions_table',1),(2,'2025_09_03_054656_create_sites_table',2),(3,'2025_09_03_054826_create_users_table',3),(4,'2025_09_03_054950_create_file_hashes_table',4),(5,'2025_09_03_054950_create_files_table',5),(6,'2025_09_03_054950_create_ip_addresses_table',6),(7,'2025_09_03_054950_create_site_users_table',7),(8,'2025_09_03_054951_create_user_invites_table',8),(9,'2025_09_03_054951_create_user_verifications_table',9),(10,'2025_09_03_152057_create_admin_test_user',10),(11,'2025_09_03_164754_add_rsx_columns_to_sessions_table',11),(12,'2025_09_09_102518_create_flash_alerts_table',12),(13,'2025_09_09_173210_update_flash_alerts_foreign_key_cascade',13),(14,'2025_09_11_235349_fix_flash_alerts_session_id_schema',14),(15,'2025_09_12_185559_add_csrf_token_to_sessions_table',15),(16,'2025_09_16_074547_add_status_to_users_table',16),(17,'2025_09_16_075234_rename_status_column_to_status_id',17),(18,'2025_09_16_075915_create_demo_products_table',18),(19,'2025_09_16_080116_fix_flash_alerts_foreign_key',19),(20,'2025_09_30_023430_drop_and_recreate_session_table',20),(21,'2025_09_30_050004_add_last_login_to_users_table',21),(22,'2025_10_14_172351_add_experience_id_to_sessions',22),(23,'2025_10_28_223500_create_geographic_data_tables',23),(24,'2025_11_02_063218_rename_file_tables_for_storage_attachment_refactor',24),(25,'2025_11_02_065329_add_metadata_fields_to_file_attachments',25),(26,'2025_11_02_065347_create_file_thumbnails_table',26),(27,'2025_11_02_070055_create_search_indexes_table',27),(28,'2025_11_02_162826_add_dimensions_to_file_attachments',28),(29,'2025_11_04_011033_add_name_and_phone_columns_to_users_table',29),(30,'2025_11_04_011050_create_user_profiles_table',30),(31,'2025_11_04_014825_add_session_id_to_file_attachments_table',31),(32,'2025_11_04_051746_create_login_users_table',32),(33,'2025_11_04_051828_refactor_site_users_to_users_table',33),(34,'2025_11_04_051907_update_sessions_table_for_login_user_id',34),(35,'2025_11_04_051935_update_user_profiles_foreign_key',35),(36,'2025_11_04_181832_add_invite_columns_to_users_table',36),(37,'2025_11_05_012345_make_login_user_id_nullable_in_users_table',37),(38,'2025_11_13_193740_modify_flash_alerts_table_for_type_based_system',38),(39,'2025_11_16_093331_drop_file_thumbnails_table',39),(40,'2025_10_10_021141_create_companies_table',40),(41,'2025_10_10_021204_create_company_departments_table',41),(42,'2025_10_10_021223_create_contacts_table',42),(43,'2025_10_10_021244_add_billing_contact_foreign_key_to_companies',43),(44,'2025_10_28_025035_create_projects_table',44),(45,'2025_10_28_025113_create_tasks_table',45),(46,'2025_10_28_043559_create_clients_table',46),(47,'2025_10_29_034934_add_soft_deletes_to_core_tables',47);
/*!40000 ALTER TABLE `migrations` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `projects`
--
DROP TABLE IF EXISTS `projects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `projects` (
`id` bigint NOT NULL AUTO_INCREMENT,
`site_id` bigint NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`client_id` bigint NOT NULL,
`client_department_id` bigint DEFAULT NULL,
`contact_id` bigint DEFAULT NULL,
`status` bigint NOT NULL DEFAULT '1',
`priority` bigint NOT NULL DEFAULT '2',
`start_date` date DEFAULT NULL,
`due_date` date DEFAULT NULL,
`completed_date` date DEFAULT NULL,
`budget` decimal(15,2) DEFAULT NULL,
`notes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`created_by` bigint DEFAULT NULL,
`owner_user_id` bigint DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint DEFAULT NULL,
`deleted_at` timestamp(3) NULL DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_site_id` (`site_id`),
KEY `idx_client_id` (`client_id`),
KEY `idx_client_department_id` (`client_department_id`),
KEY `idx_contact_id` (`contact_id`),
KEY `idx_status` (`status`),
KEY `idx_priority` (`priority`),
KEY `idx_created_by` (`created_by`),
KEY `idx_owner_user_id` (`owner_user_id`),
KEY `idx_created_at` (`created_at`),
KEY `idx_updated_at` (`updated_at`),
KEY `idx_name` (`name`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `projects`
--
LOCK TABLES `projects` WRITE;
/*!40000 ALTER TABLE `projects` DISABLE KEYS */;
/*!40000 ALTER TABLE `projects` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `regions`
--
DROP TABLE IF EXISTS `regions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `regions` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ISO 3166-2 code (US-CA, CA-ON, GB-ENG)',
`country_alpha2` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Foreign key to countries.alpha2',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Subdivision name (California, Ontario, England)',
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Type: state, province, territory, country, etc.',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Active status - disable instead of delete',
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_country_code` (`country_alpha2`,`code`),
KEY `idx_country` (`country_alpha2`),
KEY `idx_code` (`code`),
KEY `idx_enabled` (`enabled`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
CONSTRAINT `fk_regions_country` FOREIGN KEY (`country_alpha2`) REFERENCES `countries` (`alpha2`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `regions`
--
LOCK TABLES `regions` WRITE;
/*!40000 ALTER TABLE `regions` DISABLE KEYS */;
/*!40000 ALTER TABLE `regions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `search_indexes`
--
DROP TABLE IF EXISTS `search_indexes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `search_indexes` (
`id` bigint NOT NULL AUTO_INCREMENT,
`indexable_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`indexable_id` bigint NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`metadata` json DEFAULT NULL,
`indexed_at` timestamp(3) NULL DEFAULT NULL,
`extraction_method` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`language` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`site_id` bigint NOT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_indexable` (`indexable_type`,`indexable_id`),
KEY `idx_indexable` (`indexable_type`,`indexable_id`),
KEY `idx_site` (`site_id`),
KEY `idx_language` (`language`),
KEY `idx_indexed_at` (`indexed_at`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
FULLTEXT KEY `ft_content` (`content`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `search_indexes`
--
LOCK TABLES `search_indexes` WRITE;
/*!40000 ALTER TABLE `search_indexes` DISABLE KEYS */;
/*!40000 ALTER TABLE `search_indexes` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `sessions`
--
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` bigint NOT NULL AUTO_INCREMENT,
`active` tinyint(1) NOT NULL DEFAULT '1',
`site_id` bigint DEFAULT '0',
`login_user_id` bigint DEFAULT NULL,
`session_token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`csrf_token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`last_active` datetime(3) NOT NULL,
`version` bigint NOT NULL DEFAULT '1',
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
`experience_id` bigint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `sessions_session_token_unique` (`session_token`),
KEY `sessions_active_index` (`active`),
KEY `sessions_last_active_index` (`last_active`),
KEY `sessions_active_last_active_index` (`active`,`last_active`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_experience_id` (`experience_id`),
KEY `idx_token_experience` (`session_token`,`experience_id`),
KEY `idx_sessions_login_user_id` (`login_user_id`),
KEY `idx_sessions_login_user_id_active` (`login_user_id`,`active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `sessions`
--
LOCK TABLES `sessions` WRITE;
/*!40000 ALTER TABLE `sessions` DISABLE KEYS */;
/*!40000 ALTER TABLE `sessions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `sites`
--
DROP TABLE IF EXISTS `sites`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sites` (
`id` bigint NOT NULL AUTO_INCREMENT,
`slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`is_enabled` tinyint(1) NOT NULL DEFAULT '1',
`deleted_at` timestamp(3) NULL DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sites_slug` (`slug`),
KEY `idx_sites_is_enabled` (`is_enabled`),
KEY `idx_sites_deleted_at` (`deleted_at`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `sites`
--
LOCK TABLES `sites` WRITE;
/*!40000 ALTER TABLE `sites` DISABLE KEYS */;
/*!40000 ALTER TABLE `sites` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `tasks`
--
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tasks` (
`id` bigint NOT NULL AUTO_INCREMENT,
`site_id` bigint NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`taskable_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`taskable_id` bigint NOT NULL,
`status` bigint NOT NULL DEFAULT '1',
`priority` bigint NOT NULL DEFAULT '2',
`due_date` date DEFAULT NULL,
`completed_date` date DEFAULT NULL,
`assigned_to_user_id` bigint DEFAULT NULL,
`notes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`created_by` bigint DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint DEFAULT NULL,
`deleted_at` timestamp(3) NULL DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_site_id` (`site_id`),
KEY `idx_taskable` (`taskable_type`,`taskable_id`),
KEY `idx_status` (`status`),
KEY `idx_priority` (`priority`),
KEY `idx_assigned_to_user_id` (`assigned_to_user_id`),
KEY `idx_created_by` (`created_by`),
KEY `idx_created_at` (`created_at`),
KEY `idx_updated_at` (`updated_at`),
KEY `idx_title` (`title`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `tasks`
--
LOCK TABLES `tasks` WRITE;
/*!40000 ALTER TABLE `tasks` DISABLE KEYS */;
/*!40000 ALTER TABLE `tasks` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user_invites`
--
DROP TABLE IF EXISTS `user_invites`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_invites` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`site_id` bigint NOT NULL,
`invite_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`expires_at` timestamp(3) NOT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_invites_code` (`invite_code`),
KEY `idx_user_invites_user_id` (`user_id`),
KEY `idx_user_invites_site_id` (`site_id`),
KEY `idx_user_invites_expires_at` (`expires_at`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
CONSTRAINT `fk_user_invites_user_id` FOREIGN KEY (`user_id`) REFERENCES `login_users` (`id`) ON DELETE CASCADE,
CONSTRAINT `user_invites_ibfk_2` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user_invites`
--
LOCK TABLES `user_invites` WRITE;
/*!40000 ALTER TABLE `user_invites` DISABLE KEYS */;
/*!40000 ALTER TABLE `user_invites` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user_profiles`
--
DROP TABLE IF EXISTS `user_profiles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_profiles` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`department` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bio` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
CONSTRAINT `fk_user_profiles_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user_profiles`
--
LOCK TABLES `user_profiles` WRITE;
/*!40000 ALTER TABLE `user_profiles` DISABLE KEYS */;
/*!40000 ALTER TABLE `user_profiles` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `user_verifications`
--
DROP TABLE IF EXISTS `user_verifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_verifications` (
`id` bigint NOT NULL AUTO_INCREMENT,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`verification_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`verification_type_id` bigint NOT NULL,
`verified_at` timestamp(3) NULL DEFAULT NULL,
`expires_at` timestamp(3) NOT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_verifications_email` (`email`),
KEY `idx_user_verifications_code` (`verification_code`),
KEY `idx_user_verifications_type` (`verification_type_id`),
KEY `idx_user_verifications_expires_at` (`expires_at`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `user_verifications`
--
LOCK TABLES `user_verifications` WRITE;
/*!40000 ALTER TABLE `user_verifications` DISABLE KEYS */;
/*!40000 ALTER TABLE `user_verifications` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`login_user_id` bigint DEFAULT NULL,
`site_id` bigint NOT NULL,
`first_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`last_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`role_id` bigint DEFAULT NULL,
`is_enabled` tinyint(1) NOT NULL DEFAULT '1',
`user_role_id` bigint NOT NULL DEFAULT '2',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`deleted_at` timestamp(3) NULL DEFAULT NULL,
`created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`created_by` bigint DEFAULT NULL,
`updated_by` bigint DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
`invite_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`invite_accepted_at` timestamp(3) NULL DEFAULT NULL,
`invite_expires_at` timestamp(3) NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_users_login_user_site` (`login_user_id`,`site_id`),
UNIQUE KEY `uk_users_invite_code` (`invite_code`),
KEY `idx_site_users_site_id` (`site_id`),
KEY `idx_site_users_role_id` (`role_id`),
KEY `idx_site_users_is_enabled` (`is_enabled`),
KEY `idx_site_users_deleted_at` (`deleted_at`),
KEY `created_at` (`created_at`),
KEY `updated_at` (`updated_at`),
KEY `idx_users_phone` (`phone`),
KEY `idx_users_user_role_id` (`user_role_id`),
KEY `idx_users_email` (`email`),
KEY `idx_users_invite_accepted_at` (`invite_accepted_at`),
KEY `idx_users_invite_expires_at` (`invite_expires_at`),
CONSTRAINT `fk_users_login_user_id` FOREIGN KEY (`login_user_id`) REFERENCES `login_users` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_users_site_id` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-11-16 19:45:00

View File

@@ -0,0 +1,90 @@
#!/bin/bash
# RSpade Test Environment Helpers
#
# Manages test database environment and provides utilities for test setup/teardown
TEST_ENV_BACKUP="/var/www/html/system/.env.testbackup"
TEST_MODE_ACTIVE=false
# Enter test mode - switch to test database
function test_mode_enter() {
if [ "$TEST_MODE_ACTIVE" = true ]; then
echo "[ERROR] Test mode already active" >&2
return 1
fi
echo "[TEST ENV] Entering test mode..." >&2
# Only backup .env if we're NOT already in test mode
# (Don't want to backup test config and mistake it for production)
if grep -q "^DB_DATABASE=rspade_test" /var/www/html/.env; then
echo "[TEST ENV] WARNING: Already using test database - NOT creating backup" >&2
echo "[TEST ENV] This indicates .env was not properly restored from a previous test run" >&2
echo "[TEST ENV] Manually restore /var/www/html/.env to production credentials before running tests" >&2
return 1
fi
# Backup current .env (production credentials)
cp /var/www/html/.env "$TEST_ENV_BACKUP"
# Switch to test database
sed -i 's/^DB_DATABASE=.*$/DB_DATABASE=rspade_test/' /var/www/html/.env
# Add test config to include tests directory in manifest
if ! grep -q "^RSX_ADDITIONAL_CONFIG=" /var/www/html/.env; then
echo "RSX_ADDITIONAL_CONFIG=/var/www/html/system/app/RSpade/tests/_lib/rsx_test_config.php" >> /var/www/html/.env
else
sed -i 's|^RSX_ADDITIONAL_CONFIG=.*$|RSX_ADDITIONAL_CONFIG=/var/www/html/system/app/RSpade/tests/_lib/rsx_test_config.php|' /var/www/html/.env
fi
# Clear Laravel config cache and rebuild manifest
cd /var/www/html
php artisan config:clear > /dev/null 2>&1
php artisan rsx:clean > /dev/null 2>&1
TEST_MODE_ACTIVE=true
echo "[TEST ENV] Test mode active (using rspade_test database)" >&2
}
# Exit test mode - restore original database
function test_mode_exit() {
if [ "$TEST_MODE_ACTIVE" = false ]; then
return 0
fi
echo "[TEST ENV] Exiting test mode..." >&2
# Restore original .env
if [ -f "$TEST_ENV_BACKUP" ]; then
mv "$TEST_ENV_BACKUP" /var/www/html/.env
fi
# Clear Laravel config cache and rebuild manifest
cd /var/www/html
php artisan config:clear > /dev/null 2>&1
php artisan rsx:clean > /dev/null 2>&1
TEST_MODE_ACTIVE=false
echo "[TEST ENV] Test mode exited (restored original database)" >&2
}
# Trap to ensure test mode always exits
function test_trap_exit() {
test_mode_exit
# NEVER remove backup files - they contain production credentials
# Backup files are process-specific (/tmp/rspade_test_env_backup_$$)
# and will be cleaned up by system /tmp cleanup
}
# Query helper for test database
function test_db_query() {
mysql -h127.0.0.1 -urspade -prspadepass rspade_test -N -e "$1" 2>/dev/null
}
# Count helper for test database
function test_db_count() {
local table=$1
test_db_query "SELECT COUNT(*) FROM $table"
}

View File

@@ -0,0 +1,63 @@
# Framework Verification Test
Meta-test that verifies the testing framework itself works correctly.
## What it verifies
- Test environment helpers load correctly
- `test_mode_enter()` successfully switches to test database
- `.env` is modified to use `rspade_test`
- `.env` backup is created
- Database reset script works
- Migrations run successfully on test database
- `test_db_query()` helper works
- Test mode cleanup/restore works via trap
## Prerequisites
- `rspade_test` database exists
- `rspade` user has access to `rspade_test`
- Laravel is configured normally (using `rspade` database)
## How to run
```bash
./run_test.sh # Full test with database reset
./run_test.sh --skip-reset # Skip database reset (faster)
```
## What happens
1. Loads test environment helpers
2. Resets test database (unless --skip-reset)
3. Enters test mode (switches to rspade_test)
4. Runs 5 verification checks
5. Exits test mode (restores original database)
6. Outputs PASS or FAIL
## Expected output
```
[SETUP] Testing framework verification...
[SETUP] Resetting test database...
[DB RESET] Dropping rspade_test database...
[DB RESET] Creating rspade_test database...
[DB RESET] Running migrations...
[DB RESET] Database reset complete
[TEST ENV] Entering test mode...
[TEST ENV] Test mode active (using rspade_test database)
[TEST] Verifying framework components...
[TEST] ✓ Environment switched to test database
[TEST] ✓ Test database accessible
[TEST] ✓ Migrations ran successfully
[TEST] ✓ Environment backup created
[TEST] ✓ Database query helper works
PASS: Framework Verification
[TEST ENV] Exiting test mode...
[TEST ENV] Test mode exited (restored original database)
```
## Known limitations
- Database reset is slow (~5-10 seconds with migrations)
- Future: Use database snapshots for faster reset

View File

@@ -0,0 +1,87 @@
#!/bin/bash
set -e
TEST_NAME="Framework Verification"
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source test environment helpers
source "$TEST_DIR/../../_lib/test_env.sh"
# Parse arguments
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset)
SKIP_RESET=true
shift
;;
esac
done
# Ensure test mode exits on script exit (success or failure)
trap test_trap_exit EXIT
# SETUP
echo "[SETUP] Testing framework verification..." >&2
# Verify test environment helpers loaded
if ! type test_mode_enter > /dev/null 2>&1; then
echo "FAIL: $TEST_NAME - test_mode_enter function not loaded"
exit 1
fi
# Enter test mode (switches Laravel to rspade_test database and loads test config)
test_mode_enter
# Reset database to known state (unless called as sub-test)
if [ "$SKIP_RESET" = false ]; then
echo "[SETUP] Resetting test database..." >&2
"$TEST_DIR/../../_lib/db_reset.sh"
fi
# TEST LOGIC
echo "[TEST] Verifying framework components..." >&2
# Test 1: Verify .env was modified
if grep -q "DB_DATABASE=rspade_test" /var/www/html/.env; then
echo "[TEST] ✓ Environment switched to test database" >&2
else
echo "FAIL: $TEST_NAME - .env not modified to use rspade_test"
exit 1
fi
# Test 2: Verify we can query test database
if ! mysql -h127.0.0.1 -urspade -prspadepass rspade_test -e "SELECT 1" > /dev/null 2>&1; then
echo "FAIL: $TEST_NAME - Cannot connect to rspade_test database"
exit 1
fi
echo "[TEST] ✓ Test database accessible" >&2
# Test 3: Verify migrations table exists (migrations ran)
tables=$(mysql -h127.0.0.1 -urspade -prspadepass rspade_test -N -e "SHOW TABLES LIKE 'migrations'" 2>/dev/null)
if [ -z "$tables" ]; then
echo "FAIL: $TEST_NAME - Migrations table does not exist (migrations did not run)"
exit 1
fi
echo "[TEST] ✓ Migrations ran successfully" >&2
# Test 4: Verify backup was created
if [ ! -f "$TEST_ENV_BACKUP" ]; then
echo "FAIL: $TEST_NAME - .env backup not created"
exit 1
fi
echo "[TEST] ✓ Environment backup created" >&2
# Test 5: Verify test_db_query helper works
test_result=$(test_db_query "SELECT 'works'" 2>/dev/null)
if [ "$test_result" != "works" ]; then
echo "FAIL: $TEST_NAME - test_db_query helper failed"
exit 1
fi
echo "[TEST] ✓ Database query helper works" >&2
# All tests passed
echo "PASS: $TEST_NAME"
exit 0
# TEARDOWN happens automatically via trap

View File

@@ -0,0 +1,58 @@
# Database Connection Test
Tests basic database CRUD operations using the test database.
## What it verifies
- Database insert operations work
- Database query operations work
- Database update operations work
- Database delete operations work
- Data retrieval returns correct values
## Prerequisites
- `rspade_test` database exists
- `rspade` user has access to `rspade_test`
- Test environment helpers functional
## How to run
```bash
./run_test.sh # Full test with database reset
./run_test.sh --skip-reset # Skip database reset (faster)
```
## What happens
1. Creates temporary table
2. Inserts test data
3. Verifies data was inserted
4. Queries specific data
5. Updates data
6. Verifies update worked
7. Deletes data
8. Verifies deletion worked
9. Cleans up (temporary table auto-removed)
## Expected output
```
[SETUP] Preparing database connection test...
[SETUP] Resetting test database...
[DB RESET] Dropping rspade_test database...
[DB RESET] Creating rspade_test database...
[DB RESET] Running migrations...
[DB RESET] Database reset complete
[TEST ENV] Entering test mode...
[TEST ENV] Test mode active (using rspade_test database)
[TEST] Testing database connection and operations...
[TEST] ✓ Insert operations work
[TEST] ✓ Query operations work
[TEST] ✓ Data retrieval works
[TEST] ✓ Update operations work
[TEST] ✓ Delete operations work
PASS: Database Connection
[TEST ENV] Exiting test mode...
[TEST ENV] Test mode exited (restored original database)
```

View File

@@ -0,0 +1,85 @@
#!/bin/bash
set -e
TEST_NAME="Database Connection"
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source test environment helpers
source "$TEST_DIR/../../_lib/test_env.sh"
# Parse arguments
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset)
SKIP_RESET=true
shift
;;
esac
done
# Ensure test mode exits on script exit (success or failure)
trap test_trap_exit EXIT
# SETUP
echo "[SETUP] Preparing database connection test..." >&2
# Enter test mode (switches Laravel to rspade_test database and loads test config)
test_mode_enter
# Reset database to known state (unless called as sub-test)
if [ "$SKIP_RESET" = false ]; then
"$TEST_DIR/../../_lib/db_reset.sh"
fi
# TEST LOGIC
echo "[TEST] Testing database connection and operations..." >&2
# Test 1: Can we insert data?
test_db_query "CREATE TABLE IF NOT EXISTS rspade_test_temp_users (id INT, name VARCHAR(255))"
test_db_query "TRUNCATE TABLE rspade_test_temp_users"
test_db_query "INSERT INTO rspade_test_temp_users (id, name) VALUES (1, 'Alice'), (2, 'Bob')"
echo "[TEST] ✓ Insert operations work" >&2
# Test 2: Can we query data?
count=$(test_db_count rspade_test_temp_users)
if [ "$count" -ne 2 ]; then
echo "FAIL: $TEST_NAME - Expected 2 rows, found $count"
exit 1
fi
echo "[TEST] ✓ Query operations work" >&2
# Test 3: Can we retrieve specific data?
name=$(test_db_query "SELECT name FROM rspade_test_temp_users WHERE id=1")
if [ "$name" != "Alice" ]; then
echo "FAIL: $TEST_NAME - Expected 'Alice', got '$name'"
exit 1
fi
echo "[TEST] ✓ Data retrieval works" >&2
# Test 4: Can we update data?
test_db_query "UPDATE rspade_test_temp_users SET name='Charlie' WHERE id=1"
updated_name=$(test_db_query "SELECT name FROM rspade_test_temp_users WHERE id=1")
if [ "$updated_name" != "Charlie" ]; then
echo "FAIL: $TEST_NAME - Expected 'Charlie', got '$updated_name'"
exit 1
fi
echo "[TEST] ✓ Update operations work" >&2
# Test 5: Can we delete data?
test_db_query "DELETE FROM rspade_test_temp_users WHERE id=2"
remaining=$(test_db_count rspade_test_temp_users)
if [ "$remaining" -ne 1 ]; then
echo "FAIL: $TEST_NAME - Expected 1 row after delete, found $remaining"
exit 1
fi
echo "[TEST] ✓ Delete operations work" >&2
# Cleanup
test_db_query "DROP TABLE IF EXISTS rspade_test_temp_users"
# All tests passed
echo "PASS: $TEST_NAME"
exit 0
# TEARDOWN happens automatically via trap

128
app/RSpade/tests/run_all_tests.sh Executable file
View File

@@ -0,0 +1,128 @@
#!/bin/bash
# RSpade Test Runner
# Runs all automated tests and reports results
set -e
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ANSI color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Track results
PASSED=0
FAILED=0
SKIPPED=0
TOTAL=0
echo -e "${BLUE}[RSpade Test Runner]${NC}"
# Parse arguments
USE_EXISTING_SNAPSHOT=false
FULL_OUTPUT=false
for arg in "$@"; do
case $arg in
--use-existing-snapshot)
USE_EXISTING_SNAPSHOT=true
shift
;;
--full-output)
FULL_OUTPUT=true
shift
;;
esac
done
# If no arguments provided (or snapshot flag not set), recreate snapshot for fresh test run
if [ "$USE_EXISTING_SNAPSHOT" = false ]; then
echo -e "${BLUE}Creating fresh database snapshot...${NC}"
if [ "$FULL_OUTPUT" = true ]; then
"$TESTS_DIR/_lib/db_snapshot_create.sh" --full-output
else
"$TESTS_DIR/_lib/db_snapshot_create.sh"
fi
echo ""
else
echo -e "${BLUE}Using existing snapshot (if available)${NC}"
echo ""
fi
# Find all run_test.sh files (excluding run_interactive_test.sh)
echo -e "${BLUE}Finding tests...${NC}"
echo ""
# Find all test scripts
test_scripts=$(find "$TESTS_DIR" -name "run_test.sh" -type f | sort)
if [ -z "$test_scripts" ]; then
echo -e "${RED}No tests found${NC}"
exit 1
fi
# Count tests
while IFS= read -r script; do
TOTAL=$((TOTAL + 1))
done <<< "$test_scripts"
echo -e "${BLUE}Found $TOTAL tests${NC}"
echo ""
# Run each test
while IFS= read -r script; do
# Get relative path for display
rel_path="${script#$TESTS_DIR/}"
echo -e "${BLUE}Running: $rel_path${NC}"
# Run test and capture stdout/stderr separately
# Stderr goes to terminal, stdout captured for result parsing
if output=$("$script" 2>&1 | tee /dev/stderr); then
exit_code=0
else
exit_code=$?
fi
# Parse result from output (should be last non-empty line to stdout)
result=$(echo "$output" | grep -E "^(PASS|FAIL|SKIP):" | tail -n 1)
if [ -z "$result" ]; then
result="FAIL: $rel_path - No test result output"
fi
if [ $exit_code -eq 0 ] && echo "$result" | grep -q "^PASS:"; then
echo -e "${GREEN}$result${NC}"
PASSED=$((PASSED + 1))
elif echo "$result" | grep -q "^SKIP:"; then
echo -e "${YELLOW}$result${NC}"
SKIPPED=$((SKIPPED + 1))
else
echo -e "${RED}$result${NC}"
FAILED=$((FAILED + 1))
fi
echo ""
done <<< "$test_scripts"
# Summary
echo -e "${BLUE}======================================${NC}"
echo -e "${BLUE}Test Results${NC}"
echo -e "${BLUE}======================================${NC}"
echo -e "${GREEN}Passed: $PASSED${NC}"
echo -e "${RED}Failed: $FAILED${NC}"
echo -e "${YELLOW}Skipped: $SKIPPED${NC}"
echo -e "${BLUE}Total: $TOTAL${NC}"
echo ""
if [ $FAILED -gt 0 ]; then
echo -e "${RED}Some tests failed${NC}"
exit 1
else
echo -e "${GREEN}All tests passed${NC}"
exit 0
fi

View File

@@ -0,0 +1,93 @@
#!/bin/bash
# RSpade Interactive Test Runner
# Runs tests that require human verification
set -e
TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ANSI color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Track results
PASSED=0
FAILED=0
SKIPPED=0
TOTAL=0
# Find all run_interactive_test.sh files
echo -e "${BLUE}[RSpade Interactive Test Runner]${NC}"
echo -e "${BLUE}Finding interactive tests...${NC}"
echo ""
# Find all interactive test scripts
test_scripts=$(find "$TESTS_DIR" -name "run_interactive_test.sh" -type f | sort)
if [ -z "$test_scripts" ]; then
echo -e "${YELLOW}No interactive tests found${NC}"
exit 0
fi
# Count tests
while IFS= read -r script; do
TOTAL=$((TOTAL + 1))
done <<< "$test_scripts"
echo -e "${BLUE}Found $TOTAL interactive tests${NC}"
echo ""
# Run each test
while IFS= read -r script; do
# Get relative path for display
rel_path="${script#$TESTS_DIR/}"
echo -e "${BLUE}Running: $rel_path${NC}"
echo ""
# Run test and capture output
if output=$("$script" 2>&1); then
exit_code=0
else
exit_code=$?
fi
# Parse result from last line
result=$(echo "$output" | tail -n 1)
if [ $exit_code -eq 0 ] && echo "$result" | grep -q "^PASS:"; then
echo -e "${GREEN}$result${NC}"
PASSED=$((PASSED + 1))
elif echo "$result" | grep -q "^SKIP:"; then
echo -e "${YELLOW}$result${NC}"
SKIPPED=$((SKIPPED + 1))
else
echo -e "${RED}$result${NC}"
FAILED=$((FAILED + 1))
fi
echo ""
done <<< "$test_scripts"
# Summary
echo -e "${BLUE}======================================${NC}"
echo -e "${BLUE}Interactive Test Results${NC}"
echo -e "${BLUE}======================================${NC}"
echo -e "${GREEN}Passed: $PASSED${NC}"
echo -e "${RED}Failed: $FAILED${NC}"
echo -e "${YELLOW}Skipped: $SKIPPED${NC}"
echo -e "${BLUE}Total: $TOTAL${NC}"
echo ""
if [ $FAILED -gt 0 ]; then
echo -e "${RED}Some tests failed${NC}"
exit 1
else
echo -e "${GREEN}All tests passed${NC}"
exit 0
fi

View File

@@ -0,0 +1,76 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Tests\Services;
use App\RSpade\Core\Service\Rsx_Service_Abstract;
use App\RSpade\Core\Task\Task_Instance;
/**
* Scheduled Test Service
*
* Test service with scheduled tasks for testing the scheduling system.
*/
class Scheduled_Test_Service extends Rsx_Service_Abstract
{
/**
* Test task that runs every minute
*
* @param Task_Instance $task Task instance for logging
* @param array $params Task parameters
* @return array Result data
*/
#[Task_Attribute('Test scheduled task that runs every minute')]
#[Schedule('* * * * *', 'default')]
public static function every_minute(Task_Instance $task, array $params = []): array
{
$task->info('Every minute task executed at ' . date('Y-m-d H:i:s'));
return [
'executed_at' => date('Y-m-d H:i:s'),
'message' => 'Every minute task completed',
];
}
/**
* Test task that runs every 5 minutes
*
* @param Task_Instance $task Task instance for logging
* @param array $params Task parameters
* @return array Result data
*/
#[Task_Attribute('Test scheduled task that runs every 5 minutes')]
#[Schedule('*/5 * * * *', 'default')]
public static function every_five_minutes(Task_Instance $task, array $params = []): array
{
$task->info('Every 5 minutes task executed at ' . date('Y-m-d H:i:s'));
return [
'executed_at' => date('Y-m-d H:i:s'),
'message' => 'Every 5 minutes task completed',
];
}
/**
* Test task that runs daily at midnight
*
* @param Task_Instance $task Task instance for logging
* @param array $params Task parameters
* @return array Result data
*/
#[Task_Attribute('Test scheduled task that runs daily at midnight')]
#[Schedule('0 0 * * *', 'scheduled')]
public static function daily_midnight(Task_Instance $task, array $params = []): array
{
$task->info('Daily midnight task executed at ' . date('Y-m-d H:i:s'));
return [
'executed_at' => date('Y-m-d H:i:s'),
'message' => 'Daily midnight task completed',
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Tests\Services;
use App\RSpade\Core\Service\Rsx_Service_Abstract;
use App\RSpade\Core\Task\Task_Instance;
/**
* Service Test Service
*
* Test service for validating task execution system.
*/
class Service_Test_Service extends Rsx_Service_Abstract
{
/**
* Hello world test task
*
* @param Task_Instance $task Task instance for logging
* @param array $params Task parameters
* @return array Result data
*/
#[Task('Simple test task that returns hello world')]
public static function hello_world(Task_Instance $task, array $params = []): array
{
$task->info('Hello world task executed');
$task->debug('Debug message from hello_world task');
$name = $params['name'] ?? 'World';
return [
'message' => "Hello, {$name}!",
'timestamp' => date('Y-m-d H:i:s'),
];
}
/**
* Test task with logging
*
* @param Task_Instance $task Task instance for logging
* @param array $params Task parameters
* @return array Result data
*/
#[Task_Attribute('Test task that demonstrates logging functionality')]
public static function test_logging(Task_Instance $task, array $params = []): array
{
$task->info('Starting logging test');
$task->debug('This is a debug message');
$task->info('Processing step 1');
$task->info('Processing step 2');
$task->info('Logging test complete');
return [
'success' => true,
'log_count' => 5,
];
}
}

View File

@@ -0,0 +1,62 @@
# Task Dispatch and Execution Test
Tests the core task system functionality including task dispatch, execution, and scheduling.
## What it verifies
- Test services are discoverable in the manifest when in test mode
- Tasks can be dispatched to the queue
- Tasks start with 'pending' status
- Task processor can execute tasks
- Tasks complete with 'completed' status
- Task results are stored correctly
- Task logs are recorded
- Scheduled tasks are registered from manifest
- `--force-scheduled` flag dispatches scheduled tasks immediately
## Prerequisites
- Test mode must be active (test services in manifest)
- Database must have `_tasks` table (from migration)
- Test services must exist in `/system/app/RSpade/tests/services/`
## How to run
```bash
cd /system/app/RSpade/tests
./tasks/01_task_dispatch_and_execution/run_test.sh
```
## What it tests
1. **Manifest Integration**: Verifies test services with #[Task_Attribute] and #[Schedule] attributes are discovered
2. **Task Dispatch**: Creates a task and verifies it's inserted into the database
3. **Task Status**: Checks task starts as 'pending'
4. **Task Execution**: Runs the task processor to execute the pending task
5. **Task Completion**: Verifies task status changes to 'completed'
6. **Task Results**: Checks task return value is stored as JSON
7. **Task Logging**: Verifies Task_Instance logging is captured
8. **Scheduled Registration**: Tests Task_Process_Command registers scheduled tasks
9. **Force Dispatch**: Tests --force-scheduled flag creates task instances
## Expected output
```
[SETUP] Preparing task system test...
[SETUP] Resetting test database...
[TEST] Testing task dispatch and execution...
[TEST] Verifying test services in manifest...
[TEST] ✓ Test services found in manifest
[TEST] Dispatching task...
[TEST] ✓ Task dispatched with ID: 1
[TEST] ✓ Task status is pending
[TEST] Processing task...
[TEST] ✓ Task completed successfully
[TEST] ✓ Task result correct
[TEST] ✓ Task logs recorded
[TEST] Testing scheduled task registration...
[TEST] ✓ Scheduled tasks registered: 3
[TEST] Testing --force-scheduled flag...
[TEST] ✓ Force-scheduled created task instances
PASS: Task Dispatch and Execution
```

View File

@@ -0,0 +1,145 @@
#!/bin/bash
set -e
TEST_NAME="Task Dispatch and Execution"
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source test environment helpers
source "$TEST_DIR/../../_lib/test_env.sh"
# Parse arguments
SKIP_RESET=false
for arg in "$@"; do
case $arg in
--skip-reset)
SKIP_RESET=true
shift
;;
esac
done
# Ensure test mode exits on script exit (success or failure)
trap test_trap_exit EXIT
# SETUP
echo "[SETUP] Preparing task system test..." >&2
# Enter test mode (switches Laravel to rspade_test database and loads test config)
test_mode_enter
# Reset database to known state (unless called as sub-test)
if [ "$SKIP_RESET" = false ]; then
echo "[SETUP] Resetting test database..." >&2
"$TEST_DIR/../../_lib/db_reset.sh"
fi
# TEST LOGIC
echo "[TEST] Testing task dispatch and execution..." >&2
cd /var/www/html
# Test 1: Verify test services are available in manifest
echo "[TEST] Verifying test services in manifest..." >&2
php artisan tinker --execute="
\$tasks = App\RSpade\Core\Task\Task::get_scheduled_tasks();
if (count(\$tasks) < 3) {
echo 'ERROR: Expected at least 3 scheduled tasks, found ' . count(\$tasks);
exit(1);
}
echo 'OK';
" 2>&1 | grep -q "OK"
if [ $? -ne 0 ]; then
echo "FAIL: $TEST_NAME - Scheduled tasks not found in manifest"
exit 1
fi
echo "[TEST] ✓ Test services found in manifest" >&2
# Test 2: Dispatch a task
echo "[TEST] Dispatching task..." >&2
TASK_ID=$(php artisan tinker --execute="
echo App\RSpade\Core\Task\Task::dispatch('Service_Test_Service', 'hello_world', ['name' => 'Test']);
" 2>&1 | tail -1)
if [ -z "$TASK_ID" ] || [ "$TASK_ID" = "0" ]; then
echo "FAIL: $TEST_NAME - Task dispatch failed"
exit 1
fi
echo "[TEST] ✓ Task dispatched with ID: $TASK_ID" >&2
# Test 3: Verify task is pending
STATUS=$(test_db_query "SELECT status FROM _tasks WHERE id = $TASK_ID")
if [ "$STATUS" != "pending" ]; then
echo "FAIL: $TEST_NAME - Task status should be 'pending', got '$STATUS'"
exit 1
fi
echo "[TEST] ✓ Task status is pending" >&2
# Test 4: Process the task
echo "[TEST] Processing task..." >&2
php artisan rsx:task:process --once --queue=default > /dev/null 2>&1
# Test 5: Verify task completed
STATUS=$(test_db_query "SELECT status FROM _tasks WHERE id = $TASK_ID")
if [ "$STATUS" != "completed" ]; then
echo "FAIL: $TEST_NAME - Task should be completed, got '$STATUS'"
exit 1
fi
echo "[TEST] ✓ Task completed successfully" >&2
# Test 6: Verify task result
RESULT=$(test_db_query "SELECT result FROM _tasks WHERE id = $TASK_ID")
if ! echo "$RESULT" | grep -q "Hello, Test"; then
echo "FAIL: $TEST_NAME - Task result incorrect: $RESULT"
exit 1
fi
echo "[TEST] ✓ Task result correct" >&2
# Test 7: Verify task logs
LOGS=$(test_db_query "SELECT logs FROM _tasks WHERE id = $TASK_ID")
if ! echo "$LOGS" | grep -q "Hello world task executed"; then
echo "FAIL: $TEST_NAME - Task logs missing expected content"
exit 1
fi
echo "[TEST] ✓ Task logs recorded" >&2
# Test 8: Test scheduled task registration
echo "[TEST] Testing scheduled task registration..." >&2
# Clear any existing scheduled task records
test_db_query "DELETE FROM _tasks WHERE next_run_at IS NOT NULL"
# Run task processor to register scheduled tasks
php artisan rsx:task:process > /dev/null 2>&1
# Verify scheduled tasks were registered
SCHEDULED_COUNT=$(test_db_count "_tasks WHERE next_run_at IS NOT NULL")
if [ "$SCHEDULED_COUNT" -lt 3 ]; then
echo "FAIL: $TEST_NAME - Expected at least 3 scheduled tasks registered, found $SCHEDULED_COUNT"
exit 1
fi
echo "[TEST] ✓ Scheduled tasks registered: $SCHEDULED_COUNT" >&2
# Test 9: Test force-scheduled flag
echo "[TEST] Testing --force-scheduled flag..." >&2
# Count tasks before
BEFORE_COUNT=$(test_db_count "_tasks WHERE next_run_at IS NULL AND status = 'pending'")
# Force dispatch all scheduled tasks
php artisan rsx:task:process --force-scheduled > /dev/null 2>&1
# Count tasks after
AFTER_COUNT=$(test_db_count "_tasks WHERE next_run_at IS NULL AND status = 'pending'")
if [ "$AFTER_COUNT" -le "$BEFORE_COUNT" ]; then
echo "FAIL: $TEST_NAME - --force-scheduled should have created new task instances"
exit 1
fi
echo "[TEST] ✓ Force-scheduled created task instances" >&2
# All tests passed
echo "PASS: $TEST_NAME"
exit 0
# TEARDOWN happens automatically via trap