package engine import ( "os" "path/filepath" "strings" "testing" "jarl/internal/model" ) func disableReq(id int64, ptr bool) Request { return NewRequest(OpRecordDisable, id, RecordInput{PTR: ptr}) } func enableReq(id int64, ptr bool) Request { return NewRequest(OpRecordEnable, id, RecordInput{PTR: ptr}) } func (env *testEnv) zoneFile(name string) string { env.t.Helper() b, err := os.ReadFile(filepath.Join(env.dir, "zones/_default", name+".zone")) if err != nil { env.t.Fatal(err) } return string(b) } func serialOf(env *testEnv, zone string) uint32 { return env.state().ZonesNamed(zone)[0].SOA.Serial } func TestDisableRecord(t *testing.T) { env := newEnv(t) setupZone(env) env.submit(env.admin, rec("example.com", "www", "A", "192.0.2.10")) www := findRec(env.state(), "www.example.com.", "A") serial := serialOf(env, "example.com") res := env.submit(env.admin, disableReq(www.ID, false)) if len(res.Ops) != 1 || res.Ops[0].Summary != "disable www.example.com. A 192.0.2.10" { t.Fatalf("ops = %+v", res.Ops) } if r := env.state().Records[www.ID]; r == nil || !r.Disabled { t.Fatalf("record = %+v", r) } zf := env.zoneFile("example.com") if !strings.Contains(zf, "; disabled: www\t\tIN\tA\t192.0.2.10\n") || strings.Contains(zf, "\nwww\t") { t.Errorf("zone file:\n%s", zf) } if s := serialOf(env, "example.com"); s != serial+1 { t.Errorf("serial after disable = %d, want %d", s, serial+1) } // Editing a disabled record changes nothing that is served. env.submit(env.admin, NewRequest(OpRecordUpdate, www.ID, RecordInput{Comment: model.Ptr("old web server"), Data: model.Ptr("192.0.2.11")})) if s := serialOf(env, "example.com"); s != serial+1 { t.Errorf("serial after editing a disabled record = %d", s) } // Disabling it again says so. if e := env.submitErr(env.admin, disableReq(www.ID, false)); e.Kind != KindConflict || !strings.Contains(e.Message, "already disabled") { t.Errorf("second disable: %+v", e) } env.submit(env.admin, enableReq(www.ID, false)) r := env.state().Records[www.ID] if r.Disabled || r.Data != "192.0.2.11" || r.Comment != "old web server" { t.Errorf("enabled record = %+v", r) } if zf := env.zoneFile("example.com"); !strings.Contains(zf, "\nwww\t\tIN\tA\t192.0.2.11\t; old web server\n") || strings.Contains(zf, "disabled") { t.Errorf("zone file after enable:\n%s", zf) } } func TestDisableValidation(t *testing.T) { env := newEnv(t) setupZone(env) // The zone must keep a name server. ns := findRec(env.state(), "example.com.", "NS") if e := env.submitErr(env.admin, disableReq(ns.ID, false)); e.Kind != KindValidation { t.Errorf("disabling the only NS: %+v", e) } // A disabled record doesn't count for the CNAME rule... env.submit(env.admin, rec("example.com", "www", "A", "192.0.2.10")) www := findRec(env.state(), "www.example.com.", "A") env.submit(env.admin, disableReq(www.ID, false)) res := env.submit(env.admin, rec("example.com", "www", "CNAME", "ns1")) // ...but there's a warning that it can't come back, if len(res.Problems) != 1 || res.Problems[0].Code != "disabled_conflict" || res.Problems[0].ObjectID != www.ID { t.Errorf("problems = %+v", res.Problems) } // and enabling it is checked like adding it. if e := env.submitErr(env.admin, enableReq(www.ID, false)); e.Kind != KindValidation || !strings.Contains(e.Message, "CNAME") { t.Errorf("enabling next to a CNAME: %+v", e) } } func TestDisableByMatchAndPTR(t *testing.T) { env := newEnv(t) setupZone(env) env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("192.0.2.0/24")})) env.submit(env.admin, NewRequest(OpRecordCreate, 0, RecordInput{Zone: "example.com", Name: model.Ptr("nas"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.20"), PTR: true})) ptrDisabled := func() bool { return findRec(env.state(), "20.2.0.192.in-addr.arpa.", "PTR").Disabled } // By match, with the PTR record. match := RecordInput{Zone: "example.com", Name: model.Ptr("nas"), Type: model.Ptr("A"), PTR: true} res := env.submit(env.admin, NewRequest(OpRecordDisable, 0, match)) if len(res.Ops) != 2 || res.Ops[1].Summary != "disable 20.2.0.192.in-addr.arpa. PTR nas.example.com." { t.Fatalf("ops = %+v", res.Ops) } if !findRec(env.state(), "nas.example.com.", "A").Disabled || !ptrDisabled() { t.Fatal("A or PTR not disabled") } env.submit(env.admin, NewRequest(OpRecordEnable, 0, match)) if findRec(env.state(), "nas.example.com.", "A").Disabled || ptrDisabled() { t.Fatal("A or PTR not enabled") } // Without ptr, the PTR record stays as it is. a := findRec(env.state(), "nas.example.com.", "A") env.submit(env.admin, disableReq(a.ID, false)) if ptrDisabled() { t.Error("PTR disabled without ptr") } // Data narrows the match; nothing matching is an error unless missing_ok. env.submit(env.admin, rec("example.com", "api", "A", "192.0.2.30"), rec("example.com", "api", "A", "192.0.2.31")) one := RecordInput{Zone: "example.com", Name: model.Ptr("api"), Type: model.Ptr("A"), Data: model.Ptr("192.0.2.31")} env.submit(env.admin, NewRequest(OpRecordDisable, 0, one)) if findRecData(env.state(), "api.example.com.", "192.0.2.30").Disabled || !findRecData(env.state(), "api.example.com.", "192.0.2.31").Disabled { t.Error("data match wrong") } none := RecordInput{Zone: "example.com", Name: model.Ptr("nope"), Type: model.Ptr("A")} if e := env.submitErr(env.admin, NewRequest(OpRecordDisable, 0, none)); e.Kind != KindNotFound { t.Errorf("no match: %+v", e) } // missing_ok: nothing to do (an error the CLI's --missing-ok accepts). none.MissingOK = true if e := env.submitErr(env.admin, NewRequest(OpRecordDisable, 0, none)); e.Message != "nothing to do" { t.Errorf("missing_ok: %+v", e) } } func TestDisableRRSetAndRevert(t *testing.T) { env := newEnv(t) setupZone(env) env.submit(env.admin, rec("example.com", "api", "A", "192.0.2.30"), rec("example.com", "api", "A", "192.0.2.31")) r30 := findRecData(env.state(), "api.example.com.", "192.0.2.30") r31 := findRecData(env.state(), "api.example.com.", "192.0.2.31") env.submit(env.admin, disableReq(r30.ID, false), disableReq(r31.ID, false)) // Setting the RRset enables a disabled record with a given value and // leaves the other disabled one alone. env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", Name: "api", Type: "A", Data: []string{"192.0.2.30", "192.0.2.40"}})) s := env.state() if r := s.Records[r30.ID]; r == nil || r.Disabled { t.Errorf(".30 = %+v, want kept and enabled", r) } if r := s.Records[r31.ID]; r == nil || !r.Disabled { t.Errorf(".31 = %+v, want kept and disabled", r) } // Reverting a disabling changeset enables the record again. res := env.submit(env.admin, disableReq(r30.ID, false)) rev, err := env.e.Revert(env.ctx, res.ChangesetID, env.admin) if err != nil { t.Fatal(err) } if _, err := env.e.Apply(env.ctx, rev.ID, env.admin, ApplyOptions{}); err != nil { t.Fatal(err) } if env.state().Records[r30.ID].Disabled { t.Error("revert did not enable the record") } }