Ctrl K
ComunidadGuías…

cloudflare_runtime_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"netgoat.xyz/agent/internal/cloudflare"
	"netgoat.xyz/agent/internal/config"
)

func TestConfigureCloudflareAccessMapsConfigAndFailsClosed(t *testing.T) {
	cfg := &config.Config{}
	cfg.Cloudflare.Access.Enabled = true
	cfg.Cloudflare.Access.Issuer = "https://team.cloudflareaccess.com"
	cfg.Cloudflare.Access.Audience = []string{"audience-tag"}
	cfg.Cloudflare.Access.JWKSCacheSeconds = 90
	cfg.Cloudflare.Access.ClockSkewSeconds = 30
	cfg.Cloudflare.Access.FetchTimeoutSeconds = 5

	settings, err := cloudflareAccessSettingsFromConfig(cfg)
	if err != nil {
		t.Fatalf("cloudflareAccessSettingsFromConfig(): %v", err)
	}
	if settings.CacheTTL != 90*time.Second || settings.ClockSkew != 30*time.Second || settings.FetchTimeout != 5*time.Second {
		t.Fatalf("unexpected Cloudflare Access durations: %+v", settings)
	}
	validator, err := configureCloudflareAccess(cfg)
	if err != nil {
		t.Fatalf("configureCloudflareAccess(): %v", err)
	}
	if validator == nil {
		t.Fatal("configureCloudflareAccess() returned nil validator")
	}

	handler := validator.Middleware(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		t.Fatal("request with no Access assertion reached wrapped handler")
	}))
	response := httptest.NewRecorder()
	handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "https://agent.example.test/", nil))
	if response.Code != http.StatusForbidden {
		t.Fatalf("missing Access assertion status = %d, want %d", response.Code, http.StatusForbidden)
	}
}

func TestCloudflareAccessDisabledDoesNothing(t *testing.T) {
	validator, err := configureCloudflareAccess(&config.Config{})
	if err != nil || validator != nil {
		t.Fatalf("disabled Access = %v, %v", validator, err)
	}
}

func TestCloudflareReconciliationDisabledDoesNothing(t *testing.T) {
	previousFactory := newCloudflareAPIClient
	defer func() { newCloudflareAPIClient = previousFactory }()
	newCloudflareAPIClient = func(cloudflare.APIConfig) (cloudflareReconciliationClient, error) {
		t.Fatal("disabled reconciliation constructed an API client")
		return nil, nil
	}
	results, err := reconcileCloudflare(context.Background(), &config.Config{})
	if err != nil || len(results) != 0 {
		t.Fatalf("disabled reconciliation = %#v, %v", results, err)
	}
}

func TestReconcileCloudflareMapsBoundedDryRunPlan(t *testing.T) {
	cfg := &config.Config{}
	cfg.Cloudflare.Reconciliation.Enabled = true
	cfg.Cloudflare.Reconciliation.AccountID = "0123456789abcdef0123456789abcdef"
	cfg.Cloudflare.Reconciliation.DNSRecords = []config.CloudflareDNSRecord{
		{
			ZoneID: "abcdef0123456789abcdef0123456789",
			Record: map[string]any{
				"type":    "A",
				"name":    "app.example.test",
				"content": "203.0.113.10",
			},
		},
		{
			ZoneID:   "abcdef0123456789abcdef0123456789",
			RecordID: "fedcba9876543210fedcba9876543210",
			Delete:   true,
		},
	}
	cfg.Cloudflare.Reconciliation.Tunnels = []config.CloudflareTunnel{
		{
			Tunnel: map[string]any{
				"name":       "new-netgoat",
				"config_src": "cloudflare",
			},
		},
		{
			TunnelID: "01234567-89ab-cdef-0123-456789abcdef",
			Tunnel: map[string]any{
				"name":       "netgoat",
				"config_src": "cloudflare",
			},
		},
	}
	t.Setenv(cloudflareAPITokenEnvironment, "test-api-token")

	fake := &recordingCloudflareClient{}
	previousFactory := newCloudflareAPIClient
	defer func() { newCloudflareAPIClient = previousFactory }()
	var received cloudflare.APIConfig
	newCloudflareAPIClient = func(settings cloudflare.APIConfig) (cloudflareReconciliationClient, error) {
		received = settings
		return fake, nil
	}

	results, err := reconcileCloudflare(context.Background(), cfg)
	if err != nil {
		t.Fatalf("reconcileCloudflare(): %v", err)
	}
	if !received.Enabled || !received.DryRun || received.APIToken != "test-api-token" || received.AccountID != cfg.Cloudflare.Reconciliation.AccountID {
		t.Fatalf("unexpected Cloudflare API settings: %+v", received)
	}
	if len(results) != 4 || len(fake.calls) != 4 {
		t.Fatalf("reconciliation calls/results = %d/%d, want 4/4", len(fake.calls), len(results))
	}
	if fake.calls[0].kind != "dns-upsert" || fake.calls[0].desired["name"] != "app.example.test" {
		t.Fatalf("DNS creation was not mapped correctly: %+v", fake.calls[0])
	}
	if fake.calls[1].kind != "dns-delete" || fake.calls[1].id != "fedcba9876543210fedcba9876543210" {
		t.Fatalf("DNS deletion was not mapped correctly: %+v", fake.calls[1])
	}
	if fake.calls[2].kind != "tunnel-create" || fake.calls[2].desired["name"] != "new-netgoat" {
		t.Fatalf("tunnel creation was not mapped correctly: %+v", fake.calls[2])
	}
	if fake.calls[3].kind != "tunnel-update" || fake.calls[3].desired["config_src"] != "cloudflare" {
		t.Fatalf("tunnel update was not mapped correctly: %+v", fake.calls[3])
	}
}

func TestCloudflareReconciliationRequiresEnvironmentCredential(t *testing.T) {
	cfg := &config.Config{}
	cfg.Cloudflare.Reconciliation.Enabled = true
	cfg.Cloudflare.Reconciliation.DNSRecords = []config.CloudflareDNSRecord{{
		ZoneID: "abcdef0123456789abcdef0123456789",
		Record: map[string]any{"type": "A"},
	}}
	t.Setenv(cloudflareAPITokenEnvironment, "")
	if _, err := cloudflareReconciliationPlanFromConfig(cfg); err == nil || !strings.Contains(err.Error(), cloudflareAPITokenEnvironment) {
		t.Fatalf("missing token error = %v", err)
	}
}

func TestCloudflareReconciliationRequiresTunnelIDForDeletion(t *testing.T) {
	cfg := &config.Config{}
	cfg.Cloudflare.Reconciliation.Enabled = true
	cfg.Cloudflare.Reconciliation.AccountID = "0123456789abcdef0123456789abcdef"
	cfg.Cloudflare.Reconciliation.Tunnels = []config.CloudflareTunnel{{Delete: true}}
	t.Setenv(cloudflareAPITokenEnvironment, "test-api-token")

	if _, err := cloudflareReconciliationPlanFromConfig(cfg); err == nil || !strings.Contains(err.Error(), "deletion requires tunnel_id") {
		t.Fatalf("missing tunnel ID deletion error = %v", err)
	}
}

func TestCloudflareReconciliationPreflightsIdentifiers(t *testing.T) {
	cfg := &config.Config{}
	cfg.Cloudflare.Reconciliation.Enabled = true
	cfg.Cloudflare.Reconciliation.DNSRecords = []config.CloudflareDNSRecord{{
		ZoneID: "not-a-zone-id",
		Record: map[string]any{"type": "A"},
	}}
	t.Setenv(cloudflareAPITokenEnvironment, "test-api-token")
	if _, err := cloudflareReconciliationPlanFromConfig(cfg); err == nil || !strings.Contains(err.Error(), "invalid Cloudflare DNS record") {
		t.Fatalf("invalid ID error = %v", err)
	}
}

type recordingCloudflareCall struct {
	kind    string
	zoneID  string
	id      string
	desired map[string]any
}

type recordingCloudflareClient struct {
	calls []recordingCloudflareCall
}

func (client *recordingCloudflareClient) ReconcileDNSRecord(_ context.Context, zoneID, recordID string, desired any) (cloudflare.APIResult, error) {
	object, _ := desired.(map[string]any)
	client.calls = append(client.calls, recordingCloudflareCall{kind: "dns-upsert", zoneID: zoneID, id: recordID, desired: object})
	return cloudflare.APIResult{DryRun: true, Method: http.MethodPost, Path: "/dns"}, nil
}

func (client *recordingCloudflareClient) DeleteDNSRecord(_ context.Context, zoneID, recordID string) (cloudflare.APIResult, error) {
	client.calls = append(client.calls, recordingCloudflareCall{kind: "dns-delete", zoneID: zoneID, id: recordID})
	return cloudflare.APIResult{DryRun: true, Method: http.MethodDelete, Path: "/dns"}, nil
}

func (client *recordingCloudflareClient) CreateTunnel(_ context.Context, desired any) (cloudflare.APIResult, error) {
	object, _ := desired.(map[string]any)
	client.calls = append(client.calls, recordingCloudflareCall{kind: "tunnel-create", desired: object})
	return cloudflare.APIResult{DryRun: true, Method: http.MethodPost, Path: "/tunnel"}, nil
}

func (client *recordingCloudflareClient) ReconcileTunnel(_ context.Context, tunnelID string, desired any) (cloudflare.APIResult, error) {
	object, _ := desired.(map[string]any)
	client.calls = append(client.calls, recordingCloudflareCall{kind: "tunnel-update", id: tunnelID, desired: object})
	return cloudflare.APIResult{DryRun: true, Method: http.MethodPatch, Path: "/tunnel"}, nil
}

func (client *recordingCloudflareClient) DeleteTunnel(_ context.Context, tunnelID string) (cloudflare.APIResult, error) {
	client.calls = append(client.calls, recordingCloudflareCall{kind: "tunnel-delete", id: tunnelID})
	return cloudflare.APIResult{DryRun: true, Method: http.MethodDelete, Path: "/tunnel"}, nil
}