Compare commits

..
Author SHA1 Message Date
[email protected]andClaude Opus 4.6 1d4f6f0fdc fix: address PR review comments on CONTRIBUTING.md
Add missing security-critical directories (src/sandbox/, src/orchestrator/)
to Track C's list, and clarify that cargo deny check requires deny.toml.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-10 11:15:53 -07:00
[email protected]andClaude Opus 4.6 8c581e6240 fix: expand CONTRIBUTING.md with setup, workflow, and guidelines
Add getting started, development workflow, code style summary,
database change guidance, and dependency management sections.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-09 23:11:43 -07:00
[email protected]andClaude Opus 4.6 b11b0331b4 feat: add PR template with risk assessment and review tracks
Add a pull request template that includes summary, change type,
validation checklist, security/database impact sections, blast radius,
and rollback plan. Update CONTRIBUTING.md with review track definitions
(A/B/C) based on change risk level.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-09 23:03:27 -07:00
27 changed files with 302 additions and 316 deletions
+50
View File
@@ -0,0 +1,50 @@
## Summary
<!-- 2-5 bullet points: what changed and why -->
-
## Change Type
<!-- Check one -->
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor
- [ ] Documentation
- [ ] CI/Infrastructure
- [ ] Security
- [ ] Dependencies
## Linked Issue
<!-- Closes #N, or "None" -->
## Validation
<!-- How did you verify this works? -->
- [ ] `cargo fmt`
- [ ] `cargo clippy --all --benches --tests --examples --all-features`
- [ ] Relevant tests pass: <!-- list specific tests -->
- [ ] Manual testing: <!-- describe what you tested -->
## Security Impact
<!-- Does this change affect: permissions, network calls, secrets, file access, tool execution, sandbox policy? If yes, describe. If no, write "None". -->
## Database Impact
<!-- Does this add/modify migrations, change schema, or affect both PostgreSQL and libSQL? If yes, describe. If no, write "None". -->
## Blast Radius
<!-- What subsystems does this touch? What could break? -->
## Rollback Plan
<!-- How to revert if this causes problems? For Track C changes, this is mandatory. -->
---
**Review track**: <!-- A (docs/tests/chore) | B (feature/refactor) | C (security/runtime/DB/CI) -->
+49
View File
@@ -1,5 +1,34 @@
# Contributing # Contributing
## Getting Started
```bash
git clone https://github.com/nearai/ironclaw.git
cd ironclaw
./scripts/dev-setup.sh
```
This installs the Rust toolchain, WASM targets, git hooks, and runs initial checks.
## Development Workflow
```bash
cargo fmt # format
cargo clippy --all --benches --tests --examples --all-features # lint (zero warnings)
cargo test # unit tests
cargo test --features integration # + PostgreSQL tests
```
## Code Style
- Zero clippy warnings policy
- No `.unwrap()` or `.expect()` in production code (tests are fine)
- Use `thiserror` for error types, map errors with context
- Prefer `crate::` for cross-module imports
- Comments for non-obvious logic only
See `CLAUDE.md` for full style guidelines.
## Feature Parity Requirement ## Feature Parity Requirement
When your change affects a tracked capability, update `FEATURE_PARITY.md` in the same branch. When your change affects a tracked capability, update `FEATURE_PARITY.md` in the same branch.
@@ -9,3 +38,23 @@ When your change affects a tracked capability, update `FEATURE_PARITY.md` in the
1. Review the relevant parity rows in `FEATURE_PARITY.md`. 1. Review the relevant parity rows in `FEATURE_PARITY.md`.
2. Update status/notes if behavior changed. 2. Update status/notes if behavior changed.
3. Include the `FEATURE_PARITY.md` diff in your commit when applicable. 3. Include the `FEATURE_PARITY.md` diff in your commit when applicable.
## Review Tracks
All PRs follow a risk-based review process:
| Track | Scope | Requirements |
|-------|-------|-------------|
| **A** | Docs, tests, chore, dependency bumps | 1 approval + CI green |
| **B** | Features, refactors, new tools/channels | 1 approval + CI green + test evidence |
| **C** | Security (`src/safety/`, `src/secrets/`, `src/sandbox/`, `src/orchestrator/`), runtime (`src/agent/`, `src/worker/`), database schema, CI workflows | 2 approvals + rollback plan documented |
Select the appropriate track in the PR template based on what your changes touch.
## Database Changes
IronClaw uses dual-backend persistence (PostgreSQL + libSQL). All new persistence features must support both backends. See `src/db/CLAUDE.md`.
## Adding Dependencies
Run `cargo deny check` before adding new dependencies to verify license compatibility and check for known advisories (requires `deny.toml`; see the `cargo-deny` CI job).
+2 -3
View File
@@ -347,7 +347,6 @@ pub trait Channel: Send + Sync {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::TEST_REDACT_SECRET_123;
/// Stub tool that marks `"value"` as sensitive. /// Stub tool that marks `"value"` as sensitive.
struct SecretTool; struct SecretTool;
@@ -377,7 +376,7 @@ mod tests {
#[test] #[test]
fn tool_completed_redacts_sensitive_params_on_failure() { fn tool_completed_redacts_sensitive_params_on_failure() {
let params = serde_json::json!({"name": "api_key", "value": TEST_REDACT_SECRET_123}); let params = serde_json::json!({"name": "api_key", "value": "sk-secret-123"});
let err: Result<String, crate::error::Error> = let err: Result<String, crate::error::Error> =
Err(crate::error::ToolError::ExecutionFailed { Err(crate::error::ToolError::ExecutionFailed {
name: "secret_save".into(), name: "secret_save".into(),
@@ -412,7 +411,7 @@ mod tests {
param_str param_str
); );
assert!( assert!(
!param_str.contains(TEST_REDACT_SECRET_123), !param_str.contains("sk-secret-123"),
"raw secret should not appear: {}", "raw secret should not appear: {}",
param_str param_str
); );
+5 -8
View File
@@ -3059,7 +3059,6 @@ mod tests {
}; };
use crate::channels::wasm::wrapper::{HttpResponse, WasmChannel}; use crate::channels::wasm::wrapper::{HttpResponse, WasmChannel};
use crate::pairing::PairingStore; use crate::pairing::PairingStore;
use crate::testing::credentials::TEST_TELEGRAM_BOT_TOKEN;
use crate::tools::wasm::ResourceLimits; use crate::tools::wasm::ResourceLimits;
fn create_test_channel() -> WasmChannel { fn create_test_channel() -> WasmChannel {
@@ -4010,7 +4009,7 @@ mod tests {
let mut creds = std::collections::HashMap::new(); let mut creds = std::collections::HashMap::new();
creds.insert( creds.insert(
"TELEGRAM_BOT_TOKEN".to_string(), "TELEGRAM_BOT_TOKEN".to_string(),
TEST_TELEGRAM_BOT_TOKEN.to_string(), "8218490433:AAEZeUxwqZ5OO3mOCXv7fKvpdhDgsmBBNis".to_string(),
); );
creds.insert("OTHER_SECRET".to_string(), "s3cret".to_string()); creds.insert("OTHER_SECRET".to_string(), "s3cret".to_string());
@@ -4023,15 +4022,13 @@ mod tests {
Arc::new(PairingStore::new()), Arc::new(PairingStore::new()),
); );
let error = format!( let error = "HTTP request failed: error sending request for url \
"HTTP request failed: error sending request for url \ (https://api.telegram.org/bot8218490433:AAEZeUxwqZ5OO3mOCXv7fKvpdhDgsmBBNis/getUpdates)";
(https://api.telegram.org/bot{TEST_TELEGRAM_BOT_TOKEN}/getUpdates)"
);
let redacted = store.redact_credentials(&error); let redacted = store.redact_credentials(error);
assert!( assert!(
!redacted.contains(TEST_TELEGRAM_BOT_TOKEN), !redacted.contains("8218490433:AAEZeUxwqZ5OO3mOCXv7fKvpdhDgsmBBNis"),
"credential value should be redacted" "credential value should be redacted"
); );
assert!( assert!(
+26 -27
View File
@@ -83,15 +83,14 @@ pub async fn auth_middleware(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::{TEST_AUTH_SECRET_TOKEN, TEST_BEARER_TOKEN};
#[test] #[test]
fn test_auth_state_clone() { fn test_auth_state_clone() {
let state = AuthState { let state = AuthState {
token: TEST_BEARER_TOKEN.to_string(), token: "test-token".to_string(),
}; };
let cloned = state.clone(); let cloned = state.clone();
assert_eq!(cloned.token, TEST_BEARER_TOKEN); assert_eq!(cloned.token, "test-token");
} }
use axum::Router; use axum::Router;
@@ -121,10 +120,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_valid_bearer_token_passes() { async fn test_valid_bearer_token_passes() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.header("Authorization", format!("Bearer {TEST_AUTH_SECRET_TOKEN}")) .header("Authorization", "Bearer secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -133,7 +132,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_invalid_bearer_token_rejected() { async fn test_invalid_bearer_token_rejected() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.header("Authorization", "Bearer wrong-token") .header("Authorization", "Bearer wrong-token")
@@ -145,9 +144,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_query_token_allowed_for_chat_events() { async fn test_query_token_allowed_for_chat_events() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri(format!("/api/chat/events?token={TEST_AUTH_SECRET_TOKEN}")) .uri("/api/chat/events?token=secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -156,9 +155,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_query_token_allowed_for_logs_events() { async fn test_query_token_allowed_for_logs_events() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri(format!("/api/logs/events?token={TEST_AUTH_SECRET_TOKEN}")) .uri("/api/logs/events?token=secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -167,9 +166,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_query_token_allowed_for_ws_upgrade() { async fn test_query_token_allowed_for_ws_upgrade() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri(format!("/api/chat/ws?token={TEST_AUTH_SECRET_TOKEN}")) .uri("/api/chat/ws?token=secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -203,9 +202,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_query_token_rejected_for_non_sse_get() { async fn test_query_token_rejected_for_non_sse_get() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri(format!("/api/chat/history?token={TEST_AUTH_SECRET_TOKEN}")) .uri("/api/chat/history?token=secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -214,10 +213,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_query_token_rejected_for_post() { async fn test_query_token_rejected_for_post() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.method(Method::POST) .method(Method::POST)
.uri(format!("/api/chat/send?token={TEST_AUTH_SECRET_TOKEN}")) .uri("/api/chat/send?token=secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -226,7 +225,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_query_token_invalid_rejected() { async fn test_query_token_invalid_rejected() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events?token=wrong-token") .uri("/api/chat/events?token=wrong-token")
.body(Body::empty()) .body(Body::empty())
@@ -237,7 +236,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_no_auth_at_all_rejected() { async fn test_no_auth_at_all_rejected() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.body(Body::empty()) .body(Body::empty())
@@ -248,11 +247,11 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_bearer_header_works_for_post() { async fn test_bearer_header_works_for_post() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.method(Method::POST) .method(Method::POST)
.uri("/api/chat/send") .uri("/api/chat/send")
.header("Authorization", format!("Bearer {TEST_AUTH_SECRET_TOKEN}")) .header("Authorization", "Bearer secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -261,10 +260,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_bearer_prefix_case_insensitive() { async fn test_bearer_prefix_case_insensitive() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.header("Authorization", format!("bearer {TEST_AUTH_SECRET_TOKEN}")) .header("Authorization", "bearer secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -273,10 +272,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_bearer_prefix_mixed_case() { async fn test_bearer_prefix_mixed_case() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.header("Authorization", format!("BEARER {TEST_AUTH_SECRET_TOKEN}")) .header("Authorization", "BEARER secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
@@ -285,7 +284,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_empty_bearer_token_rejected() { async fn test_empty_bearer_token_rejected() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.header("Authorization", "Bearer ") .header("Authorization", "Bearer ")
@@ -297,10 +296,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_token_with_whitespace_rejected() { async fn test_token_with_whitespace_rejected() {
let app = test_app(TEST_AUTH_SECRET_TOKEN); let app = test_app("secret-token");
let req = Request::builder() let req = Request::builder()
.uri("/api/chat/events") .uri("/api/chat/events")
.header("Authorization", format!("Bearer {TEST_AUTH_SECRET_TOKEN}")) .header("Authorization", "Bearer secret-token")
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
let resp = app.oneshot(req).await.unwrap(); let resp = app.oneshot(req).await.unwrap();
+3 -4
View File
@@ -2427,7 +2427,6 @@ struct GatewayStatusResponse {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::TEST_GATEWAY_CRYPTO_KEY;
#[test] #[test]
fn test_build_turns_from_db_messages_complete() { fn test_build_turns_from_db_messages_complete() {
@@ -2601,7 +2600,7 @@ mod tests {
// Build an ExtensionManager so the handler can look up flows // Build an ExtensionManager so the handler can look up flows
let secrets = Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new( let secrets = Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new(
crate::secrets::SecretsCrypto::new(secrecy::SecretString::from( crate::secrets::SecretsCrypto::new(secrecy::SecretString::from(
TEST_GATEWAY_CRYPTO_KEY.to_string(), "test-key-at-least-32-chars-long!!".to_string(),
)) ))
.expect("crypto"), .expect("crypto"),
))); )));
@@ -2651,7 +2650,7 @@ mod tests {
let secrets: Arc<dyn crate::secrets::SecretsStore + Send + Sync> = let secrets: Arc<dyn crate::secrets::SecretsStore + Send + Sync> =
Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new( Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new(
crate::secrets::SecretsCrypto::new(secrecy::SecretString::from( crate::secrets::SecretsCrypto::new(secrecy::SecretString::from(
TEST_GATEWAY_CRYPTO_KEY.to_string(), "test-key-at-least-32-chars-long!!".to_string(),
)) ))
.expect("crypto"), .expect("crypto"),
))); )));
@@ -2757,7 +2756,7 @@ mod tests {
let secrets: Arc<dyn crate::secrets::SecretsStore + Send + Sync> = let secrets: Arc<dyn crate::secrets::SecretsStore + Send + Sync> =
Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new( Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new(
crate::secrets::SecretsCrypto::new(secrecy::SecretString::from( crate::secrets::SecretsCrypto::new(secrecy::SecretString::from(
TEST_GATEWAY_CRYPTO_KEY.to_string(), "test-key-at-least-32-chars-long!!".to_string(),
)) ))
.expect("crypto"), .expect("crypto"),
))); )));
+1 -2
View File
@@ -154,7 +154,6 @@ mod tests {
use super::*; use super::*;
use crate::config::helpers::ENV_MUTEX; use crate::config::helpers::ENV_MUTEX;
use crate::settings::{EmbeddingsSettings, Settings}; use crate::settings::{EmbeddingsSettings, Settings};
use crate::testing::credentials::*;
/// Clear all embedding-related env vars. /// Clear all embedding-related env vars.
fn clear_embedding_env() { fn clear_embedding_env() {
@@ -174,7 +173,7 @@ mod tests {
clear_embedding_env(); clear_embedding_env();
// SAFETY: Under ENV_MUTEX, no concurrent env access. // SAFETY: Under ENV_MUTEX, no concurrent env access.
unsafe { unsafe {
std::env::set_var("OPENAI_API_KEY", TEST_OPENAI_API_KEY_ISSUE_129); std::env::set_var("OPENAI_API_KEY", "sk-test-key-for-issue-129");
} }
let settings = Settings { let settings = Settings {
+7 -8
View File
@@ -385,7 +385,6 @@ mod tests {
use super::*; use super::*;
use crate::config::helpers::ENV_MUTEX; use crate::config::helpers::ENV_MUTEX;
use crate::settings::Settings; use crate::settings::Settings;
use crate::testing::credentials::*;
/// Clear all openai-compatible-related env vars. /// Clear all openai-compatible-related env vars.
fn clear_openai_compatible_env() { fn clear_openai_compatible_env() {
@@ -648,7 +647,7 @@ mod tests {
// SAFETY: Under ENV_MUTEX. // SAFETY: Under ENV_MUTEX.
unsafe { unsafe {
std::env::set_var("LLM_BACKEND", "open_ai"); std::env::set_var("LLM_BACKEND", "open_ai");
std::env::set_var("OPENAI_API_KEY", TEST_API_KEY); std::env::set_var("OPENAI_API_KEY", "test-key");
} }
let settings = Settings::default(); let settings = Settings::default();
@@ -782,7 +781,7 @@ mod tests {
clear_anthropic_env(); clear_anthropic_env();
// SAFETY: Under ENV_MUTEX. // SAFETY: Under ENV_MUTEX.
unsafe { unsafe {
std::env::set_var("ANTHROPIC_OAUTH_TOKEN", TEST_ANTHROPIC_OAUTH_TOKEN); std::env::set_var("ANTHROPIC_OAUTH_TOKEN", "sk-ant-oat01-test-token");
} }
let settings = Settings { let settings = Settings {
@@ -806,7 +805,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
provider.oauth_token.as_ref().unwrap().expose_secret(), provider.oauth_token.as_ref().unwrap().expose_secret(),
TEST_ANTHROPIC_OAUTH_TOKEN "sk-ant-oat01-test-token"
); );
clear_anthropic_env(); clear_anthropic_env();
@@ -820,8 +819,8 @@ mod tests {
clear_anthropic_env(); clear_anthropic_env();
// SAFETY: Under ENV_MUTEX. // SAFETY: Under ENV_MUTEX.
unsafe { unsafe {
std::env::set_var("ANTHROPIC_API_KEY", TEST_ANTHROPIC_API_KEY); std::env::set_var("ANTHROPIC_API_KEY", "sk-ant-real-key");
std::env::set_var("ANTHROPIC_OAUTH_TOKEN", TEST_ANTHROPIC_OAUTH_TOKEN); std::env::set_var("ANTHROPIC_OAUTH_TOKEN", "sk-ant-oat01-test-token");
} }
let settings = Settings { let settings = Settings {
@@ -836,7 +835,7 @@ mod tests {
.api_key .api_key
.as_ref() .as_ref()
.map(|k| k.expose_secret().to_string()), .map(|k| k.expose_secret().to_string()),
Some(TEST_ANTHROPIC_API_KEY.to_string()), Some("sk-ant-real-key".to_string()),
"real API key should take priority over OAuth placeholder" "real API key should take priority over OAuth placeholder"
); );
assert!( assert!(
@@ -853,7 +852,7 @@ mod tests {
clear_anthropic_env(); clear_anthropic_env();
// SAFETY: Under ENV_MUTEX. // SAFETY: Under ENV_MUTEX.
unsafe { unsafe {
std::env::set_var("ANTHROPIC_OAUTH_TOKEN", TEST_ANTHROPIC_OAUTH_TOKEN); std::env::set_var("ANTHROPIC_OAUTH_TOKEN", "sk-ant-oat01-test-token");
} }
let settings = Settings { let settings = Settings {
+10 -17
View File
@@ -272,7 +272,6 @@ fn parse_oauth_access_token(json: &str) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::config::sandbox::*; use crate::config::sandbox::*;
use crate::testing::credentials::*;
// ── SandboxModeConfig defaults ────────────────────────────────── // ── SandboxModeConfig defaults ──────────────────────────────────
@@ -406,12 +405,9 @@ mod tests {
#[test] #[test]
fn parse_oauth_token_valid() { fn parse_oauth_token_valid() {
let json = format!( let json = r#"{"claudeAiOauth": {"accessToken": "sk-ant-oat01-fake"}}"#;
r#"{{"claudeAiOauth": {{"accessToken": "{}"}}}}"#, let token = parse_oauth_access_token(json);
TEST_ANTHROPIC_OAUTH_BASIC assert_eq!(token, Some("sk-ant-oat01-fake".to_string()));
);
let token = parse_oauth_access_token(&json);
assert_eq!(token, Some(TEST_ANTHROPIC_OAUTH_BASIC.to_string()));
} }
#[test] #[test]
@@ -438,19 +434,16 @@ mod tests {
#[test] #[test]
fn parse_oauth_token_nested_extra_fields() { fn parse_oauth_token_nested_extra_fields() {
let json = format!( let json = r#"{
r#"{{ "claudeAiOauth": {
"claudeAiOauth": {{ "accessToken": "sk-ant-oat01-real-token",
"accessToken": "{}",
"refreshToken": "rt-abc", "refreshToken": "rt-abc",
"expiresAt": 1700000000 "expiresAt": 1700000000
}} }
}}"#, }"#;
TEST_ANTHROPIC_OAUTH_NESTED
);
assert_eq!( assert_eq!(
parse_oauth_access_token(&json), parse_oauth_access_token(json),
Some(TEST_ANTHROPIC_OAUTH_NESTED.to_string()) Some("sk-ant-oat01-real-token".to_string())
); );
} }
+2 -2
View File
@@ -3907,7 +3907,6 @@ mod tests {
channels_dir: std::path::PathBuf, channels_dir: std::path::PathBuf,
) -> ExtensionManager { ) -> ExtensionManager {
use crate::secrets::{InMemorySecretsStore, SecretsCrypto}; use crate::secrets::{InMemorySecretsStore, SecretsCrypto};
use crate::testing::credentials::TEST_CRYPTO_KEY;
use crate::tools::ToolRegistry; use crate::tools::ToolRegistry;
use crate::tools::mcp::process::McpProcessManager; use crate::tools::mcp::process::McpProcessManager;
use crate::tools::mcp::session::McpSessionManager; use crate::tools::mcp::session::McpSessionManager;
@@ -3915,7 +3914,8 @@ mod tests {
std::fs::create_dir_all(&tools_dir).ok(); std::fs::create_dir_all(&tools_dir).ok();
std::fs::create_dir_all(&channels_dir).ok(); std::fs::create_dir_all(&channels_dir).ok();
let master_key = secrecy::SecretString::from(TEST_CRYPTO_KEY.to_string()); let master_key =
secrecy::SecretString::from("0123456789abcdef0123456789abcdef".to_string());
let crypto = Arc::new(SecretsCrypto::new(master_key).unwrap()); let crypto = Arc::new(SecretsCrypto::new(master_key).unwrap());
ExtensionManager::new( ExtensionManager::new(
+7 -10
View File
@@ -627,9 +627,6 @@ pub async fn create_session_manager(config: SessionConfig) -> Arc<SessionManager
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::{
TEST_SESSION_NEARAI_ABC, TEST_SESSION_NEARAI_XYZ, TEST_SESSION_TOKEN,
};
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use tempfile::tempdir; use tempfile::tempdir;
@@ -650,28 +647,28 @@ mod tests {
// Save a token // Save a token
manager manager
.save_session(TEST_SESSION_TOKEN, Some("near")) .save_session("test_token_123", Some("near"))
.await .await
.unwrap(); .unwrap();
manager manager
.set_token(SecretString::from(TEST_SESSION_TOKEN)) .set_token(SecretString::from("test_token_123"))
.await; .await;
// Verify it's set // Verify it's set
assert!(manager.has_token().await); assert!(manager.has_token().await);
let token = manager.get_token().await.unwrap(); let token = manager.get_token().await.unwrap();
assert_eq!(token.expose_secret(), TEST_SESSION_TOKEN); assert_eq!(token.expose_secret(), "test_token_123");
// Create new manager and verify it loads the token // Create new manager and verify it loads the token
let manager2 = SessionManager::new_async(config).await; let manager2 = SessionManager::new_async(config).await;
assert!(manager2.has_token().await); assert!(manager2.has_token().await);
let token2 = manager2.get_token().await.unwrap(); let token2 = manager2.get_token().await.unwrap();
assert_eq!(token2.expose_secret(), TEST_SESSION_TOKEN); assert_eq!(token2.expose_secret(), "test_token_123");
// Verify file contents // Verify file contents
let data: SessionData = let data: SessionData =
serde_json::from_str(&std::fs::read_to_string(&session_path).unwrap()).unwrap(); serde_json::from_str(&std::fs::read_to_string(&session_path).unwrap()).unwrap();
assert_eq!(data.session_token, TEST_SESSION_TOKEN); assert_eq!(data.session_token, "test_token_123");
assert_eq!(data.auth_provider, Some("near".to_string())); assert_eq!(data.auth_provider, Some("near".to_string()));
} }
@@ -692,7 +689,7 @@ mod tests {
#[test] #[test]
fn test_session_data_serde_roundtrip_with_auth_provider() { fn test_session_data_serde_roundtrip_with_auth_provider() {
let original = SessionData { let original = SessionData {
session_token: TEST_SESSION_NEARAI_ABC.to_string(), session_token: "sess_abc123".to_string(),
created_at: Utc::now(), created_at: Utc::now(),
auth_provider: Some("github".to_string()), auth_provider: Some("github".to_string()),
}; };
@@ -706,7 +703,7 @@ mod tests {
#[test] #[test]
fn test_session_data_serde_roundtrip_without_auth_provider() { fn test_session_data_serde_roundtrip_without_auth_provider() {
let original = SessionData { let original = SessionData {
session_token: TEST_SESSION_NEARAI_XYZ.to_string(), session_token: "sess_xyz789".to_string(),
created_at: Utc::now(), created_at: Utc::now(),
auth_provider: None, auth_provider: None,
}; };
+5 -2
View File
@@ -661,9 +661,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn credentials_returns_secrets_when_store_configured() { async fn credentials_returns_secrets_when_store_configured() {
use crate::testing::credentials::test_secrets_store;
use secrecy::SecretString; use secrecy::SecretString;
let secrets_store = Arc::new(test_secrets_store()); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(
crate::secrets::SecretsCrypto::new(SecretString::from(key.to_string())).unwrap(),
);
let secrets_store = Arc::new(crate::secrets::InMemorySecretsStore::new(crypto));
// Create a secret // Create a secret
secrets_store secrets_store
+2 -2
View File
@@ -153,11 +153,11 @@ mod tests {
use secrecy::SecretString; use secrecy::SecretString;
use crate::secrets::crypto::SecretsCrypto; use crate::secrets::crypto::SecretsCrypto;
use crate::testing::credentials::TEST_CRYPTO_KEY;
fn test_crypto() -> SecretsCrypto { fn test_crypto() -> SecretsCrypto {
// 32-byte test key // 32-byte test key
SecretsCrypto::new(SecretString::from(TEST_CRYPTO_KEY.to_string())).unwrap() let key = "0123456789abcdef0123456789abcdef";
SecretsCrypto::new(SecretString::from(key.to_string())).unwrap()
} }
#[test] #[test]
+16 -17
View File
@@ -802,25 +802,30 @@ pub mod in_memory {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::secrets::store::SecretsStore; use std::sync::Arc;
use crate::secrets::types::CreateSecretParams;
use crate::testing::credentials::{
TEST_OPENAI_API_KEY_SHORT, TEST_SECRET_VALUE, TEST_STRIPE_KEY, test_secrets_store,
};
fn test_store() -> crate::secrets::store::in_memory::InMemorySecretsStore { use secrecy::SecretString;
test_secrets_store()
use crate::secrets::crypto::SecretsCrypto;
use crate::secrets::store::SecretsStore;
use crate::secrets::store::in_memory::InMemorySecretsStore;
use crate::secrets::types::CreateSecretParams;
fn test_store() -> InMemorySecretsStore {
let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
InMemorySecretsStore::new(crypto)
} }
#[tokio::test] #[tokio::test]
async fn test_create_and_get() { async fn test_create_and_get() {
let store = test_store(); let store = test_store();
let params = CreateSecretParams::new("api_key", TEST_SECRET_VALUE); let params = CreateSecretParams::new("api_key", "sk-test-12345");
store.create("user1", params).await.unwrap(); store.create("user1", params).await.unwrap();
let decrypted = store.get_decrypted("user1", "api_key").await.unwrap(); let decrypted = store.get_decrypted("user1", "api_key").await.unwrap();
assert_eq!(decrypted.expose(), TEST_SECRET_VALUE); assert_eq!(decrypted.expose(), "sk-test-12345");
} }
#[tokio::test] #[tokio::test]
@@ -873,17 +878,11 @@ mod tests {
async fn test_is_accessible() { async fn test_is_accessible() {
let store = test_store(); let store = test_store();
store store
.create( .create("user1", CreateSecretParams::new("openai_key", "sk-test"))
"user1",
CreateSecretParams::new("openai_key", TEST_OPENAI_API_KEY_SHORT),
)
.await .await
.unwrap(); .unwrap();
store store
.create( .create("user1", CreateSecretParams::new("stripe_key", "sk-live"))
"user1",
CreateSecretParams::new("stripe_key", TEST_STRIPE_KEY),
)
.await .await
.unwrap(); .unwrap();
-2
View File
@@ -18,8 +18,6 @@
//! } //! }
//! ``` //! ```
pub mod credentials;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
-134
View File
@@ -1,134 +0,0 @@
//! Centralized fake credential constants for tests.
//!
//! All values here are intentionally fake. Centralizing them makes security
//! audits trivial (one file to verify) and eliminates duplication across
//! the test suite.
use std::sync::Arc;
use secrecy::SecretString;
use crate::secrets::{InMemorySecretsStore, SecretsCrypto};
// ── Encryption keys ──────────────────────────────────────────────────────
/// 32-character key string for `SecretsCrypto::new()` in tests.
pub const TEST_CRYPTO_KEY: &str = "0123456789abcdef0123456789abcdef";
/// 32+ char key for web gateway `SecretsCrypto` in tests.
pub const TEST_GATEWAY_CRYPTO_KEY: &str = "test-key-at-least-32-chars-long!!";
// ── OpenAI-style API keys ────────────────────────────────────────────────
/// Generic OpenAI-style test API key.
pub const TEST_OPENAI_API_KEY: &str = "sk-test123";
/// OpenAI API key with longer format (config round-trip tests).
pub const TEST_OPENAI_API_KEY_LONG: &str = "sk-test-key-1234567890";
/// Short OpenAI-style key for secrets store accessibility tests.
pub const TEST_OPENAI_API_KEY_SHORT: &str = "sk-test";
/// OpenAI API key used in embeddings config issue-129 test.
pub const TEST_OPENAI_API_KEY_ISSUE_129: &str = "sk-test-key-for-issue-129";
// ── Anthropic keys ───────────────────────────────────────────────────────
/// Anthropic OAuth token for config tests.
pub const TEST_ANTHROPIC_OAUTH_TOKEN: &str = "sk-ant-oat01-test-token";
/// Anthropic API key for priority tests.
pub const TEST_ANTHROPIC_API_KEY: &str = "sk-ant-priority-key";
/// Anthropic OAuth token for sandbox config parse tests.
pub const TEST_ANTHROPIC_OAUTH_BASIC: &str = "sk-ant-oat01-basic";
/// Anthropic OAuth token in nested JSON parse test.
pub const TEST_ANTHROPIC_OAUTH_NESTED: &str = "sk-ant-oat01-primary-token";
// ── Google OAuth ─────────────────────────────────────────────────────────
/// Google OAuth access token (standard test).
pub const TEST_GOOGLE_OAUTH_TOKEN: &str = "ya29.test-token";
/// Google OAuth access token (fresh/non-expired variant).
pub const TEST_GOOGLE_OAUTH_FRESH: &str = "ya29.fresh-token";
/// Google OAuth access token (legacy/no-expiry variant).
pub const TEST_GOOGLE_OAUTH_LEGACY: &str = "ya29.legacy-token";
// ── GitHub ───────────────────────────────────────────────────────────────
/// GitHub personal access token (test).
pub const TEST_GITHUB_TOKEN: &str = "ghp_test123";
// ── Telegram ────────────────────────────────────────────────────────────
/// Telegram bot token for credential redaction tests.
pub const TEST_TELEGRAM_BOT_TOKEN: &str = "0000000000:AAFakeTestTokenForTestingPurposesOnly";
// ── OAuth client credentials ────────────────────────────────────────────
/// OAuth client ID for token refresh tests.
pub const TEST_OAUTH_CLIENT_ID: &str = "test-client-id";
/// OAuth client secret for token refresh tests.
pub const TEST_OAUTH_CLIENT_SECRET: &str = "test-client-secret";
// ── Bearer/auth tokens ──────────────────────────────────────────────────
/// Generic test bearer token.
pub const TEST_BEARER_TOKEN: &str = "test-token";
/// Bearer token with suffix (wasm wrapper credential injection).
pub const TEST_BEARER_TOKEN_123: &str = "test-token-123";
/// Auth token used by web gateway middleware tests.
pub const TEST_AUTH_SECRET_TOKEN: &str = "secret-token";
// ── Stripe ──────────────────────────────────────────────────────────────
/// Stripe-style test key.
pub const TEST_STRIPE_KEY: &str = "sk_test_fake123";
// ── Redaction test values ───────────────────────────────────────────────
/// Secret-prefixed key for redaction/sanitization tests.
pub const TEST_REDACT_SECRET: &str = "sk-secret";
/// Secret-prefixed key with suffix for redaction tests.
pub const TEST_REDACT_SECRET_123: &str = "sk-secret-123";
// ── Session tokens ──────────────────────────────────────────────────────
/// Generic session token for persistence tests.
pub const TEST_SESSION_TOKEN: &str = "test_token_123";
/// NEAR AI session token variant A.
pub const TEST_SESSION_NEARAI_ABC: &str = "sess_abc123";
/// NEAR AI session token variant B.
pub const TEST_SESSION_NEARAI_XYZ: &str = "sess_xyz789";
// ── Generic ──────────────────────────────────────────────────────────────
/// Generic test API key for LLM config, embedding config, nearai tests.
pub const TEST_API_KEY: &str = "test-key";
/// Stored secret value for create-and-get tests.
pub const TEST_SECRET_VALUE: &str = "sk-test-12345";
/// HTTP webhook secret for channel tests.
pub const TEST_HTTP_SECRET: &str = "test-secret-123";
// ── Helpers ──────────────────────────────────────────────────────────────
/// Create an `InMemorySecretsStore` backed by [`TEST_CRYPTO_KEY`].
///
/// Replaces the duplicated `test_store()` pattern found across multiple
/// test modules.
pub fn test_secrets_store() -> InMemorySecretsStore {
let crypto =
Arc::new(SecretsCrypto::new(SecretString::from(TEST_CRYPTO_KEY.to_string())).unwrap());
InMemorySecretsStore::new(crypto)
}
+2 -2
View File
@@ -768,11 +768,11 @@ mod tests {
/// Create a stub manager for schema tests (these don't call execute). /// Create a stub manager for schema tests (these don't call execute).
fn test_manager_stub() -> Arc<ExtensionManager> { fn test_manager_stub() -> Arc<ExtensionManager> {
use crate::secrets::{InMemorySecretsStore, SecretsCrypto}; use crate::secrets::{InMemorySecretsStore, SecretsCrypto};
use crate::testing::credentials::TEST_CRYPTO_KEY;
use crate::tools::ToolRegistry; use crate::tools::ToolRegistry;
use crate::tools::mcp::session::McpSessionManager; use crate::tools::mcp::session::McpSessionManager;
let master_key = secrecy::SecretString::from(TEST_CRYPTO_KEY.to_string()); let master_key =
secrecy::SecretString::from("0123456789abcdef0123456789abcdef".to_string());
let crypto = Arc::new(SecretsCrypto::new(master_key).unwrap()); let crypto = Arc::new(SecretsCrypto::new(master_key).unwrap());
Arc::new(ExtensionManager::new( Arc::new(ExtensionManager::new(
+25 -5
View File
@@ -609,7 +609,6 @@ impl Tool for HttpTool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::{TEST_OPENAI_API_KEY, test_secrets_store};
#[test] #[test]
fn test_http_tool_schema_headers_is_array() { fn test_http_tool_schema_headers_is_array() {
@@ -869,7 +868,12 @@ mod tests {
let tool = HttpTool::new().with_credentials( let tool = HttpTool::new().with_credentials(
registry, registry,
// secrets_store is not used in requires_approval, just needs to be present // secrets_store is not used in requires_approval, just needs to be present
Arc::new(test_secrets_store()), Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new(
crate::secrets::SecretsCrypto::new(secrecy::SecretString::from(
"0123456789abcdef0123456789abcdef".to_string(),
))
.unwrap(),
))),
); );
let params = serde_json::json!({ let params = serde_json::json!({
@@ -886,7 +890,15 @@ mod tests {
let registry = Arc::new(SharedCredentialRegistry::new()); let registry = Arc::new(SharedCredentialRegistry::new());
// Empty registry - no credential mappings // Empty registry - no credential mappings
let tool = HttpTool::new().with_credentials(registry, Arc::new(test_secrets_store())); let tool = HttpTool::new().with_credentials(
registry,
Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new(
crate::secrets::SecretsCrypto::new(secrecy::SecretString::from(
"0123456789abcdef0123456789abcdef".to_string(),
))
.unwrap(),
))),
);
let params = serde_json::json!({ let params = serde_json::json!({
"method": "GET", "method": "GET",
@@ -914,7 +926,7 @@ mod tests {
let params = serde_json::json!({ let params = serde_json::json!({
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"headers": {"X-Custom": format!("Bearer {TEST_OPENAI_API_KEY}")} "headers": {"X-Custom": "Bearer sk-test123"}
}); });
assert_eq!(tool.requires_approval(&params), ApprovalRequirement::Always); assert_eq!(tool.requires_approval(&params), ApprovalRequirement::Always);
} }
@@ -945,7 +957,15 @@ mod tests {
let registry = Arc::new(SharedCredentialRegistry::new()); let registry = Arc::new(SharedCredentialRegistry::new());
registry.add_mappings(vec![CredentialMapping::bearer("test_key", "api.test.com")]); registry.add_mappings(vec![CredentialMapping::bearer("test_key", "api.test.com")]);
let tool = HttpTool::new().with_credentials(registry, Arc::new(test_secrets_store())); let tool = HttpTool::new().with_credentials(
registry,
Arc::new(crate::secrets::InMemorySecretsStore::new(Arc::new(
crate::secrets::SecretsCrypto::new(secrecy::SecretString::from(
"0123456789abcdef0123456789abcdef".to_string(),
))
.unwrap(),
))),
);
// These calls should not panic in multi-thread runtime // These calls should not panic in multi-thread runtime
let params_no_auth = serde_json::json!({ let params_no_auth = serde_json::json!({
+13 -6
View File
@@ -1748,10 +1748,14 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_parse_credentials_missing_secret() { async fn test_parse_credentials_missing_secret() {
use crate::testing::credentials::test_secrets_store; use crate::secrets::{InMemorySecretsStore, SecretsCrypto};
use secrecy::SecretString;
let manager = Arc::new(ContextManager::new(5)); let manager = Arc::new(ContextManager::new(5));
let secrets: Arc<dyn SecretsStore + Send + Sync> = Arc::new(test_secrets_store()); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let secrets: Arc<dyn SecretsStore + Send + Sync> =
Arc::new(InMemorySecretsStore::new(crypto));
let tool = CreateJobTool::new(manager).with_secrets(Arc::clone(&secrets)); let tool = CreateJobTool::new(manager).with_secrets(Arc::clone(&secrets));
@@ -1768,17 +1772,20 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_parse_credentials_valid() { async fn test_parse_credentials_valid() {
use crate::secrets::CreateSecretParams; use crate::secrets::{CreateSecretParams, InMemorySecretsStore, SecretsCrypto};
use crate::testing::credentials::{TEST_GITHUB_TOKEN, test_secrets_store}; use secrecy::SecretString;
let manager = Arc::new(ContextManager::new(5)); let manager = Arc::new(ContextManager::new(5));
let secrets: Arc<dyn SecretsStore + Send + Sync> = Arc::new(test_secrets_store()); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let secrets: Arc<dyn SecretsStore + Send + Sync> =
Arc::new(InMemorySecretsStore::new(Arc::clone(&crypto)));
// Store a secret // Store a secret
secrets secrets
.create( .create(
"user1", "user1",
CreateSecretParams::new("github_token", TEST_GITHUB_TOKEN), CreateSecretParams::new("github_token", "ghp_test123"),
) )
.await .await
.unwrap(); .unwrap();
+8 -5
View File
@@ -158,13 +158,16 @@ impl Tool for SecretDeleteTool {
mod tests { mod tests {
use std::sync::Arc; use std::sync::Arc;
use secrecy::SecretString;
use super::*; use super::*;
use crate::context::JobContext; use crate::context::JobContext;
use crate::secrets::CreateSecretParams; use crate::secrets::{CreateSecretParams, InMemorySecretsStore, SecretsCrypto};
use crate::testing::credentials::{TEST_OPENAI_API_KEY_SHORT, test_secrets_store};
fn test_store() -> Arc<crate::secrets::InMemorySecretsStore> { fn test_store() -> Arc<InMemorySecretsStore> {
Arc::new(test_secrets_store()) let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
Arc::new(InMemorySecretsStore::new(crypto))
} }
fn test_ctx() -> JobContext { fn test_ctx() -> JobContext {
@@ -180,7 +183,7 @@ mod tests {
store store
.create( .create(
&ctx.user_id, &ctx.user_id,
CreateSecretParams::new("openai_key", TEST_OPENAI_API_KEY_SHORT), CreateSecretParams::new("openai_key", "sk-test"),
) )
.await .await
.unwrap(); .unwrap();
+2 -3
View File
@@ -480,7 +480,6 @@ pub fn validate_tool_schema(schema: &serde_json::Value, path: &str) -> Vec<Strin
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::TEST_REDACT_SECRET;
/// A simple no-op tool for testing. /// A simple no-op tool for testing.
#[derive(Debug)] #[derive(Debug)]
@@ -603,12 +602,12 @@ mod tests {
#[test] #[test]
fn test_redact_params_replaces_sensitive_key() { fn test_redact_params_replaces_sensitive_key() {
let params = serde_json::json!({"name": "openai_key", "value": TEST_REDACT_SECRET}); let params = serde_json::json!({"name": "openai_key", "value": "sk-secret"});
let redacted = redact_params(&params, &["value"]); let redacted = redact_params(&params, &["value"]);
assert_eq!(redacted["name"], "openai_key"); assert_eq!(redacted["name"], "openai_key");
assert_eq!(redacted["value"], "[REDACTED]"); assert_eq!(redacted["value"], "[REDACTED]");
// Original unchanged // Original unchanged
assert_eq!(params["value"], TEST_REDACT_SECRET); assert_eq!(params["value"], "sk-secret");
} }
#[test] #[test]
+9 -8
View File
@@ -365,18 +365,22 @@ fn base64_encode(input: &[u8]) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use secrecy::SecretString;
use crate::secrets::{ use crate::secrets::{
CreateSecretParams, CredentialLocation, CredentialMapping, InMemorySecretsStore, CreateSecretParams, CredentialLocation, CredentialMapping, InMemorySecretsStore,
SecretsStore, SecretsCrypto, SecretsStore,
}; };
use crate::testing::credentials::{TEST_OPENAI_API_KEY, test_secrets_store};
use crate::tools::wasm::credential_injector::{ use crate::tools::wasm::credential_injector::{
CredentialInjector, base64_encode, host_matches_pattern, CredentialInjector, base64_encode, host_matches_pattern,
}; };
fn test_store() -> InMemorySecretsStore { fn test_store() -> InMemorySecretsStore {
test_secrets_store() let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
InMemorySecretsStore::new(crypto)
} }
#[test] #[test]
@@ -402,10 +406,7 @@ mod tests {
async fn test_inject_bearer() { async fn test_inject_bearer() {
let store = test_store(); let store = test_store();
store store
.create( .create("user1", CreateSecretParams::new("openai_key", "sk-test123"))
"user1",
CreateSecretParams::new("openai_key", TEST_OPENAI_API_KEY),
)
.await .await
.unwrap(); .unwrap();
@@ -427,7 +428,7 @@ mod tests {
assert_eq!( assert_eq!(
result.headers.get("Authorization"), result.headers.get("Authorization"),
Some(&format!("Bearer {TEST_OPENAI_API_KEY}")) Some(&"Bearer sk-test123".to_string())
); );
} }
+4 -8
View File
@@ -694,7 +694,6 @@ mod tests {
use tempfile::TempDir; use tempfile::TempDir;
use crate::testing::credentials::{TEST_OAUTH_CLIENT_ID, TEST_OAUTH_CLIENT_SECRET};
use crate::tools::wasm::loader::{WasmLoadError, check_wit_version_compat, discover_tools}; use crate::tools::wasm::loader::{WasmLoadError, check_wit_version_compat, discover_tools};
#[test] #[test]
@@ -835,8 +834,8 @@ mod tests {
oauth: Some(OAuthConfigSchema { oauth: Some(OAuthConfigSchema {
authorization_url: "https://accounts.google.com/o/oauth2/v2/auth".to_string(), authorization_url: "https://accounts.google.com/o/oauth2/v2/auth".to_string(),
token_url: "https://oauth2.googleapis.com/token".to_string(), token_url: "https://oauth2.googleapis.com/token".to_string(),
client_id: Some(TEST_OAUTH_CLIENT_ID.to_string()), client_id: Some("test-client-id".to_string()),
client_secret: Some(TEST_OAUTH_CLIENT_SECRET.to_string()), client_secret: Some("test-client-secret".to_string()),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@@ -849,11 +848,8 @@ mod tests {
let config = config.unwrap(); let config = config.unwrap();
assert_eq!(config.token_url, "https://oauth2.googleapis.com/token"); assert_eq!(config.token_url, "https://oauth2.googleapis.com/token");
assert_eq!(config.client_id, TEST_OAUTH_CLIENT_ID); assert_eq!(config.client_id, "test-client-id");
assert_eq!( assert_eq!(config.client_secret, Some("test-client-secret".to_string()));
config.client_secret,
Some(TEST_OAUTH_CLIENT_SECRET.to_string())
);
assert_eq!(config.secret_name, "google_oauth_token"); assert_eq!(config.secret_name, "google_oauth_token");
assert_eq!(config.provider, Some("google".to_string())); assert_eq!(config.provider, Some("google".to_string()));
} }
+49 -29
View File
@@ -1212,11 +1212,6 @@ fn coerce_params_to_schema(
mod tests { mod tests {
use std::sync::Arc; use std::sync::Arc;
use crate::testing::credentials::{
TEST_BEARER_TOKEN_123, TEST_GOOGLE_OAUTH_FRESH, TEST_GOOGLE_OAUTH_LEGACY,
TEST_GOOGLE_OAUTH_TOKEN, TEST_OAUTH_CLIENT_ID, TEST_OAUTH_CLIENT_SECRET,
test_secrets_store,
};
use crate::tools::wasm::capabilities::Capabilities; use crate::tools::wasm::capabilities::Capabilities;
use crate::tools::wasm::runtime::{WasmRuntimeConfig, WasmToolRuntime}; use crate::tools::wasm::runtime::{WasmRuntimeConfig, WasmToolRuntime};
@@ -1284,12 +1279,12 @@ mod tests {
let mut h = HashMap::new(); let mut h = HashMap::new();
h.insert( h.insert(
"Authorization".to_string(), "Authorization".to_string(),
format!("Bearer {TEST_BEARER_TOKEN_123}"), "Bearer test-token-123".to_string(),
); );
h h
}, },
query_params: HashMap::new(), query_params: HashMap::new(),
secret_value: TEST_BEARER_TOKEN_123.to_string(), secret_value: "test-token-123".to_string(),
}]; }];
let store_data = StoreData::new( let store_data = StoreData::new(
@@ -1305,7 +1300,7 @@ mod tests {
store_data.inject_host_credentials("www.googleapis.com", &mut headers, &mut url); store_data.inject_host_credentials("www.googleapis.com", &mut headers, &mut url);
assert_eq!( assert_eq!(
headers.get("Authorization"), headers.get("Authorization"),
Some(&format!("Bearer {TEST_BEARER_TOKEN_123}")) Some(&"Bearer test-token-123".to_string())
); );
// Should not inject for non-matching host // Should not inject for non-matching host
@@ -1381,9 +1376,13 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_resolve_host_credentials_no_http_cap() { async fn test_resolve_host_credentials_no_http_cap() {
use crate::secrets::{InMemorySecretsStore, SecretsCrypto};
use crate::tools::wasm::wrapper::resolve_host_credentials; use crate::tools::wasm::wrapper::resolve_host_credentials;
use secrecy::SecretString;
let store = test_secrets_store(); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let store = InMemorySecretsStore::new(crypto);
let caps = Capabilities::default(); let caps = Capabilities::default();
let result = resolve_host_credentials(&caps, Some(&store), "user1", None).await; let result = resolve_host_credentials(&caps, Some(&store), "user1", None).await;
@@ -1395,17 +1394,21 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use crate::secrets::{ use crate::secrets::{
CreateSecretParams, CredentialLocation, CredentialMapping, SecretsStore, CreateSecretParams, CredentialLocation, CredentialMapping, InMemorySecretsStore,
SecretsCrypto, SecretsStore,
}; };
use crate::tools::wasm::capabilities::HttpCapability; use crate::tools::wasm::capabilities::HttpCapability;
use crate::tools::wasm::wrapper::resolve_host_credentials; use crate::tools::wasm::wrapper::resolve_host_credentials;
use secrecy::SecretString;
let store = test_secrets_store(); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let store = InMemorySecretsStore::new(crypto);
store store
.create( .create(
"user1", "user1",
CreateSecretParams::new("google_oauth_token", TEST_GOOGLE_OAUTH_TOKEN), CreateSecretParams::new("google_oauth_token", "ya29.test-token"),
) )
.await .await
.unwrap(); .unwrap();
@@ -1433,7 +1436,7 @@ mod tests {
assert_eq!(result[0].host_patterns, vec!["www.googleapis.com"]); assert_eq!(result[0].host_patterns, vec!["www.googleapis.com"]);
assert_eq!( assert_eq!(
result[0].headers.get("Authorization"), result[0].headers.get("Authorization"),
Some(&format!("Bearer {TEST_GOOGLE_OAUTH_TOKEN}")) Some(&"Bearer ya29.test-token".to_string())
); );
} }
@@ -1441,11 +1444,16 @@ mod tests {
async fn test_resolve_host_credentials_missing_secret() { async fn test_resolve_host_credentials_missing_secret() {
use std::collections::HashMap; use std::collections::HashMap;
use crate::secrets::{CredentialLocation, CredentialMapping}; use crate::secrets::{
CredentialLocation, CredentialMapping, InMemorySecretsStore, SecretsCrypto,
};
use crate::tools::wasm::capabilities::HttpCapability; use crate::tools::wasm::capabilities::HttpCapability;
use crate::tools::wasm::wrapper::resolve_host_credentials; use crate::tools::wasm::wrapper::resolve_host_credentials;
use secrecy::SecretString;
let store = test_secrets_store(); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let store = InMemorySecretsStore::new(crypto);
// No secret stored, should silently skip // No secret stored, should silently skip
let mut credentials = HashMap::new(); let mut credentials = HashMap::new();
@@ -1475,19 +1483,23 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use crate::secrets::{ use crate::secrets::{
CreateSecretParams, CredentialLocation, CredentialMapping, SecretsStore, CreateSecretParams, CredentialLocation, CredentialMapping, InMemorySecretsStore,
SecretsCrypto, SecretsStore,
}; };
use crate::tools::wasm::capabilities::HttpCapability; use crate::tools::wasm::capabilities::HttpCapability;
use crate::tools::wasm::wrapper::{OAuthRefreshConfig, resolve_host_credentials}; use crate::tools::wasm::wrapper::{OAuthRefreshConfig, resolve_host_credentials};
use secrecy::SecretString;
let store = test_secrets_store(); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let store = InMemorySecretsStore::new(crypto);
// Store a token that expires 2 hours from now (well within buffer) // Store a token that expires 2 hours from now (well within buffer)
let expires_at = chrono::Utc::now() + chrono::Duration::hours(2); let expires_at = chrono::Utc::now() + chrono::Duration::hours(2);
store store
.create( .create(
"user1", "user1",
CreateSecretParams::new("google_oauth_token", TEST_GOOGLE_OAUTH_FRESH) CreateSecretParams::new("google_oauth_token", "ya29.fresh-token")
.with_expiry(expires_at), .with_expiry(expires_at),
) )
.await .await
@@ -1513,8 +1525,8 @@ mod tests {
let oauth_config = OAuthRefreshConfig { let oauth_config = OAuthRefreshConfig {
token_url: "https://oauth2.googleapis.com/token".to_string(), token_url: "https://oauth2.googleapis.com/token".to_string(),
client_id: TEST_OAUTH_CLIENT_ID.to_string(), client_id: "test-client-id".to_string(),
client_secret: Some(TEST_OAUTH_CLIENT_SECRET.to_string()), client_secret: Some("test-client-secret".to_string()),
secret_name: "google_oauth_token".to_string(), secret_name: "google_oauth_token".to_string(),
provider: Some("google".to_string()), provider: Some("google".to_string()),
}; };
@@ -1525,7 +1537,7 @@ mod tests {
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!( assert_eq!(
result[0].headers.get("Authorization"), result[0].headers.get("Authorization"),
Some(&format!("Bearer {TEST_GOOGLE_OAUTH_FRESH}")) Some(&"Bearer ya29.fresh-token".to_string())
); );
} }
@@ -1534,12 +1546,16 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use crate::secrets::{ use crate::secrets::{
CreateSecretParams, CredentialLocation, CredentialMapping, SecretsStore, CreateSecretParams, CredentialLocation, CredentialMapping, InMemorySecretsStore,
SecretsCrypto, SecretsStore,
}; };
use crate::tools::wasm::capabilities::HttpCapability; use crate::tools::wasm::capabilities::HttpCapability;
use crate::tools::wasm::wrapper::resolve_host_credentials; use crate::tools::wasm::wrapper::resolve_host_credentials;
use secrecy::SecretString;
let store = test_secrets_store(); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let store = InMemorySecretsStore::new(crypto);
// Store an expired token // Store an expired token
let expires_at = chrono::Utc::now() - chrono::Duration::hours(1); let expires_at = chrono::Utc::now() - chrono::Duration::hours(1);
@@ -1579,18 +1595,22 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use crate::secrets::{ use crate::secrets::{
CreateSecretParams, CredentialLocation, CredentialMapping, SecretsStore, CreateSecretParams, CredentialLocation, CredentialMapping, InMemorySecretsStore,
SecretsCrypto, SecretsStore,
}; };
use crate::tools::wasm::capabilities::HttpCapability; use crate::tools::wasm::capabilities::HttpCapability;
use crate::tools::wasm::wrapper::{OAuthRefreshConfig, resolve_host_credentials}; use crate::tools::wasm::wrapper::{OAuthRefreshConfig, resolve_host_credentials};
use secrecy::SecretString;
let store = test_secrets_store(); let key = "0123456789abcdef0123456789abcdef";
let crypto = Arc::new(SecretsCrypto::new(SecretString::from(key.to_string())).unwrap());
let store = InMemorySecretsStore::new(crypto);
// Legacy token: no expires_at set // Legacy token: no expires_at set
store store
.create( .create(
"user1", "user1",
CreateSecretParams::new("google_oauth_token", TEST_GOOGLE_OAUTH_LEGACY), CreateSecretParams::new("google_oauth_token", "ya29.legacy-token"),
) )
.await .await
.unwrap(); .unwrap();
@@ -1615,8 +1635,8 @@ mod tests {
let oauth_config = OAuthRefreshConfig { let oauth_config = OAuthRefreshConfig {
token_url: "https://oauth2.googleapis.com/token".to_string(), token_url: "https://oauth2.googleapis.com/token".to_string(),
client_id: TEST_OAUTH_CLIENT_ID.to_string(), client_id: "test-client-id".to_string(),
client_secret: Some(TEST_OAUTH_CLIENT_SECRET.to_string()), client_secret: Some("test-client-secret".to_string()),
secret_name: "google_oauth_token".to_string(), secret_name: "google_oauth_token".to_string(),
provider: Some("google".to_string()), provider: Some("google".to_string()),
}; };
@@ -1627,7 +1647,7 @@ mod tests {
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!( assert_eq!(
result[0].headers.get("Authorization"), result[0].headers.get("Authorization"),
Some(&format!("Bearer {TEST_GOOGLE_OAUTH_LEGACY}")) Some(&"Bearer ya29.legacy-token".to_string())
); );
} }
+1 -2
View File
@@ -294,11 +294,10 @@ mod tests {
#[test] #[test]
fn factory_cloudflare_with_config_ok() { fn factory_cloudflare_with_config_ok() {
use crate::testing::credentials::TEST_BEARER_TOKEN;
let cfg = TunnelProviderConfig { let cfg = TunnelProviderConfig {
provider: "cloudflare".into(), provider: "cloudflare".into(),
cloudflare: Some(CloudflareTunnelConfig { cloudflare: Some(CloudflareTunnelConfig {
token: TEST_BEARER_TOKEN.into(), token: "test-token".into(),
}), }),
..Default::default() ..Default::default()
}; };
+2 -3
View File
@@ -419,14 +419,13 @@ fn parse_finish_reason(s: &str) -> FinishReason {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::testing::credentials::TEST_BEARER_TOKEN;
#[test] #[test]
fn test_url_construction() { fn test_url_construction() {
let client = WorkerHttpClient::new( let client = WorkerHttpClient::new(
"http://host.docker.internal:50051".to_string(), "http://host.docker.internal:50051".to_string(),
Uuid::nil(), Uuid::nil(),
TEST_BEARER_TOKEN.to_string(), "test-token".to_string(),
); );
assert_eq!( assert_eq!(
@@ -450,7 +449,7 @@ mod tests {
let client = WorkerHttpClient::new( let client = WorkerHttpClient::new(
"http://host.docker.internal:50051".to_string(), "http://host.docker.internal:50051".to_string(),
Uuid::nil(), Uuid::nil(),
TEST_BEARER_TOKEN.to_string(), "test-token".to_string(),
); );
assert_eq!( assert_eq!(
+2 -7
View File
@@ -12,11 +12,6 @@ use tempfile::tempdir;
use ironclaw::bootstrap::{save_bootstrap_env_to, upsert_bootstrap_var_to}; use ironclaw::bootstrap::{save_bootstrap_env_to, upsert_bootstrap_var_to};
/// Fake OpenAI API key for test use only. Mirrors the internal
/// `TEST_OPENAI_API_KEY_LONG` constant from the main crate, which is not
/// directly available to integration tests due to `#[cfg(test)]`.
const TEST_OPENAI_API_KEY_LONG: &str = "sk-test-key-1234567890";
/// Parse a .env file into a HashMap using dotenvy. /// Parse a .env file into a HashMap using dotenvy.
fn read_env_map(path: &std::path::Path) -> HashMap<String, String> { fn read_env_map(path: &std::path::Path) -> HashMap<String, String> {
dotenvy::from_path_iter(path) dotenvy::from_path_iter(path)
@@ -82,7 +77,7 @@ fn bootstrap_env_round_trips_embedding_disabled() {
&[ &[
("DATABASE_BACKEND", "libsql"), ("DATABASE_BACKEND", "libsql"),
("EMBEDDING_ENABLED", "false"), ("EMBEDDING_ENABLED", "false"),
("OPENAI_API_KEY", TEST_OPENAI_API_KEY_LONG), ("OPENAI_API_KEY", "sk-test-key-1234567890"),
("ONBOARD_COMPLETED", "true"), ("ONBOARD_COMPLETED", "true"),
], ],
) )
@@ -97,7 +92,7 @@ fn bootstrap_env_round_trips_embedding_disabled() {
); );
assert_eq!( assert_eq!(
map.get("OPENAI_API_KEY").map(String::as_str), map.get("OPENAI_API_KEY").map(String::as_str),
Some(TEST_OPENAI_API_KEY_LONG), Some("sk-test-key-1234567890"),
"OPENAI_API_KEY must be preserved alongside EMBEDDING_ENABLED" "OPENAI_API_KEY must be preserved alongside EMBEDDING_ENABLED"
); );
} }