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>
177 lines
6.9 KiB
Go
177 lines
6.9 KiB
Go
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")
|
|
}
|
|
}
|