Repository: go-acme/lego
Pull request #2564 (2025-07-07 14:23:53)

azion: improve zone lookup
also add tests.

ldez: I have some doubts, based on your API documentation:
https://api.azion.com/#ba265bc9-82a9-4ec9-b5ca-2199a1ea0f40

<details>

```
{
  "count": 1,
  "links": {
    "previous": null,
    "next": null
  },
  "total_pages": 1,
  "results": [
    {
      "domain": "youdomain.com",
      "is_active": true,
      "name": "New Hosted Zone via API POST method",
      "id": 1234
    }
  ],
  "schema_version": 3
}
```

</details>

The field `name` is a random string, and the field `domain` is the domain.

Can you confirm that?
marcusgrando: > @marcusgrando I have some doubts, based on your API documentation: https://api.azion.com/#ba265bc9-82a9-4ec9-b5ca-2199a1ea0f40
> 
> The field `name` is a random string, and the field `domain` is the domain.
> 
> Can you confirm that?

Yes, you are right. A name is just any given name that describes the domain, and the domain is exactly the FQDN.

Patch:
diff --git a/providers/dns/azion/azion.go b/providers/dns/azion/azion.go
index 660c0807..bc25586d 100644
--- a/providers/dns/azion/azion.go
+++ b/providers/dns/azion/azion.go
@@ -12,6 +12,7 @@ import (
 	"github.com/aziontech/azionapi-go-sdk/idns"
 	"github.com/go-acme/lego/v4/challenge/dns01"
 	"github.com/go-acme/lego/v4/platform/config/env"
+	"github.com/miekg/dns"
 )
 
 // Environment variables names.
@@ -182,13 +183,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 	}
 
 	defer func() {
-		// Remove the record ID from our map
+		// Cleans the record ID.
 		d.recordIDsMu.Lock()
 		delete(d.recordIDs, token)
 		d.recordIDsMu.Unlock()
 	}()
 
-	// Find the existing TXT record
 	existingRecord, err := d.findExistingTXTRecord(ctxAuth, zone.GetId(), subDomain)
 	if err != nil {
 		return fmt.Errorf("azion: find existing record: %w", err)
@@ -198,7 +198,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 		return nil
 	}
 
-	// Get current answers and remove the specific value
 	currentAnswers := existingRecord.GetAnswersList()
 
 	var updatedAnswers []string
@@ -239,11 +238,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 }
 
 func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*idns.Zone, error) {
-	authZone, err := dns01.FindZoneByFqdn(fqdn)
-	if err != nil {
-		return nil, fmt.Errorf("could not find a zone for domain %q: %w", fqdn, err)
-	}
-
 	resp, _, err := d.client.ZonesAPI.GetZones(ctx).Execute()
 	if err != nil {
 		return nil, fmt.Errorf("get zones: %w", err)
@@ -253,14 +247,19 @@ func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*idns.Zone, er
 		return nil, errors.New("get zones: no results")
 	}
 
-	targetZone := dns01.UnFqdn(authZone)
-	for _, zone := range resp.GetResults() {
-		if zone.GetName() == targetZone {
-			return &zone, nil
+	labelIndexes := dns.Split(fqdn)
+
+	for _, index := range labelIndexes {
+		domain := dns01.UnFqdn(fqdn[index:])
+
+		for _, zone := range resp.GetResults() {
+			if zone.GetDomain() == domain {
+				return &zone, nil
+			}
 		}
 	}
 
-	return nil, fmt.Errorf("zone %q not found (fqdn: %q)", authZone, fqdn)
+	return nil, fmt.Errorf("zone not found (fqdn: %q)", fqdn)
 }
 
 // findExistingTXTRecord searches for an existing TXT record with the given name in the specified zone.
diff --git a/providers/dns/azion/fixtures/zones.json b/providers/dns/azion/fixtures/zones.json
new file mode 100644
index 00000000..7dccedf1
--- /dev/null
+++ b/providers/dns/azion/fixtures/zones.json
@@ -0,0 +1,19 @@
+{
+  "count": 2,
+  "links": {
+    "previous": null,
+    "next": null
+  },
+  "total_pages": 1,
+  "results": [
+    {
+      "id": 1,
+      "domain": "example.com"
+    },
+    {
+      "id": 2,
+      "domain": "sub.example.com"
+    }
+  ],
+  "schema_version": 3
+}
diff --git a/providers/dns/azion/fixtures/zones_empty.json b/providers/dns/azion/fixtures/zones_empty.json
new file mode 100644
index 00000000..54006383
--- /dev/null
+++ b/providers/dns/azion/fixtures/zones_empty.json
@@ -0,0 +1,10 @@
+{
+  "count": 0,
+  "links": {
+    "previous": null,
+    "next": null
+  },
+  "total_pages": 0,
+  "results": null,
+  "schema_version": 3
+}


Test patch:
diff --git a/providers/dns/azion/azion_test.go b/providers/dns/azion/azion_test.go
index e96efb56..de25e7c6 100644
--- a/providers/dns/azion/azion_test.go
+++ b/providers/dns/azion/azion_test.go
@@ -1,9 +1,17 @@
 package azion
 
 import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
 	"testing"
 
+	"github.com/aziontech/azionapi-go-sdk/idns"
 	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -113,3 +121,129 @@ func TestLiveCleanUp(t *testing.T) {
 	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
 	require.NoError(t, err)
 }
+
+func TestDNSProvider_findZone(t *testing.T) {
+	provider, mux := setupTest(t)
+	mux.HandleFunc("GET /intelligent_dns", writeFixtureHandler("zones.json"))
+
+	testCases := []struct {
+		desc     string
+		fqdn     string
+		expected *idns.Zone
+	}{
+		{
+			desc: "apex",
+			fqdn: "example.com.",
+			expected: &idns.Zone{
+				Id:     idns.PtrInt32(1),
+				Domain: idns.PtrString("example.com"),
+			},
+		},
+		{
+			desc: "sub domain",
+			fqdn: "sub.example.com.",
+			expected: &idns.Zone{
+				Id:     idns.PtrInt32(2),
+				Domain: idns.PtrString("sub.example.com"),
+			},
+		},
+		{
+			desc: "long sub domain",
+			fqdn: "_acme-challenge.api.sub.example.com.",
+			expected: &idns.Zone{
+				Id:     idns.PtrInt32(2),
+				Domain: idns.PtrString("sub.example.com"),
+			},
+		},
+		{
+			desc: "long sub domain, apex",
+			fqdn: "_acme-challenge.test.example.com.",
+			expected: &idns.Zone{
+				Id:     idns.PtrInt32(1),
+				Domain: idns.PtrString("example.com"),
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			zone, err := provider.findZone(context.Background(), test.fqdn)
+			require.NoError(t, err)
+
+			assert.Equal(t, test.expected, zone)
+		})
+	}
+}
+
+func TestDNSProvider_findZone_error(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		fqdn     string
+		response string
+		expected string
+	}{
+		{
+			desc:     "no parent zone found",
+			fqdn:     "_acme-challenge.example.org.",
+			response: "zones.json",
+			expected: `zone not found (fqdn: "_acme-challenge.example.org.")`,
+		},
+		{
+			desc:     "empty zones list",
+			fqdn:     "example.com.",
+			response: "zones_empty.json",
+			expected: `zone not found (fqdn: "example.com.")`,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			provider, mux := setupTest(t)
+			mux.HandleFunc("GET /intelligent_dns", writeFixtureHandler(test.response))
+
+			zone, err := provider.findZone(context.Background(), test.fqdn)
+			require.EqualError(t, err, test.expected)
+
+			assert.Nil(t, zone)
+		})
+	}
+}
+
+func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) {
+	t.Helper()
+
+	mux := http.NewServeMux()
+	server := httptest.NewServer(mux)
+
+	config := NewDefaultConfig()
+	config.PersonalToken = "secret"
+
+	provider, err := NewDNSProviderConfig(config)
+	require.NoError(t, err)
+
+	clientConfig := provider.client.GetConfig()
+	clientConfig.HTTPClient = server.Client()
+	clientConfig.Servers = idns.ServerConfigurations{
+		{
+			URL:         server.URL,
+			Description: "Production",
+		},
+	}
+
+	return provider, mux
+}
+
+func writeFixtureHandler(filename string) http.HandlerFunc {
+	return func(rw http.ResponseWriter, req *http.Request) {
+		rw.Header().Set("Content-Type", "application/json")
+
+		file, err := os.Open(filepath.Join("fixtures", filename))
+		if err != nil {
+			http.Error(rw, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		defer func() { _ = file.Close() }()
+
+		_, _ = io.Copy(rw, file)
+	}
+}