Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
New test files: - auth_security_test.rs (12): account lockout DB state, lockout reset, password version invalidation, disabled account, refresh token revocation, boundary validation (username/password), role enforcement, TOTP 2FA flow - account_security_test.rs (9): role management, privilege escalation prevention, account disable/enable, cross-account access control, operation logs - relay_validation_test.rs (8): input validation (missing fields, empty messages, invalid roles), disabled provider, model listing, task isolation - permission_matrix_test.rs (7): super_admin full access, user allowed/ forbidden endpoints, public endpoints, unauthenticated rejection, API token lifecycle Discovered: account lockout runtime check broken — handlers.rs:213 parse_from_rfc3339 fails on PostgreSQL TIMESTAMPTZ::TEXT format, silently skipping lockout. DB state is correct but login not rejected.
221 lines
8.5 KiB
Rust
221 lines
8.5 KiB
Rust
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
use common::*;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// Relay chat input validation
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
#[tokio::test]
|
|
async fn relay_chat_missing_model() {
|
|
let (app, _pool) = build_test_app().await;
|
|
let token = register_token(&app, "nomodel").await;
|
|
let (status, _) = send(
|
|
&app,
|
|
post(
|
|
"/api/v1/relay/chat/completions",
|
|
&token,
|
|
serde_json::json!({
|
|
"messages": [{ "role": "user", "content": "hello" }]
|
|
}),
|
|
),
|
|
).await;
|
|
assert_eq!(status, StatusCode::BAD_REQUEST, "missing model should be rejected");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn relay_chat_empty_messages() {
|
|
let (app, _pool) = build_test_app().await;
|
|
let token = register_token(&app, "emptymsg").await;
|
|
let (status, _) = send(
|
|
&app,
|
|
post(
|
|
"/api/v1/relay/chat/completions",
|
|
&token,
|
|
serde_json::json!({
|
|
"model": "test-model",
|
|
"messages": []
|
|
}),
|
|
),
|
|
).await;
|
|
assert_eq!(status, StatusCode::BAD_REQUEST, "empty messages should be rejected");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn relay_chat_invalid_role() {
|
|
let (app, _pool) = build_test_app().await;
|
|
let token = register_token(&app, "badrole").await;
|
|
let (status, _) = send(
|
|
&app,
|
|
post(
|
|
"/api/v1/relay/chat/completions",
|
|
&token,
|
|
serde_json::json!({
|
|
"model": "test-model",
|
|
"messages": [{ "role": "invalid_role", "content": "hello" }]
|
|
}),
|
|
),
|
|
).await;
|
|
assert_ne!(status, StatusCode::OK, "invalid role should be rejected");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn relay_chat_unauthenticated() {
|
|
let (app, _pool) = build_test_app().await;
|
|
let (status, _) = send(
|
|
&app,
|
|
post_public(
|
|
"/api/v1/relay/chat/completions",
|
|
serde_json::json!({
|
|
"model": "test-model",
|
|
"messages": [{ "role": "user", "content": "hello" }]
|
|
}),
|
|
),
|
|
).await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED, "unauthenticated relay request should be rejected");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// Relay model with real provider+model setup
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
#[tokio::test]
|
|
async fn relay_chat_with_disabled_provider() {
|
|
let (app, pool) = build_test_app().await;
|
|
let admin = admin_token(&app, &pool, "disabledprov").await;
|
|
let user_token = register_token(&app, "disabledprovuser").await;
|
|
|
|
// Create provider
|
|
let (_, prov_body) = send(
|
|
&app,
|
|
post("/api/v1/providers", &admin,
|
|
serde_json::json!({ "name": "disabled-prov", "display_name": "Disabled Prov", "base_url": "https://disabled.test/v1" }),
|
|
),
|
|
).await;
|
|
let provider_id = prov_body["id"].as_str().unwrap();
|
|
|
|
// Create model
|
|
send(
|
|
&app,
|
|
post("/api/v1/models", &admin,
|
|
serde_json::json!({
|
|
"provider_id": provider_id,
|
|
"model_id": "disabled-model",
|
|
"alias": "Disabled Model",
|
|
"context_window": 4096
|
|
}),
|
|
),
|
|
).await;
|
|
|
|
// Add a key (so relay can try to use it)
|
|
send(
|
|
&app,
|
|
post(&format!("/api/v1/providers/{provider_id}/keys"), &admin,
|
|
serde_json::json!({ "key_label": "Test Key", "key_value": "sk-disabled-prov-key-1234567890" }),
|
|
),
|
|
).await;
|
|
|
|
// Disable the provider
|
|
send(
|
|
&app,
|
|
patch(&format!("/api/v1/providers/{provider_id}"), &admin,
|
|
serde_json::json!({ "enabled": false }),
|
|
),
|
|
).await;
|
|
|
|
// Chat request to disabled provider's model should fail
|
|
let (status, _) = send(
|
|
&app,
|
|
post(
|
|
"/api/v1/relay/chat/completions",
|
|
&user_token,
|
|
serde_json::json!({
|
|
"model": "disabled-model",
|
|
"messages": [{ "role": "user", "content": "hello" }]
|
|
}),
|
|
),
|
|
).await;
|
|
// Should be NOT_FOUND (model not found) or FORBIDDEN (provider disabled)
|
|
assert_ne!(status, StatusCode::OK, "disabled provider's model should not be usable");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// Relay model list includes configured models
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
#[tokio::test]
|
|
async fn relay_models_list_includes_active_models() {
|
|
let (app, pool) = build_test_app().await;
|
|
let admin = admin_token(&app, &pool, "listmodeladmin").await;
|
|
let user_token = register_token(&app, "listmodeluser").await;
|
|
|
|
// Create provider + model
|
|
let (_, prov) = send(
|
|
&app,
|
|
post("/api/v1/providers", &admin,
|
|
serde_json::json!({ "name": "list-prov", "display_name": "List Prov", "base_url": "https://list.test/v1" }),
|
|
),
|
|
).await;
|
|
let pid = prov["id"].as_str().unwrap();
|
|
|
|
let keys_url = format!("/api/v1/providers/{pid}/keys");
|
|
send(
|
|
&app,
|
|
post(&keys_url, &admin,
|
|
serde_json::json!({ "key_label": "K", "key_value": "sk-list-prov-key-abcdefghijklmnop" }),
|
|
),
|
|
).await;
|
|
|
|
send(
|
|
&app,
|
|
post("/api/v1/models", &admin,
|
|
serde_json::json!({
|
|
"provider_id": pid,
|
|
"model_id": "listable-model",
|
|
"alias": "Listable Model",
|
|
"context_window": 8192
|
|
}),
|
|
),
|
|
).await;
|
|
|
|
// List relay models as user
|
|
let (status, body) = send(&app, get("/api/v1/relay/models", &user_token)).await;
|
|
assert_eq!(status, StatusCode::OK, "relay models list should work: {body}");
|
|
let models = body.as_array().expect("should be array");
|
|
let found = models.iter().any(|m| m["model_id"] == "listable-model" || m["id"] == "listable-model");
|
|
assert!(found, "relay models should include the newly created model: {:?}", models);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// Task access control: user sees only own tasks
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
#[tokio::test]
|
|
async fn relay_task_access_own_only() {
|
|
let (app, _pool) = build_test_app().await;
|
|
let token_a = register_token(&app, "taskuserA").await;
|
|
let token_b = register_token(&app, "taskuserB").await;
|
|
|
|
// Both list tasks — should not see each other's
|
|
let (_, tasks_a) = send(&app, get("/api/v1/relay/tasks", &token_a)).await;
|
|
let (_, tasks_b) = send(&app, get("/api/v1/relay/tasks", &token_b)).await;
|
|
|
|
// Both should have empty task lists (no relay requests made)
|
|
let a_items = if tasks_a.is_array() { tasks_a.as_array().unwrap().len() } else { tasks_a["items"].as_array().map(|v| v.len()).unwrap_or(0) };
|
|
let b_items = if tasks_b.is_array() { tasks_b.as_array().unwrap().len() } else { tasks_b["items"].as_array().map(|v| v.len()).unwrap_or(0) };
|
|
assert_eq!(a_items, 0, "user A should have 0 tasks");
|
|
assert_eq!(b_items, 0, "user B should have 0 tasks");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn relay_models_require_auth() {
|
|
let (app, _pool) = build_test_app().await;
|
|
let req = axum::http::Request::builder()
|
|
.uri("/api/v1/relay/models")
|
|
.body(axum::body::Body::empty())
|
|
.unwrap();
|
|
let (status, _) = send(&app, req).await;
|
|
assert_eq!(status, StatusCode::UNAUTHORIZED, "relay models should require authentication");
|
|
}
|