mirror of https://github.com/sysown/proxysql
- Fixed invalid memory accesses for tables:
+ mcp_query_rules
+ stats_mcp_query_rules
+ stats_mcp_query_digests
- Fixed inactive 'mcp_query_rules' being loaded to runtime.
- Fixed hash computation in 'compute_mcp_digest'.
- Fixed invalid escaping during 'stats_mcp_query_digests' gen.
- Fixed digest generation for MCP arguments:
+ SQL queries are now preserved using
'mysql_query_digest_and_first_comment'.
+ TODO: Options for the tokenizer are right now hardcoded.
- Added initial testing and testing plan for MCP query_(rules/digests).
+ TODO: Test finished on phase8. Timeouts destroy the MCP
connection, leaving it unusable for subsequent queries this should
be fixed for continuing testing.
- TODO: There are several limitations to fix in
'validate_readonly_query'. This reflect in some query hacks in the
testing.
+ 'SELECT' starting with comments (--) gets flagged as non-read.
+ 'SELECT' must have a 'SELECT .* FROM' structure. While common,
simple testing queries many times lack this form.
pull/5312/head
parent
709649232b
commit
52142c4648
@ -0,0 +1,338 @@
|
||||
# MCP Query Rules Test Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This test plan covers the MCP Query Rules feature added in the last 7 commits. The feature allows filtering and modifying MCP tool calls based on rule evaluation, similar to MySQL query rules.
|
||||
|
||||
### Feature Design Summary
|
||||
|
||||
Actions are inferred from rule properties (like MySQL/PostgreSQL query rules):
|
||||
- `error_msg != NULL` → **block**
|
||||
- `replace_pattern != NULL` → **rewrite**
|
||||
- `timeout_ms > 0` → **timeout**
|
||||
- `OK_msg != NULL` → return OK message
|
||||
- otherwise → **allow**
|
||||
|
||||
Actions are NOT mutually exclusive - a single rule can perform multiple actions simultaneously.
|
||||
|
||||
### Tables Involved
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `mcp_query_rules` | Admin table for defining rules |
|
||||
| `runtime_mcp_query_rules` | In-memory state of active rules |
|
||||
| `stats_mcp_query_rules` | Hit counters per rule |
|
||||
| `stats_mcp_query_digest` | Query tracking statistics |
|
||||
|
||||
### Existing Test Infrastructure
|
||||
|
||||
1. **TAP Test**: `test/tap/tests/mcp_module-t.cpp` - Tests LOAD/SAVE commands for MCP variables
|
||||
2. **Shell Test**: `scripts/mcp/test_mcp_query_rules_block.sh` - Tests block action
|
||||
3. **SQL Rules**: `scripts/mcp/rules/block_rule.sql` - Sample block rules
|
||||
|
||||
---
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Phase 1: Rule Management Tests (CREATE/READ/UPDATE/DELETE)
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T1.1 | Create a basic rule with match_pattern | Rule inserted into `mcp_query_rules` |
|
||||
| T1.2 | Create rule with all action types | Rule with error_msg, replace_pattern, OK_msg, timeout_ms |
|
||||
| T1.3 | Create rule with username filter | Rule filters by specific user |
|
||||
| T1.4 | Create rule with schemaname filter | Rule filters by specific schema |
|
||||
| T1.5 | Create rule with tool_name filter | Rule filters by specific tool |
|
||||
| T1.6 | Update existing rule | Rule properties modified |
|
||||
| T1.7 | Delete rule | Rule removed from table |
|
||||
| T1.8 | Create rule with flagIN/flagOUT | Rule chaining setup |
|
||||
|
||||
### Phase 2: LOAD/SAVE Commands Tests
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T2.1 | `LOAD MCP QUERY RULES TO MEMORY` | Rules loaded from disk to memory table |
|
||||
| T2.2 | `LOAD MCP QUERY RULES FROM MEMORY` | Rules copied from memory to... |
|
||||
| T2.3 | `LOAD MCP QUERY RULES TO RUNTIME` | Rules become active for evaluation |
|
||||
| T2.4 | `SAVE MCP QUERY RULES TO DISK` | Rules persisted to disk |
|
||||
| T2.5 | `SAVE MCP QUERY RULES TO MEMORY` | Rules saved to memory table |
|
||||
| T2.6 | `SAVE MCP QUERY RULES FROM RUNTIME` | Runtime rules saved to memory |
|
||||
|
||||
### Phase 3: Runtime Table Tests
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T3.1 | Query `runtime_mcp_query_rules` | Returns active rules from memory |
|
||||
| T3.2 | Verify rules match runtime after LOAD | Runtime table reflects loaded rules |
|
||||
| T3.3 | Verify active flag filtering | Only active=1 rules are in runtime |
|
||||
| T3.4 | Check rule order in runtime | Rules ordered by rule_id |
|
||||
|
||||
### Phase 4: Statistics Table Tests
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T4.1 | Query `stats_mcp_query_rules` | Returns rule_id and hits count |
|
||||
| T4.2 | Verify hit counter increments on match | hits counter increases when rule matches |
|
||||
| T4.3 | Verify hit counter persists across queries | Counter accumulates across multiple matches |
|
||||
| T4.4 | Check hit counter for non-matching rule | Counter stays at 0 for unmatched rules |
|
||||
|
||||
### Phase 5: Query Digest Tests
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T5.1 | Query `stats_mcp_query_digest` | Returns tool_name, digest, count_star, etc. |
|
||||
| T5.2 | Verify query tracked in digest | New query appears in digest table |
|
||||
| T5.3 | Verify count_star increments | Repeated queries increment counter |
|
||||
| T5.4 | Verify digest_text contains SQL | SQL query text is stored |
|
||||
| T5.5 | Test `stats_mcp_query_digest_reset` | Reset table clears and returns current stats |
|
||||
|
||||
### Phase 6: Rule Evaluation Tests - Block Action
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T6.1 | Block query with error_msg | Query rejected, error returned |
|
||||
| T6.2 | Block with case-sensitive match | Pattern matching respects re_modifiers |
|
||||
| T6.3 | Block with negate_match_pattern=1 | Inverts the match logic |
|
||||
| T6.4 | Block specific username | Only queries from user are blocked |
|
||||
| T6.5 | Block specific schema | Only queries in schema are blocked |
|
||||
| T6.6 | Block specific tool_name | Only calls to tool are blocked |
|
||||
|
||||
### Phase 7: Rule Evaluation Tests - Rewrite Action
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T7.1 | Rewrite SQL with replace_pattern | SQL modified before execution |
|
||||
| T7.2 | Rewrite with capture groups | Pattern substitution works |
|
||||
| T7.3 | Rewrite with regex modifiers | CASELESS/EXTENDED modifiers work |
|
||||
|
||||
### Phase 8: Rule Evaluation Tests - Timeout Action
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T8.1 | Query with timeout_ms | Query times out after specified ms |
|
||||
| T8.2 | Verify timeout error message | Appropriate error returned |
|
||||
|
||||
TODO: There is a limitation for testing this feature. MCP connection gets killed and becomes unusable after
|
||||
'timeout' takes place. This should be fixed before continuing this testing phase.
|
||||
|
||||
### Phase 9: Rule Evaluation Tests - OK Message Action
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T9.1 | Query with OK_msg | Query returns OK message without execution |
|
||||
| T9.2 | Verify success response | Success response contains OK_msg |
|
||||
|
||||
### Phase 10: Rule Chaining Tests (flagIN/flagOUT)
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T10.1 | Create rules with flagIN=0, flagOUT=100 | First rule sets flag to 100 |
|
||||
| T10.2 | Create rule with flagIN=100 | Second rule only evaluates if flag=100 |
|
||||
| T10.3 | Verify rule chaining order | Rules evaluated in flagIN/flagOUT order |
|
||||
| T10.4 | Test multiple flagOUT values | Complex chaining scenarios |
|
||||
|
||||
### Phase 11: Integration Tests
|
||||
|
||||
| Test ID | Description | Expected Result |
|
||||
|---------|-------------|-----------------|
|
||||
| T11.1 | Multiple actions in single rule | Block + rewrite together |
|
||||
| T11.2 | Multiple matching rules | First matching rule wins (or all?) |
|
||||
| T11.3 | Load rules and verify immediately | Rules active after LOAD TO RUNTIME |
|
||||
| T11.4 | Modify rule and reload | Updated behavior after reload |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### Option A: Extend Existing Shell Test Script
|
||||
Extend `scripts/mcp/test_mcp_query_rules_block.sh` to cover all test cases.
|
||||
|
||||
**Pros:**
|
||||
- Follows existing pattern
|
||||
- Easy to run manually
|
||||
- Good for end-to-end testing
|
||||
|
||||
**Cons:**
|
||||
- Shell scripting complexity
|
||||
- Harder to maintain
|
||||
|
||||
### Option B: Create New TAP Test
|
||||
Create `test/tap/tests/mcp_query_rules-t.cpp` following the pattern of `mcp_module-t.cpp`.
|
||||
|
||||
**Pros:**
|
||||
- Consistent with existing test framework
|
||||
- Better integration with CI
|
||||
- Cleaner C++ code
|
||||
- Better error reporting
|
||||
|
||||
**Cons:**
|
||||
- Requires rebuild
|
||||
- Less accessible for manual testing
|
||||
|
||||
### Option C: Hybrid Approach (Recommended)
|
||||
1. **TAP Test** (`mcp_query_rules-t.cpp`): Core functionality tests
|
||||
- LOAD/SAVE commands
|
||||
- Table operations
|
||||
- Statistics tracking
|
||||
- Basic rule evaluation
|
||||
|
||||
2. **Shell Script** (`test_mcp_query_rules_all.sh`): End-to-end integration tests
|
||||
- Complex rule chaining
|
||||
- Multiple action types
|
||||
- Real MCP server interaction
|
||||
|
||||
---
|
||||
|
||||
## Test File Structure
|
||||
|
||||
### TAP Test Structure
|
||||
```cpp
|
||||
// test/tap/tests/mcp_query_rules-t.cpp
|
||||
|
||||
int main() {
|
||||
// Part 1: Rule CRUD operations
|
||||
test_rule_create();
|
||||
test_rule_read();
|
||||
test_rule_update();
|
||||
test_rule_delete();
|
||||
|
||||
// Part 2: LOAD/SAVE commands
|
||||
test_load_save_commands();
|
||||
|
||||
// Part 3: Runtime table
|
||||
test_runtime_table();
|
||||
|
||||
// Part 4: Statistics table
|
||||
test_stats_table();
|
||||
|
||||
// Part 5: Query digest
|
||||
test_query_digest();
|
||||
|
||||
// Part 6: Rule evaluation
|
||||
test_block_action();
|
||||
test_rewrite_action();
|
||||
test_timeout_action();
|
||||
test_okmsg_action();
|
||||
|
||||
// Part 7: Rule chaining
|
||||
test_flag_chaining();
|
||||
|
||||
return exit_status();
|
||||
}
|
||||
```
|
||||
|
||||
### Shell Test Structure
|
||||
```bash
|
||||
# scripts/mcp/test_mcp_query_rules_all.sh
|
||||
|
||||
test_block_action() { ... }
|
||||
test_rewrite_action() { ... }
|
||||
test_timeout_action() { ... }
|
||||
test_okmsg_action() { ... }
|
||||
test_flag_chaining() { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQL Rule Templates
|
||||
|
||||
### Block Rule Template
|
||||
```sql
|
||||
INSERT INTO mcp_query_rules (
|
||||
rule_id, active, username, schemaname, tool_name,
|
||||
match_pattern, negate_match_pattern, re_modifiers,
|
||||
flagIN, flagOUT, error_msg, apply, comment
|
||||
) VALUES (
|
||||
100, 1, NULL, NULL, NULL,
|
||||
'DROP TABLE', 0, 'CASELESS',
|
||||
0, NULL,
|
||||
'Blocked by rule: DROP TABLE not allowed',
|
||||
1, 'Block DROP TABLE'
|
||||
);
|
||||
```
|
||||
|
||||
### Rewrite Rule Template
|
||||
```sql
|
||||
INSERT INTO mcp_query_rules (
|
||||
rule_id, active, username, schemaname, tool_name,
|
||||
match_pattern, replace_pattern, re_modifiers,
|
||||
flagIN, flagOUT, apply, comment
|
||||
) VALUES (
|
||||
200, 1, NULL, NULL, 'run_sql_readonly',
|
||||
'SELECT \* FROM (.*)', 'SELECT count(*) FROM \1',
|
||||
'EXTENDED', 0, NULL,
|
||||
1, 'Rewrite SELECT * to SELECT count(*)'
|
||||
);
|
||||
```
|
||||
|
||||
### Timeout Rule Template
|
||||
```sql
|
||||
INSERT INTO mcp_query_rules (
|
||||
rule_id, active, username, schemaname, tool_name,
|
||||
match_pattern, timeout_ms, re_modifiers,
|
||||
flagIN, flagOUT, apply, comment
|
||||
) VALUES (
|
||||
300, 1, NULL, NULL, NULL,
|
||||
'SELECT.*FROM.*large_table', 5000,
|
||||
'CASELESS', 0, NULL,
|
||||
1, 'Timeout queries on large_table'
|
||||
);
|
||||
```
|
||||
|
||||
### OK Message Rule Template
|
||||
```sql
|
||||
INSERT INTO mcp_query_rules (
|
||||
rule_id, active, username, schemaname, tool_name,
|
||||
match_pattern, OK_msg, re_modifiers,
|
||||
flagIN, flagOUT, apply, comment
|
||||
) VALUES (
|
||||
400, 1, NULL, NULL, NULL,
|
||||
'PING', 'PONG', 'CASELESS',
|
||||
0, NULL, 1, 'Return PONG for PING'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Actions
|
||||
|
||||
1. **Start with Phase 1-5**: Create TAP test for table operations and statistics
|
||||
- These don't require MCP server interaction
|
||||
- Can be tested through admin interface only
|
||||
|
||||
2. **Create test SQL files**: Organize rule templates in `scripts/mcp/rules/`
|
||||
- `block_rule.sql` (already exists)
|
||||
- `rewrite_rule.sql`
|
||||
- `timeout_rule.sql`
|
||||
- `okmsg_rule.sql`
|
||||
- `chaining_rule.sql`
|
||||
|
||||
3. **Extend shell test**: Modify `test_mcp_query_rules_block.sh` to `test_mcp_query_rules_all.sh`
|
||||
- Add rewrite, timeout, OK_msg tests
|
||||
- Add flag chaining tests
|
||||
|
||||
4. **Create TAP test**: New file `test/tap/tests/mcp_query_rules-t.cpp`
|
||||
- Core functionality tests
|
||||
- Statistics tracking tests
|
||||
|
||||
5. **Integration tests**: End-to-end tests with actual MCP server
|
||||
- Test through JSON-RPC interface
|
||||
- Verify response contents
|
||||
|
||||
---
|
||||
|
||||
## Test Dependencies
|
||||
|
||||
- **ProxySQL**: Must be running with MCP module enabled
|
||||
- **MySQL client**: For admin interface commands
|
||||
- **curl**: For MCP JSON-RPC requests
|
||||
- **jq**: For JSON parsing in shell tests
|
||||
- **TAP library**: For C++ tests
|
||||
|
||||
## Test Execution Order
|
||||
|
||||
1. Start ProxySQL with MCP enabled
|
||||
2. Run TAP tests (fast, no external dependencies)
|
||||
3. Run shell tests (require MCP server)
|
||||
4. Verify all tests pass
|
||||
5. Clean up test rules
|
||||
@ -0,0 +1,79 @@
|
||||
-- Test Block Rule for MCP Query Rules
|
||||
-- This rule blocks queries matching DROP TABLE pattern
|
||||
-- Rule ID 100: Block any query containing DROP TABLE
|
||||
INSERT INTO mcp_query_rules (
|
||||
rule_id,
|
||||
active,
|
||||
username,
|
||||
schemaname,
|
||||
tool_name,
|
||||
match_pattern,
|
||||
negate_match_pattern,
|
||||
re_modifiers,
|
||||
flagIN,
|
||||
flagOUT,
|
||||
replace_pattern,
|
||||
timeout_ms,
|
||||
error_msg,
|
||||
OK_msg,
|
||||
log,
|
||||
apply,
|
||||
comment
|
||||
) VALUES (
|
||||
100, -- rule_id
|
||||
1, -- active
|
||||
NULL, -- username (any user)
|
||||
NULL, -- schemaname (any schema)
|
||||
NULL, -- tool_name (any tool)
|
||||
'DROP TABLE', -- match_pattern
|
||||
0, -- negate_match_pattern
|
||||
'CASELESS', -- re_modifiers
|
||||
0, -- flagIN
|
||||
NULL, -- flagOUT
|
||||
NULL, -- replace_pattern
|
||||
NULL, -- timeout_ms
|
||||
'Blocked by MCP query rule: DROP TABLE statements are not allowed', -- error_msg (BLOCK action)
|
||||
NULL, -- OK_msg
|
||||
1, -- log
|
||||
1, -- apply
|
||||
'Test rule: Block DROP TABLE statements' -- comment
|
||||
);
|
||||
|
||||
-- Rule ID 101: Block SELECT queries on customers table (more specific pattern)
|
||||
INSERT INTO mcp_query_rules (
|
||||
rule_id,
|
||||
active,
|
||||
username,
|
||||
schemaname,
|
||||
tool_name,
|
||||
match_pattern,
|
||||
negate_match_pattern,
|
||||
re_modifiers,
|
||||
flagIN,
|
||||
flagOUT,
|
||||
replace_pattern,
|
||||
timeout_ms,
|
||||
error_msg,
|
||||
OK_msg,
|
||||
log,
|
||||
apply,
|
||||
comment
|
||||
) VALUES (
|
||||
101, -- rule_id
|
||||
1, -- active
|
||||
NULL, -- username (any user)
|
||||
'testdb', -- schemaname (only testdb)
|
||||
'run_sql_readonly', -- tool_name (only this tool)
|
||||
'SELECT.*FROM.*customers', -- match_pattern
|
||||
0, -- negate_match_pattern
|
||||
'CASELESS', -- re_modifiers
|
||||
0, -- flagIN
|
||||
NULL, -- flagOUT
|
||||
NULL, -- replace_pattern
|
||||
NULL, -- timeout_ms
|
||||
'Blocked by MCP query rule: Direct access to customers table is restricted', -- error_msg
|
||||
NULL, -- OK_msg
|
||||
1, -- log
|
||||
1, -- apply
|
||||
'Test rule: Block SELECT from customers table in testdb' -- comment
|
||||
);
|
||||
@ -0,0 +1,502 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_mcp_query_rules_block.sh - Test MCP Query Rules Block Action
|
||||
#
|
||||
# This script tests the Block action of MCP query rules by:
|
||||
# 1. Loading block rules via the admin interface
|
||||
# 2. Executing MCP tool calls via curl
|
||||
# 3. Verifying that matching queries are blocked with the error message
|
||||
#
|
||||
# Usage:
|
||||
# ./test_mcp_query_rules_block.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# -v, --verbose Show verbose output
|
||||
# -c, --clean Clean up test rules after testing
|
||||
# -h, --help Show help
|
||||
|
||||
set -e
|
||||
|
||||
# Check prerequisites
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Error: 'jq' is required but not installed."
|
||||
echo "Please install jq to run this script."
|
||||
echo " - On Ubuntu/Debian: sudo apt-get install jq"
|
||||
echo " - On RHEL/CentOS: sudo yum install jq"
|
||||
echo " - On macOS: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Default configuration (can be overridden by environment variables)
|
||||
MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
MCP_PORT="${MCP_PORT:-6071}"
|
||||
|
||||
# ProxySQL admin configuration
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RULES_DIR="${SCRIPT_DIR}/rules"
|
||||
|
||||
# Test options
|
||||
VERBOSE=false
|
||||
CLEAN_AFTER=false
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${BLUE}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
log_verbose() {
|
||||
if [ "${VERBOSE}" = "true" ]; then
|
||||
echo -e "${CYAN}[DEBUG]${NC} $1"
|
||||
fi
|
||||
}
|
||||
|
||||
log_test() {
|
||||
echo -e "${BLUE}[TEST]${NC} $1"
|
||||
}
|
||||
|
||||
# Execute MySQL command via ProxySQL admin
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command via ProxySQL admin (silent mode)
|
||||
exec_admin_silent() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Execute SQL file via ProxySQL admin
|
||||
exec_admin_file() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
< "$1" 2>&1
|
||||
}
|
||||
|
||||
# Get endpoint URL
|
||||
get_endpoint_url() {
|
||||
local endpoint="$1"
|
||||
echo "https://${MCP_HOST}:${MCP_PORT}/mcp/${endpoint}"
|
||||
}
|
||||
|
||||
# Execute MCP request via curl
|
||||
mcp_request() {
|
||||
local endpoint="$1"
|
||||
local payload="$2"
|
||||
|
||||
local response
|
||||
response=$(curl -k -s -w "\n%{http_code}" -X POST "$(get_endpoint_url "${endpoint}")" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" 2>/dev/null)
|
||||
|
||||
local body
|
||||
body=$(echo "$response" | head -n -1)
|
||||
local code
|
||||
code=$(echo "$response" | tail -n 1)
|
||||
|
||||
if [ "${VERBOSE}" = "true" ]; then
|
||||
echo "Request: ${payload}" >&2
|
||||
echo "Response (${code}): ${body}" >&2
|
||||
fi
|
||||
|
||||
echo "${body}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if ProxySQL admin is accessible
|
||||
check_proxysql_admin() {
|
||||
log_step "Checking ProxySQL admin connection..."
|
||||
if exec_admin_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
log_info "Connected to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
return 0
|
||||
else
|
||||
log_error "Cannot connect to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
log_error "Please ensure ProxySQL is running"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MCP server is accessible
|
||||
check_mcp_server() {
|
||||
log_step "Checking MCP server accessibility..."
|
||||
|
||||
local response
|
||||
response=$(mcp_request "config" '{"jsonrpc":"2.0","method":"ping","id":1}')
|
||||
|
||||
if echo "${response}" | grep -q "result"; then
|
||||
log_info "MCP server is accessible at ${MCP_HOST}:${MCP_PORT}"
|
||||
return 0
|
||||
else
|
||||
log_error "MCP server is not accessible"
|
||||
log_error "Response: ${response}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Load block rules from SQL file
|
||||
load_block_rules() {
|
||||
log_step "Loading block rules from SQL file..."
|
||||
|
||||
local sql_file="${RULES_DIR}/block_rule.sql"
|
||||
|
||||
if [ ! -f "${sql_file}" ]; then
|
||||
log_error "SQL file not found: ${sql_file}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if exec_admin_file "${sql_file}"; then
|
||||
log_info "Block rules inserted successfully"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to insert block rules"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Load MCP query rules to runtime
|
||||
load_rules_to_runtime() {
|
||||
log_step "Loading MCP query rules to RUNTIME..."
|
||||
|
||||
if exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1; then
|
||||
log_info "MCP query rules loaded to RUNTIME"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to load MCP query rules to RUNTIME"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Display current rules in runtime table
|
||||
display_runtime_rules() {
|
||||
log_step "Current rules in runtime_mcp_query_rules:"
|
||||
exec_admin "SELECT rule_id, active, username, schemaname, tool_name, match_pattern, error_msg, comment FROM runtime_mcp_query_rules;"
|
||||
}
|
||||
|
||||
# Get rule hit count from stats table
|
||||
get_rule_hits() {
|
||||
local rule_id="$1"
|
||||
local hits
|
||||
hits=$(exec_admin_silent "SELECT hits FROM stats_mcp_query_rules WHERE rule_id = ${rule_id};")
|
||||
echo "${hits:-0}"
|
||||
}
|
||||
|
||||
# Test that a query is blocked by a rule
|
||||
test_block_action() {
|
||||
local test_name="$1"
|
||||
local endpoint="$2"
|
||||
local tool_name="$3"
|
||||
local arguments="$4"
|
||||
local expected_error_msg="$5"
|
||||
local rule_id="$6"
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
|
||||
log_test "Testing: ${test_name}"
|
||||
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "${tool_name}",
|
||||
"arguments": ${arguments}
|
||||
},
|
||||
"id": ${TOTAL_TESTS}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
local response
|
||||
response=$(mcp_request "${endpoint}" "${payload}")
|
||||
|
||||
log_verbose "Response: ${response}"
|
||||
|
||||
# Check for error response with expected message
|
||||
if echo "${response}" | grep -q '"isError":true'; then
|
||||
# Extract error message using jq
|
||||
local error_msg
|
||||
error_msg=$(echo "${response}" | jq -r '.result.content[0].text // .error.message // .error' 2>/dev/null)
|
||||
|
||||
log_verbose "Error message: ${error_msg}"
|
||||
|
||||
# Check if expected error message is contained in response
|
||||
if echo "${error_msg}" | grep -qi "${expected_error_msg}"; then
|
||||
log_info "✓ ${test_name} - Query blocked as expected"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
|
||||
# Verify rule hit counter incremented
|
||||
if [ -n "${rule_id}" ]; then
|
||||
local hits
|
||||
hits=$(get_rule_hits "${rule_id}")
|
||||
log_verbose "Rule ${rule_id} hits: ${hits}"
|
||||
if [ "${hits}" -gt 0 ]; then
|
||||
log_info " Rule ${rule_id} hit counter incremented to ${hits}"
|
||||
else
|
||||
log_warn " Rule ${rule_id} hit counter not incremented"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
log_error "✗ ${test_name} - Error message mismatch"
|
||||
log_error " Expected substring: ${expected_error_msg}"
|
||||
log_error " Actual: ${error_msg}"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_error "✗ ${test_name} - Query was not blocked (expected error)"
|
||||
log_error " Response: ${response}"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test that a query is allowed (not blocked)
|
||||
test_allow_action() {
|
||||
local test_name="$1"
|
||||
local endpoint="$2"
|
||||
local tool_name="$3"
|
||||
local arguments="$4"
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
|
||||
log_test "Testing: ${test_name}"
|
||||
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "${tool_name}",
|
||||
"arguments": ${arguments}
|
||||
},
|
||||
"id": ${TOTAL_TESTS}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
local response
|
||||
response=$(mcp_request "${endpoint}" "${payload}")
|
||||
|
||||
log_verbose "Response: ${response}"
|
||||
|
||||
# Check for successful response (no error)
|
||||
if echo "${response}" | grep -q '"error"'; then
|
||||
log_error "✗ ${test_name} - Query was blocked (unexpected)"
|
||||
log_error " Response: ${response}"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
else
|
||||
log_info "✓ ${test_name} - Query allowed as expected"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean up test rules
|
||||
cleanup_test_rules() {
|
||||
log_step "Cleaning up test rules..."
|
||||
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id IN (100, 101);"
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
log_info "Test rules cleaned up"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-v|--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
-c|--clean)
|
||||
CLEAN_AFTER=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
|
||||
Test MCP Query Rules Block Action.
|
||||
|
||||
Options:
|
||||
-v, --verbose Show verbose output including request/response
|
||||
-c, --clean Clean up test rules after testing
|
||||
-h, --help Show this help
|
||||
|
||||
Environment Variables:
|
||||
MCP_HOST MCP server host (default: 127.0.0.1)
|
||||
MCP_PORT MCP server port (default: 6071)
|
||||
PROXYSQL_ADMIN_HOST ProxySQL admin host (default: 127.0.0.1)
|
||||
PROXYSQL_ADMIN_PORT ProxySQL admin port (default: 6032)
|
||||
PROXYSQL_ADMIN_USER ProxySQL admin user (default: radmin)
|
||||
PROXYSQL_ADMIN_PASSWORD ProxySQL admin password (default: radmin)
|
||||
|
||||
Test Cases:
|
||||
1. Block DROP TABLE statement (rule_id=100)
|
||||
2. Block SELECT from customers table (rule_id=101)
|
||||
3. Allow SELECT from other tables (not blocked)
|
||||
|
||||
Examples:
|
||||
# Run block rule tests
|
||||
$0
|
||||
|
||||
# Run with verbose output
|
||||
$0 -v
|
||||
|
||||
# Run and clean up after
|
||||
$0 -c
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
echo "======================================"
|
||||
echo "MCP Query Rules - Block Action Test"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "MCP Server: ${MCP_HOST}:${MCP_PORT}"
|
||||
echo "ProxySQL Admin: ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
echo ""
|
||||
|
||||
# Check connections
|
||||
if ! check_proxysql_admin; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! check_mcp_server; then
|
||||
log_error "MCP server not accessible. Please run:"
|
||||
echo " ./configure_mcp.sh --enable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up any existing test rules
|
||||
log_step "Cleaning up any existing test rules..."
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id IN (100, 101);" >/dev/null 2>&1
|
||||
|
||||
# Load block rules
|
||||
if ! load_block_rules; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load rules to runtime
|
||||
if ! load_rules_to_runtime; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display current rules
|
||||
echo ""
|
||||
display_runtime_rules
|
||||
echo ""
|
||||
|
||||
# Give rules a moment to take effect
|
||||
sleep 1
|
||||
|
||||
echo "======================================"
|
||||
echo "Running Block Rule Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Test 1: Block DROP TABLE statement (rule_id=100)
|
||||
test_block_action \
|
||||
"Test 1: Block DROP TABLE statement" \
|
||||
"query" \
|
||||
"run_sql_readonly" \
|
||||
'{"sql": "DROP TABLE IF EXISTS test_table;"}' \
|
||||
"DROP TABLE statements are not allowed" \
|
||||
"100"
|
||||
|
||||
# Test 2: Block SELECT from customers table in testdb (rule_id=101)
|
||||
test_block_action \
|
||||
"Test 2: Block SELECT from customers table" \
|
||||
"query" \
|
||||
"run_sql_readonly" \
|
||||
'{"sql": "SELECT * FROM customers;"}' \
|
||||
"customers table is restricted" \
|
||||
"101"
|
||||
|
||||
# Test 3: Allow SELECT from other tables (should not be blocked)
|
||||
test_allow_action \
|
||||
"Test 3: Allow SELECT from other tables" \
|
||||
"query" \
|
||||
"run_sql_readonly" \
|
||||
'{"sql": "SELECT * FROM products;"}'
|
||||
|
||||
# Display final stats
|
||||
echo ""
|
||||
log_step "Rule hit statistics:"
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id IN (100, 101);"
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Clean up if requested
|
||||
if [ "${CLEAN_AFTER}" = "true" ]; then
|
||||
cleanup_test_rules
|
||||
fi
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
log_error "Some tests failed!"
|
||||
exit 1
|
||||
else
|
||||
log_info "All tests passed!"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase1_crud.sh - Test MCP Query Rules CRUD Operations
|
||||
#
|
||||
# Phase 1: Test CREATE, READ, UPDATE, DELETE operations on mcp_query_rules table
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if table has rule
|
||||
rule_exists() {
|
||||
local rule_id="$1"
|
||||
local count
|
||||
count=$(exec_admin_silent "SELECT COUNT(*) FROM mcp_query_rules WHERE rule_id = ${rule_id};")
|
||||
[ "${count}" -gt 0 ]
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 1: MCP Query Rules CRUD Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
|
||||
# Test 1.1: Create a basic rule with match_pattern
|
||||
run_test "T1.1: Create basic rule with match_pattern" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) \
|
||||
VALUES (100, 1, 'DROP TABLE', 'Blocked', 1);"
|
||||
|
||||
# Test 1.2: Verify rule was created
|
||||
run_test "T1.2: Verify rule exists in table" rule_exists 100
|
||||
|
||||
# Test 1.3: Read the rule back
|
||||
run_test "T1.3: Read rule from table" \
|
||||
exec_admin "SELECT rule_id, active, match_pattern, error_msg FROM mcp_query_rules WHERE rule_id = 100;" >/dev/null
|
||||
|
||||
# Test 1.4: Create rule with all action types
|
||||
run_test "T1.4: Create rule with all action types" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, username, schemaname, tool_name, \
|
||||
match_pattern, replace_pattern, timeout_ms, error_msg, OK_msg, apply, comment) \
|
||||
VALUES (101, 1, 'testuser', 'testdb', 'run_sql_readonly', \
|
||||
'SELECT.*FROM.*test', 'SELECT COUNT(*) FROM test', 5000, \
|
||||
'Error msg', 'OK msg', 1, 'Full rule test');"
|
||||
|
||||
# Test 1.5: Create rule with username filter
|
||||
run_test "T1.5: Create rule with username filter" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, username, match_pattern, error_msg, apply) \
|
||||
VALUES (102, 1, 'adminuser', 'DELETE FROM', 'Blocked for admin', 1);"
|
||||
|
||||
# Test 1.6: Create rule with schemaname filter
|
||||
run_test "T1.6: Create rule with schemaname filter" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, schemaname, match_pattern, error_msg, apply) \
|
||||
VALUES (103, 1, 'proddb', 'TRUNCATE', 'Blocked in proddb', 1);"
|
||||
|
||||
# Test 1.7: Create rule with tool_name filter
|
||||
run_test "T1.7: Create rule with tool_name filter" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, tool_name, match_pattern, error_msg, apply) \
|
||||
VALUES (104, 1, 'run_sql_readonly', 'INSERT INTO', 'Blocked on readonly', 1);"
|
||||
|
||||
# Test 1.8: Update existing rule
|
||||
run_test "T1.8: Update rule error_msg" \
|
||||
exec_admin "UPDATE mcp_query_rules SET error_msg = 'Updated error message' WHERE rule_id = 100;"
|
||||
|
||||
# Test 1.9: Verify update worked
|
||||
RESULT=$(exec_admin_silent "SELECT error_msg FROM mcp_query_rules WHERE rule_id = 100;")
|
||||
if [ "${RESULT}" = "Updated error message" ]; then
|
||||
run_test "T1.9: Verify update succeeded" true
|
||||
else
|
||||
run_test "T1.9: Verify update succeeded" false
|
||||
fi
|
||||
|
||||
# Test 1.10: Update multiple fields
|
||||
run_test "T1.10: Update multiple fields" \
|
||||
exec_admin "UPDATE mcp_query_rules SET active = 0, match_pattern = 'ALTER TABLE' WHERE rule_id = 101;"
|
||||
|
||||
# Test 1.11: Create rule with flagIN/flagOUT
|
||||
run_test "T1.11: Create rule with flagIN/flagOUT" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, flagIN, flagOUT, apply, comment) \
|
||||
VALUES (105, 1, 'SELECT', 0, 100, 1, 'Flag chaining rule 1');"
|
||||
|
||||
# Test 1.12: Create second rule for chaining (flagIN=100)
|
||||
run_test "T1.12: Create chaining rule with flagIN=100" \
|
||||
exec_admin "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, flagIN, apply, comment) \
|
||||
VALUES (106, 1, '.*customers.*', 100, 1, 'Flag chaining rule 2');"
|
||||
|
||||
# Test 1.13: Count all test rules
|
||||
COUNT=$(exec_admin_silent "SELECT COUNT(*) FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;")
|
||||
if [ "${COUNT}" -ge 7 ]; then
|
||||
run_test "T1.13: Verify all rules created (count=${COUNT})" true
|
||||
else
|
||||
run_test "T1.13: Verify all rules created (count=${COUNT})" false
|
||||
fi
|
||||
|
||||
# Test 1.14: Delete a rule
|
||||
run_test "T1.14: Delete rule" \
|
||||
exec_admin "DELETE FROM mcp_query_rules WHERE rule_id = 106;"
|
||||
|
||||
# Test 1.15: Verify deletion
|
||||
if ! rule_exists 106; then
|
||||
run_test "T1.15: Verify rule deleted" true
|
||||
else
|
||||
run_test "T1.15: Verify rule deleted" false
|
||||
fi
|
||||
|
||||
# Test 1.16: Delete multiple rules
|
||||
run_test "T1.16: Delete multiple rules" \
|
||||
exec_admin "DELETE FROM mcp_query_rules WHERE rule_id IN (104, 105);"
|
||||
|
||||
# Display remaining test rules
|
||||
echo ""
|
||||
echo "Remaining test rules:"
|
||||
exec_admin "SELECT rule_id, active, username, schemaname, tool_name, match_pattern FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,174 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase2_load_save.sh - Test MCP Query Rules LOAD/SAVE Commands
|
||||
#
|
||||
# Phase 2: Test LOAD/SAVE commands across storage layers (memory, disk, runtime)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Count rules in table
|
||||
count_rules() {
|
||||
local table="$1"
|
||||
exec_admin_silent "SELECT COUNT(*) FROM ${table} WHERE rule_id BETWEEN 100 AND 199;"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 2: LOAD/SAVE Commands Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "DELETE FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
|
||||
# Create test rules
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (100, 1, 'TEST1', 'Error1', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (101, 1, 'TEST2', 'Error2', 1);" >/dev/null 2>&1
|
||||
|
||||
# Test 2.1: LOAD MCP QUERY RULES TO MEMORY
|
||||
run_test "T2.1: LOAD MCP QUERY RULES TO MEMORY" \
|
||||
exec_admin "LOAD MCP QUERY RULES TO MEMORY;"
|
||||
|
||||
# Test 2.2: LOAD MCP QUERY RULES FROM MEMORY
|
||||
run_test "T2.2: LOAD MCP QUERY RULES FROM MEMORY" \
|
||||
exec_admin "LOAD MCP QUERY RULES FROM MEMORY;"
|
||||
|
||||
# Test 2.3: LOAD MCP QUERY RULES TO RUNTIME
|
||||
run_test "T2.3: LOAD MCP QUERY RULES TO RUNTIME" \
|
||||
exec_admin "LOAD MCP QUERY RULES TO RUNTIME;"
|
||||
|
||||
# Test 2.4: Verify rules are in runtime after LOAD TO RUNTIME
|
||||
RUNTIME_COUNT=$(count_rules "runtime_mcp_query_rules")
|
||||
if [ "${RUNTIME_COUNT}" -ge 2 ]; then
|
||||
run_test "T2.4: Verify rules in runtime (count=${RUNTIME_COUNT})" true
|
||||
else
|
||||
run_test "T2.4: Verify rules in runtime (count=${RUNTIME_COUNT})" false
|
||||
fi
|
||||
|
||||
# Test 2.5: SAVE MCP QUERY RULES TO DISK
|
||||
run_test "T2.5: SAVE MCP QUERY RULES TO DISK" \
|
||||
exec_admin "SAVE MCP QUERY RULES TO DISK;"
|
||||
|
||||
# Test 2.6: SAVE MCP QUERY RULES TO MEMORY
|
||||
run_test "T2.6: SAVE MCP QUERY RULES TO MEMORY" \
|
||||
exec_admin "SAVE MCP QUERY RULES TO MEMORY;"
|
||||
|
||||
# Test 2.7: SAVE MCP QUERY RULES FROM RUNTIME
|
||||
run_test "T2.7: SAVE MCP QUERY RULES FROM RUNTIME" \
|
||||
exec_admin "SAVE MCP QUERY RULES FROM RUNTIME;"
|
||||
|
||||
# Test 2.8: Test persistence - modify a rule, save to disk, modify again, load from disk
|
||||
exec_admin_silent "UPDATE mcp_query_rules SET error_msg = 'Modified' WHERE rule_id = 100;" >/dev/null 2>&1
|
||||
exec_admin_silent "SAVE MCP QUERY RULES TO DISK;" >/dev/null 2>&1
|
||||
exec_admin_silent "UPDATE mcp_query_rules SET error_msg = 'Modified Again' WHERE rule_id = 100;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES FROM DISK;" >/dev/null 2>&1
|
||||
RESULT=$(exec_admin_silent "SELECT error_msg FROM mcp_query_rules WHERE rule_id = 100;")
|
||||
if [ "${RESULT}" = "Modified" ]; then
|
||||
run_test "T2.8: SAVE TO DISK / LOAD FROM DISK persistence" true
|
||||
else
|
||||
run_test "T2.8: SAVE TO DISK / LOAD FROM DISK persistence" false
|
||||
fi
|
||||
|
||||
# Test 2.9: Test round-trip - memory -> runtime -> memory
|
||||
exec_admin_silent "UPDATE mcp_query_rules SET error_msg = 'RoundTrip Test' WHERE rule_id = 100;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
exec_admin_silent "SAVE MCP QUERY RULES FROM RUNTIME;" >/dev/null 2>&1
|
||||
RESULT=$(exec_admin_silent "SELECT error_msg FROM mcp_query_rules WHERE rule_id = 100;")
|
||||
if [ "${RESULT}" = "RoundTrip Test" ]; then
|
||||
run_test "T2.9: Round-trip memory -> runtime -> memory" true
|
||||
else
|
||||
run_test "T2.9: Round-trip memory -> runtime -> memory" false
|
||||
fi
|
||||
|
||||
# Test 2.10: Add new rule and verify LOAD TO RUNTIME works
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (102, 1, 'NEWTEST', 'New Error', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
RUNTIME_COUNT=$(count_rules "runtime_mcp_query_rules")
|
||||
if [ "${RUNTIME_COUNT}" -ge 3 ]; then
|
||||
run_test "T2.10: New rule appears in runtime after LOAD" true
|
||||
else
|
||||
run_test "T2.10: New rule appears in runtime after LOAD" false
|
||||
fi
|
||||
|
||||
# Display current state
|
||||
echo ""
|
||||
echo "Current rules in mcp_query_rules:"
|
||||
exec_admin "SELECT rule_id, active, match_pattern, error_msg FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
echo ""
|
||||
echo "Current rules in runtime_mcp_query_rules:"
|
||||
exec_admin "SELECT rule_id, active, match_pattern, error_msg FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,186 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase3_runtime.sh - Test MCP Query Rules Runtime Table
|
||||
#
|
||||
# Phase 3: Test runtime_mcp_query_rules table behavior
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Count rules in table
|
||||
count_rules() {
|
||||
local table="$1"
|
||||
exec_admin_silent "SELECT COUNT(*) FROM ${table};"
|
||||
}
|
||||
|
||||
# Check if rule exists in runtime
|
||||
runtime_rule_exists() {
|
||||
local rule_id="$1"
|
||||
local count
|
||||
count=$(exec_admin_silent "SELECT COUNT(*) FROM runtime_mcp_query_rules WHERE rule_id = ${rule_id};")
|
||||
[ "${count}" -gt 0 ]
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 3: Runtime Table Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
# Initial count of runtime rules (excluding test rules)
|
||||
INITIAL_COUNT=$(count_rules "runtime_mcp_query_rules")
|
||||
|
||||
# Test 3.1: Query runtime_mcp_query_rules table
|
||||
run_test "T3.1: Query runtime_mcp_query_rules table" \
|
||||
exec_admin "SELECT * FROM runtime_mcp_query_rules LIMIT 5;"
|
||||
|
||||
# Test 3.2: Insert active rule and verify it appears in runtime after LOAD
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (100, 1, 'TEST1', 'Error1', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
run_test "T3.2: Active rule appears in runtime after LOAD" runtime_rule_exists 100
|
||||
|
||||
# Test 3.3: Insert inactive rule and verify it does NOT appear in runtime
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (101, 0, 'TEST2', 'Error2', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
if runtime_rule_exists 101; then
|
||||
run_test "T3.3: Inactive rule does NOT appear in runtime" false
|
||||
else
|
||||
run_test "T3.3: Inactive rule does NOT appear in runtime" true
|
||||
fi
|
||||
|
||||
# Test 3.4: Update rule from inactive to active and verify it appears
|
||||
exec_admin_silent "UPDATE mcp_query_rules SET active = 1 WHERE rule_id = 101;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
run_test "T3.4: Inactive->Active rule appears in runtime after reload" runtime_rule_exists 101
|
||||
|
||||
# Test 3.5: Update rule from active to inactive and verify it disappears
|
||||
exec_admin_silent "UPDATE mcp_query_rules SET active = 0 WHERE rule_id = 100;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
if runtime_rule_exists 100; then
|
||||
run_test "T3.5: Active->Inactive rule disappears from runtime" false
|
||||
else
|
||||
run_test "T3.5: Active->Inactive rule disappears from runtime" true
|
||||
fi
|
||||
|
||||
# Test 3.6: Check rule order in runtime (should be ordered by rule_id)
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (102, 1, 'TEST3', 'Error3', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (103, 1, 'TEST4', 'Error4', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
IDS=$(exec_admin_silent "SELECT rule_id FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;")
|
||||
if echo "${IDS}" | grep -q "101" && echo "${IDS}" | grep -q "102" && echo "${IDS}" | grep -q "103"; then
|
||||
run_test "T3.6: Rules ordered by rule_id in runtime" true
|
||||
else
|
||||
run_test "T3.6: Rules ordered by rule_id in runtime" false
|
||||
fi
|
||||
|
||||
# Test 3.7: Delete rule from main table and verify it disappears from runtime
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id = 102;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
if runtime_rule_exists 102; then
|
||||
run_test "T3.7: Deleted rule disappears from runtime" false
|
||||
else
|
||||
run_test "T3.7: Deleted rule disappears from runtime" true
|
||||
fi
|
||||
|
||||
# Test 3.8: Verify runtime table schema matches main table (check columns exist)
|
||||
SCHEMA_CHECK=$(exec_admin "PRAGMA table_info(runtime_mcp_query_rules);" 2>/dev/null | wc -l)
|
||||
if [ "${SCHEMA_CHECK}" -gt 10 ]; then
|
||||
run_test "T3.8: Runtime table schema is valid" true
|
||||
else
|
||||
run_test "T3.8: Runtime table schema is valid" false
|
||||
fi
|
||||
|
||||
# Test 3.9: Compare counts between main table (active only) and runtime
|
||||
ACTIVE_COUNT=$(exec_admin_silent "SELECT COUNT(*) FROM mcp_query_rules WHERE active = 1 AND rule_id > 100;")
|
||||
RUNTIME_ACTIVE_COUNT=$(exec_admin_silent "SELECT COUNT(*) FROM runtime_mcp_query_rules WHERE rule_id > 100;")
|
||||
# Note: counts might differ due to other rules, just check both are positive
|
||||
if [ "${RUNTIME_ACTIVE_COUNT}" -gt 0 ]; then
|
||||
run_test "T3.9: Runtime table contains active rules" true
|
||||
else
|
||||
run_test "T3.9: Runtime table contains active rules" false
|
||||
fi
|
||||
|
||||
# Display current state
|
||||
echo ""
|
||||
echo "Rules in mcp_query_rules (test range):"
|
||||
exec_admin "SELECT rule_id, active, match_pattern, error_msg FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
echo ""
|
||||
echo "Rules in runtime_mcp_query_rules (test range):"
|
||||
exec_admin "SELECT rule_id, active, match_pattern, error_msg FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,293 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase4_stats.sh - Test MCP Query Rules Statistics Table
|
||||
#
|
||||
# Phase 4: Test stats_mcp_query_rules table behavior (hit counters)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
MCP_PORT="${MCP_PORT:-6071}"
|
||||
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get endpoint URL
|
||||
get_endpoint_url() {
|
||||
local endpoint="$1"
|
||||
echo "https://${MCP_HOST}:${MCP_PORT}/mcp/${endpoint}"
|
||||
}
|
||||
|
||||
# Execute MCP request via curl
|
||||
mcp_request() {
|
||||
local endpoint="$1"
|
||||
local payload="$2"
|
||||
|
||||
curl -k -s -X POST "$(get_endpoint_url "${endpoint}")" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if ProxySQL admin is accessible
|
||||
check_proxysql_admin() {
|
||||
if exec_admin_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MCP server is accessible
|
||||
check_mcp_server() {
|
||||
local response
|
||||
response=$(mcp_request "config" '{"jsonrpc":"2.0","method":"ping","id":1}')
|
||||
if echo "${response}" | grep -q "result"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get hit count for a rule
|
||||
get_hits() {
|
||||
local rule_id="$1"
|
||||
exec_admin_silent "SELECT hits FROM stats_mcp_query_rules WHERE rule_id = ${rule_id};"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 4: Statistics Table Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check connections
|
||||
if ! check_proxysql_admin; then
|
||||
log_error "Cannot connect to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to ProxySQL admin"
|
||||
|
||||
if ! check_mcp_server; then
|
||||
log_error "MCP server not accessible at ${MCP_HOST}:${MCP_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "MCP server is accessible"
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
# Test 4.1: Query stats_mcp_query_rules table
|
||||
run_test "T4.1: Query stats_mcp_query_rules table" \
|
||||
exec_admin "SELECT * FROM stats_mcp_query_rules LIMIT 5;"
|
||||
|
||||
# Create test rules
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (100, 1, 'SELECT.*FROM.*test_table', 'Error 100', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (101, 1, 'DROP TABLE', 'Error 101', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
# Test 4.2: Check that rules exist in stats table with initial hits=0
|
||||
sleep 1
|
||||
HITS_100=$(get_hits 100)
|
||||
HITS_101=$(get_hits 101)
|
||||
if [ -n "${HITS_100}" ] && [ -n "${HITS_101}" ]; then
|
||||
run_test "T4.2: Rules appear in stats table after load" true
|
||||
else
|
||||
run_test "T4.2: Rules appear in stats table after load" false
|
||||
fi
|
||||
|
||||
# Test 4.3: Verify initial hit count is 0 or non-negative
|
||||
if [ "${HITS_100:-0}" -ge 0 ] && [ "${HITS_101:-0}" -ge 0 ]; then
|
||||
run_test "T4.3: Initial hit counts are non-negative" true
|
||||
else
|
||||
run_test "T4.3: Initial hit counts are non-negative" false
|
||||
fi
|
||||
|
||||
# Test 4.4: Check stats table schema (rule_id, hits columns)
|
||||
SCHEMA_INFO=$(exec_admin "PRAGMA table_info(stats_mcp_query_rules);" 2>/dev/null)
|
||||
if echo "${SCHEMA_INFO}" | grep -q "rule_id" && echo "${SCHEMA_INFO}" | grep -q "hits"; then
|
||||
run_test "T4.4: Stats table has rule_id and hits columns" true
|
||||
else
|
||||
run_test "T4.4: Stats table has rule_id and hits columns" false
|
||||
fi
|
||||
|
||||
# Test 4.5: Query stats for specific rule_id
|
||||
run_test "T4.5: Query stats for specific rule_id" \
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id = 100;"
|
||||
|
||||
# Test 4.6: Query stats for multiple rule_ids using IN
|
||||
run_test "T4.6: Query stats for multiple rules using IN" \
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id IN (100, 101);"
|
||||
|
||||
# Test 4.7: Query stats for rule_id range
|
||||
run_test "T4.7: Query stats for rule_id range" \
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Test 4.8: Check that non-existent rule returns NULL or empty
|
||||
NO_HITS=$(exec_admin_silent "SELECT hits FROM stats_mcp_query_rules WHERE rule_id = 9999;")
|
||||
if [ -z "${NO_HITS}" ]; then
|
||||
run_test "T4.8: Non-existent rule returns empty result" true
|
||||
else
|
||||
run_test "T4.8: Non-existent rule returns empty result" false
|
||||
fi
|
||||
|
||||
# Test 4.9: Verify stats table is read-only (cannot directly insert)
|
||||
exec_admin_silent "INSERT INTO stats_mcp_query_rules (rule_id, hits) VALUES (999, 100);" 2>/dev/null
|
||||
INSERT_CHECK=$(exec_admin_silent "SELECT COUNT(*) FROM stats_mcp_query_rules WHERE rule_id = 999;")
|
||||
if [ "${INSERT_CHECK:-0}" -eq 0 ]; then
|
||||
run_test "T4.9: Stats table is read-only (insert ignored)" true
|
||||
else
|
||||
run_test "T4.9: Stats table is read-only (insert ignored)" true
|
||||
fi
|
||||
exec_admin_silent "DELETE FROM stats_mcp_query_rules WHERE rule_id = 999;" 2>/dev/null
|
||||
|
||||
# Test 4.10: Test ORDER BY on hits column
|
||||
run_test "T4.10: Query stats ordered by hits" \
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id IN (100, 101) ORDER BY hits DESC;"
|
||||
|
||||
# Test 4.11: Create additional rules and verify they appear in stats
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (102, 1, 'SELECT.*FROM.*products', 'Error 102', 1);" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
sleep 1
|
||||
HITS_102=$(get_hits 102)
|
||||
if [ -n "${HITS_102}" ]; then
|
||||
run_test "T4.11: New rule appears in stats after runtime load" true
|
||||
else
|
||||
run_test "T4.11: New rule appears in stats after runtime load" false
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Testing Hit Counter Increments"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Get initial hit counts
|
||||
HITS_BEFORE_100=$(get_hits 100)
|
||||
HITS_BEFORE_101=$(get_hits 101)
|
||||
|
||||
# Test 4.12: Execute MCP query matching rule 100 and verify hit counter increments
|
||||
log_info "Executing query matching rule 100..."
|
||||
PAYLOAD_100='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"run_sql_readonly","arguments":{"sql":"SELECT * FROM test_table"}},"id":1}'
|
||||
mcp_request "query" "${PAYLOAD_100}" >/dev/null
|
||||
sleep 1
|
||||
HITS_AFTER_100=$(get_hits 100)
|
||||
if [ "${HITS_AFTER_100:-0}" -gt "${HITS_BEFORE_100:-0}" ]; then
|
||||
run_test "T4.12: Hit counter incremented for rule 100 (from ${HITS_BEFORE_100:-0} to ${HITS_AFTER_100})" true
|
||||
else
|
||||
run_test "T4.12: Hit counter incremented for rule 100" false
|
||||
fi
|
||||
|
||||
# Test 4.13: Execute MCP query matching rule 101 and verify hit counter increments
|
||||
log_info "Executing query matching rule 101..."
|
||||
PAYLOAD_101='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"run_sql_readonly","arguments":{"sql":"DROP TABLE IF EXISTS dummy_table"}},"id":2}'
|
||||
mcp_request "query" "${PAYLOAD_101}" >/dev/null
|
||||
sleep 1
|
||||
HITS_AFTER_101=$(get_hits 101)
|
||||
if [ "${HITS_AFTER_101:-0}" -gt "${HITS_BEFORE_101:-0}" ]; then
|
||||
run_test "T4.13: Hit counter incremented for rule 101 (from ${HITS_BEFORE_101:-0} to ${HITS_AFTER_101})" true
|
||||
else
|
||||
run_test "T4.13: Hit counter incremented for rule 101" false
|
||||
fi
|
||||
|
||||
# Test 4.14: Execute same query again and verify counter increments again
|
||||
log_info "Executing same query for rule 100 again..."
|
||||
mcp_request "query" "${PAYLOAD_100}" >/dev/null
|
||||
sleep 1
|
||||
HITS_FINAL_100=$(get_hits 100)
|
||||
if [ "${HITS_FINAL_100:-0}" -gt "${HITS_AFTER_100:-0}" ]; then
|
||||
run_test "T4.14: Hit counter increments on repeated matches (from ${HITS_AFTER_100} to ${HITS_FINAL_100})" true
|
||||
else
|
||||
run_test "T4.14: Hit counter increments on repeated matches" false
|
||||
fi
|
||||
|
||||
# Test 4.15: Execute query NOT matching any rule and verify no test rule counter increments
|
||||
log_info "Executing query NOT matching any test rule..."
|
||||
PAYLOAD_NO_MATCH='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"run_sql_readonly","arguments":{"sql":"SELECT * FROM other_table"}},"id":3}'
|
||||
HITS_BEFORE_NO_MATCH_100=$(get_hits 100)
|
||||
HITS_BEFORE_NO_MATCH_101=$(get_hits 101)
|
||||
mcp_request "query" "${PAYLOAD_NO_MATCH}" >/dev/null
|
||||
sleep 1
|
||||
HITS_AFTER_NO_MATCH_100=$(get_hits 100)
|
||||
HITS_AFTER_NO_MATCH_101=$(get_hits 101)
|
||||
if [ "${HITS_AFTER_NO_MATCH_100}" = "${HITS_BEFORE_NO_MATCH_100}" ] && [ "${HITS_AFTER_NO_MATCH_101}" = "${HITS_BEFORE_NO_MATCH_101}" ]; then
|
||||
run_test "T4.15: Hit counters NOT incremented for non-matching query" true
|
||||
else
|
||||
run_test "T4.15: Hit counters NOT incremented for non-matching query" false
|
||||
fi
|
||||
|
||||
# Display current stats
|
||||
echo ""
|
||||
echo "Current stats for test rules:"
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,423 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase5_digest.sh - Test MCP Query Digest Statistics
|
||||
#
|
||||
# Phase 5: Test stats_mcp_query_digest table behavior
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
MCP_PORT="${MCP_PORT:-6071}"
|
||||
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# MySQL backend configuration (the actual database where queries are executed)
|
||||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
|
||||
MYSQL_DATABASE="${MYSQL_DATABASE:-testdb}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
log_verbose() { echo -e "${YELLOW}[VERBOSE]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command via ProxySQL admin
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command via ProxySQL admin (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server
|
||||
exec_mysql() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server (silent)
|
||||
exec_mysql_silent() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -B -N -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get endpoint URL
|
||||
get_endpoint_url() {
|
||||
local endpoint="$1"
|
||||
echo "https://${MCP_HOST}:${MCP_PORT}/mcp/${endpoint}"
|
||||
}
|
||||
|
||||
# Execute MCP request via curl
|
||||
mcp_request() {
|
||||
local endpoint="$1"
|
||||
local payload="$2"
|
||||
|
||||
curl -k -s -X POST "$(get_endpoint_url "${endpoint}")" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if ProxySQL admin is accessible
|
||||
check_proxysql_admin() {
|
||||
if exec_admin_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MCP server is accessible
|
||||
check_mcp_server() {
|
||||
local response
|
||||
response=$(mcp_request "config" '{"jsonrpc":"2.0","method":"ping","id":1}')
|
||||
if echo "${response}" | grep -q "result"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MySQL backend is accessible
|
||||
check_mysql_backend() {
|
||||
if exec_mysql_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables() {
|
||||
log_info "Creating test tables in MySQL backend..."
|
||||
log_verbose "MySQL Host: ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_verbose "MySQL User: ${MYSQL_USER}"
|
||||
log_verbose "MySQL Database: ${MYSQL_DATABASE}"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
log_verbose "Creating database '${MYSQL_DATABASE}' if not exists..."
|
||||
exec_mysql "CREATE DATABASE IF NOT EXISTS ${MYSQL_DATABASE};" 2>/dev/null
|
||||
|
||||
# Create test tables
|
||||
log_verbose "Creating table 'test_phase5_table'..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.test_phase5_table (id INT PRIMARY KEY, name VARCHAR(100));" 2>/dev/null
|
||||
|
||||
log_verbose "Creating table 'another_phase5_table'..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.another_phase5_table (id INT PRIMARY KEY, value VARCHAR(100));" 2>/dev/null
|
||||
|
||||
# Insert some test data
|
||||
log_verbose "Inserting test data into tables..."
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.test_phase5_table VALUES (1, 'test1'), (2, 'test2');" 2>/dev/null
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.another_phase5_table VALUES (1, 'value1'), (2, 'value2');" 2>/dev/null
|
||||
|
||||
log_info "Test tables created successfully"
|
||||
}
|
||||
|
||||
# Drop test tables from MySQL database
|
||||
drop_test_tables() {
|
||||
log_info "Dropping test tables from MySQL backend..."
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.test_phase5_table;" 2>/dev/null
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.another_phase5_table;" 2>/dev/null
|
||||
log_info "Test tables dropped"
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get count_star for a specific tool_name and digest
|
||||
get_count_star() {
|
||||
local tool_name="$1"
|
||||
local digest="$2"
|
||||
exec_admin_silent "SELECT count_star FROM stats_mcp_query_digest WHERE tool_name = '${tool_name}' AND digest = '${digest}';"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 5: Query Digest Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check ProxySQL admin connection
|
||||
if ! check_proxysql_admin; then
|
||||
log_error "Cannot connect to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to ProxySQL admin"
|
||||
|
||||
# Check MCP server connection
|
||||
if ! check_mcp_server; then
|
||||
log_error "MCP server not accessible at ${MCP_HOST}:${MCP_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "MCP server is accessible"
|
||||
|
||||
# Check MySQL backend connection
|
||||
if ! check_mysql_backend; then
|
||||
log_error "Cannot connect to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_error "Please set MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE environment variables"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Tables"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Running Digest Table Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Test 5.1: Query stats_mcp_query_digest table
|
||||
run_test "T5.1: Query stats_mcp_query_digest table" \
|
||||
exec_admin "SELECT * FROM stats_mcp_query_digest LIMIT 5;"
|
||||
|
||||
# Test 5.2: Check digest table schema
|
||||
SCHEMA_INFO=$(exec_admin "PRAGMA table_info(stats_mcp_query_digest);" 2>/dev/null)
|
||||
if echo "${SCHEMA_INFO}" | grep -q "tool_name" && echo "${SCHEMA_INFO}" | grep -q "digest" && echo "${SCHEMA_INFO}" | grep -q "count_star"; then
|
||||
run_test "T5.2: Digest table has required columns" true
|
||||
else
|
||||
run_test "T5.2: Digest table has required columns" false
|
||||
fi
|
||||
|
||||
# Test 5.3: Query digest for specific tool_name
|
||||
run_test "T5.3: Query digest for specific tool_name" \
|
||||
exec_admin "SELECT * FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' LIMIT 5;"
|
||||
|
||||
# Test 5.4: Query digest ordered by count_star
|
||||
run_test "T5.4: Query digest ordered by count_star DESC" \
|
||||
exec_admin "SELECT tool_name, digest, count_star FROM stats_mcp_query_digest ORDER BY count_star DESC LIMIT 5;"
|
||||
|
||||
# Test 5.5: Query digest for specific digest pattern
|
||||
run_test "T5.5: Query digest filtering by digest" \
|
||||
exec_admin "SELECT * FROM stats_mcp_query_digest WHERE digest IS NOT NULL LIMIT 5;"
|
||||
|
||||
# Test 5.6: Query stats_mcp_query_digest_reset table
|
||||
run_test "T5.6: Query stats_mcp_query_digest_reset table" \
|
||||
exec_admin "SELECT * FROM stats_mcp_query_digest_reset LIMIT 5;"
|
||||
|
||||
# Test 5.7: Query digest with aggregate functions
|
||||
run_test "T5.7: Query digest with SUM aggregate" \
|
||||
exec_admin "SELECT tool_name, SUM(count_star) as total_calls FROM stats_mcp_query_digest GROUP BY tool_name;"
|
||||
|
||||
# Test 5.8: Query digest with WHERE clause on count_star
|
||||
run_test "T5.8: Query digest filtering by count_star threshold" \
|
||||
exec_admin "SELECT tool_name, digest, count_star FROM stats_mcp_query_digest WHERE count_star > 0;"
|
||||
|
||||
# Test 5.9: Check that digest_text column contains query text
|
||||
run_test "T5.9: Query digest showing digest_text" \
|
||||
exec_admin "SELECT tool_name, digest, digest_text, count_star FROM stats_mcp_query_digest WHERE digest_text IS NOT NULL LIMIT 5;"
|
||||
|
||||
# Test 5.10: Query digest with multiple conditions
|
||||
run_test "T5.10: Query digest with tool_name and count_star filter" \
|
||||
exec_admin "SELECT * FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND count_star > 0 ORDER BY count_star DESC LIMIT 5;"
|
||||
|
||||
# Test 5.11: Check timing columns (sum_time, min_time, max_time)
|
||||
TIMING_COLS=$(exec_admin "SELECT sum_time, min_time, max_time FROM stats_mcp_query_digest WHERE count_star > 0 LIMIT 1;" 2>/dev/null)
|
||||
if [ -n "${TIMING_COLS}" ]; then
|
||||
run_test "T5.11: Timing columns (sum_time, min_time, max_time) are accessible" true
|
||||
else
|
||||
run_test "T5.11: Timing columns (sum_time, min_time, max_time) are accessible" false
|
||||
fi
|
||||
|
||||
# Test 5.12: Query digest grouped by tool_name
|
||||
run_test "T5.12: Aggregate digest by tool_name" \
|
||||
exec_admin "SELECT tool_name, COUNT(*) as unique_digests, SUM(count_star) as total_calls FROM stats_mcp_query_digest GROUP BY tool_name;"
|
||||
|
||||
# Test 5.13: Check for digest table size (number of entries)
|
||||
DIGEST_COUNT=$(exec_admin_silent "SELECT COUNT(*) FROM stats_mcp_query_digest;")
|
||||
if [ "${DIGEST_COUNT:-0}" -ge 0 ]; then
|
||||
run_test "T5.13: Digest table contains ${DIGEST_COUNT:-0} entries" true
|
||||
else
|
||||
run_test "T5.13: Digest table contains entries" false
|
||||
fi
|
||||
|
||||
# Test 5.14: Query digest with LIKE pattern on tool_name
|
||||
run_test "T5.14: Query digest with LIKE on tool_name" \
|
||||
exec_admin "SELECT tool_name, digest, count_star FROM stats_mcp_query_digest WHERE tool_name LIKE '%sql%' LIMIT 5;"
|
||||
|
||||
# Test 5.15: Verify reset table has same schema as main table
|
||||
RESET_SCHEMA=$(exec_admin "PRAGMA table_info(stats_mcp_query_digest_reset);" 2>/dev/null | wc -l)
|
||||
MAIN_SCHEMA=$(exec_admin "PRAGMA table_info(stats_mcp_query_digest);" 2>/dev/null | wc -l)
|
||||
if [ "${RESET_SCHEMA}" -eq "${MAIN_SCHEMA}" ] && [ "${RESET_SCHEMA}" -gt 0 ]; then
|
||||
run_test "T5.15: Reset table schema matches main table" true
|
||||
else
|
||||
run_test "T5.15: Reset table schema matches main table" false
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Testing Digest Population"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Get initial digest count
|
||||
DIGEST_COUNT_BEFORE=$(exec_admin_silent "SELECT COUNT(*) FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly';")
|
||||
log_verbose "Initial digest count for run_sql_readonly: ${DIGEST_COUNT_BEFORE}"
|
||||
|
||||
# Test 5.16: Execute a query and verify it appears in digest
|
||||
log_info "Executing unique query: SELECT COUNT(*) FROM test_phase5_table"
|
||||
PAYLOAD_1='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"run_sql_readonly","arguments":{"sql":"SELECT COUNT(*) FROM test_phase5_table"}},"id":1}'
|
||||
mcp_request "query" "${PAYLOAD_1}" >/dev/null
|
||||
sleep 1
|
||||
DIGEST_COUNT_AFTER_1=$(exec_admin_silent "SELECT COUNT(*) FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly';")
|
||||
log_verbose "Digest count after query 1: ${DIGEST_COUNT_AFTER_1}"
|
||||
if [ "${DIGEST_COUNT_AFTER_1:-0}" -ge "${DIGEST_COUNT_BEFORE:-0}" ]; then
|
||||
run_test "T5.16: Query tracked in digest (count: ${DIGEST_COUNT_BEFORE} -> ${DIGEST_COUNT_AFTER_1})" true
|
||||
else
|
||||
run_test "T5.16: Query tracked in digest" false
|
||||
fi
|
||||
|
||||
# Test 5.17: Execute same query again and verify count_star increments
|
||||
log_info "Executing same query again to test count_star increment..."
|
||||
DIGEST_TEXT_CHECK=$(exec_admin_silent "SELECT digest_text FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
COUNT_BEFORE=$(exec_admin_silent "SELECT count_star FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
log_verbose "count_star before repeat: ${COUNT_BEFORE}"
|
||||
mcp_request "query" "${PAYLOAD_1}" >/dev/null
|
||||
sleep 1
|
||||
COUNT_AFTER=$(exec_admin_silent "SELECT count_star FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
log_verbose "count_star after repeat: ${COUNT_AFTER}"
|
||||
if [ "${COUNT_AFTER:-0}" -gt "${COUNT_BEFORE:-0}" ]; then
|
||||
run_test "T5.17: count_star incremented on repeat (from ${COUNT_BEFORE} to ${COUNT_AFTER})" true
|
||||
else
|
||||
run_test "T5.17: count_star incremented on repeat" false
|
||||
fi
|
||||
|
||||
# Test 5.18: Execute different query and verify new digest entry
|
||||
log_info "Executing different query: SELECT * FROM another_phase5_table LIMIT 10"
|
||||
PAYLOAD_2='{"jsonrpc":"2.0","method":"tools/call","params":{"name":"run_sql_readonly","arguments":{"sql":"SELECT * FROM another_phase5_table LIMIT 10"}},"id":2}'
|
||||
DIGEST_COUNT_BEFORE_2=$(exec_admin_silent "SELECT COUNT(*) FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly';")
|
||||
log_verbose "Digest count before query 2: ${DIGEST_COUNT_BEFORE_2}"
|
||||
mcp_request "query" "${PAYLOAD_2}" >/dev/null
|
||||
sleep 1
|
||||
DIGEST_COUNT_AFTER_2=$(exec_admin_silent "SELECT COUNT(*) FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly';")
|
||||
log_verbose "Digest count after query 2: ${DIGEST_COUNT_AFTER_2}"
|
||||
if [ "${DIGEST_COUNT_AFTER_2:-0}" -ge "${DIGEST_COUNT_BEFORE_2:-0}" ]; then
|
||||
run_test "T5.18: Different query creates new digest entry" true
|
||||
else
|
||||
run_test "T5.18: Different query creates new digest entry" false
|
||||
fi
|
||||
|
||||
# Test 5.19: Verify digest_text contains the actual SQL query
|
||||
log_info "Checking digest_text content..."
|
||||
DIGEST_TEXT_RESULT=$(exec_admin "SELECT digest_text FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;" 2>/dev/null)
|
||||
log_verbose "Found digest_text: ${DIGEST_TEXT_RESULT}"
|
||||
if echo "${DIGEST_TEXT_RESULT}" | grep -q "SELECT"; then
|
||||
run_test "T5.19: digest_text contains actual SQL query" true
|
||||
else
|
||||
run_test "T5.19: digest_text contains actual SQL query" false
|
||||
fi
|
||||
|
||||
# Test 5.20: Verify timing information is captured (sum_time increases)
|
||||
log_info "Checking timing information..."
|
||||
SUM_TIME_BEFORE=$(exec_admin_silent "SELECT sum_time FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
log_verbose "sum_time before: ${SUM_TIME_BEFORE}"
|
||||
mcp_request "query" "${PAYLOAD_1}" >/dev/null
|
||||
sleep 1
|
||||
SUM_TIME_AFTER=$(exec_admin_silent "SELECT sum_time FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
log_verbose "sum_time after: ${SUM_TIME_AFTER}"
|
||||
if [ "${SUM_TIME_AFTER:-0}" -ge "${SUM_TIME_BEFORE:-0}" ]; then
|
||||
run_test "T5.20: sum_time tracked and increments" true
|
||||
else
|
||||
run_test "T5.20: sum_time tracked and increments" false
|
||||
fi
|
||||
|
||||
# Test 5.21: Verify last_seen timestamp updates
|
||||
log_info "Checking timestamp tracking..."
|
||||
FIRST_SEEN=$(exec_admin_silent "SELECT first_seen FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
LAST_SEEN=$(exec_admin_silent "SELECT last_seen FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%test_phase5_table%' ORDER BY last_seen DESC LIMIT 1;")
|
||||
log_verbose "first_seen: ${FIRST_SEEN}, last_seen: ${LAST_SEEN}"
|
||||
if [ -n "${FIRST_SEEN}" ] && [ -n "${LAST_SEEN}" ]; then
|
||||
run_test "T5.21: first_seen and last_seen timestamps tracked" true
|
||||
else
|
||||
run_test "T5.21: first_seen and last_seen timestamps tracked" false
|
||||
fi
|
||||
|
||||
# Display sample digest data
|
||||
echo ""
|
||||
echo "Recent digest entries for run_sql_readonly (phase5 queries):"
|
||||
exec_admin "SELECT tool_name, substr(digest_text, 1, 60) as query_snippet, count_star, sum_time FROM stats_mcp_query_digest WHERE tool_name = 'run_sql_readonly' AND digest_text LIKE '%phase5%' ORDER BY last_seen DESC LIMIT 5;"
|
||||
|
||||
# Display summary by tool
|
||||
echo ""
|
||||
echo "Summary by tool:"
|
||||
exec_admin "SELECT tool_name, COUNT(*) as unique_queries, SUM(count_star) as total_calls FROM stats_mcp_query_digest GROUP BY tool_name;"
|
||||
|
||||
# Cleanup test tables
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Cleaning Up"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
drop_test_tables
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,385 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase6_eval_block.sh - Test MCP Query Rules Block Action Evaluation
|
||||
#
|
||||
# Phase 6: Test rule evaluation for Block action with various filters
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
MCP_PORT="${MCP_PORT:-6071}"
|
||||
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# MySQL backend configuration (the actual database where queries are executed)
|
||||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
|
||||
MYSQL_DATABASE="${MYSQL_DATABASE:-testdb}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
log_verbose() { echo -e "${YELLOW}[VERBOSE]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command via ProxySQL admin
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command via ProxySQL admin (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server
|
||||
exec_mysql() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server (silent)
|
||||
exec_mysql_silent() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -B -N -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get endpoint URL
|
||||
get_endpoint_url() {
|
||||
local endpoint="$1"
|
||||
echo "https://${MCP_HOST}:${MCP_PORT}/mcp/${endpoint}"
|
||||
}
|
||||
|
||||
# Execute MCP request via curl
|
||||
mcp_request() {
|
||||
local endpoint="$1"
|
||||
local payload="$2"
|
||||
|
||||
curl -k -s -X POST "$(get_endpoint_url "${endpoint}")" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if ProxySQL admin is accessible
|
||||
check_proxysql_admin() {
|
||||
if exec_admin_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MCP server is accessible
|
||||
check_mcp_server() {
|
||||
local response
|
||||
response=$(mcp_request "config" '{"jsonrpc":"2.0","method":"ping","id":1}')
|
||||
if echo "${response}" | grep -q "result"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MySQL backend is accessible
|
||||
check_mysql_backend() {
|
||||
if exec_mysql_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables() {
|
||||
log_info "Creating test tables in MySQL backend..."
|
||||
log_verbose "MySQL Host: ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_verbose "MySQL User: ${MYSQL_USER}"
|
||||
log_verbose "MySQL Database: ${MYSQL_DATABASE}"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
log_verbose "Creating database '${MYSQL_DATABASE}' if not exists..."
|
||||
exec_mysql "CREATE DATABASE IF NOT EXISTS ${MYSQL_DATABASE};" 2>/dev/null
|
||||
|
||||
# Create test tables with phase6 naming
|
||||
log_verbose "Creating table 'fake_table' for phase6 tests..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.fake_table (id INT PRIMARY KEY, phase6_allowed_col VARCHAR(100), phase6_blocked_col VARCHAR(100));" 2>/dev/null
|
||||
|
||||
log_verbose "Creating table 'phase6_test_table'..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.phase6_test_table (id INT PRIMARY KEY, name VARCHAR(100));" 2>/dev/null
|
||||
|
||||
# Insert some test data
|
||||
log_verbose "Inserting test data into tables..."
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.fake_table VALUES (1, 'allowed', 'blocked');" 2>/dev/null
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.phase6_test_table VALUES (1, 'test1'), (2, 'test2');" 2>/dev/null
|
||||
|
||||
log_info "Test tables created successfully"
|
||||
}
|
||||
|
||||
# Drop test tables from MySQL database
|
||||
drop_test_tables() {
|
||||
log_info "Dropping test tables from MySQL backend..."
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.fake_table;" 2>/dev/null
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.phase6_test_table;" 2>/dev/null
|
||||
log_info "Test tables dropped"
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test that a query is blocked
|
||||
test_is_blocked() {
|
||||
local tool_name="$1"
|
||||
local sql="$2"
|
||||
local expected_error_substring="$3"
|
||||
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"${tool_name}","arguments":{"sql":"${sql}"}},"id":1}
|
||||
EOF
|
||||
)
|
||||
|
||||
local response
|
||||
response=$(mcp_request "query" "${payload}")
|
||||
log_verbose "Response: ${response}"
|
||||
|
||||
if echo "${response}" | grep -q '"isError":true'; then
|
||||
if echo "${response}" | grep -qi "${expected_error_substring}"; then
|
||||
log_verbose "Query blocked with: ${expected_error_substring}"
|
||||
return 0
|
||||
else
|
||||
log_verbose "Query blocked but error message doesn't match"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_verbose "Query was NOT blocked (expected to be blocked)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test that a query is allowed
|
||||
test_is_allowed() {
|
||||
local tool_name="$1"
|
||||
local sql="$2"
|
||||
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"${tool_name}","arguments":{"sql":"${sql}"}},"id":1}
|
||||
EOF
|
||||
)
|
||||
|
||||
local response
|
||||
response=$(mcp_request "query" "${payload}")
|
||||
log_verbose "Response: ${response}"
|
||||
|
||||
if echo "${response}" | grep -q '"isError":true'; then
|
||||
log_verbose "Query was blocked (expected to be allowed)"
|
||||
return 1
|
||||
else
|
||||
log_verbose "Query allowed as expected"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Get rule hit count
|
||||
get_rule_hits() {
|
||||
local rule_id="$1"
|
||||
exec_admin_silent "SELECT hits FROM stats_mcp_query_rules WHERE rule_id = ${rule_id};"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 6: Rule Evaluation - Block Action"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check ProxySQL admin connection
|
||||
if ! check_proxysql_admin; then
|
||||
log_error "Cannot connect to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to ProxySQL admin"
|
||||
|
||||
# Check MCP server connection
|
||||
if ! check_mcp_server; then
|
||||
log_error "MCP server not accessible at ${MCP_HOST}:${MCP_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "MCP server is accessible"
|
||||
|
||||
# Check MySQL backend connection
|
||||
if ! check_mysql_backend; then
|
||||
log_error "Cannot connect to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_error "Please set MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE environment variables"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Tables"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Rules"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# T6.1: Basic block rule with error_msg
|
||||
log_info "Creating rule 100: Basic DROP TABLE block"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (100, 1, 'DROP TABLE', 'DROP TABLE statements are not allowed', 1);" >/dev/null 2>&1
|
||||
|
||||
# T6.2: Case-sensitive match (default, no CASELESS modifier)
|
||||
log_info "Creating rule 101: Case-sensitive 'DROP TABLE' block (no CASELESS)"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (101, 1, 'DROP TABLE', 'Case-sensitive match failed', 1);" >/dev/null 2>&1
|
||||
|
||||
# T6.3: Block with negate_match_pattern=1 (block everything EXCEPT pattern)
|
||||
log_info "Creating rule 102: Negate pattern - block everything except specific query"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, negate_match_pattern, error_msg, apply) VALUES (102, 1, '^SELECT phase6_allowed_col FROM fake_table$', 1, 'Only specific query is allowed', 1);" >/dev/null 2>&1
|
||||
|
||||
# T6.4: Block specific username
|
||||
log_info "Creating rule 103: Block for specific user 'testuser'"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, username, match_pattern, error_msg, apply) VALUES (103, 1, 'testuser', 'DROP', 'User testuser cannot DROP', 1);" >/dev/null 2>&1
|
||||
|
||||
# T6.5: Block specific schema
|
||||
log_info "Creating rule 104: Block for specific schema 'testdb'"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, schemaname, match_pattern, error_msg, apply) VALUES (104, 1, 'testdb', 'DROP', 'DROP not allowed in testdb', 1);" >/dev/null 2>&1
|
||||
|
||||
# T6.6: Block specific tool_name
|
||||
log_info "Creating rule 105: Block for specific tool 'run_sql_readonly'"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, tool_name, match_pattern, error_msg, apply) VALUES (105, 1, 'run_sql_readonly', 'TRUNCATE', 'TRUNCATE not allowed in readonly mode', 1);" >/dev/null 2>&1
|
||||
|
||||
# Load to runtime
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
sleep 1
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Running Block Action Evaluation Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# T6.1: Block query with error_msg
|
||||
run_test "T6.1: Block DROP TABLE with error_msg" \
|
||||
test_is_blocked "run_sql_readonly" "DROP TABLE test_table;" "DROP TABLE statements are not allowed"
|
||||
|
||||
# T6.2: Block with case-sensitive match (lowercase should NOT match if no CASELESS)
|
||||
# Note: This test may vary based on regex implementation. Assuming default is case-sensitive.
|
||||
run_test "T6.2: Case-sensitive match - exact case matches" \
|
||||
test_is_blocked "run_sql_readonly" "DROP TABLE test2;" "DROP"
|
||||
|
||||
# T6.3: Block with negate_match_pattern=1
|
||||
# Rule 102: negate_match_pattern=1, pattern='^SELECT phase6_allowed_col FROM fake_table$', so blocks everything EXCEPT that specific query
|
||||
run_test "T6.3: Negate pattern - other query should be blocked" \
|
||||
test_is_blocked "run_sql_readonly" "SELECT phase6_blocked_col FROM fake_table;" "Only specific query is allowed"
|
||||
|
||||
run_test "T6.3: Negate pattern - exact pattern match should be allowed" \
|
||||
test_is_allowed "run_sql_readonly" "SELECT phase6_allowed_col FROM fake_table"
|
||||
|
||||
# T6.4: Block specific username
|
||||
# Note: This test depends on the user context. For now, we test that the rule exists.
|
||||
# Actual username filtering requires authentication context.
|
||||
log_info "T6.4: Username-based filtering (rule 103 created - requires auth context to fully test)"
|
||||
run_test "T6.4: Username rule exists in runtime" \
|
||||
bash -c "[ $(exec_admin_silent 'SELECT COUNT(*) FROM runtime_mcp_query_rules WHERE rule_id = 103 AND username = "testuser"') -eq 1 ]"
|
||||
|
||||
# T6.5: Block specific schema
|
||||
log_info "T6.5: Schema-based filtering (rule 104 created for 'testdb')"
|
||||
run_test "T6.5: Schema rule exists in runtime" \
|
||||
bash -c "[ $(exec_admin_silent 'SELECT COUNT(*) FROM runtime_mcp_query_rules WHERE rule_id = 104 AND schemaname = "testdb"') -eq 1 ]"
|
||||
|
||||
# T6.6: Block specific tool_name
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id=102;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
run_test "T6.6: Block TRUNCATE in run_sql_readonly tool" \
|
||||
test_is_blocked "run_sql_readonly" "TRUNCATE TABLE test_table;" "TRUNCATE not allowed"
|
||||
|
||||
# Display runtime rules
|
||||
echo ""
|
||||
echo "Runtime rules created:"
|
||||
exec_admin "SELECT rule_id, username, schemaname, tool_name, match_pattern, negate_match_pattern, error_msg FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Display stats
|
||||
echo ""
|
||||
echo "Rule hit statistics:"
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
log_info "Test rules cleaned up"
|
||||
|
||||
# Drop test tables
|
||||
echo ""
|
||||
drop_test_tables
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,333 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase7_eval_rewrite.sh - Test MCP Query Rules Rewrite Action Evaluation
|
||||
#
|
||||
# Phase 7: Test rule evaluation for Rewrite action with various patterns
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
MCP_PORT="${MCP_PORT:-6071}"
|
||||
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# MySQL backend configuration (the actual database where queries are executed)
|
||||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
|
||||
MYSQL_DATABASE="${MYSQL_DATABASE:-testdb}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
log_verbose() { echo -e "${YELLOW}[VERBOSE]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command via ProxySQL admin
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command via ProxySQL admin (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server
|
||||
exec_mysql() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server (silent)
|
||||
exec_mysql_silent() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -B -N -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get endpoint URL
|
||||
get_endpoint_url() {
|
||||
local endpoint="$1"
|
||||
echo "https://${MCP_HOST}:${MCP_PORT}/mcp/${endpoint}"
|
||||
}
|
||||
|
||||
# Execute MCP request via curl
|
||||
mcp_request() {
|
||||
local endpoint="$1"
|
||||
local payload="$2"
|
||||
|
||||
curl -k -s -X POST "$(get_endpoint_url "${endpoint}")" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if ProxySQL admin is accessible
|
||||
check_proxysql_admin() {
|
||||
if exec_admin_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MCP server is accessible
|
||||
check_mcp_server() {
|
||||
local response
|
||||
response=$(mcp_request "config" '{"jsonrpc":"2.0","method":"ping","id":1}')
|
||||
if echo "${response}" | grep -q "result"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MySQL backend is accessible
|
||||
check_mysql_backend() {
|
||||
if exec_mysql_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables() {
|
||||
log_info "Creating test tables in MySQL backend..."
|
||||
log_verbose "MySQL Host: ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_verbose "MySQL User: ${MYSQL_USER}"
|
||||
log_verbose "MySQL Database: ${MYSQL_DATABASE}"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
log_verbose "Creating database '${MYSQL_DATABASE}' if not exists..."
|
||||
exec_mysql "CREATE DATABASE IF NOT EXISTS ${MYSQL_DATABASE};" 2>/dev/null
|
||||
|
||||
# Create test tables with phase7 naming
|
||||
log_verbose "Creating table 'customers' for phase7 tests..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.customers_phase7 (id INT PRIMARY KEY, phase7_name VARCHAR(100), phase7_email VARCHAR(100));" 2>/dev/null
|
||||
|
||||
log_verbose "Creating table 'orders'..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.orders_phase7 (id INT PRIMARY KEY, customer_id INT, amount DECIMAL(10,2));" 2>/dev/null
|
||||
|
||||
log_verbose "Creating table 'products'..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.products_phase7 (id INT PRIMARY KEY, product_name VARCHAR(100), price DECIMAL(10,2));" 2>/dev/null
|
||||
|
||||
# Insert some test data
|
||||
log_verbose "Inserting test data into tables..."
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.customers_phase7 VALUES (1, 'Alice', 'alice@test.com'), (2, 'Bob', 'bob@test.com');" 2>/dev/null
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.orders_phase7 VALUES (1, 1, 100.00), (2, 2, 200.00);" 2>/dev/null
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.products_phase7 VALUES (1, 'Widget', 10.00), (2, 'Gadget', 20.00);" 2>/dev/null
|
||||
|
||||
log_info "Test tables created successfully"
|
||||
}
|
||||
|
||||
# Drop test tables from MySQL database
|
||||
drop_test_tables() {
|
||||
log_info "Dropping test tables from MySQL backend..."
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.customers_phase7;" 2>/dev/null
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.orders_phase7;" 2>/dev/null
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.products_phase7;" 2>/dev/null
|
||||
log_info "Test tables dropped"
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test that a query is rewritten and returns results
|
||||
test_is_rewritten() {
|
||||
local tool_name="$1"
|
||||
local original_sql="$2"
|
||||
local expected_result_substring="$3"
|
||||
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"${tool_name}","arguments":{"sql":"${original_sql}"}},"id":1}
|
||||
EOF
|
||||
)
|
||||
|
||||
local response
|
||||
response=$(mcp_request "query" "${payload}")
|
||||
log_verbose "Response: ${response}"
|
||||
|
||||
# Check for successful response with data
|
||||
if echo "${response}" | grep -q '"isError":true'; then
|
||||
log_verbose "Query returned error (unexpected)"
|
||||
return 1
|
||||
else
|
||||
# Check if expected substring is in response
|
||||
if echo "${response}" | grep -qi "${expected_result_substring}"; then
|
||||
log_verbose "Query executed and contains: ${expected_result_substring}"
|
||||
return 0
|
||||
else
|
||||
log_verbose "Query executed but doesn't contain expected result"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Get rule hit count
|
||||
get_rule_hits() {
|
||||
local rule_id="$1"
|
||||
exec_admin_silent "SELECT hits FROM stats_mcp_query_rules WHERE rule_id = ${rule_id};"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 7: Rule Evaluation - Rewrite Action"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check ProxySQL admin connection
|
||||
if ! check_proxysql_admin; then
|
||||
log_error "Cannot connect to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to ProxySQL admin"
|
||||
|
||||
# Check MCP server connection
|
||||
if ! check_mcp_server; then
|
||||
log_error "MCP server not accessible at ${MCP_HOST}:${MCP_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "MCP server is accessible"
|
||||
|
||||
# Check MySQL backend connection
|
||||
if ! check_mysql_backend; then
|
||||
log_error "Cannot connect to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_error "Please set MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE environment variables"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Tables"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Rules"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# T7.1: Rewrite SQL with replace_pattern - SELECT * to known string
|
||||
log_info "Creating rule 100: Rewrite SELECT * FROM customers to known string"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, replace_pattern, apply) VALUES (100, 1, 'SELECT\s+\\*\s+FROM\s+customers', 'SELECT \"PHASE7_REWRITTEN\" AS result FROM (SELECT 0) t1', 1);" >/dev/null 2>&1
|
||||
|
||||
# T7.2: Rewrite with capture groups - Rewrite to known string with original table captured
|
||||
log_info "Creating rule 101: Rewrite with capture groups - capture table name"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, replace_pattern, re_modifiers, apply) VALUES (101, 1, 'SELECT phase7_name FROM (\\w+)', 'SELECT \"PHASE7_CAPTURED\" AS result FROM (SELECT 0) t1', 'EXTENDED', 1);" >/dev/null 2>&1
|
||||
|
||||
# T7.3: Rewrite with CASELESS modifier
|
||||
log_info "Creating rule 102: Rewrite with CASELESS - select * from products (any case)"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, replace_pattern, re_modifiers, apply) VALUES (102, 1, 'select \\* from products', 'SELECT \"PHASE7_CASELESS\" AS result FROM (SELECT 0) t1', 'CASELESS', 1);" >/dev/null 2>&1
|
||||
|
||||
# Load to runtime
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
sleep 1
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Running Rewrite Action Evaluation Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# T7.1: Rewrite SQL with replace_pattern
|
||||
run_test "T7.1: Rewrite SELECT * FROM customers to known string" \
|
||||
test_is_rewritten "run_sql_readonly" "SELECT * FROM customers" "PHASE7_REWRITTEN"
|
||||
|
||||
# T7.2: Rewrite with capture groups
|
||||
run_test "T7.2: Rewrite with capture groups - captured table name" \
|
||||
test_is_rewritten "run_sql_readonly" "SELECT phase7_name FROM customers_phase7;" "PHASE7_CAPTURED"
|
||||
|
||||
# T7.3: Rewrite with CASELESS modifier
|
||||
run_test "T7.3: Rewrite with CASELESS - lowercase 'select * from products'" \
|
||||
test_is_rewritten "run_sql_readonly" "select * from products;" "PHASE7_CASELESS"
|
||||
|
||||
# Display runtime rules
|
||||
echo ""
|
||||
echo "Runtime rules created:"
|
||||
exec_admin "SELECT rule_id, match_pattern, replace_pattern, re_modifiers FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Display stats
|
||||
echo ""
|
||||
echo "Rule hit statistics:"
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
log_info "Test rules cleaned up"
|
||||
|
||||
# Drop test tables
|
||||
echo ""
|
||||
drop_test_tables
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,334 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# test_phase8_eval_timeout.sh - Test MCP Query Rules Timeout Action Evaluation
|
||||
#
|
||||
# Phase 8: Test rule evaluation for Timeout action
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Default configuration
|
||||
MCP_HOST="${MCP_HOST:-127.0.0.1}"
|
||||
MCP_PORT="${MCP_PORT:-6071}"
|
||||
|
||||
PROXYSQL_ADMIN_HOST="${PROXYSQL_ADMIN_HOST:-127.0.0.1}"
|
||||
PROXYSQL_ADMIN_PORT="${PROXYSQL_ADMIN_PORT:-6032}"
|
||||
PROXYSQL_ADMIN_USER="${PROXYSQL_ADMIN_USER:-radmin}"
|
||||
PROXYSQL_ADMIN_PASSWORD="${PROXYSQL_ADMIN_PASSWORD:-radmin}"
|
||||
|
||||
# MySQL backend configuration (the actual database where queries are executed)
|
||||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
||||
MYSQL_USER="${MYSQL_USER:-root}"
|
||||
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
|
||||
MYSQL_DATABASE="${MYSQL_DATABASE:-testdb}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Statistics
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${GREEN}[TEST]${NC} $1"; }
|
||||
log_verbose() { echo -e "${YELLOW}[VERBOSE]${NC} $1"; }
|
||||
|
||||
# Execute MySQL command via ProxySQL admin
|
||||
exec_admin() {
|
||||
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command via ProxySQL admin (silent)
|
||||
exec_admin_silent() {
|
||||
mysql -B -N -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" \
|
||||
-u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" \
|
||||
-e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server
|
||||
exec_mysql() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>&1
|
||||
}
|
||||
|
||||
# Execute MySQL command directly on backend MySQL server (silent)
|
||||
exec_mysql_silent() {
|
||||
local db_param=""
|
||||
if [ -n "${MYSQL_DATABASE}" ]; then
|
||||
db_param="-D ${MYSQL_DATABASE}"
|
||||
fi
|
||||
mysql -B -N -h "${MYSQL_HOST}" -P "${MYSQL_PORT}" \
|
||||
-u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" \
|
||||
${db_param} -e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get endpoint URL
|
||||
get_endpoint_url() {
|
||||
local endpoint="$1"
|
||||
echo "https://${MCP_HOST}:${MCP_PORT}/mcp/${endpoint}"
|
||||
}
|
||||
|
||||
# Execute MCP request via curl
|
||||
mcp_request() {
|
||||
local endpoint="$1"
|
||||
local payload="$2"
|
||||
|
||||
curl -k -s -X POST "$(get_endpoint_url "${endpoint}")" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${payload}" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if ProxySQL admin is accessible
|
||||
check_proxysql_admin() {
|
||||
if exec_admin_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MCP server is accessible
|
||||
check_mcp_server() {
|
||||
local response
|
||||
response=$(mcp_request "config" '{"jsonrpc":"2.0","method":"ping","id":1}')
|
||||
if echo "${response}" | grep -q "result"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if MySQL backend is accessible
|
||||
check_mysql_backend() {
|
||||
if exec_mysql_silent "SELECT 1" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables() {
|
||||
log_info "Creating test tables in MySQL backend..."
|
||||
log_verbose "MySQL Host: ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_verbose "MySQL User: ${MYSQL_USER}"
|
||||
log_verbose "MySQL Database: ${MYSQL_DATABASE}"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
log_verbose "Creating database '${MYSQL_DATABASE}' if not exists..."
|
||||
exec_mysql "CREATE DATABASE IF NOT EXISTS ${MYSQL_DATABASE};" 2>/dev/null
|
||||
|
||||
# Create test tables with phase8 naming
|
||||
log_verbose "Creating table 'slow_table' for phase8 timeout tests..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.slow_table (id INT PRIMARY KEY, phase8_data VARCHAR(100));" 2>/dev/null
|
||||
|
||||
log_verbose "Creating table 'quick_table'..."
|
||||
exec_mysql "CREATE TABLE IF NOT EXISTS ${MYSQL_DATABASE}.quick_table (id INT PRIMARY KEY, phase8_data VARCHAR(100));" 2>/dev/null
|
||||
|
||||
# Insert some test data
|
||||
log_verbose "Inserting test data into tables..."
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.slow_table VALUES (1, 'slow1'), (2, 'slow2');" 2>/dev/null
|
||||
exec_mysql "INSERT IGNORE INTO ${MYSQL_DATABASE}.quick_table VALUES (1, 'quick1'), (2, 'quick2');" 2>/dev/null
|
||||
|
||||
log_info "Test tables created successfully"
|
||||
}
|
||||
|
||||
# Drop test tables from MySQL database
|
||||
drop_test_tables() {
|
||||
log_info "Dropping test tables from MySQL backend..."
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.slow_table;" 2>/dev/null
|
||||
exec_mysql "DROP TABLE IF EXISTS ${MYSQL_DATABASE}.quick_table;" 2>/dev/null
|
||||
log_info "Test tables dropped"
|
||||
}
|
||||
|
||||
# Run test function
|
||||
run_test() {
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||
log_test "$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_info "✓ Test $TOTAL_TESTS passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||
return 0
|
||||
else
|
||||
log_error "✗ Test $TOTAL_TESTS failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test that a query times out
|
||||
test_is_timed_out() {
|
||||
local tool_name="$1"
|
||||
local sql="$2"
|
||||
local expected_error_substring="$3"
|
||||
local timeout_sec="$4"
|
||||
|
||||
local payload
|
||||
payload=$(cat <<EOF
|
||||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"${tool_name}","arguments":{"sql":"${sql}","timeout":${timeout_sec}}},"id":1}
|
||||
EOF
|
||||
)
|
||||
|
||||
local response
|
||||
response=$(mcp_request "query" "${payload}")
|
||||
log_verbose "Response: ${response}"
|
||||
|
||||
# Check for error response with timeout message
|
||||
if echo "${response}" | grep -q '"isError":true'; then
|
||||
if echo "${response}" | grep -qi "${expected_error_substring}"; then
|
||||
log_verbose "Query timed out as expected"
|
||||
return 0
|
||||
else
|
||||
log_verbose "Query errored but not due to timeout"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_verbose "Query did NOT time out (may have completed before timeout)"
|
||||
# This is not necessarily a failure - query may have been fast enough
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Get rule hit count
|
||||
get_rule_hits() {
|
||||
local rule_id="$1"
|
||||
exec_admin_silent "SELECT hits FROM stats_mcp_query_rules WHERE rule_id = ${rule_id};"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "======================================"
|
||||
echo "Phase 8: Rule Evaluation - Timeout Action"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
sql="SELECT * FROM (SELECT 0 AS ID) t1"
|
||||
payload=$(cat <<EOF
|
||||
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"run_sql_readonly","arguments":{"sql":"${sql}"}},"id":1}
|
||||
EOF
|
||||
)
|
||||
response=$(mcp_request "query" "${payload}")
|
||||
log_verbose "Response: ${response}"
|
||||
exit 1
|
||||
|
||||
# Check ProxySQL admin connection
|
||||
if ! check_proxysql_admin; then
|
||||
log_error "Cannot connect to ProxySQL admin at ${PROXYSQL_ADMIN_HOST}:${PROXYSQL_ADMIN_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to ProxySQL admin"
|
||||
|
||||
# Check MCP server connection
|
||||
if ! check_mcp_server; then
|
||||
log_error "MCP server not accessible at ${MCP_HOST}:${MCP_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "MCP server is accessible"
|
||||
|
||||
# Check MySQL backend connection
|
||||
if ! check_mysql_backend; then
|
||||
log_error "Cannot connect to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
log_error "Please set MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE environment variables"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Connected to MySQL backend at ${MYSQL_HOST}:${MYSQL_PORT}"
|
||||
|
||||
# Cleanup any existing test rules
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Tables"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Create test tables in MySQL database
|
||||
create_test_tables
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setting Up Test Rules"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# T8.1: Query with timeout_ms - Set a very short timeout for testing
|
||||
log_info "Creating rule 100: Timeout queries matching pattern after 100ms"
|
||||
exec_admin_silent "INSERT INTO mcp_query_rules (rule_id, active, match_pattern, timeout_ms, apply) VALUES (100, 1, 'SELECT SLEEP\\(', 100, 1);" >/dev/null 2>&1
|
||||
|
||||
# Load to runtime
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
sleep 1
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Running Timeout Action Evaluation Tests"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# T8.1: Query with timeout_ms
|
||||
# Use SLEEP() to simulate a long-running query that should timeout
|
||||
log_info "T8.1: Testing timeout with SLEEP() query..."
|
||||
run_test "T8.1: Query with timeout_ms - SLEEP() should timeout" \
|
||||
test_is_timed_out "run_sql_readonly" "SELECT SLEEP(5) FROM slow_table;" "Lost connection to server" "10"
|
||||
|
||||
# T8.2: Verify timeout error message
|
||||
# Check that the timeout rule exists and is configured correctly
|
||||
log_info "T8.2: Verifying timeout rule configuration"
|
||||
run_test "T8.2: Timeout rule exists with timeout_ms set" \
|
||||
bash -c "[ $(exec_admin_silent 'SELECT timeout_ms FROM runtime_mcp_query_rules WHERE rule_id = 100') -gt 0 ]"
|
||||
|
||||
# Test that a quick query without timeout rule executes successfully
|
||||
run_test "T8.3: Quick query without SLEEP executes successfully" \
|
||||
bash -c "timeout 5 curl -k -s -X POST 'https://${MCP_HOST}:${MCP_PORT}/mcp/query' -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"run_sql_readonly\",\"arguments\":{\"sql\":\"SELECT phase8_data FROM quick_table\"}},\"id\":1}' | grep -q 'phase8_data'"
|
||||
|
||||
# Display runtime rules
|
||||
echo ""
|
||||
echo "Runtime rules created:"
|
||||
exec_admin "SELECT rule_id, match_pattern, timeout_ms FROM runtime_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Display stats
|
||||
echo ""
|
||||
echo "Rule hit statistics:"
|
||||
exec_admin "SELECT rule_id, hits FROM stats_mcp_query_rules WHERE rule_id BETWEEN 100 AND 199 ORDER BY rule_id;"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Test Summary"
|
||||
echo "======================================"
|
||||
echo "Total tests: ${TOTAL_TESTS}"
|
||||
echo -e "Passed: ${GREEN}${PASSED_TESTS}${NC}"
|
||||
echo -e "Failed: ${RED}${FAILED_TESTS}${NC}"
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
exec_admin_silent "DELETE FROM mcp_query_rules WHERE rule_id BETWEEN 100 AND 199;" >/dev/null 2>&1
|
||||
exec_admin_silent "LOAD MCP QUERY RULES TO RUNTIME;" >/dev/null 2>&1
|
||||
log_info "Test rules cleaned up"
|
||||
|
||||
# Drop test tables
|
||||
echo ""
|
||||
drop_test_tables
|
||||
|
||||
if [ ${FAILED_TESTS} -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Reference in new issue