Files
osmedeus/test/integration/cloud_integration_test.go
T
j3ssie 459d5939fb feat: add cloud infrastructure and nmap/tmux function support
- Implement cloud provider infrastructure (DigitalOcean, AWS, GCP, Linode, Azure) with Pulumi integration for distributed scanning
- Add nmap and tmux utility functions for port scanning results processing and long-running background session management
- Introduce webhook-triggered run execution with unique UUID and authentication key support for external integrations
2026-02-16 15:23:24 +07:00

589 lines
16 KiB
Go

package integration
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/j3ssie/osmedeus/v5/internal/cloud"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestCloudConfig_LoadAndSave tests cloud configuration persistence
func TestCloudConfig_LoadAndSave(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "cloud-settings.yaml")
// Create a test cloud config
cfg := &config.CloudConfigs{
Providers: config.Providers{
DigitalOcean: config.DigitalOceanConfig{
Token: "test-token-12345",
Region: "nyc3",
Size: "s-2vcpu-4gb",
Image: "ubuntu-22-04-x64",
},
},
Defaults: config.Defaults{
Provider: "digitalocean",
Mode: "vm",
MaxInstances: 5,
Timeout: "10m",
CleanupOnFailure: true,
},
Limits: config.Limits{
MaxHourlySpend: 1.00,
MaxTotalSpend: 10.00,
MaxInstances: 20,
},
State: config.State{
Backend: "local",
Path: filepath.Join(tempDir, "cloud-state"),
},
}
// Save configuration
err := cloud.SaveCloudConfig(cfg, configPath)
require.NoError(t, err)
// Verify file was created
_, err = os.Stat(configPath)
require.NoError(t, err)
// Load configuration back
loadedCfg, err := cloud.LoadCloudConfig(configPath)
require.NoError(t, err)
// Verify loaded config matches original
assert.Equal(t, cfg.Providers.DigitalOcean.Token, loadedCfg.Providers.DigitalOcean.Token)
assert.Equal(t, cfg.Providers.DigitalOcean.Region, loadedCfg.Providers.DigitalOcean.Region)
assert.Equal(t, cfg.Defaults.Provider, loadedCfg.Defaults.Provider)
assert.Equal(t, cfg.Defaults.MaxInstances, loadedCfg.Defaults.MaxInstances)
assert.Equal(t, cfg.Limits.MaxHourlySpend, loadedCfg.Limits.MaxHourlySpend)
}
// TestCloudConfig_EnvironmentVariableResolution tests environment variable substitution
func TestCloudConfig_EnvironmentVariableResolution(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "cloud-settings.yaml")
// Set test environment variables
testToken := "env-resolved-token-67890"
require.NoError(t, os.Setenv("TEST_DO_TOKEN", testToken))
defer func() {
_ = os.Unsetenv("TEST_DO_TOKEN")
}()
// Create config with environment variable reference
cfg := &config.CloudConfigs{
Providers: config.Providers{
DigitalOcean: config.DigitalOceanConfig{
Token: "${TEST_DO_TOKEN}",
Region: "nyc3",
},
},
Defaults: config.Defaults{
Provider: "digitalocean",
},
}
// Save configuration
err := cloud.SaveCloudConfig(cfg, configPath)
require.NoError(t, err)
// Load and resolve environment variables
loadedCfg, err := cloud.LoadCloudConfig(configPath)
require.NoError(t, err)
// Verify environment variable was resolved
assert.Equal(t, testToken, loadedCfg.Providers.DigitalOcean.Token)
}
// TestCloudConfig_Validation tests configuration validation
func TestCloudConfig_Validation(t *testing.T) {
tests := []struct {
name string
config *config.CloudConfigs
shouldError bool
errorMsg string
}{
{
name: "valid_config",
config: &config.CloudConfigs{
Providers: config.Providers{
DigitalOcean: config.DigitalOceanConfig{
Token: "valid-token",
Region: "nyc3",
},
},
Defaults: config.Defaults{
Provider: "digitalocean",
MaxInstances: 5,
},
Limits: config.Limits{
MaxHourlySpend: 10.0,
MaxTotalSpend: 100.0,
MaxInstances: 20,
},
},
shouldError: false,
},
{
name: "missing_provider_token",
config: &config.CloudConfigs{
Defaults: config.Defaults{
Provider: "digitalocean",
MaxInstances: 5,
},
Limits: config.Limits{
MaxHourlySpend: 10.0,
MaxTotalSpend: 100.0,
MaxInstances: 20,
},
},
shouldError: true,
errorMsg: "token",
},
{
name: "invalid_provider",
config: &config.CloudConfigs{
Defaults: config.Defaults{
Provider: "invalid-provider",
MaxInstances: 5,
},
Limits: config.Limits{
MaxHourlySpend: 10.0,
MaxTotalSpend: 100.0,
MaxInstances: 20,
},
},
shouldError: true,
errorMsg: "provider",
},
{
name: "invalid_max_instances",
config: &config.CloudConfigs{
Providers: config.Providers{
DigitalOcean: config.DigitalOceanConfig{
Token: "valid-token",
Region: "nyc3",
},
},
Defaults: config.Defaults{
Provider: "digitalocean",
MaxInstances: 0,
},
Limits: config.Limits{
MaxHourlySpend: 10.0,
MaxTotalSpend: 100.0,
MaxInstances: 20,
},
},
shouldError: true,
errorMsg: "instances",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cloud.ValidateCloudConfig(tt.config)
if tt.shouldError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
// TestCostTracking tests cost tracking functionality
func TestCostTracking(t *testing.T) {
hourlyRate := 0.02232 // s-2vcpu-4gb DigitalOcean droplet
maxSpend := 10.00
tracker := cloud.NewCostTracker(hourlyRate, maxSpend)
// Initial cost should be very close to 0
initialCost := tracker.GetCurrentCost()
assert.InDelta(t, 0.0, initialCost, 0.001)
// Wait a bit
time.Sleep(100 * time.Millisecond)
// Cost should have increased
cost := tracker.GetCurrentCost()
assert.Greater(t, cost, 0.0)
// Elapsed time should be positive
elapsed := tracker.GetElapsedTime()
assert.Greater(t, elapsed, time.Duration(0))
// Check limits (should pass with low spend)
err := tracker.CheckLimits()
assert.NoError(t, err)
// Get summary
summary := tracker.GetCostSummary()
assert.Contains(t, summary, "Elapsed")
assert.Contains(t, summary, "Cost")
assert.Contains(t, summary, "Hourly Rate")
// Test estimated final cost
estimatedCost := tracker.GetEstimatedFinalCost(1 * time.Hour)
assert.Equal(t, hourlyRate, estimatedCost)
}
// TestCostLimitCheck tests cost limit validation
func TestCostLimitCheck(t *testing.T) {
// Use very high hourly rate to trigger limit quickly
hourlyRate := 100000.0 // $100k/hour
maxSpend := 0.01 // $0.01 limit
tracker := cloud.NewCostTracker(hourlyRate, maxSpend)
// Wait for cost to accumulate
time.Sleep(100 * time.Millisecond)
// Should exceed limit
err := tracker.CheckLimits()
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum total spend")
}
// TestInfrastructureState tests infrastructure state management
func TestInfrastructureState(t *testing.T) {
tempDir := t.TempDir()
stateDir := filepath.Join(tempDir, "cloud-state")
// Create a test infrastructure
infra := &cloud.Infrastructure{
ID: "test-infra-12345",
Provider: "digitalocean",
Mode: "vm",
CreatedAt: time.Now(),
PulumiStackID: "test-stack",
Resources: []cloud.Resource{
{
Type: "vm",
ID: "droplet-123",
PublicIP: "203.0.113.1",
PrivateIP: "10.0.0.1",
Status: "active",
},
},
Metadata: map[string]interface{}{
"region": "nyc3",
"size": "s-2vcpu-4gb",
},
}
// Save infrastructure state
err := cloud.SaveInfrastructureState(infra, stateDir)
require.NoError(t, err)
// Verify state file was created
stateFile := filepath.Join(stateDir, "infrastructure", infra.ID+".json")
_, err = os.Stat(stateFile)
require.NoError(t, err)
// Check if infrastructure exists
exists := cloud.InfrastructureExists(infra.ID, stateDir)
assert.True(t, exists)
// Load infrastructure state
loadedInfra, err := cloud.LoadInfrastructureState(infra.ID, stateDir)
require.NoError(t, err)
// Verify loaded state matches original
assert.Equal(t, infra.ID, loadedInfra.ID)
assert.Equal(t, infra.Provider, loadedInfra.Provider)
assert.Equal(t, infra.Mode, loadedInfra.Mode)
assert.Equal(t, len(infra.Resources), len(loadedInfra.Resources))
assert.Equal(t, infra.Resources[0].ID, loadedInfra.Resources[0].ID)
// List all infrastructure states
states, err := cloud.ListInfrastructures(stateDir)
require.NoError(t, err)
assert.Len(t, states, 1)
assert.Equal(t, infra.ID, states[0].ID)
// Remove infrastructure state
err = cloud.RemoveInfrastructureState(infra.ID, stateDir)
require.NoError(t, err)
// Verify state file was deleted
exists = cloud.InfrastructureExists(infra.ID, stateDir)
assert.False(t, exists)
_, err = os.Stat(stateFile)
assert.True(t, os.IsNotExist(err))
}
// TestMultipleInfrastructureStates tests managing multiple infrastructure states
func TestMultipleInfrastructureStates(t *testing.T) {
tempDir := t.TempDir()
stateDir := filepath.Join(tempDir, "cloud-state")
// Create multiple infrastructure states
infras := []*cloud.Infrastructure{
{
ID: "infra-1",
Provider: "digitalocean",
Mode: "vm",
},
{
ID: "infra-2",
Provider: "aws",
Mode: "vm",
},
{
ID: "infra-3",
Provider: "gcp",
Mode: "serverless",
},
}
// Save all states
for _, infra := range infras {
err := cloud.SaveInfrastructureState(infra, stateDir)
require.NoError(t, err)
}
// List all states
states, err := cloud.ListInfrastructures(stateDir)
require.NoError(t, err)
assert.Len(t, states, 3)
// Verify each state can be loaded
for _, infra := range infras {
loaded, err := cloud.LoadInfrastructureState(infra.ID, stateDir)
require.NoError(t, err)
assert.Equal(t, infra.ID, loaded.ID)
assert.Equal(t, infra.Provider, loaded.Provider)
}
// Remove one state
err = cloud.RemoveInfrastructureState("infra-2", stateDir)
require.NoError(t, err)
// Verify only 2 states remain
states, err = cloud.ListInfrastructures(stateDir)
require.NoError(t, err)
assert.Len(t, states, 2)
}
// TestDefaultCloudConfig tests default configuration generation
func TestDefaultCloudConfig(t *testing.T) {
cfg := config.DefaultCloudConfigs()
// Verify defaults are set
assert.Equal(t, "local", cfg.State.Backend)
assert.Equal(t, "vm", cfg.Defaults.Mode)
assert.Equal(t, "digitalocean", cfg.Defaults.Provider)
assert.Greater(t, cfg.Defaults.MaxInstances, 0)
assert.Greater(t, cfg.Limits.MaxInstances, 0)
assert.NotEmpty(t, cfg.Defaults.Timeout)
// Verify provider configs have placeholder values
assert.Contains(t, cfg.Providers.DigitalOcean.Token, "${")
assert.Contains(t, cfg.Providers.AWS.AccessKeyID, "${")
assert.Contains(t, cfg.Providers.GCP.ProjectID, "${")
// Verify sensible default regions
assert.NotEmpty(t, cfg.Providers.DigitalOcean.Region)
assert.NotEmpty(t, cfg.Providers.AWS.Region)
assert.NotEmpty(t, cfg.Providers.GCP.Region)
}
// TestInfrastructureStateEmpty tests loading from empty state directory
func TestInfrastructureStateEmpty(t *testing.T) {
tempDir := t.TempDir()
stateDir := filepath.Join(tempDir, "cloud-state")
// List infrastructures from non-existent directory
states, err := cloud.ListInfrastructures(stateDir)
require.NoError(t, err)
assert.Len(t, states, 0)
// Check non-existent infrastructure
exists := cloud.InfrastructureExists("non-existent-id", stateDir)
assert.False(t, exists)
// Try to load non-existent infrastructure
_, err = cloud.LoadInfrastructureState("non-existent-id", stateDir)
assert.Error(t, err)
}
// TestCloudConfigMultipleProviders tests configuring multiple cloud providers
func TestCloudConfigMultipleProviders(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "cloud-settings.yaml")
// Create config with multiple providers
cfg := &config.CloudConfigs{
Providers: config.Providers{
DigitalOcean: config.DigitalOceanConfig{
Token: "do-token-12345",
Region: "nyc3",
Size: "s-2vcpu-4gb",
},
AWS: config.AWSConfig{
AccessKeyID: "aws-key-12345",
SecretAccessKey: "aws-secret-12345",
Region: "us-east-1",
InstanceType: "t3.medium",
},
GCP: config.GCPConfig{
ProjectID: "gcp-project-12345",
CredentialsFile: "/path/to/credentials.json",
Region: "us-central1",
MachineType: "n1-standard-2",
},
},
Defaults: config.Defaults{
Provider: "digitalocean",
MaxInstances: 10,
},
Limits: config.Limits{
MaxHourlySpend: 5.0,
MaxTotalSpend: 50.0,
MaxInstances: 20,
},
}
// Save configuration
err := cloud.SaveCloudConfig(cfg, configPath)
require.NoError(t, err)
// Load configuration back
loadedCfg, err := cloud.LoadCloudConfig(configPath)
require.NoError(t, err)
// Verify all providers are configured
assert.Equal(t, cfg.Providers.DigitalOcean.Token, loadedCfg.Providers.DigitalOcean.Token)
assert.Equal(t, cfg.Providers.AWS.AccessKeyID, loadedCfg.Providers.AWS.AccessKeyID)
assert.Equal(t, cfg.Providers.GCP.ProjectID, loadedCfg.Providers.GCP.ProjectID)
}
// TestInfrastructureMetadata tests infrastructure metadata handling
func TestInfrastructureMetadata(t *testing.T) {
tempDir := t.TempDir()
stateDir := filepath.Join(tempDir, "cloud-state")
// Create infrastructure with metadata
infra := &cloud.Infrastructure{
ID: "test-infra-meta",
Provider: "digitalocean",
Mode: "vm",
Metadata: map[string]interface{}{
"region": "nyc3",
"size": "s-2vcpu-4gb",
"worker_count": 3,
"tags": []string{"production", "web"},
"custom_config": map[string]string{"key": "value"},
},
}
// Save and reload
err := cloud.SaveInfrastructureState(infra, stateDir)
require.NoError(t, err)
loadedInfra, err := cloud.LoadInfrastructureState(infra.ID, stateDir)
require.NoError(t, err)
// Verify metadata is preserved
assert.Equal(t, "nyc3", loadedInfra.Metadata["region"])
assert.Equal(t, "s-2vcpu-4gb", loadedInfra.Metadata["size"])
assert.Equal(t, float64(3), loadedInfra.Metadata["worker_count"])
}
// TestCloudConfigValidation_AWS tests AWS-specific validation
func TestCloudConfigValidation_AWS(t *testing.T) {
cfg := &config.CloudConfigs{
Providers: config.Providers{
AWS: config.AWSConfig{
AccessKeyID: "test-key",
SecretAccessKey: "test-secret",
Region: "us-east-1",
},
},
Defaults: config.Defaults{
Provider: "aws",
MaxInstances: 5,
},
Limits: config.Limits{
MaxHourlySpend: 10.0,
MaxTotalSpend: 100.0,
MaxInstances: 20,
},
}
err := cloud.ValidateCloudConfig(cfg)
assert.NoError(t, err)
// Test with missing credentials
cfg.Providers.AWS.AccessKeyID = ""
err = cloud.ValidateCloudConfig(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "AWS credentials")
}
// TestCloudConfigValidation_GCP tests GCP-specific validation
func TestCloudConfigValidation_GCP(t *testing.T) {
cfg := &config.CloudConfigs{
Providers: config.Providers{
GCP: config.GCPConfig{
ProjectID: "test-project",
CredentialsFile: "/path/to/creds.json",
Region: "us-central1",
},
},
Defaults: config.Defaults{
Provider: "gcp",
MaxInstances: 5,
},
Limits: config.Limits{
MaxHourlySpend: 10.0,
MaxTotalSpend: 100.0,
MaxInstances: 20,
},
}
err := cloud.ValidateCloudConfig(cfg)
assert.NoError(t, err)
// Test with missing credentials
cfg.Providers.GCP.ProjectID = ""
err = cloud.ValidateCloudConfig(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "GCP credentials")
}
// TestCostEstimation tests cost estimation for different durations
func TestCostEstimation(t *testing.T) {
hourlyRate := 0.02232 // DigitalOcean s-2vcpu-4gb
maxSpend := 100.0
tracker := cloud.NewCostTracker(hourlyRate, maxSpend)
tests := []struct {
duration time.Duration
expectedCost float64
}{
{1 * time.Hour, 0.02232},
{24 * time.Hour, 0.53568}, // Daily
{168 * time.Hour, 3.74976}, // Weekly (7 * 24 hours)
}
for _, tt := range tests {
cost := tracker.GetEstimatedFinalCost(tt.duration)
assert.InDelta(t, tt.expectedCost, cost, 0.00001)
}
}