package cmd import ( "context" "errors" "strings" "testing" "github.com/Pingu-Studio/MaidnCLI/internal/cloudflare" "github.com/Pingu-Studio/MaidnCLI/internal/config" ) type fakeCloudflareRouteAPI struct { ensured bool deleted bool ensureErr error deleteCalls int } func (*fakeCloudflareRouteAPI) ListAccounts(context.Context) ([]cloudflare.Account, error) { return nil, nil } func (*fakeCloudflareRouteAPI) ListTunnels(context.Context, string, string) ([]cloudflare.Tunnel, error) { return nil, nil } func (*fakeCloudflareRouteAPI) CreateTunnel(context.Context, string, string) (cloudflare.Tunnel, error) { return cloudflare.Tunnel{}, nil } func (f *fakeCloudflareRouteAPI) EnsureCNAME(_ context.Context, zoneID, hostname, tunnelID string) error { if zoneID != "zone-id" || hostname != "app.example.test" || tunnelID != "tunnel-id" { return &routeTestError{} } f.ensured = true return f.ensureErr } func (f *fakeCloudflareRouteAPI) DeleteCNAME(_ context.Context, zoneID, hostname, tunnelID string) error { if zoneID != "zone-id" || hostname != "app.example.test" || tunnelID != "tunnel-id" { return &routeTestError{} } f.deleted = true f.deleteCalls++ return nil } type routeTestError struct{} func (*routeTestError) Error() string { return "unexpected route request" } func TestCloudflareTunnelRouteCommandsUpdateEncryptedStateAndDNS(t *testing.T) { originalLoad := loadCloudflareTunnelConfig originalRead := readCloudflareTunnelSecrets originalWrite := writeCloudflareTunnelSecrets originalClient := newCloudflareRouteClient originalInitialize := initializeCloudflareTunnelOpenBao originalPath, originalHostname, originalService, originalZoneID := cloudflareTunnelConfigPath, cloudflareTunnelHostname, cloudflareTunnelService, cloudflareTunnelZoneID t.Cleanup(func() { loadCloudflareTunnelConfig = originalLoad readCloudflareTunnelSecrets = originalRead writeCloudflareTunnelSecrets = originalWrite newCloudflareRouteClient = originalClient initializeCloudflareTunnelOpenBao = originalInitialize cloudflareTunnelConfigPath, cloudflareTunnelHostname, cloudflareTunnelService, cloudflareTunnelZoneID = originalPath, originalHostname, originalService, originalZoneID }) values, err := (cloudflare.StoredTunnel{ Credentials: cloudflare.Credentials{AccountTag: "account-id", TunnelSecret: "test-tunnel-secret", TunnelID: "tunnel-id"}, Config: cloudflare.NewConfig("tunnel-id"), }).Values() if err != nil { t.Fatal(err) } state := map[string]map[string]string{ "platform/cloudflare": {"api-token": "test-api-token"}, "platform/cloudflare-tunnel": values, } client := &fakeCloudflareRouteAPI{} loadCloudflareTunnelConfig = func(string) (config.Config, error) { return config.Config{SOPS: config.SOPSConfig{OperationalSecretsPath: "secrets", AgeKeyPath: "age"}}, nil } readCloudflareTunnelSecrets = func(string, string) (map[string]map[string]string, error) { return state, nil } writes := 0 writeCloudflareTunnelSecrets = func(_ string, _ string, secrets map[string]map[string]string) error { if writes == 0 && !client.ensured { t.Fatal("route add saved config before ensuring the CNAME") } if writes == 1 && client.deleted { t.Fatal("route remove deleted the CNAME before saving config") } state = secrets writes++ return nil } newCloudflareRouteClient = func(token string) cloudflare.API { if token != "test-api-token" { t.Fatal("Cloudflare route command leaked the token beyond the API boundary") } return client } seeds := 0 var seedErr error initializeCloudflareTunnelOpenBao = func(config.Config) error { stored, present, err := cloudflare.ParseStoredTunnel(state["platform/cloudflare-tunnel"]) wantIngress := 2 if seeds%2 == 1 { wantIngress = 1 } if err != nil || !present || len(stored.Config.Ingress) != wantIngress { t.Fatal("OpenBao was not seeded with the saved tunnel config") } seeds++ return seedErr } cloudflareTunnelConfigPath = "bootstrap.yaml" cloudflareTunnelHostname = "app.example.test" cloudflareTunnelService = "http://service.default.svc:8080" cloudflareTunnelZoneID = "zone-id" if err := runCloudflareTunnelRoute(nil, true); err != nil { t.Fatal(err) } stored, present, err := cloudflare.ParseStoredTunnel(state["platform/cloudflare-tunnel"]) if err != nil || !present || len(stored.Config.Ingress) != 2 || !client.ensured || seeds != 1 { t.Fatal("route add did not save config and ensure the CNAME") } if err := runCloudflareTunnelRoute(nil, false); err != nil { t.Fatal(err) } stored, present, err = cloudflare.ParseStoredTunnel(state["platform/cloudflare-tunnel"]) if err != nil || !present || len(stored.Config.Ingress) != 1 || !client.deleted || seeds != 2 { t.Fatal("route remove did not save config and delete the CNAME") } client.ensureErr = errors.New("unavailable") if err := runCloudflareTunnelRoute(nil, true); err == nil { t.Fatal("route add accepted a CNAME failure") } stored, present, err = cloudflare.ParseStoredTunnel(state["platform/cloudflare-tunnel"]) if err != nil || !present || len(stored.Config.Ingress) != 1 || writes != 2 || seeds != 2 { t.Fatal("route add saved ingress config after a CNAME failure") } client.ensureErr = nil seedErr = errors.New("unavailable") if err := runCloudflareTunnelRoute(nil, true); err == nil || strings.Contains(err.Error(), "unavailable") { t.Fatal("route add did not return a safe OpenBao seeding error") } stored, present, err = cloudflare.ParseStoredTunnel(state["platform/cloudflare-tunnel"]) if err != nil || !present || len(stored.Config.Ingress) != 2 || writes != 3 || seeds != 3 { t.Fatal("route add did not retain DNS-safe state before OpenBao seeding failed") } deletes := client.deleteCalls if err := runCloudflareTunnelRoute(nil, false); err == nil || strings.Contains(err.Error(), "unavailable") { t.Fatal("route remove did not return a safe OpenBao seeding error") } stored, present, err = cloudflare.ParseStoredTunnel(state["platform/cloudflare-tunnel"]) if err != nil || !present || len(stored.Config.Ingress) != 1 || writes != 4 || seeds != 4 || client.deleteCalls != deletes { t.Fatal("route remove deleted DNS before OpenBao seeding completed") } } func TestCloudflareTunnelRouteErrorsDoNotRevealToken(t *testing.T) { originalLoad := loadCloudflareTunnelConfig originalRead := readCloudflareTunnelSecrets originalPath, originalHostname, originalService, originalZoneID := cloudflareTunnelConfigPath, cloudflareTunnelHostname, cloudflareTunnelService, cloudflareTunnelZoneID t.Cleanup(func() { loadCloudflareTunnelConfig = originalLoad readCloudflareTunnelSecrets = originalRead cloudflareTunnelConfigPath, cloudflareTunnelHostname, cloudflareTunnelService, cloudflareTunnelZoneID = originalPath, originalHostname, originalService, originalZoneID }) loadCloudflareTunnelConfig = func(string) (config.Config, error) { return config.Config{}, nil } readCloudflareTunnelSecrets = func(string, string) (map[string]map[string]string, error) { return map[string]map[string]string{"platform/cloudflare": {"api-token": "test-api-token"}}, nil } cloudflareTunnelConfigPath = "bootstrap.yaml" cloudflareTunnelHostname = "app.example.test" cloudflareTunnelService = "http://service.default.svc:8080" cloudflareTunnelZoneID = "zone-id" err := runCloudflareTunnelRoute(nil, true) if err == nil || strings.Contains(err.Error(), "test-api-token") { t.Fatal("route command error revealed the Cloudflare token") } }