sigs.k8s.io/external-dns@v0.14.1/provider/digitalocean/digital_ocean_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 digitalocean 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "reflect" 24 "sort" 25 "testing" 26 27 "github.com/digitalocean/godo" 28 "github.com/google/go-cmp/cmp" 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 32 "sigs.k8s.io/external-dns/endpoint" 33 "sigs.k8s.io/external-dns/plan" 34 ) 35 36 type mockDigitalOceanClient struct{} 37 38 func (m *mockDigitalOceanClient) RecordsByName(context.Context, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 39 // not used, here only to correctly implement the interface 40 return nil, nil, nil 41 } 42 43 func (m *mockDigitalOceanClient) RecordsByTypeAndName(context.Context, string, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 44 // not used, here only to correctly implement the interface 45 return nil, nil, nil 46 } 47 48 func (m *mockDigitalOceanClient) RecordsByType(context.Context, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 49 // not used, here only to correctly implement the interface 50 return nil, nil, nil 51 } 52 53 func (m *mockDigitalOceanClient) List(ctx context.Context, opt *godo.ListOptions) ([]godo.Domain, *godo.Response, error) { 54 if opt == nil || opt.Page == 0 { 55 return []godo.Domain{{Name: "foo.com"}, {Name: "example.com"}}, &godo.Response{ 56 Links: &godo.Links{ 57 Pages: &godo.Pages{ 58 Next: "http://example.com/v2/domains/?page=2", 59 Last: "1234", 60 }, 61 }, 62 }, nil 63 } 64 return []godo.Domain{{Name: "bar.com"}, {Name: "bar.de"}}, nil, nil 65 } 66 67 func (m *mockDigitalOceanClient) Create(context.Context, *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) { 68 return &godo.Domain{Name: "example.com"}, nil, nil 69 } 70 71 func (m *mockDigitalOceanClient) CreateRecord(context.Context, string, *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) { 72 return &godo.DomainRecord{ID: 1, Name: "new", Type: "CNAME"}, nil, nil 73 } 74 75 func (m *mockDigitalOceanClient) Delete(context.Context, string) (*godo.Response, error) { 76 return nil, nil 77 } 78 79 func (m *mockDigitalOceanClient) DeleteRecord(ctx context.Context, domain string, id int) (*godo.Response, error) { 80 return nil, nil 81 } 82 83 func (m *mockDigitalOceanClient) EditRecord(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) { 84 return &godo.DomainRecord{ID: 1}, nil, nil 85 } 86 87 func (m *mockDigitalOceanClient) Get(ctx context.Context, name string) (*godo.Domain, *godo.Response, error) { 88 return &godo.Domain{Name: "example.com"}, nil, nil 89 } 90 91 func (m *mockDigitalOceanClient) Record(ctx context.Context, domain string, id int) (*godo.DomainRecord, *godo.Response, error) { 92 return &godo.DomainRecord{ID: 1}, nil, nil 93 } 94 95 func (m *mockDigitalOceanClient) Records(ctx context.Context, domain string, opt *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 96 switch domain { 97 case "foo.com": 98 if opt == nil || opt.Page == 0 { 99 return []godo.DomainRecord{ 100 {ID: 1, Name: "foo.ext-dns-test", Type: "CNAME"}, 101 {ID: 2, Name: "bar.ext-dns-test", Type: "CNAME"}, 102 {ID: 3, Name: "@", Type: endpoint.RecordTypeCNAME}, 103 }, &godo.Response{ 104 Links: &godo.Links{ 105 Pages: &godo.Pages{ 106 Next: "http://example.com/v2/domains/?page=2", 107 Last: "1234", 108 }, 109 }, 110 }, nil 111 } 112 return []godo.DomainRecord{{ID: 3, Name: "baz.ext-dns-test", Type: "A"}}, nil, nil 113 case "example.com": 114 if opt == nil || opt.Page == 0 { 115 return []godo.DomainRecord{{ID: 1, Name: "new", Type: "CNAME"}}, &godo.Response{ 116 Links: &godo.Links{ 117 Pages: &godo.Pages{ 118 Next: "http://example.com/v2/domains/?page=2", 119 Last: "1234", 120 }, 121 }, 122 }, nil 123 } 124 return nil, nil, nil 125 default: 126 return nil, nil, nil 127 } 128 } 129 130 type mockDigitalOceanRecordsFail struct{} 131 132 func (m *mockDigitalOceanRecordsFail) RecordsByName(context.Context, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 133 // not used, here only to correctly implement the interface 134 return nil, nil, nil 135 } 136 137 func (m *mockDigitalOceanRecordsFail) RecordsByTypeAndName(context.Context, string, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 138 // not used, here only to correctly implement the interface 139 return nil, nil, nil 140 } 141 142 func (m *mockDigitalOceanRecordsFail) RecordsByType(context.Context, string, string, *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 143 // not used, here only to correctly implement the interface 144 return nil, nil, nil 145 } 146 147 func (m *mockDigitalOceanRecordsFail) List(context.Context, *godo.ListOptions) ([]godo.Domain, *godo.Response, error) { 148 return []godo.Domain{{Name: "foo.com"}, {Name: "bar.com"}}, nil, nil 149 } 150 151 func (m *mockDigitalOceanRecordsFail) Create(context.Context, *godo.DomainCreateRequest) (*godo.Domain, *godo.Response, error) { 152 return &godo.Domain{Name: "example.com"}, nil, nil 153 } 154 155 func (m *mockDigitalOceanRecordsFail) CreateRecord(context.Context, string, *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) { 156 return &godo.DomainRecord{ID: 1}, nil, nil 157 } 158 159 func (m *mockDigitalOceanRecordsFail) Delete(context.Context, string) (*godo.Response, error) { 160 return nil, nil 161 } 162 163 func (m *mockDigitalOceanRecordsFail) DeleteRecord(ctx context.Context, domain string, id int) (*godo.Response, error) { 164 return nil, nil 165 } 166 167 func (m *mockDigitalOceanRecordsFail) EditRecord(ctx context.Context, domain string, id int, editRequest *godo.DomainRecordEditRequest) (*godo.DomainRecord, *godo.Response, error) { 168 return &godo.DomainRecord{ID: 1}, nil, nil 169 } 170 171 func (m *mockDigitalOceanRecordsFail) Get(ctx context.Context, name string) (*godo.Domain, *godo.Response, error) { 172 return &godo.Domain{Name: "example.com"}, nil, nil 173 } 174 175 func (m *mockDigitalOceanRecordsFail) Record(ctx context.Context, domain string, id int) (*godo.DomainRecord, *godo.Response, error) { 176 return nil, nil, fmt.Errorf("Failed to get records") 177 } 178 179 func (m *mockDigitalOceanRecordsFail) Records(ctx context.Context, domain string, opt *godo.ListOptions) ([]godo.DomainRecord, *godo.Response, error) { 180 return []godo.DomainRecord{}, nil, fmt.Errorf("Failed to get records") 181 } 182 183 func isEmpty(xs interface{}) bool { 184 if xs != nil { 185 objValue := reflect.ValueOf(xs) 186 return objValue.Len() == 0 187 } 188 return true 189 } 190 191 // This function is an adapted copy of the testify package's ElementsMatch function with the 192 // call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to 193 // other structs. It also ignores ordering when comparing unlike cmp.Equal. 194 func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { 195 if listA == nil && listB == nil { 196 return true 197 } else if listA == nil { 198 return isEmpty(listB) 199 } else if listB == nil { 200 return isEmpty(listA) 201 } 202 203 aKind := reflect.TypeOf(listA).Kind() 204 bKind := reflect.TypeOf(listB).Kind() 205 206 if aKind != reflect.Array && aKind != reflect.Slice { 207 return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...) 208 } 209 210 if bKind != reflect.Array && bKind != reflect.Slice { 211 return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...) 212 } 213 214 aValue := reflect.ValueOf(listA) 215 bValue := reflect.ValueOf(listB) 216 217 aLen := aValue.Len() 218 bLen := bValue.Len() 219 220 if aLen != bLen { 221 return assert.Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...) 222 } 223 224 // Mark indexes in bValue that we already used 225 visited := make([]bool, bLen) 226 for i := 0; i < aLen; i++ { 227 element := aValue.Index(i).Interface() 228 found := false 229 for j := 0; j < bLen; j++ { 230 if visited[j] { 231 continue 232 } 233 if cmp.Equal(bValue.Index(j).Interface(), element) { 234 visited[j] = true 235 found = true 236 break 237 } 238 } 239 if !found { 240 return assert.Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...) 241 } 242 } 243 244 return true 245 } 246 247 // Test adapted from test in testify library. 248 // https://github.com/stretchr/testify/blob/b8f7d52a4a7c581d5ed42333572e7fb857c687c2/assert/assertions_test.go#L768-L796 249 func TestElementsMatch(t *testing.T) { 250 mockT := new(testing.T) 251 252 cases := []struct { 253 expected interface{} 254 actual interface{} 255 result bool 256 }{ 257 // matching 258 {nil, nil, true}, 259 260 {nil, nil, true}, 261 {[]int{}, []int{}, true}, 262 {[]int{1}, []int{1}, true}, 263 {[]int{1, 1}, []int{1, 1}, true}, 264 {[]int{1, 2}, []int{1, 2}, true}, 265 {[]int{1, 2}, []int{2, 1}, true}, 266 {[2]int{1, 2}, [2]int{2, 1}, true}, 267 {[]string{"hello", "world"}, []string{"world", "hello"}, true}, 268 {[]string{"hello", "hello"}, []string{"hello", "hello"}, true}, 269 {[]string{"hello", "hello", "world"}, []string{"hello", "world", "hello"}, true}, 270 {[3]string{"hello", "hello", "world"}, [3]string{"hello", "world", "hello"}, true}, 271 {[]int{}, nil, true}, 272 273 // not matching 274 {[]int{1}, []int{1, 1}, false}, 275 {[]int{1, 2}, []int{2, 2}, false}, 276 {[]string{"hello", "hello"}, []string{"hello"}, false}, 277 } 278 279 for _, c := range cases { 280 t.Run(fmt.Sprintf("ElementsMatch(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { 281 res := elementsMatch(mockT, c.actual, c.expected) 282 283 if res != c.result { 284 t.Errorf("elementsMatch(%#v, %#v) should return %v", c.actual, c.expected, c.result) 285 } 286 }) 287 } 288 } 289 290 func TestDigitalOceanZones(t *testing.T) { 291 provider := &DigitalOceanProvider{ 292 Client: &mockDigitalOceanClient{}, 293 domainFilter: endpoint.NewDomainFilter([]string{"com"}), 294 } 295 296 zones, err := provider.Zones(context.Background()) 297 if err != nil { 298 t.Fatal(err) 299 } 300 301 validateDigitalOceanZones(t, zones, []godo.Domain{ 302 {Name: "foo.com"}, {Name: "example.com"}, {Name: "bar.com"}, 303 }) 304 } 305 306 func TestDigitalOceanMakeDomainEditRequest(t *testing.T) { 307 // Ensure that records at the root of the zone get `@` as the name. 308 r1 := makeDomainEditRequest("example.com", "example.com", endpoint.RecordTypeA, 309 "1.2.3.4", digitalOceanRecordTTL) 310 assert.Equal(t, &godo.DomainRecordEditRequest{ 311 Type: endpoint.RecordTypeA, 312 Name: "@", 313 Data: "1.2.3.4", 314 TTL: digitalOceanRecordTTL, 315 }, r1) 316 317 // Ensure the CNAME records have a `.` appended. 318 r2 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeCNAME, 319 "bar.example.com", digitalOceanRecordTTL) 320 assert.Equal(t, &godo.DomainRecordEditRequest{ 321 Type: endpoint.RecordTypeCNAME, 322 Name: "foo", 323 Data: "bar.example.com.", 324 TTL: digitalOceanRecordTTL, 325 }, r2) 326 327 // Ensure that CNAME records do not have an extra `.` appended if they already have a `.` 328 r3 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeCNAME, 329 "bar.example.com.", digitalOceanRecordTTL) 330 assert.Equal(t, &godo.DomainRecordEditRequest{ 331 Type: endpoint.RecordTypeCNAME, 332 Name: "foo", 333 Data: "bar.example.com.", 334 TTL: digitalOceanRecordTTL, 335 }, r3) 336 337 // Ensure that custom TTLs can be set 338 customTTL := 600 339 r4 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeCNAME, 340 "bar.example.com.", customTTL) 341 assert.Equal(t, &godo.DomainRecordEditRequest{ 342 Type: endpoint.RecordTypeCNAME, 343 Name: "foo", 344 Data: "bar.example.com.", 345 TTL: customTTL, 346 }, r4) 347 } 348 349 func TestDigitalOceanApplyChanges(t *testing.T) { 350 changes := &plan.Changes{} 351 provider := &DigitalOceanProvider{ 352 Client: &mockDigitalOceanClient{}, 353 } 354 changes.Create = []*endpoint.Endpoint{ 355 {DNSName: "new.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}, 356 {DNSName: "new.ext-dns-test-with-ttl.bar.com", Targets: endpoint.Targets{"target"}, RecordTTL: 100}, 357 {DNSName: "new.ext-dns-test.unexpected.com", Targets: endpoint.Targets{"target"}}, 358 {DNSName: "bar.com", Targets: endpoint.Targets{"target"}}, 359 } 360 changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}} 361 changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.de", Targets: endpoint.Targets{"target-old"}}} 362 changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 100}} 363 err := provider.ApplyChanges(context.Background(), changes) 364 if err != nil { 365 t.Errorf("should not fail, %s", err) 366 } 367 } 368 369 func TestDigitalOceanProcessCreateActions(t *testing.T) { 370 recordsByDomain := map[string][]godo.DomainRecord{ 371 "example.com": nil, 372 } 373 374 createsByDomain := map[string][]*endpoint.Endpoint{ 375 "example.com": { 376 endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"), 377 endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "foo.example.com"), 378 }, 379 } 380 381 var changes digitalOceanChanges 382 err := processCreateActions(recordsByDomain, createsByDomain, &changes) 383 require.NoError(t, err) 384 385 assert.Equal(t, 2, len(changes.Creates)) 386 assert.Equal(t, 0, len(changes.Updates)) 387 assert.Equal(t, 0, len(changes.Deletes)) 388 389 expectedCreates := []*digitalOceanChangeCreate{ 390 { 391 Domain: "example.com", 392 Options: &godo.DomainRecordEditRequest{ 393 Name: "foo", 394 Type: endpoint.RecordTypeA, 395 Data: "1.2.3.4", 396 TTL: digitalOceanRecordTTL, 397 }, 398 }, 399 { 400 Domain: "example.com", 401 Options: &godo.DomainRecordEditRequest{ 402 Name: "@", 403 Type: endpoint.RecordTypeCNAME, 404 Data: "foo.example.com.", 405 TTL: digitalOceanRecordTTL, 406 }, 407 }, 408 } 409 410 if !elementsMatch(t, expectedCreates, changes.Creates) { 411 assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates)) 412 } 413 } 414 415 func TestDigitalOceanProcessUpdateActions(t *testing.T) { 416 recordsByDomain := map[string][]godo.DomainRecord{ 417 "example.com": { 418 { 419 ID: 1, 420 Name: "foo", 421 Type: endpoint.RecordTypeA, 422 Data: "1.2.3.4", 423 TTL: digitalOceanRecordTTL, 424 }, 425 { 426 ID: 2, 427 Name: "foo", 428 Type: endpoint.RecordTypeA, 429 Data: "5.6.7.8", 430 TTL: digitalOceanRecordTTL, 431 }, 432 { 433 ID: 3, 434 Name: "@", 435 Type: endpoint.RecordTypeCNAME, 436 Data: "foo.example.com.", 437 TTL: digitalOceanRecordTTL, 438 }, 439 }, 440 } 441 442 updatesByDomain := map[string][]*endpoint.Endpoint{ 443 "example.com": { 444 endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "10.11.12.13"), 445 endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "bar.example.com"), 446 }, 447 } 448 449 var changes digitalOceanChanges 450 err := processUpdateActions(recordsByDomain, updatesByDomain, &changes) 451 require.NoError(t, err) 452 453 assert.Equal(t, 2, len(changes.Creates)) 454 assert.Equal(t, 0, len(changes.Updates)) 455 assert.Equal(t, 3, len(changes.Deletes)) 456 457 expectedCreates := []*digitalOceanChangeCreate{ 458 { 459 Domain: "example.com", 460 Options: &godo.DomainRecordEditRequest{ 461 Name: "foo", 462 Type: endpoint.RecordTypeA, 463 Data: "10.11.12.13", 464 TTL: digitalOceanRecordTTL, 465 }, 466 }, 467 { 468 Domain: "example.com", 469 Options: &godo.DomainRecordEditRequest{ 470 Name: "@", 471 Type: endpoint.RecordTypeCNAME, 472 Data: "bar.example.com.", 473 TTL: digitalOceanRecordTTL, 474 }, 475 }, 476 } 477 478 if !elementsMatch(t, expectedCreates, changes.Creates) { 479 assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates)) 480 } 481 482 expectedDeletes := []*digitalOceanChangeDelete{ 483 { 484 Domain: "example.com", 485 RecordID: 1, 486 }, 487 { 488 Domain: "example.com", 489 RecordID: 2, 490 }, 491 { 492 Domain: "example.com", 493 RecordID: 3, 494 }, 495 } 496 497 if !elementsMatch(t, expectedDeletes, changes.Deletes) { 498 assert.Failf(t, "diff: %s", cmp.Diff(expectedDeletes, changes.Deletes)) 499 } 500 } 501 502 func TestDigitalOceanProcessDeleteActions(t *testing.T) { 503 recordsByDomain := map[string][]godo.DomainRecord{ 504 "example.com": { 505 { 506 ID: 1, 507 Name: "foo", 508 Type: endpoint.RecordTypeA, 509 Data: "1.2.3.4", 510 TTL: digitalOceanRecordTTL, 511 }, 512 // This record will not be deleted because it represents a target not specified to be deleted. 513 { 514 ID: 2, 515 Name: "foo", 516 Type: endpoint.RecordTypeA, 517 Data: "5.6.7.8", 518 TTL: digitalOceanRecordTTL, 519 }, 520 { 521 ID: 3, 522 Name: "@", 523 Type: endpoint.RecordTypeCNAME, 524 Data: "foo.example.com.", 525 TTL: digitalOceanRecordTTL, 526 }, 527 }, 528 } 529 530 deletesByDomain := map[string][]*endpoint.Endpoint{ 531 "example.com": { 532 endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"), 533 endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "foo.example.com"), 534 }, 535 } 536 537 var changes digitalOceanChanges 538 err := processDeleteActions(recordsByDomain, deletesByDomain, &changes) 539 require.NoError(t, err) 540 541 assert.Equal(t, 0, len(changes.Creates)) 542 assert.Equal(t, 0, len(changes.Updates)) 543 assert.Equal(t, 2, len(changes.Deletes)) 544 545 expectedDeletes := []*digitalOceanChangeDelete{ 546 { 547 Domain: "example.com", 548 RecordID: 1, 549 }, 550 { 551 Domain: "example.com", 552 RecordID: 3, 553 }, 554 } 555 556 if !elementsMatch(t, expectedDeletes, changes.Deletes) { 557 assert.Failf(t, "diff: %s", cmp.Diff(expectedDeletes, changes.Deletes)) 558 } 559 } 560 561 func TestNewDigitalOceanProvider(t *testing.T) { 562 _ = os.Setenv("DO_TOKEN", "xxxxxxxxxxxxxxxxx") 563 _, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50) 564 if err != nil { 565 t.Errorf("should not fail, %s", err) 566 } 567 _ = os.Unsetenv("DO_TOKEN") 568 _, err = NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50) 569 if err == nil { 570 t.Errorf("expected to fail") 571 } 572 } 573 574 func TestDigitalOceanGetMatchingDomainRecords(t *testing.T) { 575 records := []godo.DomainRecord{ 576 { 577 ID: 1, 578 Name: "foo", 579 Type: endpoint.RecordTypeCNAME, 580 Data: "baz.org.", 581 }, 582 { 583 ID: 2, 584 Name: "baz", 585 Type: endpoint.RecordTypeA, 586 Data: "1.2.3.4", 587 }, 588 { 589 ID: 3, 590 Name: "baz", 591 Type: endpoint.RecordTypeA, 592 Data: "5.6.7.8", 593 }, 594 { 595 ID: 4, 596 Name: "@", 597 Type: endpoint.RecordTypeA, 598 Data: "9.10.11.12", 599 }, 600 } 601 602 ep1 := endpoint.NewEndpoint("foo.com", endpoint.RecordTypeCNAME) 603 assert.Equal(t, 1, len(getMatchingDomainRecords(records, "com", ep1))) 604 605 ep2 := endpoint.NewEndpoint("foo.com", endpoint.RecordTypeA) 606 assert.Equal(t, 0, len(getMatchingDomainRecords(records, "com", ep2))) 607 608 ep3 := endpoint.NewEndpoint("baz.org", endpoint.RecordTypeA) 609 r := getMatchingDomainRecords(records, "org", ep3) 610 assert.Equal(t, 2, len(r)) 611 assert.ElementsMatch(t, r, []godo.DomainRecord{ 612 { 613 ID: 2, 614 Name: "baz", 615 Type: endpoint.RecordTypeA, 616 Data: "1.2.3.4", 617 }, 618 { 619 ID: 3, 620 Name: "baz", 621 Type: endpoint.RecordTypeA, 622 Data: "5.6.7.8", 623 }, 624 }) 625 626 ep4 := endpoint.NewEndpoint("example.com", endpoint.RecordTypeA) 627 r2 := getMatchingDomainRecords(records, "example.com", ep4) 628 assert.Equal(t, 1, len(r2)) 629 assert.Equal(t, "9.10.11.12", r2[0].Data) 630 } 631 632 func validateDigitalOceanZones(t *testing.T, zones []godo.Domain, expected []godo.Domain) { 633 require.Len(t, zones, len(expected)) 634 635 for i, zone := range zones { 636 assert.Equal(t, expected[i].Name, zone.Name) 637 } 638 } 639 640 func TestDigitalOceanRecord(t *testing.T) { 641 provider := &DigitalOceanProvider{ 642 Client: &mockDigitalOceanClient{}, 643 } 644 645 records, err := provider.fetchRecords(context.Background(), "example.com") 646 if err != nil { 647 t.Fatal(err) 648 } 649 expected := []godo.DomainRecord{{ID: 1, Name: "new", Type: "CNAME"}} 650 require.Len(t, records, len(expected)) 651 for i, record := range records { 652 assert.Equal(t, expected[i].Name, record.Name) 653 } 654 } 655 656 func TestDigitalOceanAllRecords(t *testing.T) { 657 provider := &DigitalOceanProvider{ 658 Client: &mockDigitalOceanClient{}, 659 } 660 ctx := context.Background() 661 662 records, err := provider.Records(ctx) 663 if err != nil { 664 t.Errorf("should not fail, %s", err) 665 } 666 require.Equal(t, 5, len(records)) 667 668 provider.Client = &mockDigitalOceanRecordsFail{} 669 _, err = provider.Records(ctx) 670 if err == nil { 671 t.Errorf("expected to fail, %s", err) 672 } 673 } 674 675 func TestDigitalOceanMergeRecordsByNameType(t *testing.T) { 676 xs := []*endpoint.Endpoint{ 677 endpoint.NewEndpoint("foo.example.com", "A", "1.2.3.4"), 678 endpoint.NewEndpoint("bar.example.com", "A", "1.2.3.4"), 679 endpoint.NewEndpoint("foo.example.com", "A", "5.6.7.8"), 680 endpoint.NewEndpoint("foo.example.com", "CNAME", "somewhere.out.there.com"), 681 } 682 683 merged := mergeEndpointsByNameType(xs) 684 685 assert.Equal(t, 3, len(merged)) 686 sort.SliceStable(merged, func(i, j int) bool { 687 if merged[i].DNSName != merged[j].DNSName { 688 return merged[i].DNSName < merged[j].DNSName 689 } 690 return merged[i].RecordType < merged[j].RecordType 691 }) 692 assert.Equal(t, "bar.example.com", merged[0].DNSName) 693 assert.Equal(t, "A", merged[0].RecordType) 694 assert.Equal(t, 1, len(merged[0].Targets)) 695 assert.Equal(t, "1.2.3.4", merged[0].Targets[0]) 696 697 assert.Equal(t, "foo.example.com", merged[1].DNSName) 698 assert.Equal(t, "A", merged[1].RecordType) 699 assert.Equal(t, 2, len(merged[1].Targets)) 700 assert.ElementsMatch(t, []string{"1.2.3.4", "5.6.7.8"}, merged[1].Targets) 701 702 assert.Equal(t, "foo.example.com", merged[2].DNSName) 703 assert.Equal(t, "CNAME", merged[2].RecordType) 704 assert.Equal(t, 1, len(merged[2].Targets)) 705 assert.Equal(t, "somewhere.out.there.com", merged[2].Targets[0]) 706 }