Disable records instead of deleting them
A disabled record stays in Jarl with its ID, TTL and comment but is left out of the zone; the zone file lists it as a comment. Web UI toggle, jarl record disable|enable, record.disable|enable operations and API endpoints. Database schema 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f04ab523b
commit
295c549479
29 files changed
+815
-60
No files matched your search
@@ -0,0 +1,176 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user