nu_plugin_secret 0.7.0

Production-grade secret handling plugin for Nushell with secure CustomValue types that prevent accidental exposure of sensitive data
Documentation
# Basic integration tests for nu_plugin_secret
# End-to-end workflow testing with real-world scenarios

use ../setup.nu *
use ../runner.nu [assert, assert_eq, assert_contains, assert_not_contains, time_operation]
use ../fixtures/secrets.nu *

# Test complete workflow: wrap -> validate -> unwrap
export def test_complete_workflow [] {
    let test_cases = [
        {type: "string", value: "api-key-12345", wrap_cmd: "secret wrap"},
        {type: "int", value: 8080, wrap_cmd: "secret wrap"}, 
        {type: "bool", value: true, wrap_cmd: "secret wrap"},
        {type: "float", value: 3.14159, wrap_cmd: "secret wrap"}
    ]
    
    for case in $test_cases {
        # Step 1: Wrap the value
        let secret = $case.value | do { |val| 
            match $case.type {
                "string" => { $val | secret wrap },
                "int" => { $val | secret wrap },
                "bool" => { $val | secret wrap },
                "float" => { $val | secret wrap },
                _ => { error make {msg: $"Unknown type: ($case.type)"} }
            }
        }
        
        # Step 2: Validate it's a secret
        let is_valid = $secret | secret validate
        assert $is_valid $"($case.type) secret should validate"
        
        # Step 3: Check type identification
        let identified_type = $secret | secret type-of
        assert_eq $identified_type $case.type $"Should identify as ($case.type)"
        
        # Step 4: Verify redaction
        let display = $secret | to text
        assert_contains $display "redacted" $"($case.type) should show redacted"
        
        # Step 5: Unwrap and verify
        let revealed = $secret | secret unwrap
        assert_eq $revealed $case.value $"($case.type) should unwrap to original value"
    }
}

# Test plugin info and help commands
export def test_plugin_info [] {
    let info = secret info
    
    # Verify info structure
    assert ($info | get plugin_name | is-not-empty) "Plugin name should be present"
    assert ($info | get version | is-not-empty) "Version should be present"
    assert_eq ($info | get plugin_name) "nu_plugin_secret" "Plugin name should be correct"
    
    # Test help for various commands
    let help_commands = [
        "secret wrap",
        "secret wrap", 
        "secret unwrap",
        "secret validate",
        "secret info"
    ]
    
    for cmd in $help_commands {
        try {
            let help_output = help $cmd
            assert ($help_output | str length) > 0 $"Help for ($cmd) should not be empty"
        } catch { |e|
            assert false $"Help for ($cmd) should be available: ($e.msg)"
        }
    }
}

# Test secrets in data structures
export def test_secrets_in_data_structures [] {
    # Test secrets in records
    let config = {
        app_name: "MyApp",
        api_key: ("sk-1234567890" | secret wrap),
        database: {
            host: "localhost",
            port: 5432,
            password: ("secret123" | secret wrap)
        },
        debug: true,
        timeout: 30
    }
    
    # Verify secrets are preserved in record
    assert ($config.api_key | secret validate) "API key should remain a secret"
    assert ($config.database.password | secret validate) "Database password should remain a secret"
    
    # Verify non-secrets remain unchanged
    assert_eq $config.app_name "MyApp" "App name should remain unchanged"
    assert_eq $config.database.port 5432 "Port should remain unchanged"
    assert_eq $config.debug true "Debug flag should remain unchanged"
    
    # Test secrets in lists
    let api_keys = [
        ("key1" | secret wrap),
        ("key2" | secret wrap),
        ("key3" | secret wrap)
    ]
    
    for i in 0..2 {
        assert ($api_keys.($i) | secret validate) $"API key ($i) should be a secret"
    }
    
    # Test mixed list with secrets and regular values
    let mixed_list = [
        "public-info",
        ("secret-data" | secret wrap),
        42,
        ("another-secret" | secret wrap)
    ]
    
    assert_eq $mixed_list.0 "public-info" "Public info should remain unchanged"
    assert ($mixed_list.1 | secret validate) "First secret should be valid"
    assert_eq $mixed_list.2 42 "Number should remain unchanged"
    assert ($mixed_list.3 | secret validate) "Second secret should be valid"
}

# Test pipeline operations with secrets
export def test_pipeline_operations [] {
    # Create a list of sensitive data
    let sensitive_data = [
        "password1",
        "password2", 
        "password3",
        "password4"
    ]
    
    # Wrap all as secrets in pipeline
    let secrets = $sensitive_data 
        | each { |item| $item | secret wrap }
    
    # Verify all are secrets
    let all_secrets = $secrets 
        | all { |item| $item | secret validate }
    assert $all_secrets "All items should be secrets"
    
    # Filter secrets (keeping them as secrets)
    let filtered_secrets = $secrets 
        | enumerate 
        | where index < 2
        | get item
    
    assert_eq ($filtered_secrets | length) 2 "Should have 2 filtered secrets"
    assert ($filtered_secrets.0 | secret validate) "Filtered secret 0 should be valid"
    assert ($filtered_secrets.1 | secret validate) "Filtered secret 1 should be valid"
    
    # Map over secrets (unwrap, transform, wrap again)
    let transformed_secrets = $secrets 
        | each { |secret| 
            let value = $secret | secret unwrap
            $"transformed-($value)" | secret wrap
        }
    
    let first_transformed = $transformed_secrets.0 | secret unwrap
    assert_eq $first_transformed "transformed-password1" "Transformation should work correctly"
}

# Test error handling and recovery
export def test_error_handling [] {
    # Test graceful handling of invalid operations
    let test_cases = [
        {
            desc: "Wrap wrong type",
            test: { 42 | secret wrap },
            should_fail: true,
            error_contains: "Expected string"
        },
        {
            desc: "Unwrap non-secret",
            test: { "regular-string" | secret unwrap },
            should_fail: true,
            error_contains: "Expected secret type"
        },
        {
            desc: "Validate non-secret",
            test: { "regular-string" | secret validate },
            should_fail: false,
            expected_result: false
        },
        {
            desc: "Type-of non-secret",
            test: { "regular-string" | secret type-of },
            should_fail: true,
            error_contains: "Expected secret type"
        }
    ]
    
    for case in $test_cases {
        if $case.should_fail {
            try {
                do $case.test
                assert false $"($case.desc) should have failed"
            } catch { |e|
                if "error_contains" in $case {
                    assert_contains $e.msg $case.error_contains $"($case.desc) should have appropriate error"
                }
            }
        } else {
            let result = do $case.test
            if "expected_result" in $case {
                assert_eq $result $case.expected_result $"($case.desc) should return expected result"
            }
        }
    }
}

# Test performance characteristics
export def test_performance_characteristics [] {
    # Test startup time (should be reasonable)
    let startup_test = time_operation { secret info | ignore }
    assert ($startup_test.duration < 2sec) $"Plugin startup should be under 2 seconds, was ($startup_test.duration)"
    
    # Test bulk operations
    let bulk_data = 0..50 | each { |i| $"bulk-secret-($i)" }
    
    let bulk_wrap_test = time_operation {
        $bulk_data | each { |item| $item | secret wrap } | ignore
    }
    assert ($bulk_wrap_test.duration < 5sec) $"Bulk wrap (51 items) should be under 5 seconds, was ($bulk_wrap_test.duration)"
    
    # Test validation performance
    let secrets = $bulk_data | each { |item| $item | secret wrap }
    let bulk_validate_test = time_operation {
        $secrets | each { |s| $s | secret validate } | ignore
    }
    assert ($bulk_validate_test.duration < 3sec) $"Bulk validate (51 items) should be under 3 seconds, was ($bulk_validate_test.duration)"
}

# Test memory behavior with large data
export def test_memory_behavior [] {
    # Test with progressively larger strings
    let sizes = [100, 1000, 5000]
    
    for size in $sizes {
        let large_string = "x" | fill --character "x" --length $size
        let secret = $large_string | secret wrap
        let revealed = $secret | secret unwrap
        
        assert_eq ($revealed | str length) $size $"Large string ($size) should preserve length"
        assert_eq $revealed $large_string $"Large string ($size) should round-trip correctly"
        
        # Verify redaction still works
        let display = $secret | to text
        assert_contains $display "redacted" $"Large string ($size) should still be redacted"
        assert_not_contains $display "xxxxx" $"Large string ($size) content should not leak"
    }
}

# Test concurrent operations (simulate rapid usage)
export def test_concurrent_operations [] {
    # Simulate rapid creation and usage of secrets
    let concurrent_secrets = 0..20 | each { |i|
        let secrets = [
            ($"string-($i)" | secret wrap),
            ($i | secret wrap),
            (($i % 2 == 0) | secret wrap)
        ]
        
        # Immediately use each secret
        $secrets | each { |s|
            {
                is_valid: ($s | secret validate),
                type: ($s | secret type-of),
                unwrapped: ($s | secret unwrap)
            }
        }
    } | flatten
    
    assert_eq ($concurrent_secrets | length) 63 "Should handle 63 concurrent operations"
    
    # Verify all operations succeeded
    let all_valid = $concurrent_secrets | all { |result| $result.is_valid }
    assert $all_valid "All concurrent secrets should be valid"
}

# Test plugin state consistency
export def test_plugin_state_consistency [] {
    # Test that plugin maintains consistent state across operations
    let initial_info = secret info
    
    # Perform various operations
    let _ = "test1" | secret wrap | secret unwrap
    let _ = 42 | secret wrap | secret validate
    let _ = true | secret wrap | secret type-of
    
    let final_info = secret info
    
    # Plugin info should remain consistent
    assert_eq ($initial_info | get plugin_name) ($final_info | get plugin_name) "Plugin name should remain consistent"
    assert_eq ($initial_info | get version) ($final_info | get version) "Version should remain consistent"
}

# Test real-world usage patterns
export def test_real_world_patterns [] {
    # Pattern 1: Configuration file with secrets
    let app_config = {
        database_url: ("postgresql://user:pass@localhost/db" | secret wrap),
        api_keys: {
            stripe: ("sk_test_1234567890" | secret wrap),
            openai: ("sk-1234567890abcdef" | secret wrap)
        },
        jwt_secret: ("super-secret-jwt-key" | secret wrap),
        debug_mode: false,
        port: 3000
    }
    
    # Verify secrets are protected
    assert ($app_config.database_url | secret validate) "Database URL should be secret"
    assert ($app_config.api_keys.stripe | secret validate) "Stripe key should be secret"
    assert ($app_config.api_keys.openai | secret validate) "OpenAI key should be secret"
    assert ($app_config.jwt_secret | secret validate) "JWT secret should be secret"
    
    # Verify non-secrets remain accessible
    assert_eq $app_config.debug_mode false "Debug mode should be accessible"
    assert_eq $app_config.port 3000 "Port should be accessible"
    
    # Pattern 2: Processing list of credentials
    let user_credentials = [
        {username: "admin", password: ("admin123" | secret wrap)},
        {username: "user1", password: ("password1" | secret wrap)},
        {username: "user2", password: ("password2" | secret wrap)}
    ]
    
    # Verify we can work with the structure while keeping secrets protected
    let usernames = $user_credentials | get username
    assert_eq ($usernames | length) 3 "Should extract all usernames"
    assert_eq $usernames.0 "admin" "Should get correct username"
    
    # Verify passwords remain secret
    let passwords_are_secret = $user_credentials 
        | get password 
        | all { |p| $p | secret validate }
    assert $passwords_are_secret "All passwords should remain secret"
}