//go:build integration package godns_test import ( "context" "io" "net" "net/netip" "os" "path/filepath" "runtime" "strconv" "testing" "time" godns "gitea.neitzel.de/konrad/go-dns" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/network" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) func TestAXFRFetchesFullZoneOverTCPWithTSIG(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() server := startBIND(ctx, t) client, err := godns.NewClient(server, "example.test", fixtureTSIG()) if err != nil { t.Fatal(err) } result, err := client.AXFR(ctx) if err != nil { t.Fatal(err) } if result.Serial != 1 { t.Fatalf("serial: %d", result.Serial) } if !hasRecord(result.Zone.Records, "www.example.test.", "A", "192.0.2.10") { t.Fatalf("missing www A record in %#v", result.Zone.Records) } if !hasRecord(result.Zone.Records, "ns.example.test.", "A", "192.0.2.1") { t.Fatalf("missing ns A record in %#v", result.Zone.Records) } } func TestUpdateAddAndDeleteAgainstBIND(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() server := startBIND(ctx, t) client, err := godns.NewClient(server, "example.test", fixtureTSIG()) if err != nil { t.Fatal(err) } added, err := godns.A("new.example.test", 300, net.ParseIP("192.0.2.99")) if err != nil { t.Fatal(err) } if _, err := client.Update(ctx, godns.Add(added)); err != nil { t.Fatal(err) } afterAdd, err := client.AXFR(ctx) if err != nil { t.Fatal(err) } if !hasRecord(afterAdd.Zone.Records, "new.example.test.", "A", "192.0.2.99") { t.Fatalf("add did not appear in AXFR: %#v", afterAdd.Zone.Records) } if _, err := client.Update(ctx, godns.Delete(added)); err != nil { t.Fatal(err) } afterDelete, err := client.AXFR(ctx) if err != nil { t.Fatal(err) } if hasRecord(afterDelete.Zone.Records, "new.example.test.", "A", "192.0.2.99") { t.Fatal("delete left the record in AXFR") } } func startBIND(ctx context.Context, t *testing.T) string { t.Helper() _, thisFile, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } bindDir := filepath.Join(filepath.Dir(thisFile), "testdata", "bind") listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } hostPort := listener.Addr().(*net.TCPAddr).Port if err := listener.Close(); err != nil { t.Fatal(err) } tcp53 := network.MustParsePort("53/tcp") udp53 := network.MustParsePort("53/udp") binding := network.PortBinding{ HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: strconv.Itoa(hostPort), } req := testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ Context: bindDir, Dockerfile: "Dockerfile", }, ExposedPorts: []string{"53/tcp", "53/udp"}, WaitingFor: wait.ForListeningPort("53/tcp").WithStartupTimeout(2 * time.Minute), HostConfigModifier: func(hc *container.HostConfig) { hc.PortBindings = network.PortMap{ tcp53: {binding}, udp53: {binding}, } }, } ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) if err != nil { t.Fatalf("start BIND: %v", err) } t.Cleanup(func() { if t.Failed() { reader, logErr := ctr.Logs(context.Background()) if logErr == nil { defer reader.Close() data, _ := io.ReadAll(reader) t.Logf("BIND logs:\n%s", data) } } _ = ctr.Terminate(context.Background()) }) return net.JoinHostPort("127.0.0.1", strconv.Itoa(hostPort)) } func hasRecord(records []godns.Record, owner, typ, rdata string) bool { for _, rec := range records { if rec.Owner == owner && rec.Type == typ && rec.RData == rdata { return true } } return false } func TestMain(m *testing.M) { if os.Getenv("DOCKER_HOST") == "" { sock := filepath.Join(os.Getenv("HOME"), ".rd", "docker.sock") if _, err := os.Stat(sock); err == nil { _ = os.Setenv("DOCKER_HOST", "unix://"+sock) } } // Ryuk cannot mount Rancher Desktop's user-scoped docker.sock. if os.Getenv("TESTCONTAINERS_RYUK_DISABLED") == "" { _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") } os.Exit(m.Run()) }