maidn-cli/cmd/cloudflare_tunnel_test.go

183 lines
7.4 KiB
Go

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")
}
}