sigs.k8s.io/external-dns@v0.14.1/provider/ovh/ovh_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 ovh
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"sort"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/miekg/dns"
    28  	"github.com/ovh/go-ovh/ovh"
    29  	"github.com/patrickmn/go-cache"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/mock"
    32  	"go.uber.org/ratelimit"
    33  	"sigs.k8s.io/external-dns/endpoint"
    34  	"sigs.k8s.io/external-dns/plan"
    35  )
    36  
    37  type mockOvhClient struct {
    38  	mock.Mock
    39  }
    40  
    41  func (c *mockOvhClient) Post(endpoint string, input interface{}, output interface{}) error {
    42  	stub := c.Called(endpoint, input)
    43  	data, _ := json.Marshal(stub.Get(0))
    44  	json.Unmarshal(data, output)
    45  	return stub.Error(1)
    46  }
    47  
    48  func (c *mockOvhClient) Get(endpoint string, output interface{}) error {
    49  	stub := c.Called(endpoint)
    50  	data, _ := json.Marshal(stub.Get(0))
    51  	json.Unmarshal(data, output)
    52  	return stub.Error(1)
    53  }
    54  
    55  func (c *mockOvhClient) Delete(endpoint string, output interface{}) error {
    56  	stub := c.Called(endpoint)
    57  	data, _ := json.Marshal(stub.Get(0))
    58  	json.Unmarshal(data, output)
    59  	return stub.Error(1)
    60  }
    61  
    62  type mockDnsClient struct {
    63  	mock.Mock
    64  }
    65  
    66  func (c *mockDnsClient) ExchangeContext(ctx context.Context, m *dns.Msg, addr string) (*dns.Msg, time.Duration, error) {
    67  	args := c.Called(ctx, m, addr)
    68  
    69  	msg := args.Get(0).(*dns.Msg)
    70  	err := args.Error(1)
    71  
    72  	return msg, time.Duration(0), err
    73  }
    74  
    75  func TestOvhZones(t *testing.T) {
    76  	assert := assert.New(t)
    77  	client := new(mockOvhClient)
    78  	provider := &OVHProvider{
    79  		client:         client,
    80  		apiRateLimiter: ratelimit.New(10),
    81  		domainFilter:   endpoint.NewDomainFilter([]string{"com"}),
    82  		cacheInstance:  cache.New(cache.NoExpiration, cache.NoExpiration),
    83  		dnsClient:      new(mockDnsClient),
    84  	}
    85  
    86  	// Basic zones
    87  	client.On("Get", "/domain/zone").Return([]string{"example.com", "example.net"}, nil).Once()
    88  	domains, err := provider.zones()
    89  	assert.NoError(err)
    90  	assert.Contains(domains, "example.com")
    91  	assert.NotContains(domains, "example.net")
    92  	client.AssertExpectations(t)
    93  
    94  	// Error on getting zones
    95  	client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
    96  	domains, err = provider.zones()
    97  	assert.Error(err)
    98  	assert.Nil(domains)
    99  	client.AssertExpectations(t)
   100  }
   101  
   102  func TestOvhZoneRecords(t *testing.T) {
   103  	assert := assert.New(t)
   104  	client := new(mockOvhClient)
   105  	provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration), dnsClient: nil, UseCache: true}
   106  
   107  	// Basic zones records
   108  	t.Log("Basic zones records")
   109  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   110  	client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090901}, nil).Once()
   111  	client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
   112  	client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   113  	client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   114  	zones, records, err := provider.zonesRecords(context.TODO())
   115  	assert.NoError(err)
   116  	assert.ElementsMatch(zones, []string{"example.org"})
   117  	assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
   118  	client.AssertExpectations(t)
   119  
   120  	// Error on getting zones list
   121  	t.Log("Error on getting zones list")
   122  	client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
   123  	zones, records, err = provider.zonesRecords(context.TODO())
   124  	assert.Error(err)
   125  	assert.Nil(zones)
   126  	assert.Nil(records)
   127  	client.AssertExpectations(t)
   128  
   129  	// Error on getting zone SOA
   130  	t.Log("Error on getting zone SOA")
   131  	provider.cacheInstance = cache.New(cache.NoExpiration, cache.NoExpiration)
   132  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   133  	client.On("Get", "/domain/zone/example.org/soa").Return(nil, ovh.ErrAPIDown).Once()
   134  	zones, records, err = provider.zonesRecords(context.TODO())
   135  	assert.Error(err)
   136  	assert.Nil(zones)
   137  	assert.Nil(records)
   138  	client.AssertExpectations(t)
   139  
   140  	// Error on getting zone records
   141  	t.Log("Error on getting zone records")
   142  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   143  	client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
   144  	client.On("Get", "/domain/zone/example.org/record").Return(nil, ovh.ErrAPIDown).Once()
   145  	zones, records, err = provider.zonesRecords(context.TODO())
   146  	assert.Error(err)
   147  	assert.Nil(zones)
   148  	assert.Nil(records)
   149  	client.AssertExpectations(t)
   150  
   151  	// Error on getting zone record detail
   152  	t.Log("Error on getting zone record detail")
   153  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   154  	client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
   155  	client.On("Get", "/domain/zone/example.org/record").Return([]uint64{42}, nil).Once()
   156  	client.On("Get", "/domain/zone/example.org/record/42").Return(nil, ovh.ErrAPIDown).Once()
   157  	zones, records, err = provider.zonesRecords(context.TODO())
   158  	assert.Error(err)
   159  	assert.Nil(zones)
   160  	assert.Nil(records)
   161  	client.AssertExpectations(t)
   162  }
   163  
   164  func TestOvhZoneRecordsCache(t *testing.T) {
   165  	assert := assert.New(t)
   166  	client := new(mockOvhClient)
   167  	dnsClient := new(mockDnsClient)
   168  	provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration), dnsClient: dnsClient, UseCache: true}
   169  
   170  	// First call, cache miss
   171  	t.Log("First call, cache miss")
   172  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   173  	client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090901}, nil).Once()
   174  	client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
   175  	client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   176  	client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   177  
   178  	zones, records, err := provider.zonesRecords(context.TODO())
   179  	assert.NoError(err)
   180  	assert.ElementsMatch(zones, []string{"example.org"})
   181  	assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
   182  	client.AssertExpectations(t)
   183  	dnsClient.AssertExpectations(t)
   184  
   185  	// reset mock
   186  	client = new(mockOvhClient)
   187  	dnsClient = new(mockDnsClient)
   188  	provider.client, provider.dnsClient = client, dnsClient
   189  
   190  	// second call, cache hit
   191  	t.Log("second call, cache hit")
   192  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   193  	dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
   194  		Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090901}}}, nil)
   195  	zones, records, err = provider.zonesRecords(context.TODO())
   196  	assert.NoError(err)
   197  	assert.ElementsMatch(zones, []string{"example.org"})
   198  	assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
   199  	client.AssertExpectations(t)
   200  	dnsClient.AssertExpectations(t)
   201  
   202  	// reset mock
   203  	client = new(mockOvhClient)
   204  	dnsClient = new(mockDnsClient)
   205  	provider.client, provider.dnsClient = client, dnsClient
   206  
   207  	// third call, cache out of date
   208  	t.Log("third call, cache out of date")
   209  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   210  	dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
   211  		Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090902}}}, nil)
   212  	client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
   213  	client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24}, nil).Once()
   214  	client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   215  
   216  	zones, records, err = provider.zonesRecords(context.TODO())
   217  	assert.NoError(err)
   218  	assert.ElementsMatch(zones, []string{"example.org"})
   219  	assert.ElementsMatch(records, []ovhRecord{{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
   220  	client.AssertExpectations(t)
   221  	dnsClient.AssertExpectations(t)
   222  
   223  	// reset mock
   224  	client = new(mockOvhClient)
   225  	dnsClient = new(mockDnsClient)
   226  	provider.client, provider.dnsClient = client, dnsClient
   227  
   228  	// fourth call, cache hit
   229  	t.Log("fourth call, cache hit")
   230  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   231  	dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
   232  		Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090902}}}, nil)
   233  
   234  	zones, records, err = provider.zonesRecords(context.TODO())
   235  	assert.NoError(err)
   236  	assert.ElementsMatch(zones, []string{"example.org"})
   237  	assert.ElementsMatch(records, []ovhRecord{{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
   238  	client.AssertExpectations(t)
   239  	dnsClient.AssertExpectations(t)
   240  
   241  	// reset mock
   242  	client = new(mockOvhClient)
   243  	dnsClient = new(mockDnsClient)
   244  	provider.client, provider.dnsClient = client, dnsClient
   245  
   246  	// fifth call, dns issue
   247  	t.Log("fourth call, cache hit")
   248  	client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
   249  	dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
   250  		Return(&dns.Msg{Answer: []dns.RR{}}, errors.New("dns issue"))
   251  	client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090903}, nil).Once()
   252  	client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
   253  	client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   254  	client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   255  
   256  	zones, records, err = provider.zonesRecords(context.TODO())
   257  	assert.NoError(err)
   258  	assert.ElementsMatch(zones, []string{"example.org"})
   259  	assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
   260  	client.AssertExpectations(t)
   261  	dnsClient.AssertExpectations(t)
   262  }
   263  
   264  func TestOvhRecords(t *testing.T) {
   265  	assert := assert.New(t)
   266  	client := new(mockOvhClient)
   267  	provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
   268  
   269  	// Basic zones records
   270  	client.On("Get", "/domain/zone").Return([]string{"example.org", "example.net"}, nil).Once()
   271  	client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
   272  	client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   273  	client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "www", FieldType: "CNAME", TTL: 10, Target: "example.org."}}, nil).Once()
   274  	client.On("Get", "/domain/zone/example.net/record").Return([]uint64{24, 42}, nil).Once()
   275  	client.On("Get", "/domain/zone/example.net/record/24").Return(ovhRecord{ID: 24, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
   276  	client.On("Get", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.43"}}, nil).Once()
   277  	endpoints, err := provider.Records(context.TODO())
   278  	assert.NoError(err)
   279  	// Little fix for multi targets endpoint
   280  	for _, endpoint := range endpoints {
   281  		sort.Strings(endpoint.Targets)
   282  	}
   283  	assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
   284  		{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}},
   285  		{DNSName: "www.example.org", RecordType: "CNAME", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"example.org"}},
   286  		{DNSName: "ovh.example.net", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42", "203.0.113.43"}},
   287  	})
   288  	client.AssertExpectations(t)
   289  
   290  	// Error getting zone
   291  	client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
   292  	endpoints, err = provider.Records(context.TODO())
   293  	assert.Error(err)
   294  	assert.Nil(endpoints)
   295  	client.AssertExpectations(t)
   296  }
   297  
   298  func TestOvhRefresh(t *testing.T) {
   299  	client := new(mockOvhClient)
   300  	provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
   301  
   302  	// Basic zone refresh
   303  	client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
   304  	provider.refresh("example.net")
   305  	client.AssertExpectations(t)
   306  }
   307  
   308  func TestOvhNewChange(t *testing.T) {
   309  	assert := assert.New(t)
   310  	endpoints := []*endpoint.Endpoint{
   311  		{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
   312  		{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
   313  		{DNSName: "ovh2.example.net", RecordType: "CNAME", Targets: []string{"ovh.example.net"}},
   314  		{DNSName: "test.example.org"},
   315  	}
   316  
   317  	// Create change
   318  	changes := newOvhChange(ovhCreate, endpoints, []string{"example.net"}, []ovhRecord{})
   319  	assert.ElementsMatch(changes, []ovhChange{
   320  		{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}},
   321  		{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.43"}}},
   322  		{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh2", FieldType: "CNAME", TTL: ovhDefaultTTL, Target: "ovh.example.net."}}},
   323  	})
   324  
   325  	// Delete change
   326  	endpoints = []*endpoint.Endpoint{
   327  		{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.42"}},
   328  	}
   329  	records := []ovhRecord{
   330  		{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", SubDomain: "ovh", Target: "203.0.113.42"}},
   331  	}
   332  	changes = newOvhChange(ovhDelete, endpoints, []string{"example.net"}, records)
   333  	assert.ElementsMatch(changes, []ovhChange{
   334  		{Action: ovhDelete, ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}},
   335  	})
   336  }
   337  
   338  func TestOvhApplyChanges(t *testing.T) {
   339  	assert := assert.New(t)
   340  	client := new(mockOvhClient)
   341  	provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
   342  	changes := plan.Changes{
   343  		Create: []*endpoint.Endpoint{
   344  			{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
   345  		},
   346  		Delete: []*endpoint.Endpoint{
   347  			{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
   348  		},
   349  	}
   350  
   351  	client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
   352  	client.On("Get", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
   353  	client.On("Get", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.43"}}, nil).Once()
   354  	client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, nil).Once()
   355  	client.On("Delete", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
   356  	client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
   357  
   358  	// Basic changes
   359  	assert.NoError(provider.ApplyChanges(context.TODO(), &changes))
   360  	client.AssertExpectations(t)
   361  
   362  	// Getting zones failed
   363  	client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
   364  	assert.Error(provider.ApplyChanges(context.TODO(), &changes))
   365  	client.AssertExpectations(t)
   366  
   367  	// Apply change failed
   368  	client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
   369  	client.On("Get", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
   370  	client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, ovh.ErrAPIDown).Once()
   371  	assert.Error(provider.ApplyChanges(context.TODO(), &plan.Changes{
   372  		Create: []*endpoint.Endpoint{
   373  			{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
   374  		},
   375  	}))
   376  	client.AssertExpectations(t)
   377  
   378  	// Refresh failed
   379  	client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
   380  	client.On("Get", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
   381  	client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, nil).Once()
   382  	client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, ovh.ErrAPIDown).Once()
   383  	assert.Error(provider.ApplyChanges(context.TODO(), &plan.Changes{
   384  		Create: []*endpoint.Endpoint{
   385  			{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
   386  		},
   387  	}))
   388  	client.AssertExpectations(t)
   389  }
   390  
   391  func TestOvhChange(t *testing.T) {
   392  	assert := assert.New(t)
   393  	client := new(mockOvhClient)
   394  	provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
   395  
   396  	// Record creation
   397  	client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "ovh"}).Return(nil, nil).Once()
   398  	assert.NoError(provider.change(ovhChange{
   399  		Action:    ovhCreate,
   400  		ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
   401  	}))
   402  	client.AssertExpectations(t)
   403  
   404  	// Record deletion
   405  	client.On("Delete", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
   406  	assert.NoError(provider.change(ovhChange{
   407  		Action:    ovhDelete,
   408  		ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
   409  	}))
   410  	client.AssertExpectations(t)
   411  
   412  	// Record deletion error
   413  	assert.Error(provider.change(ovhChange{
   414  		Action:    ovhDelete,
   415  		ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
   416  	}))
   417  	client.AssertExpectations(t)
   418  }
   419  
   420  func TestOvhCountTargets(t *testing.T) {
   421  	cases := []struct {
   422  		endpoints [][]*endpoint.Endpoint
   423  		count     int
   424  	}{
   425  		{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 1},
   426  		{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}, {DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 2},
   427  		{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target", "target"}}}}, 3},
   428  		{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}, {{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}}, 4},
   429  	}
   430  	for _, test := range cases {
   431  		count := countTargets(test.endpoints...)
   432  		if count != test.count {
   433  			t.Errorf("Wrong targets counts (Should be %d, get %d)", test.count, count)
   434  		}
   435  	}
   436  }