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)!