Prevent more specific scopes from overriding option values locked at a higher scope (e.g. admin). Includes defense-in-depth stripping on Set/Add operations, merge-time preservation, and frontend filtering.
1654 lines
49 KiB
Go
1654 lines
49 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2025 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package checker_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.happydns.org/happyDomain/internal/checker"
|
|
checkerUC "git.happydns.org/happyDomain/internal/usecase/checker"
|
|
"git.happydns.org/happyDomain/model"
|
|
)
|
|
|
|
// --- helpers ---
|
|
|
|
func idPtr() *happydns.Identifier {
|
|
id, _ := happydns.NewRandomIdentifier()
|
|
return &id
|
|
}
|
|
|
|
// optionsStore is a minimal in-memory CheckerOptionsStorage that supports
|
|
// multi-scope positional lookup and update.
|
|
type optionsStore struct {
|
|
// key: "checker|userId|domainId|serviceId"
|
|
data map[string]happydns.CheckerOptions
|
|
}
|
|
|
|
func newOptionsStore() *optionsStore {
|
|
return &optionsStore{data: make(map[string]happydns.CheckerOptions)}
|
|
}
|
|
|
|
func posKey(checkerName string, userId, domainId, serviceId *happydns.Identifier) string {
|
|
f := func(id *happydns.Identifier) string {
|
|
if id == nil {
|
|
return ""
|
|
}
|
|
return id.String()
|
|
}
|
|
return checkerName + "|" + f(userId) + "|" + f(domainId) + "|" + f(serviceId)
|
|
}
|
|
|
|
func (s *optionsStore) ListAllCheckerConfigurations() (happydns.Iterator[happydns.CheckerOptions], error) {
|
|
return nil, nil
|
|
}
|
|
func (s *optionsStore) ListCheckerConfiguration(checkerName string) ([]*happydns.CheckerOptionsPositional, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// GetCheckerConfiguration returns positionals from least to most specific.
|
|
// It constructs the hierarchy: admin -> user -> domain -> service.
|
|
func (s *optionsStore) GetCheckerConfiguration(checkerName string, userId, domainId, serviceId *happydns.Identifier) ([]*happydns.CheckerOptionsPositional, error) {
|
|
var result []*happydns.CheckerOptionsPositional
|
|
|
|
// admin level
|
|
if opts, ok := s.data[posKey(checkerName, nil, nil, nil)]; ok {
|
|
result = append(result, &happydns.CheckerOptionsPositional{
|
|
CheckName: checkerName, Options: opts,
|
|
})
|
|
}
|
|
// user level
|
|
if userId != nil {
|
|
if opts, ok := s.data[posKey(checkerName, userId, nil, nil)]; ok {
|
|
result = append(result, &happydns.CheckerOptionsPositional{
|
|
CheckName: checkerName, UserId: userId, Options: opts,
|
|
})
|
|
}
|
|
}
|
|
// domain level
|
|
if domainId != nil {
|
|
if opts, ok := s.data[posKey(checkerName, userId, domainId, nil)]; ok {
|
|
result = append(result, &happydns.CheckerOptionsPositional{
|
|
CheckName: checkerName, UserId: userId, DomainId: domainId, Options: opts,
|
|
})
|
|
}
|
|
}
|
|
// service level
|
|
if serviceId != nil {
|
|
if opts, ok := s.data[posKey(checkerName, userId, domainId, serviceId)]; ok {
|
|
result = append(result, &happydns.CheckerOptionsPositional{
|
|
CheckName: checkerName, UserId: userId, DomainId: domainId, ServiceId: serviceId, Options: opts,
|
|
})
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *optionsStore) UpdateCheckerConfiguration(checkerName string, userId, domainId, serviceId *happydns.Identifier, opts happydns.CheckerOptions) error {
|
|
s.data[posKey(checkerName, userId, domainId, serviceId)] = opts
|
|
return nil
|
|
}
|
|
|
|
func (s *optionsStore) DeleteCheckerConfiguration(checkerName string, userId, domainId, serviceId *happydns.Identifier) error {
|
|
delete(s.data, posKey(checkerName, userId, domainId, serviceId))
|
|
return nil
|
|
}
|
|
|
|
func (s *optionsStore) ClearCheckerConfigurations() error {
|
|
s.data = make(map[string]happydns.CheckerOptions)
|
|
return nil
|
|
}
|
|
|
|
// --- test rule/checker types ---
|
|
|
|
// validatingRule is a CheckRule that also implements OptionsValidator.
|
|
type validatingRule struct {
|
|
name string
|
|
validateErr error
|
|
}
|
|
|
|
func (r *validatingRule) Name() string { return r.name }
|
|
func (r *validatingRule) Description() string { return "validating rule" }
|
|
func (r *validatingRule) Evaluate(_ context.Context, _ happydns.ObservationGetter, _ happydns.CheckerOptions) happydns.CheckState {
|
|
return happydns.CheckState{Status: happydns.StatusOK}
|
|
}
|
|
func (r *validatingRule) ValidateOptions(_ happydns.CheckerOptions) error {
|
|
return r.validateErr
|
|
}
|
|
|
|
// ruleWithOptions is a CheckRule that implements CheckRuleWithOptions.
|
|
type ruleWithOptions struct {
|
|
name string
|
|
opts happydns.CheckerOptionsDocumentation
|
|
}
|
|
|
|
func (r *ruleWithOptions) Name() string { return r.name }
|
|
func (r *ruleWithOptions) Description() string { return "rule with options" }
|
|
func (r *ruleWithOptions) Evaluate(_ context.Context, _ happydns.ObservationGetter, _ happydns.CheckerOptions) happydns.CheckState {
|
|
return happydns.CheckState{Status: happydns.StatusOK}
|
|
}
|
|
func (r *ruleWithOptions) Options() happydns.CheckerOptionsDocumentation {
|
|
return r.opts
|
|
}
|
|
|
|
// --- CRUD tests ---
|
|
|
|
func TestSetAndGetCheckerOptions(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
opts := happydns.CheckerOptions{"key1": "value1", "key2": float64(42)}
|
|
|
|
if err := uc.SetCheckerOptions("c1", uid, nil, nil, opts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := uc.GetCheckerOptions("c1", uid, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["key1"] != "value1" || got["key2"] != float64(42) {
|
|
t.Errorf("unexpected options: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSetCheckerOptions_FiltersEmptyValues(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
opts := happydns.CheckerOptions{"keep": "yes", "drop_nil": nil, "drop_empty": ""}
|
|
if err := uc.SetCheckerOptions("c1", nil, nil, nil, opts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := uc.GetCheckerOptions("c1", nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := got["drop_nil"]; ok {
|
|
t.Error("nil value should have been filtered")
|
|
}
|
|
if _, ok := got["drop_empty"]; ok {
|
|
t.Error("empty string value should have been filtered")
|
|
}
|
|
if got["keep"] != "yes" {
|
|
t.Error("non-empty value should be kept")
|
|
}
|
|
}
|
|
|
|
func TestAddCheckerOptions_MergesIntoExisting(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"a": "1", "b": "2"})
|
|
|
|
merged, err := uc.AddCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"b": "updated", "c": "3"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if merged["a"] != "1" {
|
|
t.Errorf("existing key 'a' should be preserved, got %v", merged["a"])
|
|
}
|
|
if merged["b"] != "updated" {
|
|
t.Errorf("key 'b' should be updated, got %v", merged["b"])
|
|
}
|
|
if merged["c"] != "3" {
|
|
t.Errorf("key 'c' should be added, got %v", merged["c"])
|
|
}
|
|
}
|
|
|
|
func TestAddCheckerOptions_DeletesEmptyValues(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"a": "1", "b": "2"})
|
|
|
|
merged, err := uc.AddCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"a": nil, "b": ""})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := merged["a"]; ok {
|
|
t.Error("nil value should delete the key")
|
|
}
|
|
if _, ok := merged["b"]; ok {
|
|
t.Error("empty string value should delete the key")
|
|
}
|
|
}
|
|
|
|
func TestGetCheckerOption_SingleKey(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"x": "hello"})
|
|
|
|
val, err := uc.GetCheckerOption("c1", uid, nil, nil, "x")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if val != "hello" {
|
|
t.Errorf("expected 'hello', got %v", val)
|
|
}
|
|
|
|
val, err = uc.GetCheckerOption("c1", uid, nil, nil, "missing")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if val != nil {
|
|
t.Errorf("expected nil for missing key, got %v", val)
|
|
}
|
|
}
|
|
|
|
func TestSetCheckerOption_SingleKey(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"a": "1"})
|
|
|
|
if err := uc.SetCheckerOption("c1", uid, nil, nil, "b", "2"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := uc.GetCheckerOptions("c1", uid, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["a"] != "1" || got["b"] != "2" {
|
|
t.Errorf("unexpected options: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSetCheckerOption_DeletesEmpty(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"a": "1", "b": "2"})
|
|
|
|
if err := uc.SetCheckerOption("c1", uid, nil, nil, "a", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := uc.GetCheckerOptions("c1", uid, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := got["a"]; ok {
|
|
t.Error("key 'a' should have been deleted")
|
|
}
|
|
if got["b"] != "2" {
|
|
t.Error("key 'b' should be preserved")
|
|
}
|
|
}
|
|
|
|
// --- Scope merging tests ---
|
|
|
|
func TestGetCheckerOptions_MergesScopes(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Admin sets defaults.
|
|
_ = uc.SetCheckerOptions("c1", nil, nil, nil, happydns.CheckerOptions{"a": "admin", "shared": "admin"})
|
|
// User overrides shared.
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"b": "user", "shared": "user"})
|
|
// Domain overrides shared again.
|
|
_ = uc.SetCheckerOptions("c1", uid, did, nil, happydns.CheckerOptions{"c": "domain", "shared": "domain"})
|
|
|
|
got, err := uc.GetCheckerOptions("c1", uid, did, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got["a"] != "admin" {
|
|
t.Errorf("admin key 'a' should be visible, got %v", got["a"])
|
|
}
|
|
if got["b"] != "user" {
|
|
t.Errorf("user key 'b' should be visible, got %v", got["b"])
|
|
}
|
|
if got["c"] != "domain" {
|
|
t.Errorf("domain key 'c' should be visible, got %v", got["c"])
|
|
}
|
|
if got["shared"] != "domain" {
|
|
t.Errorf("'shared' should be overridden to 'domain', got %v", got["shared"])
|
|
}
|
|
}
|
|
|
|
func TestGetCheckerOptions_ServiceScopeOverridesAll(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
sid := idPtr()
|
|
|
|
_ = uc.SetCheckerOptions("c1", nil, nil, nil, happydns.CheckerOptions{"key": "admin"})
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"key": "user"})
|
|
_ = uc.SetCheckerOptions("c1", uid, did, nil, happydns.CheckerOptions{"key": "domain"})
|
|
_ = uc.SetCheckerOptions("c1", uid, did, sid, happydns.CheckerOptions{"key": "service"})
|
|
|
|
got, err := uc.GetCheckerOptions("c1", uid, did, sid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["key"] != "service" {
|
|
t.Errorf("service scope should win, got %v", got["key"])
|
|
}
|
|
}
|
|
|
|
func TestGetCheckerOptionsPositional_ReturnsAllLevels(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
_ = uc.SetCheckerOptions("c1", nil, nil, nil, happydns.CheckerOptions{"a": "1"})
|
|
_ = uc.SetCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"b": "2"})
|
|
_ = uc.SetCheckerOptions("c1", uid, did, nil, happydns.CheckerOptions{"c": "3"})
|
|
|
|
positionals, err := uc.GetCheckerOptionsPositional("c1", uid, did, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(positionals) != 3 {
|
|
t.Fatalf("expected 3 positional levels, got %d", len(positionals))
|
|
}
|
|
// Least specific first.
|
|
if positionals[0].UserId != nil {
|
|
t.Error("first positional should be admin (no userId)")
|
|
}
|
|
if positionals[1].DomainId != nil {
|
|
t.Error("second positional should be user (no domainId)")
|
|
}
|
|
if positionals[2].DomainId == nil {
|
|
t.Error("third positional should be domain level")
|
|
}
|
|
}
|
|
|
|
func TestGetCheckerOptions_EmptyStore(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
got, err := uc.GetCheckerOptions("nonexistent", nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("expected empty options, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAddCheckerOptions_CreatesNewScope(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
merged, err := uc.AddCheckerOptions("c1", uid, nil, nil, happydns.CheckerOptions{"new": "value"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if merged["new"] != "value" {
|
|
t.Errorf("expected 'value', got %v", merged["new"])
|
|
}
|
|
}
|
|
|
|
// --- BuildMergedCheckerOptions tests ---
|
|
|
|
func TestBuildMergedCheckerOptions(t *testing.T) {
|
|
stored := happydns.CheckerOptions{"a": "stored", "shared": "stored"}
|
|
run := happydns.CheckerOptions{"b": "run", "shared": "run"}
|
|
|
|
result := checkerUC.BuildMergedCheckerOptions(stored, run)
|
|
if result["a"] != "stored" {
|
|
t.Errorf("stored key should be preserved")
|
|
}
|
|
if result["b"] != "run" {
|
|
t.Errorf("run key should be added")
|
|
}
|
|
if result["shared"] != "run" {
|
|
t.Errorf("run should override stored, got %v", result["shared"])
|
|
}
|
|
}
|
|
|
|
func TestBuildMergedCheckerOptions_NilInputs(t *testing.T) {
|
|
result := checkerUC.BuildMergedCheckerOptions(nil, nil)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty result, got %v", result)
|
|
}
|
|
|
|
result = checkerUC.BuildMergedCheckerOptions(happydns.CheckerOptions{"a": "1"}, nil)
|
|
if result["a"] != "1" {
|
|
t.Errorf("stored key should be preserved with nil runOpts")
|
|
}
|
|
|
|
result = checkerUC.BuildMergedCheckerOptions(nil, happydns.CheckerOptions{"b": "2"})
|
|
if result["b"] != "2" {
|
|
t.Errorf("run key should be present with nil storedOpts")
|
|
}
|
|
}
|
|
|
|
// --- Validation tests ---
|
|
|
|
// registerTestChecker is a helper that registers a checker in the global
|
|
// registry and returns its ID. Each call should use a unique ID.
|
|
func registerTestChecker(id string, def *happydns.CheckerDefinition) {
|
|
def.ID = id
|
|
def.Name = id
|
|
checker.RegisterChecker(def)
|
|
}
|
|
|
|
func TestValidateOptions_UnknownChecker(t *testing.T) {
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
err := uc.ValidateOptions("no_such_checker", nil, nil, nil, happydns.CheckerOptions{}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown checker")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_AdminScope_AcceptsAdminOpts(t *testing.T) {
|
|
registerTestChecker("val_admin_ok", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "admin_key", Type: "string"},
|
|
},
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
// Admin scope (all nil) — admin_key is valid.
|
|
err := uc.ValidateOptions("val_admin_ok", nil, nil, nil, happydns.CheckerOptions{"admin_key": "hello"}, false)
|
|
if err != nil {
|
|
t.Fatalf("expected no error for valid admin opt, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_AdminScope_RejectsDomainOpt(t *testing.T) {
|
|
registerTestChecker("val_admin_reject_domain", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "admin_key", Type: "string"},
|
|
},
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
// Admin scope — domain_key should be rejected as unknown.
|
|
err := uc.ValidateOptions("val_admin_reject_domain", nil, nil, nil, happydns.CheckerOptions{"domain_key": "x"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for domain opt at admin scope")
|
|
}
|
|
if !strings.Contains(err.Error(), "unknown") {
|
|
t.Errorf("expected 'unknown' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_DomainScope_AcceptsDomainOpts(t *testing.T) {
|
|
registerTestChecker("val_domain_ok", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "admin_key", Type: "string"},
|
|
},
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
err := uc.ValidateOptions("val_domain_ok", uid, did, nil, happydns.CheckerOptions{"domain_key": "hello"}, false)
|
|
if err != nil {
|
|
t.Fatalf("expected no error for valid domain opt, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_DomainScope_RejectsAdminOpt(t *testing.T) {
|
|
registerTestChecker("val_domain_reject_admin", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "admin_key", Type: "string"},
|
|
},
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
err := uc.ValidateOptions("val_domain_reject_admin", uid, did, nil, happydns.CheckerOptions{"admin_key": "x"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for admin opt at domain scope")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_UserScope_AcceptsUserOpts(t *testing.T) {
|
|
registerTestChecker("val_user_ok", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
UserOpts: []happydns.Field{
|
|
{Id: "user_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
err := uc.ValidateOptions("val_user_ok", uid, nil, nil, happydns.CheckerOptions{"user_key": "val"}, false)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_ServiceScope_AcceptsServiceOpts(t *testing.T) {
|
|
registerTestChecker("val_service_ok", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
ServiceOpts: []happydns.Field{
|
|
{Id: "svc_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
sid := idPtr()
|
|
err := uc.ValidateOptions("val_service_ok", uid, did, sid, happydns.CheckerOptions{"svc_key": "val"}, false)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_ServiceScope_RejectsRunOpts(t *testing.T) {
|
|
registerTestChecker("val_service_reject_run", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
ServiceOpts: []happydns.Field{
|
|
{Id: "svc_key", Type: "string"},
|
|
},
|
|
RunOpts: []happydns.Field{
|
|
{Id: "run_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
sid := idPtr()
|
|
err := uc.ValidateOptions("val_service_reject_run", uid, did, sid, happydns.CheckerOptions{"run_key": "val"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for run opt at service scope")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_DomainScope_RejectsRunOpts(t *testing.T) {
|
|
registerTestChecker("val_domain_reject_run", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
RunOpts: []happydns.Field{
|
|
{Id: "run_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
err := uc.ValidateOptions("val_domain_reject_run", uid, did, nil, happydns.CheckerOptions{"run_key": "val"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for run opt at domain scope")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_RequiredField(t *testing.T) {
|
|
registerTestChecker("val_required", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "must_have", Type: "string", Required: true, Label: "Must Have"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Missing required field.
|
|
err := uc.ValidateOptions("val_required", uid, did, nil, happydns.CheckerOptions{}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing required field")
|
|
}
|
|
if !strings.Contains(err.Error(), "required") {
|
|
t.Errorf("expected 'required' error, got: %v", err)
|
|
}
|
|
|
|
// Present but empty.
|
|
err = uc.ValidateOptions("val_required", uid, did, nil, happydns.CheckerOptions{"must_have": ""}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty required field")
|
|
}
|
|
|
|
// Valid.
|
|
err = uc.ValidateOptions("val_required", uid, did, nil, happydns.CheckerOptions{"must_have": "ok"}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_ChoicesField(t *testing.T) {
|
|
registerTestChecker("val_choices", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
UserOpts: []happydns.Field{
|
|
{Id: "mode", Type: "string", Choices: []string{"fast", "slow", "auto"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
|
|
err := uc.ValidateOptions("val_choices", uid, nil, nil, happydns.CheckerOptions{"mode": "fast"}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
err = uc.ValidateOptions("val_choices", uid, nil, nil, happydns.CheckerOptions{"mode": "invalid"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid choice")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_TypeCheckNumber(t *testing.T) {
|
|
registerTestChecker("val_type_num", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "count", Type: "int"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// float64 is fine (JSON numbers are float64).
|
|
err := uc.ValidateOptions("val_type_num", uid, did, nil, happydns.CheckerOptions{"count": float64(10)}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// String is not a number.
|
|
err = uc.ValidateOptions("val_type_num", uid, did, nil, happydns.CheckerOptions{"count": "ten"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong type")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_TypeCheckBool(t *testing.T) {
|
|
registerTestChecker("val_type_bool", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "enabled", Type: "bool"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
err := uc.ValidateOptions("val_type_bool", nil, nil, nil, happydns.CheckerOptions{"enabled": true}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
err = uc.ValidateOptions("val_type_bool", nil, nil, nil, happydns.CheckerOptions{"enabled": "true"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for string instead of bool")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_EmptyOptionsValid(t *testing.T) {
|
|
registerTestChecker("val_empty_ok", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "optional_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
err := uc.ValidateOptions("val_empty_ok", uid, did, nil, happydns.CheckerOptions{}, false)
|
|
if err != nil {
|
|
t.Fatalf("empty options should be valid when no required fields, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_NoFieldsAtScope_AcceptsEmpty(t *testing.T) {
|
|
registerTestChecker("val_no_fields_scope", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
// At admin scope there are no fields defined — empty opts should pass.
|
|
err := uc.ValidateOptions("val_no_fields_scope", nil, nil, nil, happydns.CheckerOptions{}, false)
|
|
if err != nil {
|
|
t.Fatalf("empty options at scope with no fields should be valid, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_NoFieldsAtScope_AcceptsAnything(t *testing.T) {
|
|
// When no fields are defined at the target scope, validation is skipped
|
|
// (the OptionsValidator may still reject), so unknown keys at a scope
|
|
// without field definitions pass through.
|
|
registerTestChecker("val_no_fields_pass", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
// At user scope, no fields are declared, so any key is accepted.
|
|
uid := idPtr()
|
|
err := uc.ValidateOptions("val_no_fields_pass", uid, nil, nil, happydns.CheckerOptions{"anything": "value"}, false)
|
|
if err != nil {
|
|
t.Fatalf("scope with no fields should skip validation, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- OptionsValidator interface tests ---
|
|
|
|
func TestValidateOptions_OptionsValidatorCalled(t *testing.T) {
|
|
registerTestChecker("val_validator_err", &happydns.CheckerDefinition{
|
|
Rules: []happydns.CheckRule{
|
|
&validatingRule{name: "r1", validateErr: fmt.Errorf("custom validation failed")},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
err := uc.ValidateOptions("val_validator_err", nil, nil, nil, happydns.CheckerOptions{}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error from OptionsValidator")
|
|
}
|
|
if !strings.Contains(err.Error(), "custom validation failed") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_OptionsValidatorPasses(t *testing.T) {
|
|
registerTestChecker("val_validator_ok", &happydns.CheckerDefinition{
|
|
Rules: []happydns.CheckRule{
|
|
&validatingRule{name: "r1", validateErr: nil},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
err := uc.ValidateOptions("val_validator_ok", nil, nil, nil, happydns.CheckerOptions{}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_MultipleValidators_StopsAtFirst(t *testing.T) {
|
|
registerTestChecker("val_multi_validators", &happydns.CheckerDefinition{
|
|
Rules: []happydns.CheckRule{
|
|
&validatingRule{name: "r1", validateErr: nil},
|
|
&validatingRule{name: "r2", validateErr: fmt.Errorf("r2 failed")},
|
|
&validatingRule{name: "r3", validateErr: fmt.Errorf("r3 failed")},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
err := uc.ValidateOptions("val_multi_validators", nil, nil, nil, happydns.CheckerOptions{}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error from second validator")
|
|
}
|
|
if !strings.Contains(err.Error(), "r2 failed") {
|
|
t.Errorf("expected r2 error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Rule-level options tests ---
|
|
|
|
func TestValidateOptions_RuleOptionsAtCorrectScope(t *testing.T) {
|
|
registerTestChecker("val_rule_opts", &happydns.CheckerDefinition{
|
|
Rules: []happydns.CheckRule{
|
|
&ruleWithOptions{
|
|
name: "rule_with_domain_opt",
|
|
opts: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "rule_domain_opt", Type: "string"},
|
|
},
|
|
RunOpts: []happydns.Field{
|
|
{Id: "rule_run_opt", Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Domain scope should accept rule's domain opt.
|
|
err := uc.ValidateOptions("val_rule_opts", uid, did, nil, happydns.CheckerOptions{"rule_domain_opt": "val"}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Domain scope should reject rule's run opt.
|
|
err = uc.ValidateOptions("val_rule_opts", uid, did, nil, happydns.CheckerOptions{"rule_run_opt": "val"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for run opt from rule at domain scope")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_CombinesDefAndRuleFields(t *testing.T) {
|
|
registerTestChecker("val_combined", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "def_opt", Type: "string"},
|
|
},
|
|
},
|
|
Rules: []happydns.CheckRule{
|
|
&ruleWithOptions{
|
|
name: "rule_extra",
|
|
opts: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "rule_opt", Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Both def and rule opts should be accepted.
|
|
err := uc.ValidateOptions("val_combined", uid, did, nil, happydns.CheckerOptions{
|
|
"def_opt": "a",
|
|
"rule_opt": "b",
|
|
}, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Unknown key should be rejected.
|
|
err = uc.ValidateOptions("val_combined", uid, did, nil, happydns.CheckerOptions{"unknown": "x"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown key")
|
|
}
|
|
}
|
|
|
|
// --- Validation + OptionsValidator combined ---
|
|
|
|
func TestValidateOptions_FieldValidationRunsBeforeOptionsValidator(t *testing.T) {
|
|
registerTestChecker("val_order", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "a", Type: "string", Required: true, Label: "A"},
|
|
},
|
|
},
|
|
Rules: []happydns.CheckRule{
|
|
&validatingRule{name: "r1", validateErr: fmt.Errorf("should not reach here")},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
// Field validation should fail before reaching OptionsValidator.
|
|
err := uc.ValidateOptions("val_order", nil, nil, nil, happydns.CheckerOptions{}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if strings.Contains(err.Error(), "should not reach here") {
|
|
t.Error("OptionsValidator should not have been called — field validation should fail first")
|
|
}
|
|
if !strings.Contains(err.Error(), "required") {
|
|
t.Errorf("expected 'required' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Scope isolation tests ---
|
|
|
|
func TestValidateOptions_DomainScope_DoesNotEnforceUserRequired(t *testing.T) {
|
|
registerTestChecker("val_scope_isolation", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
UserOpts: []happydns.Field{
|
|
{Id: "user_required", Type: "string", Required: true},
|
|
},
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_opt", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Domain scope should not enforce user-level required field.
|
|
err := uc.ValidateOptions("val_scope_isolation", uid, did, nil, happydns.CheckerOptions{"domain_opt": "val"}, false)
|
|
if err != nil {
|
|
t.Fatalf("domain scope should not enforce user required field, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_AdminScope_DoesNotEnforceServiceRequired(t *testing.T) {
|
|
registerTestChecker("val_admin_no_svc", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.Field{
|
|
{Id: "admin_opt", Type: "string"},
|
|
},
|
|
ServiceOpts: []happydns.Field{
|
|
{Id: "svc_required", Type: "string", Required: true},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
err := uc.ValidateOptions("val_admin_no_svc", nil, nil, nil, happydns.CheckerOptions{"admin_opt": "val"}, false)
|
|
if err != nil {
|
|
t.Fatalf("admin scope should not enforce service required field, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_UserScope_RejectsDomainOpt(t *testing.T) {
|
|
registerTestChecker("val_user_reject_domain", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
UserOpts: []happydns.Field{
|
|
{Id: "user_opt", Type: "string"},
|
|
},
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_opt", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
err := uc.ValidateOptions("val_user_reject_domain", uid, nil, nil, happydns.CheckerOptions{"domain_opt": "x"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for domain opt at user scope")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_ServiceScope_RejectsDomainOpt(t *testing.T) {
|
|
registerTestChecker("val_svc_reject_domain", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_opt", Type: "string"},
|
|
},
|
|
ServiceOpts: []happydns.Field{
|
|
{Id: "svc_opt", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
sid := idPtr()
|
|
err := uc.ValidateOptions("val_svc_reject_domain", uid, did, sid, happydns.CheckerOptions{"domain_opt": "x"}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error for domain opt at service scope")
|
|
}
|
|
}
|
|
|
|
// --- withRunOpts=true tests ---
|
|
|
|
func TestValidateOptions_WithRunOpts_AcceptsRunOptKeys(t *testing.T) {
|
|
registerTestChecker("trig_run_accept", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
RunOpts: []happydns.Field{
|
|
{Id: "run_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// With withRunOpts=true, run_key should be accepted alongside domain_key.
|
|
err := uc.ValidateOptions("trig_run_accept", uid, did, nil, happydns.CheckerOptions{
|
|
"domain_key": "foo",
|
|
"run_key": "bar",
|
|
}, true)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_WithRunOpts_EnforcesRequiredRunOpt(t *testing.T) {
|
|
registerTestChecker("trig_run_required", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
RunOpts: []happydns.Field{
|
|
{Id: "must_run", Type: "string", Required: true, Label: "Must Run"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Missing required run opt.
|
|
err := uc.ValidateOptions("trig_run_required", uid, did, nil, happydns.CheckerOptions{}, true)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing required run opt")
|
|
}
|
|
if !strings.Contains(err.Error(), "required") {
|
|
t.Errorf("expected 'required' error, got: %v", err)
|
|
}
|
|
|
|
// Present and non-empty.
|
|
err = uc.ValidateOptions("trig_run_required", uid, did, nil, happydns.CheckerOptions{"must_run": "ok"}, true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_WithRunOpts_StillRejectsUnknownKeys(t *testing.T) {
|
|
registerTestChecker("trig_run_unknown", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
RunOpts: []happydns.Field{
|
|
{Id: "run_key", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
err := uc.ValidateOptions("trig_run_unknown", uid, did, nil, happydns.CheckerOptions{"totally_unknown": "x"}, true)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown key even with withRunOpts=true")
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_WithRunOpts_RequiredRunOptNotEnforcedWhenFalse(t *testing.T) {
|
|
registerTestChecker("trig_run_not_enforced", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_key", Type: "string"},
|
|
},
|
|
RunOpts: []happydns.Field{
|
|
{Id: "must_run", Type: "string", Required: true},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// withRunOpts=false: required run opt is not enforced, run_key is not known.
|
|
err := uc.ValidateOptions("trig_run_not_enforced", uid, did, nil, happydns.CheckerOptions{"domain_key": "val"}, false)
|
|
if err != nil {
|
|
t.Fatalf("persisted scope should not enforce run opt required field, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_WithRunOpts_RuleRunOptsAccepted(t *testing.T) {
|
|
registerTestChecker("trig_rule_run_accept", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "def_domain_opt", Type: "string"},
|
|
},
|
|
},
|
|
Rules: []happydns.CheckRule{
|
|
&ruleWithOptions{
|
|
name: "rule_with_run",
|
|
opts: happydns.CheckerOptionsDocumentation{
|
|
RunOpts: []happydns.Field{
|
|
{Id: "rule_run_opt", Type: "string", Required: true, Label: "Rule Run Opt"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// withRunOpts=true: rule run opt is accepted and required.
|
|
err := uc.ValidateOptions("trig_rule_run_accept", uid, did, nil, happydns.CheckerOptions{
|
|
"def_domain_opt": "x",
|
|
}, true)
|
|
if err == nil {
|
|
t.Fatal("expected error: rule's required run opt is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "required") {
|
|
t.Errorf("expected 'required' error, got: %v", err)
|
|
}
|
|
|
|
err = uc.ValidateOptions("trig_rule_run_accept", uid, did, nil, happydns.CheckerOptions{
|
|
"def_domain_opt": "x",
|
|
"rule_run_opt": "y",
|
|
}, true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// withRunOpts=false: rule run opt is unknown (rejected).
|
|
err = uc.ValidateOptions("trig_rule_run_accept", uid, did, nil, happydns.CheckerOptions{
|
|
"rule_run_opt": "y",
|
|
}, false)
|
|
if err == nil {
|
|
t.Fatal("expected error: rule run opt should be unknown at domain scope without withRunOpts")
|
|
}
|
|
}
|
|
|
|
// --- Auto-fill tests ---
|
|
|
|
// autoFillStore is a minimal in-memory store satisfying CheckAutoFillStorage.
|
|
type autoFillStore struct {
|
|
domains map[string]*happydns.Domain
|
|
zones map[string]*happydns.ZoneMessage
|
|
users map[string]*happydns.User
|
|
}
|
|
|
|
func newAutoFillStore() *autoFillStore {
|
|
return &autoFillStore{
|
|
domains: make(map[string]*happydns.Domain),
|
|
zones: make(map[string]*happydns.ZoneMessage),
|
|
users: make(map[string]*happydns.User),
|
|
}
|
|
}
|
|
|
|
func (s *autoFillStore) GetDomain(id happydns.Identifier) (*happydns.Domain, error) {
|
|
if d, ok := s.domains[id.String()]; ok {
|
|
return d, nil
|
|
}
|
|
return nil, fmt.Errorf("domain %s not found", id)
|
|
}
|
|
|
|
func (s *autoFillStore) GetZone(id happydns.Identifier) (*happydns.ZoneMessage, error) {
|
|
if z, ok := s.zones[id.String()]; ok {
|
|
return z, nil
|
|
}
|
|
return nil, fmt.Errorf("zone %s not found", id)
|
|
}
|
|
|
|
func (s *autoFillStore) ListDomains(u *happydns.User) ([]*happydns.Domain, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *autoFillStore) GetUser(id happydns.Identifier) (*happydns.User, error) {
|
|
if u, ok := s.users[id.String()]; ok {
|
|
return u, nil
|
|
}
|
|
return nil, fmt.Errorf("user %s not found", id)
|
|
}
|
|
|
|
func TestBuildMergedCheckerOptionsWithAutoFill_InjectsValues(t *testing.T) {
|
|
registerTestChecker("af_inject", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "domain_name_field", Type: "string", AutoFill: happydns.AutoFillDomainName},
|
|
{Id: "user_opt", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
optStore := newOptionsStore()
|
|
afStore := newAutoFillStore()
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// Set up domain in auto-fill store.
|
|
zoneId, _ := happydns.NewRandomIdentifier()
|
|
afStore.domains[did.String()] = &happydns.Domain{
|
|
Id: *did,
|
|
Owner: *uid,
|
|
DomainName: "example.com.",
|
|
ZoneHistory: []happydns.Identifier{zoneId},
|
|
}
|
|
afStore.zones[zoneId.String()] = &happydns.ZoneMessage{}
|
|
|
|
uc := checkerUC.NewCheckerOptionsUsecase(optStore, afStore)
|
|
_ = uc.SetCheckerOptions("af_inject", uid, nil, nil, happydns.CheckerOptions{"user_opt": "hello"})
|
|
|
|
merged, err := uc.BuildMergedCheckerOptionsWithAutoFill("af_inject", uid, did, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if merged["domain_name_field"] != "example.com." {
|
|
t.Errorf("expected auto-filled domain name, got %v", merged["domain_name_field"])
|
|
}
|
|
if merged["user_opt"] != "hello" {
|
|
t.Errorf("expected stored opt to be preserved, got %v", merged["user_opt"])
|
|
}
|
|
}
|
|
|
|
func TestBuildMergedCheckerOptionsWithAutoFill_OverridesRunOpts(t *testing.T) {
|
|
registerTestChecker("af_override", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "dn", Type: "string", AutoFill: happydns.AutoFillDomainName},
|
|
},
|
|
},
|
|
})
|
|
|
|
optStore := newOptionsStore()
|
|
afStore := newAutoFillStore()
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
zoneId, _ := happydns.NewRandomIdentifier()
|
|
afStore.domains[did.String()] = &happydns.Domain{
|
|
Id: *did,
|
|
Owner: *uid,
|
|
DomainName: "real.example.com.",
|
|
ZoneHistory: []happydns.Identifier{zoneId},
|
|
}
|
|
afStore.zones[zoneId.String()] = &happydns.ZoneMessage{}
|
|
|
|
uc := checkerUC.NewCheckerOptionsUsecase(optStore, afStore)
|
|
|
|
// Even if runOpts tries to set the auto-fill field, auto-fill wins.
|
|
merged, err := uc.BuildMergedCheckerOptionsWithAutoFill("af_override", uid, did, nil,
|
|
happydns.CheckerOptions{"dn": "user-provided.com."})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if merged["dn"] != "real.example.com." {
|
|
t.Errorf("auto-fill should override run opts, got %v", merged["dn"])
|
|
}
|
|
}
|
|
|
|
func TestSetCheckerOptions_StripsAutoFillKeys(t *testing.T) {
|
|
registerTestChecker("af_strip", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "dn", Type: "string", AutoFill: happydns.AutoFillDomainName},
|
|
{Id: "normal", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
optStore := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(optStore, nil)
|
|
|
|
uid := idPtr()
|
|
err := uc.SetCheckerOptions("af_strip", uid, nil, nil, happydns.CheckerOptions{
|
|
"dn": "should-be-stripped",
|
|
"normal": "kept",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := uc.GetCheckerOptions("af_strip", uid, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := got["dn"]; ok {
|
|
t.Error("auto-fill key should have been stripped from persisted options")
|
|
}
|
|
if got["normal"] != "kept" {
|
|
t.Errorf("normal key should be preserved, got %v", got["normal"])
|
|
}
|
|
}
|
|
|
|
func TestValidateOptions_SkipsAutoFillFields(t *testing.T) {
|
|
registerTestChecker("af_validate_skip", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
DomainOpts: []happydns.Field{
|
|
{Id: "dn", Type: "string", AutoFill: happydns.AutoFillDomainName, Required: true},
|
|
{Id: "normal", Type: "string"},
|
|
},
|
|
},
|
|
})
|
|
|
|
optStore := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(optStore, nil)
|
|
|
|
uid := idPtr()
|
|
did := idPtr()
|
|
|
|
// The auto-fill field "dn" is required, but since it's auto-filled,
|
|
// validation should not enforce it as a user-provided requirement.
|
|
err := uc.ValidateOptions("af_validate_skip", uid, did, nil, happydns.CheckerOptions{
|
|
"normal": "val",
|
|
}, false)
|
|
if err != nil {
|
|
t.Fatalf("auto-fill required field should be skipped during validation, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- NoOverride tests ---
|
|
|
|
func TestGetCheckerOptions_NoOverridePreservesAdminValue(t *testing.T) {
|
|
registerTestChecker("no_override_merge", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "locked", Type: "boolean", NoOverride: true},
|
|
},
|
|
UserOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "threshold", Type: "number"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
|
|
// Set at admin scope.
|
|
store.UpdateCheckerConfiguration("no_override_merge", nil, nil, nil, happydns.CheckerOptions{
|
|
"locked": true,
|
|
})
|
|
// Attempt to override at user scope (should be ignored during merge).
|
|
store.UpdateCheckerConfiguration("no_override_merge", uid, nil, nil, happydns.CheckerOptions{
|
|
"locked": false,
|
|
"threshold": float64(42),
|
|
})
|
|
|
|
merged, err := uc.GetCheckerOptions("no_override_merge", uid, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if merged["locked"] != true {
|
|
t.Errorf("expected locked=true (admin value preserved), got %v", merged["locked"])
|
|
}
|
|
if merged["threshold"] != float64(42) {
|
|
t.Errorf("expected threshold=42 (user value applied), got %v", merged["threshold"])
|
|
}
|
|
}
|
|
|
|
func TestGetCheckerOptions_NoOverrideAllowsSameScope(t *testing.T) {
|
|
registerTestChecker("no_override_same_scope", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "locked", Type: "boolean", NoOverride: true},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
// Only admin scope sets the value — no conflict.
|
|
store.UpdateCheckerConfiguration("no_override_same_scope", nil, nil, nil, happydns.CheckerOptions{
|
|
"locked": true,
|
|
})
|
|
|
|
merged, err := uc.GetCheckerOptions("no_override_same_scope", nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if merged["locked"] != true {
|
|
t.Errorf("expected locked=true, got %v", merged["locked"])
|
|
}
|
|
}
|
|
|
|
func TestBuildMergedCheckerOptionsWithAutoFill_NoOverrideBlocksRunOpts(t *testing.T) {
|
|
registerTestChecker("no_override_runopt", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "locked", Type: "boolean", NoOverride: true},
|
|
},
|
|
UserOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "threshold", Type: "number"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
|
|
// Admin sets locked=true.
|
|
store.UpdateCheckerConfiguration("no_override_runopt", nil, nil, nil, happydns.CheckerOptions{
|
|
"locked": true,
|
|
})
|
|
// User sets threshold.
|
|
store.UpdateCheckerConfiguration("no_override_runopt", uid, nil, nil, happydns.CheckerOptions{
|
|
"threshold": float64(10),
|
|
})
|
|
|
|
// RunOpts tries to override locked.
|
|
merged, err := uc.BuildMergedCheckerOptionsWithAutoFill("no_override_runopt", uid, nil, nil, happydns.CheckerOptions{
|
|
"locked": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if merged["locked"] != true {
|
|
t.Errorf("expected locked=true (NoOverride should block runOpts), got %v", merged["locked"])
|
|
}
|
|
if merged["threshold"] != float64(10) {
|
|
t.Errorf("expected threshold=10, got %v", merged["threshold"])
|
|
}
|
|
}
|
|
|
|
func TestSetCheckerOptions_StripsNoOverrideAtLowerScope(t *testing.T) {
|
|
registerTestChecker("no_override_set", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "locked", Type: "boolean", NoOverride: true},
|
|
},
|
|
UserOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "threshold", Type: "number"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
|
|
// Try to set locked at user scope — should be silently stripped.
|
|
err := uc.SetCheckerOptions("no_override_set", uid, nil, nil, happydns.CheckerOptions{
|
|
"locked": true,
|
|
"threshold": float64(99),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Check what was actually stored.
|
|
stored := store.data[posKey("no_override_set", uid, nil, nil)]
|
|
if _, ok := stored["locked"]; ok {
|
|
t.Error("expected locked to be stripped from user-scope storage")
|
|
}
|
|
if stored["threshold"] != float64(99) {
|
|
t.Errorf("expected threshold=99 to be stored, got %v", stored["threshold"])
|
|
}
|
|
}
|
|
|
|
func TestAddCheckerOptions_StripsNoOverrideAtLowerScope(t *testing.T) {
|
|
registerTestChecker("no_override_add", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "locked", Type: "boolean", NoOverride: true},
|
|
},
|
|
UserOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "threshold", Type: "number"},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
|
|
// Pre-populate user scope with threshold.
|
|
store.UpdateCheckerConfiguration("no_override_add", uid, nil, nil, happydns.CheckerOptions{
|
|
"threshold": float64(50),
|
|
})
|
|
|
|
// Try to add locked at user scope — should be silently skipped.
|
|
result, err := uc.AddCheckerOptions("no_override_add", uid, nil, nil, happydns.CheckerOptions{
|
|
"locked": true,
|
|
"threshold": float64(75),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if _, ok := result["locked"]; ok {
|
|
t.Error("expected locked to be skipped in AddCheckerOptions result")
|
|
}
|
|
if result["threshold"] != float64(75) {
|
|
t.Errorf("expected threshold=75, got %v", result["threshold"])
|
|
}
|
|
}
|
|
|
|
func TestSetCheckerOption_RejectsNoOverrideAtLowerScope(t *testing.T) {
|
|
registerTestChecker("no_override_set_single", &happydns.CheckerDefinition{
|
|
Options: happydns.CheckerOptionsDocumentation{
|
|
AdminOpts: []happydns.CheckerOptionDocumentation{
|
|
{Id: "locked", Type: "boolean", NoOverride: true},
|
|
},
|
|
},
|
|
})
|
|
|
|
store := newOptionsStore()
|
|
uc := checkerUC.NewCheckerOptionsUsecase(store, nil)
|
|
|
|
uid := idPtr()
|
|
|
|
// Setting at admin scope should work.
|
|
err := uc.SetCheckerOption("no_override_set_single", nil, nil, nil, "locked", true)
|
|
if err != nil {
|
|
t.Fatalf("expected SetCheckerOption at admin scope to succeed, got: %v", err)
|
|
}
|
|
|
|
// Setting at user scope should fail.
|
|
err = uc.SetCheckerOption("no_override_set_single", uid, nil, nil, "locked", false)
|
|
if err == nil {
|
|
t.Fatal("expected error when setting NoOverride field at lower scope")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot be overridden") {
|
|
t.Errorf("unexpected error message: %v", err)
|
|
}
|
|
}
|