Skip to content

Commit 6197c9a

Browse files
committed
JOKER: TTLs may not always be set for CAA and other records (#4019)
<!-- ## Before submiting a pull request Please make sure you've run the following commands from the root directory. bin/generate-all.sh (this runs commands like "go generate", fixes formatting, and so on) ## Release changelog section Help keep the release changelog clear by pre-naming the proper section in the GitHub pull request title. Some examples: * CICD: Add required GHA permissions for goreleaser * DOCS: Fixed providers with "contributor support" table * ROUTE53: Allow R53_ALIAS records to enable target health evaluation More examples/context can be found in the file .goreleaser.yml under the 'build' > 'changelog' key. !-->
1 parent fb062d2 commit 6197c9a

File tree

2 files changed

+329
-7
lines changed

2 files changed

+329
-7
lines changed

providers/joker/records.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func (api *jokerProvider) parseZoneRecords(domain, zoneData string) (models.Reco
230230
// Parse TTL from the end if present (position 5)
231231
if len(parts) >= 6 {
232232
if ttlParsed, err := strconv.ParseUint(parts[5], 10, 32); err == nil {
233-
ttl = uint32(ttlParsed)
233+
rc.TTL = uint32(ttlParsed)
234234
}
235235
}
236236
}
@@ -266,7 +266,7 @@ func (api *jokerProvider) parseZoneRecords(domain, zoneData string) (models.Reco
266266
// Parse TTL from position 4
267267
if len(parts) >= 5 {
268268
if ttlParsed, err := strconv.ParseUint(parts[4], 10, 32); err == nil {
269-
ttl = uint32(ttlParsed)
269+
rc.TTL = uint32(ttlParsed)
270270
}
271271
}
272272
// Parse flags, service, and regex from positions 7, 8, 9
@@ -357,7 +357,7 @@ func (api *jokerProvider) updateZoneRecords(domain string, records models.Record
357357
}
358358

359359
// recordsToZoneFormat converts RecordConfig records to Joker zone format.
360-
func (api *jokerProvider) recordsToZoneFormat(domain string, records models.Records) string {
360+
func (api *jokerProvider) recordsToZoneFormat(_ string, records models.Records) string {
361361
var lines []string
362362

363363
for _, rc := range records {

providers/joker/records_test.go

Lines changed: 326 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package joker
33
import (
44
"reflect"
55
"testing"
6+
7+
"github.com/StackExchange/dnscontrol/v4/models"
68
)
79

810
func TestParseZoneLine(t *testing.T) {
@@ -63,13 +65,13 @@ func TestParseZoneLine(t *testing.T) {
6365
},
6466
{
6567
name: "CAA record",
66-
input: "@ CAA 0 0 issue \"letsencrypt.org\" 300",
67-
expected: []string{"@", "CAA", "0", "0", "issue", "\"letsencrypt.org\"", "300"},
68+
input: "@ CAA 0 0 issue \"letsencrypt.org\" 303",
69+
expected: []string{"@", "CAA", "0", "0", "issue", "\"letsencrypt.org\"", "303"},
6870
},
6971
{
7072
name: "NAPTR record",
71-
input: "@ NAPTR 100/50 target.example.com. \"u\" \"sip+E2U\" \"!^.*$!sip:info@example.com!\" 300",
72-
expected: []string{"@", "NAPTR", "100/50", "target.example.com.", "\"u\"", "\"sip+E2U\"", "\"!^.*$!sip:info@example.com!\"", "300"},
73+
input: "@ NAPTR 100/50 target.example.com. \"u\" \"sip+E2U\" \"!^.*$!sip:info@example.com!\" 313",
74+
expected: []string{"@", "NAPTR", "100/50", "target.example.com.", "\"u\"", "\"sip+E2U\"", "\"!^.*$!sip:info@example.com!\"", "313"},
7375
},
7476
{
7577
name: "multiple consecutive spaces",
@@ -193,3 +195,323 @@ func TestParseZoneLineEdgeCases(t *testing.T) {
193195
})
194196
}
195197
}
198+
func TestParseZoneRecords(t *testing.T) {
199+
api := &jokerProvider{}
200+
domain := "example.com"
201+
202+
tests := []struct {
203+
name string
204+
zoneData string
205+
wantCount int
206+
validate func(t *testing.T, records models.Records)
207+
}{
208+
{
209+
name: "single A record",
210+
zoneData: `www A 0 1.2.3.4 300`,
211+
wantCount: 1,
212+
validate: func(t *testing.T, records models.Records) {
213+
if records[0].Type != "A" {
214+
t.Errorf("Type = %v, want A", records[0].Type)
215+
}
216+
if records[0].GetLabelFQDN() != "www.example.com" {
217+
t.Errorf("Label = %v, want www.example.com", records[0].GetLabelFQDN())
218+
}
219+
if records[0].GetTargetField() != "1.2.3.4" {
220+
t.Errorf("Target = %v, want 1.2.3.4", records[0].GetTargetField())
221+
}
222+
if records[0].TTL != 300 {
223+
t.Errorf("TTL = %v, want 300", records[0].TTL)
224+
}
225+
},
226+
},
227+
{
228+
name: "root domain A record with @",
229+
zoneData: `@ A 0 127.0.0.1 300`,
230+
wantCount: 1,
231+
validate: func(t *testing.T, records models.Records) {
232+
if records[0].GetLabelFQDN() != "example.com" {
233+
t.Errorf("Label = %v, want example.com", records[0].GetLabelFQDN())
234+
}
235+
if records[0].GetTargetField() != "127.0.0.1" {
236+
t.Errorf("Target = %v, want 127.0.0.1", records[0].GetTargetField())
237+
}
238+
},
239+
},
240+
{
241+
name: "AAAA record",
242+
zoneData: `www AAAA 0 2001:db8::1 300`,
243+
wantCount: 1,
244+
validate: func(t *testing.T, records models.Records) {
245+
if records[0].Type != "AAAA" {
246+
t.Errorf("Type = %v, want AAAA", records[0].Type)
247+
}
248+
if records[0].GetTargetField() != "2001:db8::1" {
249+
t.Errorf("Target = %v, want 2001:db8::1", records[0].GetTargetField())
250+
}
251+
},
252+
},
253+
{
254+
name: "CNAME record with FQDN",
255+
zoneData: `mail CNAME 0 example.com. 300`,
256+
wantCount: 1,
257+
validate: func(t *testing.T, records models.Records) {
258+
if records[0].Type != "CNAME" {
259+
t.Errorf("Type = %v, want CNAME", records[0].Type)
260+
}
261+
if records[0].GetTargetField() != "example.com." {
262+
t.Errorf("Target = %v, want example.com.", records[0].GetTargetField())
263+
}
264+
},
265+
},
266+
{
267+
name: "CNAME record without trailing dot",
268+
zoneData: `mail CNAME 0 target.example.com 300`,
269+
wantCount: 1,
270+
validate: func(t *testing.T, records models.Records) {
271+
if records[0].GetTargetField() != "target.example.com." {
272+
t.Errorf("Target = %v, want target.example.com.", records[0].GetTargetField())
273+
}
274+
},
275+
},
276+
{
277+
name: "NS record",
278+
zoneData: `sub NS 0 ns1.example.com. 300`,
279+
wantCount: 1,
280+
validate: func(t *testing.T, records models.Records) {
281+
if records[0].Type != "NS" {
282+
t.Errorf("Type = %v, want NS", records[0].Type)
283+
}
284+
if records[0].GetTargetField() != "ns1.example.com." {
285+
t.Errorf("Target = %v, want ns1.example.com.", records[0].GetTargetField())
286+
}
287+
},
288+
},
289+
{
290+
name: "MX record",
291+
zoneData: `@ MX 10 mail.example.com. 300`,
292+
wantCount: 1,
293+
validate: func(t *testing.T, records models.Records) {
294+
if records[0].Type != "MX" {
295+
t.Errorf("Type = %v, want MX", records[0].Type)
296+
}
297+
if records[0].MxPreference != 10 {
298+
t.Errorf("MxPreference = %v, want 10", records[0].MxPreference)
299+
}
300+
if records[0].GetTargetField() != "mail.example.com." {
301+
t.Errorf("Target = %v, want mail.example.com.", records[0].GetTargetField())
302+
}
303+
},
304+
},
305+
{
306+
name: "TXT record with quoted content",
307+
zoneData: `@ TXT 0 "v=spf1 include:_spf.google.com ~all" 301`,
308+
wantCount: 1,
309+
validate: func(t *testing.T, records models.Records) {
310+
if records[0].Type != "TXT" {
311+
t.Errorf("Type = %v, want TXT", records[0].Type)
312+
}
313+
want := "v=spf1 include:_spf.google.com ~all"
314+
if records[0].GetTargetField() != want {
315+
t.Errorf("Target = %v, want %v", records[0].GetTargetField(), want)
316+
}
317+
wantTTL := uint32(301)
318+
if records[0].TTL != wantTTL {
319+
t.Errorf("TTL = %v, want %d", records[0].TTL, wantTTL)
320+
}
321+
},
322+
},
323+
{
324+
name: "TXT record with escaped quotes",
325+
zoneData: `test TXT 0 "value with \\\"escaped\\\" quotes" 399`,
326+
wantCount: 1,
327+
validate: func(t *testing.T, records models.Records) {
328+
want := "value with \\\"escaped\\\" quotes"
329+
if records[0].GetTargetField() != want {
330+
t.Errorf("Target = %v, want %v", records[0].GetTargetField(), want)
331+
}
332+
},
333+
},
334+
{
335+
name: "TXT record with escaped backslashes",
336+
zoneData: `test TXT 0 "path\\\\to\\\\file" 300`,
337+
wantCount: 1,
338+
validate: func(t *testing.T, records models.Records) {
339+
want := "path\\\\to\\\\file"
340+
if records[0].GetTargetField() != want {
341+
t.Errorf("Target = %v, want %v", records[0].GetTargetField(), want)
342+
}
343+
},
344+
},
345+
{
346+
name: "SRV record",
347+
zoneData: `_sip._tcp SRV 10/20 sip.example.com:5060 300`,
348+
wantCount: 1,
349+
validate: func(t *testing.T, records models.Records) {
350+
if records[0].Type != "SRV" {
351+
t.Errorf("Type = %v, want SRV", records[0].Type)
352+
}
353+
if records[0].SrvPriority != 10 {
354+
t.Errorf("SrvPriority = %v, want 10", records[0].SrvPriority)
355+
}
356+
if records[0].SrvWeight != 20 {
357+
t.Errorf("SrvWeight = %v, want 20", records[0].SrvWeight)
358+
}
359+
if records[0].SrvPort != 5060 {
360+
t.Errorf("SrvPort = %v, want 5060", records[0].SrvPort)
361+
}
362+
if records[0].GetTargetField() != "sip.example.com." {
363+
t.Errorf("Target = %v, want sip.example.com.", records[0].GetTargetField())
364+
}
365+
},
366+
},
367+
{
368+
name: "CAA record",
369+
zoneData: `@ CAA 0 issue "letsencrypt.org" 303`,
370+
wantCount: 1,
371+
validate: func(t *testing.T, records models.Records) {
372+
if records[0].Type != "CAA" {
373+
t.Errorf("Type = %v, want CAA", records[0].Type)
374+
}
375+
if records[0].CaaFlag != 0 {
376+
t.Errorf("CaaFlag = %v, want 0", records[0].CaaFlag)
377+
}
378+
if records[0].CaaTag != "issue" {
379+
t.Errorf("CaaTag = %v, want issue", records[0].CaaTag)
380+
}
381+
if records[0].GetTargetField() != "letsencrypt.org" {
382+
t.Errorf("Target = %v, want letsencrypt.org", records[0].GetTargetField())
383+
}
384+
if records[0].TTL != 303 {
385+
t.Errorf("TTL = %v, want 303", records[0].TTL)
386+
}
387+
},
388+
},
389+
{
390+
name: "NAPTR record",
391+
zoneData: `@ NAPTR 100/50 target.example.com. 315 0 0 "u" "sip+E2U" "!^.*$!sip:info@example.com!"`,
392+
wantCount: 1,
393+
validate: func(t *testing.T, records models.Records) {
394+
if records[0].Type != "NAPTR" {
395+
t.Errorf("Type = %v, want NAPTR", records[0].Type)
396+
}
397+
if records[0].NaptrOrder != 100 {
398+
t.Errorf("NaptrOrder = %v, want 100", records[0].NaptrOrder)
399+
}
400+
if records[0].NaptrPreference != 50 {
401+
t.Errorf("NaptrPreference = %v, want 50", records[0].NaptrPreference)
402+
}
403+
if records[0].NaptrFlags != "u" {
404+
t.Errorf("NaptrFlags = %v, want u", records[0].NaptrFlags)
405+
}
406+
if records[0].NaptrService != "sip+E2U" {
407+
t.Errorf("NaptrService = %v, want sip+E2U", records[0].NaptrService)
408+
}
409+
if records[0].NaptrRegexp != "!^.*$!sip:info@example.com!" {
410+
t.Errorf("NaptrRegexp = %v, want !^.*$!sip:info@example.com!", records[0].NaptrRegexp)
411+
}
412+
if records[0].GetTargetField() != "target.example.com." {
413+
t.Errorf("Target = %v, want target.example.com.", records[0].GetTargetField())
414+
}
415+
if records[0].TTL != 315 {
416+
t.Errorf("TTL = %v, want 315", records[0].TTL)
417+
}
418+
},
419+
},
420+
{
421+
name: "multiple records",
422+
zoneData: `www A 0 1.2.3.4 300
423+
mail CNAME 0 example.com. 300
424+
@ MX 10 mail.example.com. 300`,
425+
wantCount: 3,
426+
validate: func(t *testing.T, records models.Records) {
427+
if records[0].Type != "A" {
428+
t.Errorf("First record type = %v, want A", records[0].Type)
429+
}
430+
if records[1].Type != "CNAME" {
431+
t.Errorf("Second record type = %v, want CNAME", records[1].Type)
432+
}
433+
if records[2].Type != "MX" {
434+
t.Errorf("Third record type = %v, want MX", records[2].Type)
435+
}
436+
},
437+
},
438+
{
439+
name: "skip comments and directives",
440+
zoneData: `# This is a comment
441+
$ORIGIN example.com.
442+
www A 0 1.2.3.4 300`,
443+
wantCount: 1,
444+
validate: func(t *testing.T, records models.Records) {
445+
if records[0].Type != "A" {
446+
t.Errorf("Type = %v, want A", records[0].Type)
447+
}
448+
},
449+
},
450+
{
451+
name: "skip empty lines",
452+
zoneData: `www A 0 1.2.3.4 300
453+
454+
mail CNAME 0 example.com. 300`,
455+
wantCount: 2,
456+
validate: func(t *testing.T, records models.Records) {
457+
if len(records) != 2 {
458+
t.Errorf("Record count = %v, want 2", len(records))
459+
}
460+
},
461+
},
462+
{
463+
name: "skip malformed lines",
464+
zoneData: `www A 0
465+
valid A 0 1.2.3.4 300`,
466+
wantCount: 1,
467+
validate: func(t *testing.T, records models.Records) {
468+
if records[0].GetLabelFQDN() != "valid.example.com" {
469+
t.Errorf("Label = %v, want valid.example.com", records[0].GetLabelFQDN())
470+
}
471+
},
472+
},
473+
{
474+
name: "default TTL when missing",
475+
zoneData: `www A 0 1.2.3.4`,
476+
wantCount: 1,
477+
validate: func(t *testing.T, records models.Records) {
478+
if records[0].TTL != 300 {
479+
t.Errorf("TTL = %v, want 300", records[0].TTL)
480+
}
481+
},
482+
},
483+
{
484+
name: "skip unsupported record types",
485+
zoneData: `www A 0 1.2.3.4 300
486+
test UNKNOWN 0 value 300
487+
mail CNAME 0 example.com. 300`,
488+
wantCount: 2,
489+
validate: func(t *testing.T, records models.Records) {
490+
if len(records) != 2 {
491+
t.Errorf("Record count = %v, want 2", len(records))
492+
}
493+
},
494+
},
495+
{
496+
name: "empty zone data",
497+
zoneData: "",
498+
wantCount: 0,
499+
validate: func(t *testing.T, records models.Records) {},
500+
},
501+
}
502+
503+
for _, tt := range tests {
504+
t.Run(tt.name, func(t *testing.T) {
505+
records, err := api.parseZoneRecords(domain, tt.zoneData)
506+
if err != nil {
507+
t.Fatalf("parseZoneRecords() error = %v", err)
508+
}
509+
if len(records) != tt.wantCount {
510+
t.Errorf("parseZoneRecords() returned %d records, want %d", len(records), tt.wantCount)
511+
}
512+
if tt.validate != nil {
513+
tt.validate(t, records)
514+
}
515+
})
516+
}
517+
}

0 commit comments

Comments
 (0)