aboutsummaryrefslogtreecommitdiff
path: root/provider/dns_record_test.go
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2026-07-03 15:21:56 +0200
committerRuben Beltran del Rio <jj@r.bdr.sh>2026-07-03 15:21:58 +0200
commit95dc66cb108a311065917362a00b86c08767303d (patch)
tree46ca65be6548224be0dd5a42bb86f2ecede14da8 /provider/dns_record_test.go
Add source controlHEADmain
Diffstat (limited to 'provider/dns_record_test.go')
-rw-r--r--provider/dns_record_test.go408
1 files changed, 408 insertions, 0 deletions
diff --git a/provider/dns_record_test.go b/provider/dns_record_test.go
new file mode 100644
index 0000000..deaea67
--- /dev/null
+++ b/provider/dns_record_test.go
@@ -0,0 +1,408 @@
+package provider_test
+
+import (
+ "context"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/blang/semver"
+ p "github.com/pulumi/pulumi-go-provider"
+ "github.com/pulumi/pulumi-go-provider/integration"
+ presource "github.com/pulumi/pulumi/sdk/v3/go/common/resource"
+ "github.com/pulumi/pulumi/sdk/v3/go/property"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "git.sr.ht/~rbdr/pulumi-porkbun/provider"
+)
+
+const dnsRecordToken = "porkbun:index:DnsRecord"
+
+var testURN = presource.NewURN("test", "provider", "", dnsRecordToken, "test")
+
+func newTestServer(t *testing.T, fake *fakePorkbun) integration.Server {
+ t.Helper()
+ httpSrv := httptest.NewServer(fake)
+ t.Cleanup(httpSrv.Close)
+
+ server := newIntegrationServer(t)
+ require.NoError(t, server.Configure(p.ConfigureRequest{
+ Args: property.NewMap(map[string]property.Value{
+ "apiKey": property.New(testAPIKey),
+ "secretApiKey": property.New(testSecretKey),
+ "baseUrl": property.New(httpSrv.URL),
+ }),
+ }))
+ return server
+}
+
+func newIntegrationServer(t *testing.T) integration.Server {
+ t.Helper()
+ prov, err := provider.New()
+ require.NoError(t, err)
+ server, err := integration.NewServer(context.Background(), provider.Name,
+ semver.MustParse("1.0.0"), integration.WithProvider(prov))
+ require.NoError(t, err)
+ return server
+}
+
+func recordInputs(overrides map[string]property.Value) property.Map {
+ inputs := map[string]property.Value{
+ "domain": property.New("example.com"),
+ "name": property.New("www"),
+ "type": property.New("A"),
+ "content": property.New("192.0.2.1"),
+ }
+ for k, v := range overrides {
+ inputs[k] = v
+ }
+ return property.NewMap(inputs)
+}
+
+func checkedValues(overrides map[string]property.Value, extra map[string]property.Value) property.Map {
+ values := map[string]property.Value{
+ "domain": property.New("example.com"),
+ "name": property.New("www"),
+ "type": property.New("A"),
+ "content": property.New("192.0.2.1"),
+ "ttl": property.New(600.0),
+ "prio": property.New(0.0),
+ }
+ for k, v := range extra {
+ values[k] = v
+ }
+ for k, v := range overrides {
+ values[k] = v
+ }
+ return property.NewMap(values)
+}
+
+func checkedInputs(overrides map[string]property.Value) property.Map {
+ return checkedValues(overrides, nil)
+}
+
+func checkedState(overrides map[string]property.Value) property.Map {
+ return checkedValues(overrides, map[string]property.Value{
+ "fqdn": property.New("www.example.com"),
+ })
+}
+
+func TestDnsRecordLifecycle(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+
+ expectCreate := checkedState(nil)
+ expectContentUpdate := checkedState(map[string]property.Value{
+ "content": property.New("192.0.2.2"),
+ })
+ expectRename := checkedState(map[string]property.Value{
+ "content": property.New("192.0.2.2"),
+ "name": property.New("api"),
+ "fqdn": property.New("api.example.com"),
+ })
+
+ integration.LifeCycleTest{
+ Resource: dnsRecordToken,
+ Create: integration.Operation{
+ Inputs: recordInputs(nil),
+ ExpectedOutput: &expectCreate,
+ Hook: func(inputs, output property.Map) {
+ assert.Equal(t, 1, fake.recordCount(), "exactly one record after create")
+ },
+ },
+ Updates: []integration.Operation{
+ {
+ Inputs: recordInputs(map[string]property.Value{"content": property.New("192.0.2.2")}),
+ ExpectedOutput: &expectContentUpdate,
+ },
+ {
+ Inputs: recordInputs(map[string]property.Value{
+ "content": property.New("192.0.2.2"),
+ "name": property.New("api"),
+ }),
+ ExpectedOutput: &expectRename,
+ Hook: func(inputs, output property.Map) {
+ assert.Equal(t, 2, fake.recordCount(), "replacement creates the new record before deleting the old")
+ },
+ },
+ },
+ }.Run(t, server)
+
+ assert.Equal(t, 0, fake.recordCount(), "destroy must remove the record")
+ assert.Equal(t, 2, fake.createCalls, "one create plus one replacement")
+ assert.Equal(t, 1, fake.editCalls, "content change should edit in place")
+ assert.Equal(t, 2, fake.deleteCalls, "replacement delete plus final destroy")
+}
+
+func TestCreateStoresPorkbunID(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+
+ resp, err := server.Create(p.CreateRequest{
+ Urn: testURN,
+ Properties: checkedInputs(nil),
+ })
+ require.NoError(t, err)
+ assert.NotEmpty(t, resp.ID)
+ require.NotNil(t, fake.record("example.com", resp.ID), "resource ID must be the Porkbun record ID")
+
+ rec := fake.record("example.com", resp.ID)
+ assert.Equal(t, "www", rec.name)
+ assert.Equal(t, "A", rec.typ)
+ assert.Equal(t, "192.0.2.1", rec.content)
+ assert.Equal(t, 600, rec.ttl)
+}
+
+func TestCreatePreviewMakesNoAPICalls(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+
+ resp, err := server.Create(p.CreateRequest{
+ Urn: testURN,
+ Properties: checkedInputs(nil),
+ DryRun: true,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, 0, fake.createCalls, "preview must not create records")
+ assert.Equal(t, property.New("www"), resp.Properties.Get("name"), "inputs pass through the preview")
+ assert.Equal(t, property.New(property.Computed), resp.Properties.Get("fqdn"),
+ "computed outputs are unknown during preview")
+}
+
+func TestUpdatePreviewMakesNoAPICalls(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+ id := fake.seed("example.com", fakeRecord{name: "www", typ: "A", content: "192.0.2.1", ttl: 600})
+
+ _, err := server.Update(p.UpdateRequest{
+ ID: id,
+ Urn: testURN,
+ State: checkedState(nil),
+ Inputs: checkedInputs(map[string]property.Value{"content": property.New("192.0.2.9")}),
+ DryRun: true,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, 0, fake.editCalls, "preview must not edit records")
+ assert.Equal(t, "192.0.2.1", fake.record("example.com", id).content)
+}
+
+func TestDiff(t *testing.T) {
+ server := newTestServer(t, newFakePorkbun(t))
+
+ diff := func(newInputs property.Map) p.DiffResponse {
+ resp, err := server.Diff(p.DiffRequest{
+ ID: "42",
+ Urn: testURN,
+ State: checkedState(nil),
+ Inputs: newInputs,
+ })
+ require.NoError(t, err)
+ return resp
+ }
+
+ t.Run("no changes", func(t *testing.T) {
+ resp := diff(checkedInputs(nil))
+ assert.False(t, resp.HasChanges)
+ })
+
+ t.Run("content and ttl update in place", func(t *testing.T) {
+ resp := diff(checkedInputs(map[string]property.Value{
+ "content": property.New("192.0.2.9"),
+ "ttl": property.New(900.0),
+ }))
+ assert.True(t, resp.HasChanges)
+ assert.Equal(t, p.Update, resp.DetailedDiff["content"].Kind)
+ assert.Equal(t, p.Update, resp.DetailedDiff["ttl"].Kind)
+ })
+
+ t.Run("domain change forces replacement", func(t *testing.T) {
+ resp := diff(checkedInputs(map[string]property.Value{
+ "domain": property.New("example.org"),
+ }))
+ assert.True(t, resp.HasChanges)
+ assert.Equal(t, p.UpdateReplace, resp.DetailedDiff["domain"].Kind)
+ })
+
+ t.Run("name change forces replacement", func(t *testing.T) {
+ resp := diff(checkedInputs(map[string]property.Value{
+ "name": property.New("api"),
+ }))
+ assert.True(t, resp.HasChanges)
+ assert.Equal(t, p.UpdateReplace, resp.DetailedDiff["name"].Kind)
+ })
+
+ t.Run("type change updates in place", func(t *testing.T) {
+ resp := diff(checkedInputs(map[string]property.Value{
+ "type": property.New("CNAME"),
+ "content": property.New("target.example.net"),
+ }))
+ assert.True(t, resp.HasChanges)
+ assert.Equal(t, p.Update, resp.DetailedDiff["type"].Kind)
+ })
+}
+
+func TestCheck(t *testing.T) {
+ server := newTestServer(t, newFakePorkbun(t))
+
+ check := func(inputs property.Map) p.CheckResponse {
+ resp, err := server.Check(p.CheckRequest{Urn: testURN, Inputs: inputs})
+ require.NoError(t, err)
+ return resp
+ }
+
+ failedProperties := func(resp p.CheckResponse) []string {
+ props := []string{}
+ for _, f := range resp.Failures {
+ props = append(props, f.Property)
+ }
+ return props
+ }
+
+ t.Run("applies defaults", func(t *testing.T) {
+ resp := check(recordInputs(nil))
+ assert.Empty(t, resp.Failures)
+ assert.Equal(t, property.New(600.0), resp.Inputs.Get("ttl"))
+ assert.Equal(t, property.New(0.0), resp.Inputs.Get("prio"))
+ })
+
+ t.Run("normalizes case", func(t *testing.T) {
+ resp := check(recordInputs(map[string]property.Value{
+ "type": property.New("a"),
+ "domain": property.New("EXAMPLE.com"),
+ "name": property.New("WWW"),
+ }))
+ assert.Empty(t, resp.Failures)
+ assert.Equal(t, property.New("A"), resp.Inputs.Get("type"))
+ assert.Equal(t, property.New("example.com"), resp.Inputs.Get("domain"))
+ assert.Equal(t, property.New("www"), resp.Inputs.Get("name"))
+ })
+
+ t.Run("rejects unknown record type", func(t *testing.T) {
+ resp := check(recordInputs(map[string]property.Value{"type": property.New("BOGUS")}))
+ assert.Equal(t, []string{"type"}, failedProperties(resp))
+ })
+
+ t.Run("accepts ttl below the typical minimum", func(t *testing.T) {
+ resp := check(recordInputs(map[string]property.Value{"ttl": property.New(300.0)}))
+ assert.Empty(t, resp.Failures)
+ assert.Equal(t, property.New(300.0), resp.Inputs.Get("ttl"))
+ })
+
+ t.Run("rejects negative ttl", func(t *testing.T) {
+ resp := check(recordInputs(map[string]property.Value{"ttl": property.New(-1.0)}))
+ assert.Equal(t, []string{"ttl"}, failedProperties(resp))
+ })
+
+ t.Run("rejects out of range prio", func(t *testing.T) {
+ resp := check(recordInputs(map[string]property.Value{"prio": property.New(70000.0)}))
+ assert.Equal(t, []string{"prio"}, failedProperties(resp))
+ })
+
+ t.Run("rejects fully qualified name", func(t *testing.T) {
+ resp := check(recordInputs(map[string]property.Value{"name": property.New("www.example.com")}))
+ assert.Equal(t, []string{"name"}, failedProperties(resp))
+ })
+}
+
+func TestReadRefreshesDrift(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+ id := fake.seed("example.com", fakeRecord{name: "www", typ: "A", content: "192.0.2.99", ttl: 900})
+
+ resp, err := server.Read(p.ReadRequest{
+ ID: id,
+ Urn: testURN,
+ Properties: checkedState(nil),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, id, resp.ID)
+ assert.Equal(t, property.New("192.0.2.99"), resp.Properties.Get("content"))
+ assert.Equal(t, property.New(900.0), resp.Properties.Get("ttl"))
+ assert.Equal(t, property.New("www"), resp.Properties.Get("name"))
+ assert.Equal(t, property.New("www.example.com"), resp.Properties.Get("fqdn"))
+}
+
+func TestReadRootRecord(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+ id := fake.seed("example.com", fakeRecord{name: "", typ: "A", content: "192.0.2.1", ttl: 600})
+
+ resp, err := server.Read(p.ReadRequest{
+ ID: id,
+ Urn: testURN,
+ Properties: checkedState(map[string]property.Value{
+ "name": property.New(""),
+ "fqdn": property.New("example.com"),
+ }),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, property.New(""), resp.Properties.Get("name"))
+ assert.Equal(t, property.New("example.com"), resp.Properties.Get("fqdn"))
+}
+
+func TestReadDeletedRecord(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+
+ resp, err := server.Read(p.ReadRequest{
+ ID: "9999",
+ Urn: testURN,
+ Properties: checkedState(nil),
+ })
+ require.NoError(t, err)
+ assert.Empty(t, resp.ID, "an empty ID signals the record no longer exists")
+}
+
+func TestImportWithCompositeID(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+ id := fake.seed("example.com", fakeRecord{name: "mail", typ: "MX", content: "mx.example.com", ttl: 600, prio: 10})
+
+ resp, err := server.Read(p.ReadRequest{
+ ID: "example.com/" + id,
+ Urn: testURN,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, id, resp.ID, "the canonical ID is the bare record ID")
+ assert.Equal(t, property.New("example.com"), resp.Properties.Get("domain"))
+ assert.Equal(t, property.New("mail"), resp.Properties.Get("name"))
+ assert.Equal(t, property.New("MX"), resp.Properties.Get("type"))
+ assert.Equal(t, property.New(10.0), resp.Properties.Get("prio"))
+ assert.Equal(t, property.New("mail.example.com"), resp.Properties.Get("fqdn"))
+}
+
+func TestImportWithoutDomainFails(t *testing.T) {
+ server := newTestServer(t, newFakePorkbun(t))
+
+ _, err := server.Read(p.ReadRequest{ID: "12345", Urn: testURN})
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "<domain>/<recordId>")
+}
+
+func TestDeleteIsIdempotent(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+
+ err := server.Delete(p.DeleteRequest{
+ ID: "4242",
+ Urn: testURN,
+ Properties: checkedState(nil),
+ })
+ require.NoError(t, err)
+ assert.Equal(t, 1, fake.deleteCalls)
+ assert.Equal(t, 1, fake.retrieveCalls, "a failed delete should be verified with a lookup")
+}
+
+func TestUpdateSurfacesAPIErrors(t *testing.T) {
+ fake := newFakePorkbun(t)
+ server := newTestServer(t, fake)
+
+ _, err := server.Update(p.UpdateRequest{
+ ID: "4242",
+ Urn: testURN,
+ State: checkedState(nil),
+ Inputs: checkedInputs(map[string]property.Value{"content": property.New("192.0.2.9")}),
+ })
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "Invalid record ID.")
+}