sigs.k8s.io/external-dns@v0.14.1/provider/cloudflare/cloudflare_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cloudflare
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"sort"
    25  	"strings"
    26  	"testing"
    27  
    28  	cloudflare "github.com/cloudflare/cloudflare-go"
    29  	"github.com/stretchr/testify/assert"
    30  
    31  	"github.com/maxatome/go-testdeep/td"
    32  	"sigs.k8s.io/external-dns/endpoint"
    33  	"sigs.k8s.io/external-dns/plan"
    34  	"sigs.k8s.io/external-dns/provider"
    35  )
    36  
    37  type MockAction struct {
    38  	Name       string
    39  	ZoneId     string
    40  	RecordId   string
    41  	RecordData cloudflare.DNSRecord
    42  }
    43  
    44  type mockCloudFlareClient struct {
    45  	User            cloudflare.User
    46  	Zones           map[string]string
    47  	Records         map[string]map[string]cloudflare.DNSRecord
    48  	Actions         []MockAction
    49  	listZonesError  error
    50  	dnsRecordsError error
    51  }
    52  
    53  var ExampleDomain = []cloudflare.DNSRecord{
    54  	{
    55  		ID:      "1234567890",
    56  		ZoneID:  "001",
    57  		Name:    "foobar.bar.com",
    58  		Type:    endpoint.RecordTypeA,
    59  		TTL:     120,
    60  		Content: "1.2.3.4",
    61  		Proxied: proxyDisabled,
    62  	},
    63  	{
    64  		ID:      "2345678901",
    65  		ZoneID:  "001",
    66  		Name:    "foobar.bar.com",
    67  		Type:    endpoint.RecordTypeA,
    68  		TTL:     120,
    69  		Content: "3.4.5.6",
    70  		Proxied: proxyDisabled,
    71  	},
    72  	{
    73  		ID:      "1231231233",
    74  		ZoneID:  "002",
    75  		Name:    "bar.foo.com",
    76  		Type:    endpoint.RecordTypeA,
    77  		TTL:     1,
    78  		Content: "2.3.4.5",
    79  		Proxied: proxyDisabled,
    80  	},
    81  }
    82  
    83  func NewMockCloudFlareClient() *mockCloudFlareClient {
    84  	return &mockCloudFlareClient{
    85  		User: cloudflare.User{ID: "xxxxxxxxxxxxxxxxxxx"},
    86  		Zones: map[string]string{
    87  			"001": "bar.com",
    88  			"002": "foo.com",
    89  		},
    90  		Records: map[string]map[string]cloudflare.DNSRecord{
    91  			"001": {},
    92  			"002": {},
    93  		},
    94  	}
    95  }
    96  
    97  func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecord) *mockCloudFlareClient {
    98  	m := NewMockCloudFlareClient()
    99  
   100  	for zoneID, zoneRecords := range records {
   101  		if zone, ok := m.Records[zoneID]; ok {
   102  			for _, record := range zoneRecords {
   103  				zone[record.ID] = record
   104  			}
   105  		}
   106  	}
   107  
   108  	return m
   109  }
   110  
   111  func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
   112  	switch params := rp.(type) {
   113  	case cloudflare.CreateDNSRecordParams:
   114  		return cloudflare.DNSRecord{
   115  			Name:    params.Name,
   116  			TTL:     params.TTL,
   117  			Proxied: params.Proxied,
   118  			Type:    params.Type,
   119  			Content: params.Content,
   120  		}
   121  	case cloudflare.UpdateDNSRecordParams:
   122  		return cloudflare.DNSRecord{
   123  			Name:    params.Name,
   124  			TTL:     params.TTL,
   125  			Proxied: params.Proxied,
   126  			Type:    params.Type,
   127  			Content: params.Content,
   128  		}
   129  	default:
   130  		return cloudflare.DNSRecord{}
   131  	}
   132  }
   133  
   134  func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
   135  	recordData := getDNSRecordFromRecordParams(rp)
   136  	m.Actions = append(m.Actions, MockAction{
   137  		Name:       "Create",
   138  		ZoneId:     rc.Identifier,
   139  		RecordId:   rp.ID,
   140  		RecordData: recordData,
   141  	})
   142  	if zone, ok := m.Records[rc.Identifier]; ok {
   143  		zone[rp.ID] = recordData
   144  	}
   145  
   146  	if recordData.Name == "newerror.bar.com" {
   147  		return cloudflare.DNSRecord{}, fmt.Errorf("failed to create record")
   148  	}
   149  	return cloudflare.DNSRecord{}, nil
   150  }
   151  
   152  func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) {
   153  	if m.dnsRecordsError != nil {
   154  		return nil, &cloudflare.ResultInfo{}, m.dnsRecordsError
   155  	}
   156  	result := []cloudflare.DNSRecord{}
   157  	if zone, ok := m.Records[rc.Identifier]; ok {
   158  		for _, record := range zone {
   159  			result = append(result, record)
   160  		}
   161  	}
   162  
   163  	if len(result) == 0 || rp.PerPage == 0 {
   164  		return result, &cloudflare.ResultInfo{Page: 1, TotalPages: 1, Count: 0, Total: 0}, nil
   165  	}
   166  
   167  	// if not pagination options were passed in, return the result as is
   168  	if rp.Page == 0 {
   169  		return result, &cloudflare.ResultInfo{Page: 1, TotalPages: 1, Count: len(result), Total: len(result)}, nil
   170  	}
   171  
   172  	// otherwise, split the result into chunks of size rp.PerPage to simulate the pagination from the API
   173  	chunks := [][]cloudflare.DNSRecord{}
   174  
   175  	// to ensure consistency in the multiple calls to this function, sort the result slice
   176  	sort.Slice(result, func(i, j int) bool { return strings.Compare(result[i].ID, result[j].ID) > 0 })
   177  	for rp.PerPage < len(result) {
   178  		result, chunks = result[rp.PerPage:], append(chunks, result[0:rp.PerPage])
   179  	}
   180  	chunks = append(chunks, result)
   181  
   182  	// return the requested page
   183  	partialResult := chunks[rp.Page-1]
   184  	return partialResult, &cloudflare.ResultInfo{
   185  		PerPage:    rp.PerPage,
   186  		Page:       rp.Page,
   187  		TotalPages: len(chunks),
   188  		Count:      len(partialResult),
   189  		Total:      len(result),
   190  	}, nil
   191  }
   192  
   193  func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error {
   194  	recordData := getDNSRecordFromRecordParams(rp)
   195  	m.Actions = append(m.Actions, MockAction{
   196  		Name:       "Update",
   197  		ZoneId:     rc.Identifier,
   198  		RecordId:   rp.ID,
   199  		RecordData: recordData,
   200  	})
   201  	if zone, ok := m.Records[rc.Identifier]; ok {
   202  		if _, ok := zone[rp.ID]; ok {
   203  			zone[rp.ID] = recordData
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
   210  	m.Actions = append(m.Actions, MockAction{
   211  		Name:     "Delete",
   212  		ZoneId:   rc.Identifier,
   213  		RecordId: recordID,
   214  	})
   215  	if zone, ok := m.Records[rc.Identifier]; ok {
   216  		if _, ok := zone[recordID]; ok {
   217  			delete(zone, recordID)
   218  			return nil
   219  		}
   220  	}
   221  	return nil
   222  }
   223  
   224  func (m *mockCloudFlareClient) UserDetails(ctx context.Context) (cloudflare.User, error) {
   225  	return m.User, nil
   226  }
   227  
   228  func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) {
   229  	for id, name := range m.Zones {
   230  		if name == zoneName {
   231  			return id, nil
   232  		}
   233  	}
   234  
   235  	return "", errors.New("Unknown zone: " + zoneName)
   236  }
   237  
   238  func (m *mockCloudFlareClient) ListZones(ctx context.Context, zoneID ...string) ([]cloudflare.Zone, error) {
   239  	if m.listZonesError != nil {
   240  		return nil, m.listZonesError
   241  	}
   242  
   243  	result := []cloudflare.Zone{}
   244  
   245  	for zoneID, zoneName := range m.Zones {
   246  		result = append(result, cloudflare.Zone{
   247  			ID:   zoneID,
   248  			Name: zoneName,
   249  		})
   250  	}
   251  
   252  	return result, nil
   253  }
   254  
   255  func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
   256  	if m.listZonesError != nil {
   257  		return cloudflare.ZonesResponse{}, m.listZonesError
   258  	}
   259  
   260  	result := []cloudflare.Zone{}
   261  
   262  	for zoneId, zoneName := range m.Zones {
   263  		result = append(result, cloudflare.Zone{
   264  			ID:   zoneId,
   265  			Name: zoneName,
   266  		})
   267  	}
   268  
   269  	return cloudflare.ZonesResponse{
   270  		Result: result,
   271  		ResultInfo: cloudflare.ResultInfo{
   272  			Page:       1,
   273  			TotalPages: 1,
   274  		},
   275  	}, nil
   276  }
   277  
   278  func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
   279  	for id, zoneName := range m.Zones {
   280  		if zoneID == id {
   281  			return cloudflare.Zone{
   282  				ID:   zoneID,
   283  				Name: zoneName,
   284  			}, nil
   285  		}
   286  	}
   287  
   288  	return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
   289  }
   290  
   291  func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) {
   292  	t.Helper()
   293  
   294  	var client *mockCloudFlareClient
   295  
   296  	if provider.Client == nil {
   297  		client = NewMockCloudFlareClient()
   298  		provider.Client = client
   299  	} else {
   300  		client = provider.Client.(*mockCloudFlareClient)
   301  	}
   302  
   303  	ctx := context.Background()
   304  
   305  	records, err := provider.Records(ctx)
   306  	if err != nil {
   307  		t.Fatalf("cannot fetch records, %s", err)
   308  	}
   309  
   310  	endpoints, err = provider.AdjustEndpoints(endpoints)
   311  	assert.NoError(t, err)
   312  	domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
   313  	plan := &plan.Plan{
   314  		Current:        records,
   315  		Desired:        endpoints,
   316  		DomainFilter:   endpoint.MatchAllDomainFilters{&domainFilter},
   317  		ManagedRecords: managedRecords,
   318  	}
   319  
   320  	changes := plan.Calculate().Changes
   321  
   322  	// Records other than A, CNAME and NS are not supported by planner, just create them
   323  	for _, endpoint := range endpoints {
   324  		if endpoint.RecordType != "A" && endpoint.RecordType != "CNAME" && endpoint.RecordType != "NS" {
   325  			changes.Create = append(changes.Create, endpoint)
   326  		}
   327  	}
   328  
   329  	err = provider.ApplyChanges(context.Background(), changes)
   330  
   331  	if err != nil {
   332  		t.Fatalf("cannot apply changes, %s", err)
   333  	}
   334  
   335  	td.Cmp(t, client.Actions, actions, args...)
   336  }
   337  
   338  func TestCloudflareA(t *testing.T) {
   339  	endpoints := []*endpoint.Endpoint{
   340  		{
   341  			RecordType: "A",
   342  			DNSName:    "bar.com",
   343  			Targets:    endpoint.Targets{"127.0.0.1", "127.0.0.2"},
   344  		},
   345  	}
   346  
   347  	AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
   348  		{
   349  			Name:   "Create",
   350  			ZoneId: "001",
   351  			RecordData: cloudflare.DNSRecord{
   352  				Type:    "A",
   353  				Name:    "bar.com",
   354  				Content: "127.0.0.1",
   355  				TTL:     1,
   356  				Proxied: proxyDisabled,
   357  			},
   358  		},
   359  		{
   360  			Name:   "Create",
   361  			ZoneId: "001",
   362  			RecordData: cloudflare.DNSRecord{
   363  				Type:    "A",
   364  				Name:    "bar.com",
   365  				Content: "127.0.0.2",
   366  				TTL:     1,
   367  				Proxied: proxyDisabled,
   368  			},
   369  		},
   370  	},
   371  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   372  	)
   373  }
   374  
   375  func TestCloudflareCname(t *testing.T) {
   376  	endpoints := []*endpoint.Endpoint{
   377  		{
   378  			RecordType: "CNAME",
   379  			DNSName:    "cname.bar.com",
   380  			Targets:    endpoint.Targets{"google.com", "facebook.com"},
   381  		},
   382  	}
   383  
   384  	AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
   385  		{
   386  			Name:   "Create",
   387  			ZoneId: "001",
   388  			RecordData: cloudflare.DNSRecord{
   389  				Type:    "CNAME",
   390  				Name:    "cname.bar.com",
   391  				Content: "google.com",
   392  				TTL:     1,
   393  				Proxied: proxyDisabled,
   394  			},
   395  		},
   396  		{
   397  			Name:   "Create",
   398  			ZoneId: "001",
   399  			RecordData: cloudflare.DNSRecord{
   400  				Type:    "CNAME",
   401  				Name:    "cname.bar.com",
   402  				Content: "facebook.com",
   403  				TTL:     1,
   404  				Proxied: proxyDisabled,
   405  			},
   406  		},
   407  	},
   408  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   409  	)
   410  }
   411  
   412  func TestCloudflareCustomTTL(t *testing.T) {
   413  	endpoints := []*endpoint.Endpoint{
   414  		{
   415  			RecordType: "A",
   416  			DNSName:    "ttl.bar.com",
   417  			Targets:    endpoint.Targets{"127.0.0.1"},
   418  			RecordTTL:  120,
   419  		},
   420  	}
   421  
   422  	AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
   423  		{
   424  			Name:   "Create",
   425  			ZoneId: "001",
   426  			RecordData: cloudflare.DNSRecord{
   427  				Type:    "A",
   428  				Name:    "ttl.bar.com",
   429  				Content: "127.0.0.1",
   430  				TTL:     120,
   431  				Proxied: proxyDisabled,
   432  			},
   433  		},
   434  	},
   435  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   436  	)
   437  }
   438  
   439  func TestCloudflareProxiedDefault(t *testing.T) {
   440  	endpoints := []*endpoint.Endpoint{
   441  		{
   442  			RecordType: "A",
   443  			DNSName:    "bar.com",
   444  			Targets:    endpoint.Targets{"127.0.0.1"},
   445  		},
   446  	}
   447  
   448  	AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{
   449  		{
   450  			Name:   "Create",
   451  			ZoneId: "001",
   452  			RecordData: cloudflare.DNSRecord{
   453  				Type:    "A",
   454  				Name:    "bar.com",
   455  				Content: "127.0.0.1",
   456  				TTL:     1,
   457  				Proxied: proxyEnabled,
   458  			},
   459  		},
   460  	},
   461  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   462  	)
   463  }
   464  
   465  func TestCloudflareProxiedOverrideTrue(t *testing.T) {
   466  	endpoints := []*endpoint.Endpoint{
   467  		{
   468  			RecordType: "A",
   469  			DNSName:    "bar.com",
   470  			Targets:    endpoint.Targets{"127.0.0.1"},
   471  			ProviderSpecific: endpoint.ProviderSpecific{
   472  				endpoint.ProviderSpecificProperty{
   473  					Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   474  					Value: "true",
   475  				},
   476  			},
   477  		},
   478  	}
   479  
   480  	AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
   481  		{
   482  			Name:   "Create",
   483  			ZoneId: "001",
   484  			RecordData: cloudflare.DNSRecord{
   485  				Type:    "A",
   486  				Name:    "bar.com",
   487  				Content: "127.0.0.1",
   488  				TTL:     1,
   489  				Proxied: proxyEnabled,
   490  			},
   491  		},
   492  	},
   493  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   494  	)
   495  }
   496  
   497  func TestCloudflareProxiedOverrideFalse(t *testing.T) {
   498  	endpoints := []*endpoint.Endpoint{
   499  		{
   500  			RecordType: "A",
   501  			DNSName:    "bar.com",
   502  			Targets:    endpoint.Targets{"127.0.0.1"},
   503  			ProviderSpecific: endpoint.ProviderSpecific{
   504  				endpoint.ProviderSpecificProperty{
   505  					Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   506  					Value: "false",
   507  				},
   508  			},
   509  		},
   510  	}
   511  
   512  	AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{
   513  		{
   514  			Name:   "Create",
   515  			ZoneId: "001",
   516  			RecordData: cloudflare.DNSRecord{
   517  				Type:    "A",
   518  				Name:    "bar.com",
   519  				Content: "127.0.0.1",
   520  				TTL:     1,
   521  				Proxied: proxyDisabled,
   522  			},
   523  		},
   524  	},
   525  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   526  	)
   527  }
   528  
   529  func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
   530  	endpoints := []*endpoint.Endpoint{
   531  		{
   532  			RecordType: "A",
   533  			DNSName:    "bar.com",
   534  			Targets:    endpoint.Targets{"127.0.0.1"},
   535  			ProviderSpecific: endpoint.ProviderSpecific{
   536  				endpoint.ProviderSpecificProperty{
   537  					Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   538  					Value: "asfasdfa",
   539  				},
   540  			},
   541  		},
   542  	}
   543  
   544  	AssertActions(t, &CloudFlareProvider{proxiedByDefault: true}, endpoints, []MockAction{
   545  		{
   546  			Name:   "Create",
   547  			ZoneId: "001",
   548  			RecordData: cloudflare.DNSRecord{
   549  				Type:    "A",
   550  				Name:    "bar.com",
   551  				Content: "127.0.0.1",
   552  				TTL:     1,
   553  				Proxied: proxyEnabled,
   554  			},
   555  		},
   556  	},
   557  		[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
   558  	)
   559  }
   560  
   561  func TestCloudflareSetProxied(t *testing.T) {
   562  	var proxied *bool = proxyEnabled
   563  	var notProxied *bool = proxyDisabled
   564  	testCases := []struct {
   565  		recordType string
   566  		domain     string
   567  		proxiable  *bool
   568  	}{
   569  		{"A", "bar.com", proxied},
   570  		{"CNAME", "bar.com", proxied},
   571  		{"TXT", "bar.com", notProxied},
   572  		{"MX", "bar.com", notProxied},
   573  		{"NS", "bar.com", notProxied},
   574  		{"SPF", "bar.com", notProxied},
   575  		{"SRV", "bar.com", notProxied},
   576  		{"A", "*.bar.com", proxied},
   577  		{"CNAME", "*.docs.bar.com", proxied},
   578  	}
   579  
   580  	for _, testCase := range testCases {
   581  		endpoints := []*endpoint.Endpoint{
   582  			{
   583  				RecordType: testCase.recordType,
   584  				DNSName:    testCase.domain,
   585  				Targets:    endpoint.Targets{"127.0.0.1"},
   586  				ProviderSpecific: endpoint.ProviderSpecific{
   587  					endpoint.ProviderSpecificProperty{
   588  						Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   589  						Value: "true",
   590  					},
   591  				},
   592  			},
   593  		}
   594  
   595  		AssertActions(t, &CloudFlareProvider{}, endpoints, []MockAction{
   596  			{
   597  				Name:   "Create",
   598  				ZoneId: "001",
   599  				RecordData: cloudflare.DNSRecord{
   600  					Type:    testCase.recordType,
   601  					Name:    testCase.domain,
   602  					Content: "127.0.0.1",
   603  					TTL:     1,
   604  					Proxied: testCase.proxiable,
   605  				},
   606  			},
   607  		}, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, testCase.recordType+" record on "+testCase.domain)
   608  	}
   609  }
   610  
   611  func TestCloudflareZones(t *testing.T) {
   612  	provider := &CloudFlareProvider{
   613  		Client:       NewMockCloudFlareClient(),
   614  		domainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
   615  		zoneIDFilter: provider.NewZoneIDFilter([]string{""}),
   616  	}
   617  
   618  	zones, err := provider.Zones(context.Background())
   619  	if err != nil {
   620  		t.Fatal(err)
   621  	}
   622  
   623  	assert.Equal(t, 1, len(zones))
   624  	assert.Equal(t, "bar.com", zones[0].Name)
   625  }
   626  
   627  func TestCloudFlareZonesWithIDFilter(t *testing.T) {
   628  	client := NewMockCloudFlareClient()
   629  	client.listZonesError = errors.New("shouldn't need to list zones when ZoneIDFilter in use")
   630  	provider := &CloudFlareProvider{
   631  		Client:       client,
   632  		domainFilter: endpoint.NewDomainFilter([]string{"bar.com", "foo.com"}),
   633  		zoneIDFilter: provider.NewZoneIDFilter([]string{"001"}),
   634  	}
   635  
   636  	zones, err := provider.Zones(context.Background())
   637  	if err != nil {
   638  		t.Fatal(err)
   639  	}
   640  
   641  	// foo.com should *not* be returned as it doesn't match ZoneID filter
   642  	assert.Equal(t, 1, len(zones))
   643  	assert.Equal(t, "bar.com", zones[0].Name)
   644  }
   645  
   646  func TestCloudflareRecords(t *testing.T) {
   647  	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
   648  		"001": ExampleDomain,
   649  	})
   650  
   651  	// Set DNSRecordsPerPage to 1 test the pagination behaviour
   652  	provider := &CloudFlareProvider{
   653  		Client:            client,
   654  		DNSRecordsPerPage: 1,
   655  	}
   656  	ctx := context.Background()
   657  
   658  	records, err := provider.Records(ctx)
   659  	if err != nil {
   660  		t.Errorf("should not fail, %s", err)
   661  	}
   662  
   663  	assert.Equal(t, 2, len(records))
   664  	client.dnsRecordsError = errors.New("failed to list dns records")
   665  	_, err = provider.Records(ctx)
   666  	if err == nil {
   667  		t.Errorf("expected to fail")
   668  	}
   669  	client.dnsRecordsError = nil
   670  	client.listZonesError = errors.New("failed to list zones")
   671  	_, err = provider.Records(ctx)
   672  	if err == nil {
   673  		t.Errorf("expected to fail")
   674  	}
   675  }
   676  
   677  func TestCloudflareProvider(t *testing.T) {
   678  	_ = os.Setenv("CF_API_TOKEN", "abc123def")
   679  	_, err := NewCloudFlareProvider(
   680  		endpoint.NewDomainFilter([]string{"bar.com"}),
   681  		provider.NewZoneIDFilter([]string{""}),
   682  		false,
   683  		true,
   684  		5000)
   685  	if err != nil {
   686  		t.Errorf("should not fail, %s", err)
   687  	}
   688  
   689  	_ = os.Unsetenv("CF_API_TOKEN")
   690  	tokenFile := "/tmp/cf_api_token"
   691  	if err := os.WriteFile(tokenFile, []byte("abc123def"), 0o644); err != nil {
   692  		t.Errorf("failed to write token file, %s", err)
   693  	}
   694  	_ = os.Setenv("CF_API_TOKEN", tokenFile)
   695  	_, err = NewCloudFlareProvider(
   696  		endpoint.NewDomainFilter([]string{"bar.com"}),
   697  		provider.NewZoneIDFilter([]string{""}),
   698  		false,
   699  		true,
   700  		5000)
   701  	if err != nil {
   702  		t.Errorf("should not fail, %s", err)
   703  	}
   704  
   705  	_ = os.Unsetenv("CF_API_TOKEN")
   706  	_ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx")
   707  	_ = os.Setenv("CF_API_EMAIL", "test@test.com")
   708  	_, err = NewCloudFlareProvider(
   709  		endpoint.NewDomainFilter([]string{"bar.com"}),
   710  		provider.NewZoneIDFilter([]string{""}),
   711  		false,
   712  		true,
   713  		5000)
   714  	if err != nil {
   715  		t.Errorf("should not fail, %s", err)
   716  	}
   717  
   718  	_ = os.Unsetenv("CF_API_KEY")
   719  	_ = os.Unsetenv("CF_API_EMAIL")
   720  	_, err = NewCloudFlareProvider(
   721  		endpoint.NewDomainFilter([]string{"bar.com"}),
   722  		provider.NewZoneIDFilter([]string{""}),
   723  		false,
   724  		true,
   725  		5000)
   726  	if err == nil {
   727  		t.Errorf("expected to fail")
   728  	}
   729  }
   730  
   731  func TestCloudflareApplyChanges(t *testing.T) {
   732  	changes := &plan.Changes{}
   733  	client := NewMockCloudFlareClient()
   734  	provider := &CloudFlareProvider{
   735  		Client: client,
   736  	}
   737  	changes.Create = []*endpoint.Endpoint{{
   738  		DNSName: "new.bar.com",
   739  		Targets: endpoint.Targets{"target"},
   740  	}, {
   741  		DNSName: "new.ext-dns-test.unrelated.to",
   742  		Targets: endpoint.Targets{"target"},
   743  	}}
   744  	changes.Delete = []*endpoint.Endpoint{{
   745  		DNSName: "foobar.bar.com",
   746  		Targets: endpoint.Targets{"target"},
   747  	}}
   748  	changes.UpdateOld = []*endpoint.Endpoint{{
   749  		DNSName: "foobar.bar.com",
   750  		Targets: endpoint.Targets{"target-old"},
   751  	}}
   752  	changes.UpdateNew = []*endpoint.Endpoint{{
   753  		DNSName: "foobar.bar.com",
   754  		Targets: endpoint.Targets{"target-new"},
   755  	}}
   756  	err := provider.ApplyChanges(context.Background(), changes)
   757  	if err != nil {
   758  		t.Errorf("should not fail, %s", err)
   759  	}
   760  
   761  	td.Cmp(t, client.Actions, []MockAction{
   762  		{
   763  			Name:   "Create",
   764  			ZoneId: "001",
   765  			RecordData: cloudflare.DNSRecord{
   766  				Name:    "new.bar.com",
   767  				Content: "target",
   768  				TTL:     1,
   769  				Proxied: proxyDisabled,
   770  			},
   771  		},
   772  		{
   773  			Name:   "Create",
   774  			ZoneId: "001",
   775  			RecordData: cloudflare.DNSRecord{
   776  				Name:    "foobar.bar.com",
   777  				Content: "target-new",
   778  				TTL:     1,
   779  				Proxied: proxyDisabled,
   780  			},
   781  		},
   782  	})
   783  
   784  	// empty changes
   785  	changes.Create = []*endpoint.Endpoint{}
   786  	changes.Delete = []*endpoint.Endpoint{}
   787  	changes.UpdateOld = []*endpoint.Endpoint{}
   788  	changes.UpdateNew = []*endpoint.Endpoint{}
   789  
   790  	err = provider.ApplyChanges(context.Background(), changes)
   791  	if err != nil {
   792  		t.Errorf("should not fail, %s", err)
   793  	}
   794  }
   795  
   796  func TestCloudflareApplyChangesError(t *testing.T) {
   797  	changes := &plan.Changes{}
   798  	client := NewMockCloudFlareClient()
   799  	provider := &CloudFlareProvider{
   800  		Client: client,
   801  	}
   802  	changes.Create = []*endpoint.Endpoint{{
   803  		DNSName: "newerror.bar.com",
   804  		Targets: endpoint.Targets{"target"},
   805  	}}
   806  	err := provider.ApplyChanges(context.Background(), changes)
   807  	if err == nil {
   808  		t.Errorf("should fail, %s", err)
   809  	}
   810  }
   811  
   812  func TestCloudflareGetRecordID(t *testing.T) {
   813  	p := &CloudFlareProvider{}
   814  	records := []cloudflare.DNSRecord{
   815  		{
   816  			Name:    "foo.com",
   817  			Type:    endpoint.RecordTypeCNAME,
   818  			Content: "foobar",
   819  			ID:      "1",
   820  		},
   821  		{
   822  			Name: "bar.de",
   823  			Type: endpoint.RecordTypeA,
   824  			ID:   "2",
   825  		},
   826  		{
   827  			Name:    "bar.de",
   828  			Type:    endpoint.RecordTypeA,
   829  			Content: "1.2.3.4",
   830  			ID:      "2",
   831  		},
   832  	}
   833  
   834  	assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
   835  		Name:    "foo.com",
   836  		Type:    endpoint.RecordTypeA,
   837  		Content: "foobar",
   838  	}))
   839  
   840  	assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
   841  		Name:    "foo.com",
   842  		Type:    endpoint.RecordTypeCNAME,
   843  		Content: "fizfuz",
   844  	}))
   845  
   846  	assert.Equal(t, "1", p.getRecordID(records, cloudflare.DNSRecord{
   847  		Name:    "foo.com",
   848  		Type:    endpoint.RecordTypeCNAME,
   849  		Content: "foobar",
   850  	}))
   851  	assert.Equal(t, "", p.getRecordID(records, cloudflare.DNSRecord{
   852  		Name:    "bar.de",
   853  		Type:    endpoint.RecordTypeA,
   854  		Content: "2.3.4.5",
   855  	}))
   856  	assert.Equal(t, "2", p.getRecordID(records, cloudflare.DNSRecord{
   857  		Name:    "bar.de",
   858  		Type:    endpoint.RecordTypeA,
   859  		Content: "1.2.3.4",
   860  	}))
   861  }
   862  
   863  func TestCloudflareGroupByNameAndType(t *testing.T) {
   864  	testCases := []struct {
   865  		Name              string
   866  		Records           []cloudflare.DNSRecord
   867  		ExpectedEndpoints []*endpoint.Endpoint
   868  	}{
   869  		{
   870  			Name:              "empty",
   871  			Records:           []cloudflare.DNSRecord{},
   872  			ExpectedEndpoints: []*endpoint.Endpoint{},
   873  		},
   874  		{
   875  			Name: "single record - single target",
   876  			Records: []cloudflare.DNSRecord{
   877  				{
   878  					Name:    "foo.com",
   879  					Type:    endpoint.RecordTypeA,
   880  					Content: "10.10.10.1",
   881  					TTL:     defaultCloudFlareRecordTTL,
   882  					Proxied: proxyDisabled,
   883  				},
   884  			},
   885  			ExpectedEndpoints: []*endpoint.Endpoint{
   886  				{
   887  					DNSName:    "foo.com",
   888  					Targets:    endpoint.Targets{"10.10.10.1"},
   889  					RecordType: endpoint.RecordTypeA,
   890  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
   891  					Labels:     endpoint.Labels{},
   892  					ProviderSpecific: endpoint.ProviderSpecific{
   893  						{
   894  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   895  							Value: "false",
   896  						},
   897  					},
   898  				},
   899  			},
   900  		},
   901  		{
   902  			Name: "single record - multiple targets",
   903  			Records: []cloudflare.DNSRecord{
   904  				{
   905  					Name:    "foo.com",
   906  					Type:    endpoint.RecordTypeA,
   907  					Content: "10.10.10.1",
   908  					TTL:     defaultCloudFlareRecordTTL,
   909  					Proxied: proxyDisabled,
   910  				},
   911  				{
   912  					Name:    "foo.com",
   913  					Type:    endpoint.RecordTypeA,
   914  					Content: "10.10.10.2",
   915  					TTL:     defaultCloudFlareRecordTTL,
   916  					Proxied: proxyDisabled,
   917  				},
   918  			},
   919  			ExpectedEndpoints: []*endpoint.Endpoint{
   920  				{
   921  					DNSName:    "foo.com",
   922  					Targets:    endpoint.Targets{"10.10.10.1", "10.10.10.2"},
   923  					RecordType: endpoint.RecordTypeA,
   924  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
   925  					Labels:     endpoint.Labels{},
   926  					ProviderSpecific: endpoint.ProviderSpecific{
   927  						{
   928  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   929  							Value: "false",
   930  						},
   931  					},
   932  				},
   933  			},
   934  		},
   935  		{
   936  			Name: "multiple record - multiple targets",
   937  			Records: []cloudflare.DNSRecord{
   938  				{
   939  					Name:    "foo.com",
   940  					Type:    endpoint.RecordTypeA,
   941  					Content: "10.10.10.1",
   942  					TTL:     defaultCloudFlareRecordTTL,
   943  					Proxied: proxyDisabled,
   944  				},
   945  				{
   946  					Name:    "foo.com",
   947  					Type:    endpoint.RecordTypeA,
   948  					Content: "10.10.10.2",
   949  					TTL:     defaultCloudFlareRecordTTL,
   950  					Proxied: proxyDisabled,
   951  				},
   952  				{
   953  					Name:    "bar.de",
   954  					Type:    endpoint.RecordTypeA,
   955  					Content: "10.10.10.1",
   956  					TTL:     defaultCloudFlareRecordTTL,
   957  					Proxied: proxyDisabled,
   958  				},
   959  				{
   960  					Name:    "bar.de",
   961  					Type:    endpoint.RecordTypeA,
   962  					Content: "10.10.10.2",
   963  					TTL:     defaultCloudFlareRecordTTL,
   964  					Proxied: proxyDisabled,
   965  				},
   966  			},
   967  			ExpectedEndpoints: []*endpoint.Endpoint{
   968  				{
   969  					DNSName:    "foo.com",
   970  					Targets:    endpoint.Targets{"10.10.10.1", "10.10.10.2"},
   971  					RecordType: endpoint.RecordTypeA,
   972  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
   973  					Labels:     endpoint.Labels{},
   974  					ProviderSpecific: endpoint.ProviderSpecific{
   975  						{
   976  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   977  							Value: "false",
   978  						},
   979  					},
   980  				},
   981  				{
   982  					DNSName:    "bar.de",
   983  					Targets:    endpoint.Targets{"10.10.10.1", "10.10.10.2"},
   984  					RecordType: endpoint.RecordTypeA,
   985  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
   986  					Labels:     endpoint.Labels{},
   987  					ProviderSpecific: endpoint.ProviderSpecific{
   988  						{
   989  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
   990  							Value: "false",
   991  						},
   992  					},
   993  				},
   994  			},
   995  		},
   996  		{
   997  			Name: "multiple record - mixed single/multiple targets",
   998  			Records: []cloudflare.DNSRecord{
   999  				{
  1000  					Name:    "foo.com",
  1001  					Type:    endpoint.RecordTypeA,
  1002  					Content: "10.10.10.1",
  1003  					TTL:     defaultCloudFlareRecordTTL,
  1004  					Proxied: proxyDisabled,
  1005  				},
  1006  				{
  1007  					Name:    "foo.com",
  1008  					Type:    endpoint.RecordTypeA,
  1009  					Content: "10.10.10.2",
  1010  					TTL:     defaultCloudFlareRecordTTL,
  1011  					Proxied: proxyDisabled,
  1012  				},
  1013  				{
  1014  					Name:    "bar.de",
  1015  					Type:    endpoint.RecordTypeA,
  1016  					Content: "10.10.10.1",
  1017  					TTL:     defaultCloudFlareRecordTTL,
  1018  					Proxied: proxyDisabled,
  1019  				},
  1020  			},
  1021  			ExpectedEndpoints: []*endpoint.Endpoint{
  1022  				{
  1023  					DNSName:    "foo.com",
  1024  					Targets:    endpoint.Targets{"10.10.10.1", "10.10.10.2"},
  1025  					RecordType: endpoint.RecordTypeA,
  1026  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
  1027  					Labels:     endpoint.Labels{},
  1028  					ProviderSpecific: endpoint.ProviderSpecific{
  1029  						{
  1030  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
  1031  							Value: "false",
  1032  						},
  1033  					},
  1034  				},
  1035  				{
  1036  					DNSName:    "bar.de",
  1037  					Targets:    endpoint.Targets{"10.10.10.1"},
  1038  					RecordType: endpoint.RecordTypeA,
  1039  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
  1040  					Labels:     endpoint.Labels{},
  1041  					ProviderSpecific: endpoint.ProviderSpecific{
  1042  						{
  1043  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
  1044  							Value: "false",
  1045  						},
  1046  					},
  1047  				},
  1048  			},
  1049  		},
  1050  		{
  1051  			Name: "unsupported record type",
  1052  			Records: []cloudflare.DNSRecord{
  1053  				{
  1054  					Name:    "foo.com",
  1055  					Type:    endpoint.RecordTypeA,
  1056  					Content: "10.10.10.1",
  1057  					TTL:     defaultCloudFlareRecordTTL,
  1058  					Proxied: proxyDisabled,
  1059  				},
  1060  				{
  1061  					Name:    "foo.com",
  1062  					Type:    endpoint.RecordTypeA,
  1063  					Content: "10.10.10.2",
  1064  					TTL:     defaultCloudFlareRecordTTL,
  1065  					Proxied: proxyDisabled,
  1066  				},
  1067  				{
  1068  					Name:    "bar.de",
  1069  					Type:    "NOT SUPPORTED",
  1070  					Content: "10.10.10.1",
  1071  					TTL:     defaultCloudFlareRecordTTL,
  1072  					Proxied: proxyDisabled,
  1073  				},
  1074  			},
  1075  			ExpectedEndpoints: []*endpoint.Endpoint{
  1076  				{
  1077  					DNSName:    "foo.com",
  1078  					Targets:    endpoint.Targets{"10.10.10.1", "10.10.10.2"},
  1079  					RecordType: endpoint.RecordTypeA,
  1080  					RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
  1081  					Labels:     endpoint.Labels{},
  1082  					ProviderSpecific: endpoint.ProviderSpecific{
  1083  						{
  1084  							Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
  1085  							Value: "false",
  1086  						},
  1087  					},
  1088  				},
  1089  			},
  1090  		},
  1091  	}
  1092  
  1093  	for _, tc := range testCases {
  1094  		assert.ElementsMatch(t, groupByNameAndType(tc.Records), tc.ExpectedEndpoints)
  1095  	}
  1096  }
  1097  
  1098  func TestProviderPropertiesIdempotency(t *testing.T) {
  1099  	testCases := []struct {
  1100  		Name                     string
  1101  		ProviderProxiedByDefault bool
  1102  		RecordsAreProxied        *bool
  1103  		ShouldBeUpdated          bool
  1104  	}{
  1105  		{
  1106  			Name:                     "ProxyDefault: false, ShouldBeProxied: false, ExpectUpdates: false",
  1107  			ProviderProxiedByDefault: false,
  1108  			RecordsAreProxied:        proxyDisabled,
  1109  			ShouldBeUpdated:          false,
  1110  		},
  1111  		{
  1112  			Name:                     "ProxyDefault: true, ShouldBeProxied: true, ExpectUpdates: false",
  1113  			ProviderProxiedByDefault: true,
  1114  			RecordsAreProxied:        proxyEnabled,
  1115  			ShouldBeUpdated:          false,
  1116  		},
  1117  		{
  1118  			Name:                     "ProxyDefault: true, ShouldBeProxied: false, ExpectUpdates: true",
  1119  			ProviderProxiedByDefault: true,
  1120  			RecordsAreProxied:        proxyDisabled,
  1121  			ShouldBeUpdated:          true,
  1122  		},
  1123  		{
  1124  			Name:                     "ProxyDefault: false, ShouldBeProxied: true, ExpectUpdates: true",
  1125  			ProviderProxiedByDefault: false,
  1126  			RecordsAreProxied:        proxyEnabled,
  1127  			ShouldBeUpdated:          true,
  1128  		},
  1129  	}
  1130  
  1131  	for _, test := range testCases {
  1132  		t.Run(test.Name, func(t *testing.T) {
  1133  			client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
  1134  				"001": {
  1135  					{
  1136  						ID:      "1234567890",
  1137  						ZoneID:  "001",
  1138  						Name:    "foobar.bar.com",
  1139  						Type:    endpoint.RecordTypeA,
  1140  						TTL:     120,
  1141  						Content: "1.2.3.4",
  1142  						Proxied: test.RecordsAreProxied,
  1143  					},
  1144  				},
  1145  			})
  1146  
  1147  			provider := &CloudFlareProvider{
  1148  				Client:           client,
  1149  				proxiedByDefault: test.ProviderProxiedByDefault,
  1150  			}
  1151  			ctx := context.Background()
  1152  
  1153  			current, err := provider.Records(ctx)
  1154  			if err != nil {
  1155  				t.Errorf("should not fail, %s", err)
  1156  			}
  1157  			assert.Equal(t, 1, len(current))
  1158  
  1159  			desired := []*endpoint.Endpoint{}
  1160  			for _, c := range current {
  1161  				// Copy all except ProviderSpecific fields
  1162  				desired = append(desired, &endpoint.Endpoint{
  1163  					DNSName:       c.DNSName,
  1164  					Targets:       c.Targets,
  1165  					RecordType:    c.RecordType,
  1166  					SetIdentifier: c.SetIdentifier,
  1167  					RecordTTL:     c.RecordTTL,
  1168  					Labels:        c.Labels,
  1169  				})
  1170  			}
  1171  
  1172  			desired, err = provider.AdjustEndpoints(desired)
  1173  			assert.NoError(t, err)
  1174  
  1175  			plan := plan.Plan{
  1176  				Current:        current,
  1177  				Desired:        desired,
  1178  				ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
  1179  			}
  1180  
  1181  			plan = *plan.Calculate()
  1182  			assert.NotNil(t, plan.Changes, "should have plan")
  1183  			if plan.Changes == nil {
  1184  				return
  1185  			}
  1186  			assert.Equal(t, 0, len(plan.Changes.Create), "should not have creates")
  1187  			assert.Equal(t, 0, len(plan.Changes.Delete), "should not have deletes")
  1188  
  1189  			if test.ShouldBeUpdated {
  1190  				assert.Equal(t, 1, len(plan.Changes.UpdateNew), "should not have new updates")
  1191  				assert.Equal(t, 1, len(plan.Changes.UpdateOld), "should not have old updates")
  1192  			} else {
  1193  				assert.Equal(t, 0, len(plan.Changes.UpdateNew), "should not have new updates")
  1194  				assert.Equal(t, 0, len(plan.Changes.UpdateOld), "should not have old updates")
  1195  			}
  1196  		})
  1197  	}
  1198  }
  1199  
  1200  func TestCloudflareComplexUpdate(t *testing.T) {
  1201  	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
  1202  		"001": ExampleDomain,
  1203  	})
  1204  
  1205  	provider := &CloudFlareProvider{
  1206  		Client: client,
  1207  	}
  1208  	ctx := context.Background()
  1209  
  1210  	records, err := provider.Records(ctx)
  1211  	if err != nil {
  1212  		t.Errorf("should not fail, %s", err)
  1213  	}
  1214  
  1215  	domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
  1216  	endpoints, err := provider.AdjustEndpoints([]*endpoint.Endpoint{
  1217  		{
  1218  			DNSName:    "foobar.bar.com",
  1219  			Targets:    endpoint.Targets{"1.2.3.4", "2.3.4.5"},
  1220  			RecordType: endpoint.RecordTypeA,
  1221  			RecordTTL:  endpoint.TTL(defaultCloudFlareRecordTTL),
  1222  			Labels:     endpoint.Labels{},
  1223  			ProviderSpecific: endpoint.ProviderSpecific{
  1224  				{
  1225  					Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
  1226  					Value: "true",
  1227  				},
  1228  			},
  1229  		},
  1230  	})
  1231  	assert.NoError(t, err)
  1232  	plan := &plan.Plan{
  1233  		Current:        records,
  1234  		Desired:        endpoints,
  1235  		DomainFilter:   endpoint.MatchAllDomainFilters{&domainFilter},
  1236  		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
  1237  	}
  1238  
  1239  	planned := plan.Calculate()
  1240  
  1241  	err = provider.ApplyChanges(context.Background(), planned.Changes)
  1242  
  1243  	if err != nil {
  1244  		t.Errorf("should not fail, %s", err)
  1245  	}
  1246  
  1247  	td.CmpDeeply(t, client.Actions, []MockAction{
  1248  		{
  1249  			Name:     "Delete",
  1250  			ZoneId:   "001",
  1251  			RecordId: "2345678901",
  1252  		},
  1253  		{
  1254  			Name:   "Create",
  1255  			ZoneId: "001",
  1256  			RecordData: cloudflare.DNSRecord{
  1257  				Name:    "foobar.bar.com",
  1258  				Type:    "A",
  1259  				Content: "2.3.4.5",
  1260  				TTL:     1,
  1261  				Proxied: proxyEnabled,
  1262  			},
  1263  		},
  1264  		{
  1265  			Name:     "Update",
  1266  			ZoneId:   "001",
  1267  			RecordId: "1234567890",
  1268  			RecordData: cloudflare.DNSRecord{
  1269  				Name:    "foobar.bar.com",
  1270  				Type:    "A",
  1271  				Content: "1.2.3.4",
  1272  				TTL:     1,
  1273  				Proxied: proxyEnabled,
  1274  			},
  1275  		},
  1276  	})
  1277  }
  1278  
  1279  func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
  1280  	client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
  1281  		"001": {
  1282  			{
  1283  				ID:      "1234567890",
  1284  				ZoneID:  "001",
  1285  				Name:    "foobar.bar.com",
  1286  				Type:    endpoint.RecordTypeA,
  1287  				TTL:     1,
  1288  				Content: "1.2.3.4",
  1289  				Proxied: proxyEnabled,
  1290  			},
  1291  		},
  1292  	})
  1293  
  1294  	provider := &CloudFlareProvider{
  1295  		Client: client,
  1296  	}
  1297  
  1298  	records, err := provider.Records(context.Background())
  1299  	if err != nil {
  1300  		t.Errorf("should not fail, %s", err)
  1301  	}
  1302  
  1303  	endpoints := []*endpoint.Endpoint{
  1304  		{
  1305  			DNSName:    "foobar.bar.com",
  1306  			Targets:    endpoint.Targets{"1.2.3.4"},
  1307  			RecordType: endpoint.RecordTypeA,
  1308  			RecordTTL:  300,
  1309  			Labels:     endpoint.Labels{},
  1310  			ProviderSpecific: endpoint.ProviderSpecific{
  1311  				{
  1312  					Name:  "external-dns.alpha.kubernetes.io/cloudflare-proxied",
  1313  					Value: "true",
  1314  				},
  1315  			},
  1316  		},
  1317  	}
  1318  
  1319  	provider.AdjustEndpoints(endpoints)
  1320  
  1321  	domainFilter := endpoint.NewDomainFilter([]string{"bar.com"})
  1322  	plan := &plan.Plan{
  1323  		Current:        records,
  1324  		Desired:        endpoints,
  1325  		DomainFilter:   endpoint.MatchAllDomainFilters{&domainFilter},
  1326  		ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
  1327  	}
  1328  
  1329  	planned := plan.Calculate()
  1330  
  1331  	assert.Equal(t, 0, len(planned.Changes.Create), "no new changes should be here")
  1332  	assert.Equal(t, 0, len(planned.Changes.UpdateNew), "no new changes should be here")
  1333  	assert.Equal(t, 0, len(planned.Changes.UpdateOld), "no new changes should be here")
  1334  	assert.Equal(t, 0, len(planned.Changes.Delete), "no new changes should be here")
  1335  }