Skip to content

Latest commit

 

History

History
407 lines (313 loc) · 10.9 KB

File metadata and controls

407 lines (313 loc) · 10.9 KB

Environment-Dependent Behavior - The Hidden Time Bomb

The Problem

Code that behaves differently across environments creates:

  • False confidence - Tests pass but production fails
  • Impossible debugging - Works locally, breaks in production
  • Security vulnerabilities - Protection only in some environments
  • Silent failures - Different behavior with no warnings

The Deadly Pattern

// BAD: Validation only in development
if (WP_DEBUG) {
    if (!is_email($user->user_email)) {
        return false;
    }
}

// Sends to invalid emails in production!
return wp_mail($user->user_email, $subject, $message);

What happens:

  • ✅ Development: WP_DEBUG = true → Validation runs → Works fine
  • ❌ Staging: WP_DEBUG = false → No validation → Sometimes fails
  • ❌ Production: WP_DEBUG = false → No validation → Silent failures
  • ✅ Tests: WP_DEBUG = true → Validation runs → All pass

Result: Tests pass, production fails for 15 months!

Real-World Impact

Scenario 1: Email Notifications

The Code:

function send_email_notification($user_id, $subject, $message) {
    $user = get_user_by('id', $user_id);
    
    // Only validate in development
    if (WP_DEBUG) {
        if (!is_email($user->user_email)) {
            error_log("Invalid email for user {$user_id}");
            return false;
        }
    }
    
    return wp_mail($user->user_email, $subject, $message);
}

Timeline:

  • Dev: Clean test data, all emails valid → ✅ Works
  • Tests: Mock data, all emails valid → ✅ Pass
  • Production: Legacy users with invalid emails → ❌ Fails silently
  • 15 months later: Users report missing notifications
  • Impact: Thousands of users never received critical emails

Scenario 2: API Rate Limiting

The Code:

function api_request($endpoint, $data) {
    // Only check rate limit in production
    if (!WP_DEBUG && !check_rate_limit(get_current_user_id())) {
        return new WP_Error('rate_limit', 'Too many requests');
    }
    
    return wp_remote_post(API_URL . $endpoint, ['body' => $data]);
}

What happened:

  • Dev: No rate limiting → Easy to test → ✅ Happy developers
  • Tests: Rate limiting bypassed → ✅ All pass
  • Production: Rate limiting enabled but has a bug → ❌ Fails
  • Result: Bug not caught until production, $5000 unexpected API bill

Scenario 3: Payment Validation

The Code:

function process_payment($amount, $card_token) {
    // Skip validation in dev
    if (!WP_DEBUG) {
        if ($amount < 0.50) {
            return new WP_Error('amount_too_small', 'Minimum $0.50');
        }
        
        if (!validate_card_token($card_token)) {
            return new WP_Error('invalid_card', 'Invalid card token');
        }
    }
    
    return charge_card($amount, $card_token);
}

Result:

  • Dev: Validation skipped → Can test quickly
  • Tests: Validation skipped → Can't test validation logic!
  • Production: Validation runs but has bugs → Chargebacks and fraud

Why This Is So Dangerous

1. Tests Give False Confidence

public function test_send_email() {
    // Test runs with WP_DEBUG = true
    $result = send_email_notification(123, 'Test', 'Message');
    
    $this->assertTrue($result);
    // ✅ Test passes!
}

But in production (WP_DEBUG = false), the code path is completely different!

2. Security Only in Production

// SQL injection protection only in production
if (WP_DEBUG) {
    $sql = "SELECT * FROM products WHERE name LIKE '%{$search}%'";
} else {
    $sql = $wpdb->prepare("SELECT * FROM products WHERE name LIKE %s", $search);
}

Security audit discovers:

  • Development environment: Vulnerable to SQL injection
  • Questions entire codebase security practices
  • Even though production is "safe," the practice is condemned

3. Silent Failures

// Error logging completely disabled
try {
    process_import($data);
} catch (Exception $e) {
    // error_log('Import failed: ' . $e->getMessage());
    return false;
}

In development: IDE shows exceptions
In production: Silent failures, no logs, impossible to debug

4. Debug Info Exposure

function process_order($order_id) {
    $order = wc_get_order($order_id);
    
    // Condition commented out!
    // if (WP_DEBUG) {
        echo '<pre>';
        print_r($order);  // Contains payment info!
        var_dump($_POST);  // Contains card details!
    // }
    
    return finalize_order($order);
}

Result: Customer payment information exposed to all users!

The Environment Matrix

Code Behavior Development Staging Production Tests
Validation ✅ Runs ❌ Skipped ❌ Skipped ✅ Runs
Rate Limiting ❌ Disabled ✅ Enabled ✅ Enabled ❌ Disabled
SQL Protection ❌ Vulnerable ✅ Protected ✅ Protected ❌ Vulnerable
Permission Checks ❌ Skipped ✅ Enforced ✅ Enforced ❌ Skipped
File Upload Validation ❌ Skipped ✅ Enforced ✅ Enforced ❌ Skipped
Caching ❌ Disabled ❌ Disabled ❌ Disabled ❌ Disabled
Error Logging ❌ Disabled ❌ Disabled ❌ Disabled ❌ Disabled

Every environment runs different code!

Good Practice

Rule 1: Same Code, Different Config

// BAD: Different code paths
if (WP_DEBUG) {
    // Validation in dev only
}

// GOOD: Same code, different settings
class RateLimiter {
    private function get_max_requests(): int {
        // Config-driven, not code-driven
        return defined('API_RATE_LIMIT') ? API_RATE_LIMIT : 60;
    }
}

Configuration per environment:

// wp-config.php - Development
define('API_RATE_LIMIT', 1000);  // High limit for testing

// wp-config.php - Production
define('API_RATE_LIMIT', 60);    // Low limit for safety

Rule 2: Always Validate, Always Protect

// GOOD: Always validate
function send_email_notification(int $user_id, string $subject, string $message): bool {
    $user = get_user_by('id', $user_id);
    
    // ALWAYS validate - no exceptions!
    if (!$user || !is_email($user->user_email)) {
        error_log("Invalid email for user {$user_id}");
        return false;
    }
    
    return wp_mail($user->user_email, $subject, $message);
}

Rule 3: Environment-Aware Logging

// GOOD: Log differently per environment, but always log
function process_order(int $order_id): bool {
    $order = wc_get_order($order_id);
    
    // Log to file in dev
    if (WP_DEBUG && WP_DEBUG_LOG) {
        error_log("Processing order {$order_id}");
    }
    
    // Send to monitoring service in production
    if (defined('SENTRY_DSN') && !WP_DEBUG) {
        sentry_capture_message("Processing order {$order_id}");
    }
    
    // No debug output to screen - EVER
    return finalize_order($order);
}

Rule 4: Feature Flags, Not Comments

// BAD: Comments to enable features
function calculate_loyalty_points($amount) {
    // return $amount * 1.5;  // New algorithm
    return $amount * 1.0;      // Old algorithm
}

// GOOD: Proper feature flags
function calculate_loyalty_points(float $amount): float {
    $features = new FeatureFlags();
    
    if ($features->is_enabled('new_loyalty_algorithm')) {
        return $amount * 1.5;
    }
    
    return $amount * 1.0;
}

Rule 5: Test What You Deploy

// GOOD: Tests run same code as production
class EmailNotificationTest extends WP_UnitTestCase {
    public function test_invalid_email_handling() {
        // Create user with invalid email
        $user_id = $this->factory->user->create([
            'user_email' => 'not-an-email'
        ]);
        
        // Should fail validation
        $result = send_email_notification($user_id, 'Test', 'Message');
        
        $this->assertFalse($result);
        // This test actually tests production code path!
    }
}

Common Excuses (And Why They're Wrong)

"Validation is too slow in development"

Wrong: If validation is slow, optimize it. Don't skip it.

"I need to test with invalid data"

Right: Mock the validator, don't disable it globally.

"Rate limiting breaks my tests"

Wrong: Your tests should test rate limiting! Use dependency injection.

"It's just for debugging"

Wrong: Use proper logging and debugging tools, not conditional code.

How to Fix

Step 1: Find Environment-Dependent Code

# Search for WP_DEBUG conditions
grep -r "if.*WP_DEBUG" . --include="*.php"

# Search for environment checks
grep -r "WP_ENVIRONMENT_TYPE" . --include="*.php"

# Search for commented conditionals
grep -r "//.*if.*WP_DEBUG" . --include="*.php"

Step 2: For Each Occurrence, Ask:

  1. Does this change behavior? → ❌ Remove it
  2. Does this change settings? → ✅ Use config
  3. Does this add logging? → ✅ Keep, but log differently
  4. Does this skip validation? → ❌ Always validate

Step 3: Use Proper Environment Config

// wp-config-development.php
define('API_RATE_LIMIT', 1000);
define('CACHE_TTL', 60);
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', true);

// wp-config-production.php
define('API_RATE_LIMIT', 60);
define('CACHE_TTL', 3600);
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SENTRY_DSN', 'https://...');

Step 4: Write Tests That Match Production

// Set test environment to match production
class ProductionLikeTest extends WP_UnitTestCase {
    protected function setUp(): void {
        parent::setUp();
        
        // Temporarily disable WP_DEBUG for this test
        $this->wp_debug_original = WP_DEBUG;
        if (!defined('WP_DEBUG')) {
            define('WP_DEBUG', false);
        }
    }
    
    protected function tearDown(): void {
        // Restore
        parent::tearDown();
    }
}

Key Takeaways

Same code in all environments
Different config, not different code
Always validate, always protect
Test production code paths
Log differently, never skip logging
Use feature flags, not comments
Never skip security in dev

❌ Don't use if (WP_DEBUG) for behavior changes
❌ Don't skip validation in development
❌ Don't disable security for testing
❌ Don't comment out protection
❌ Don't have different code paths per environment
❌ Don't give tests false confidence

The Bottom Line

If your code behaves differently in development and production, your tests are lying to you.

// This is a time bomb:
if (WP_DEBUG) {
    validate($input);  // Only in dev
}
process($input);  // Always runs

// This is safe:
validate($input);  // Always runs
process($input);  // Always runs

Write code that works the same everywhere. Use configuration for environment differences.

Your production users will thank you (because they'll actually receive their notifications)!