From 6333b7577594d4a7f91a3ef11194f188b50ab846 Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Mon, 23 Dec 2024 14:12:54 +0100 Subject: [PATCH] feat: add mode selection --- pkg/config/__mocks__/testLoadCanFindFile.yaml | 9 +-- pkg/config/__mocks__/testLoadInvalidMode.yaml | 2 + pkg/config/__mocks__/testLoadInvalidYAML.yaml | 3 + .../testLoadMissingCheckInterval.yaml | 3 + pkg/config/config.go | 49 ++++++++--- pkg/config/config_test.go | 81 ++++++++----------- .../realDynDns.go} | 32 +++++++- .../realDynDns_test.go} | 6 +- 8 files changed, 113 insertions(+), 72 deletions(-) create mode 100644 pkg/config/__mocks__/testLoadInvalidMode.yaml create mode 100644 pkg/config/__mocks__/testLoadInvalidYAML.yaml create mode 100644 pkg/config/__mocks__/testLoadMissingCheckInterval.yaml rename pkg/{changeDetector/changeDetector.go => realDynDns/realDynDns.go} (70%) rename pkg/{changeDetector/changeDetector_test.go => realDynDns/realDynDns_test.go} (94%) diff --git a/pkg/config/__mocks__/testLoadCanFindFile.yaml b/pkg/config/__mocks__/testLoadCanFindFile.yaml index 48007ce..9aa504b 100644 --- a/pkg/config/__mocks__/testLoadCanFindFile.yaml +++ b/pkg/config/__mocks__/testLoadCanFindFile.yaml @@ -1,16 +1,15 @@ ---- ip_provider: type: plain config: - url: https://ifconfig.me + url: https://example.com dns_provider: type: ionos config: - api_key: exampleAPIKey + api_key: exampleapikey base_url: https://example.com domains: - tld: example.com subdomains: - "@" - - www -check_interval: 0 0 0/6 * * * * +check_interval: 0 0 * * * * +mode: RunOnce diff --git a/pkg/config/__mocks__/testLoadInvalidMode.yaml b/pkg/config/__mocks__/testLoadInvalidMode.yaml new file mode 100644 index 0000000..7eb8dc2 --- /dev/null +++ b/pkg/config/__mocks__/testLoadInvalidMode.yaml @@ -0,0 +1,2 @@ +mode: "InvalidMode" +check_interval: "5m" diff --git a/pkg/config/__mocks__/testLoadInvalidYAML.yaml b/pkg/config/__mocks__/testLoadInvalidYAML.yaml new file mode 100644 index 0000000..80984cc --- /dev/null +++ b/pkg/config/__mocks__/testLoadInvalidYAML.yaml @@ -0,0 +1,3 @@ +mode: "Scheduled" +check_interval: "5m" +- invalid_content diff --git a/pkg/config/__mocks__/testLoadMissingCheckInterval.yaml b/pkg/config/__mocks__/testLoadMissingCheckInterval.yaml new file mode 100644 index 0000000..ecd177b --- /dev/null +++ b/pkg/config/__mocks__/testLoadMissingCheckInterval.yaml @@ -0,0 +1,3 @@ +mode: "Scheduled" +ip_provider: + type: "plain" diff --git a/pkg/config/config.go b/pkg/config/config.go index 28b9488..00560ee 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,17 +1,15 @@ package config import ( + "errors" + "fmt" "os" "gopkg.in/yaml.v3" ) -type DomainConfig struct { - TLD string `yaml:"tld"` - Subdomains []string `yaml:"subdomains"` -} - type Config struct { + Mode string `yaml:"mode"` ExternalIPProvider ExternalIpProviderConfig `yaml:"ip_provider"` DNSProvider DNSProviderConfig `yaml:"dns_provider"` NotificationProvider NotificationProviderConfig `yaml:"notification_provider,omitempty"` @@ -19,6 +17,16 @@ type Config struct { CheckInterval string `yaml:"check_interval"` } +const ( + RunOnceMode = "RunOnce" + ScheduledMode = "Scheduled" +) + +type DomainConfig struct { + TLD string `yaml:"tld"` + Subdomains []string `yaml:"subdomains"` +} + type ExternalIpProviderConfig struct { Type string `yaml:"type"` ProviderConfig yaml.Node `yaml:"config"` @@ -35,15 +43,30 @@ type NotificationProviderConfig struct { } func (c *Config) Load(filePath string) error { - err := yaml.Unmarshal([]byte(filePath), c) + inputConfig, err := os.ReadFile(filePath) if err != nil { - inputConfig, err := os.ReadFile(filePath) - if err != nil { - return err - } - - return yaml.Unmarshal(inputConfig, c) + return fmt.Errorf("failed to read config file: %w", err) } - return err + if err := yaml.Unmarshal(inputConfig, c); err != nil { + return fmt.Errorf("failed to unmarshal config file: %w", err) + } + + if err := c.validate(); err != nil { + return fmt.Errorf("failed to validate config: %w", err) + } + + return nil; } + +func (c *Config) validate() error { + if c.Mode != RunOnceMode && c.Mode != ScheduledMode { + return errors.New("mode must be one of 'RunOnce' or 'Scheduled'") + } + + if c.Mode == ScheduledMode && c.CheckInterval == "" { + return errors.New("check interval must be set when mode is 'Scheduled'") + } + + return nil; +} \ No newline at end of file diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 4e60fbd..5f5f5e9 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,69 +1,52 @@ package config import ( + "fmt" "testing" ) +func testFactoryFileRelatedError(fileName string, expectedErrorText string) func(t *testing.T) { + return func(t *testing.T) { + c := Config{} + err := c.Load(fmt.Sprintf("./__mocks__/%s", fileName)) + + want := err != nil && err.Error() == expectedErrorText + + if !want { + t.Fatalf("Expected error message %s, but got %s", expectedErrorText, err.Error()) + } + } +} + func TestLoad(t *testing.T) { t.Run("Can find file", testLoadCanFindFile()) - t.Run("Cannot find file", testLoadCannotFindFile()) - t.Run("Unmarshals from direct input", testLoadUnmarshalsFromDirectInput()) + t.Run("Cannot find file", testFactoryFileRelatedError( + "nonexistent.yaml", + "failed to read config file: open ./__mocks__/nonexistent.yaml: no such file or directory", + )) + t.Run("Missing CheckInterval in Scheduled mode", testFactoryFileRelatedError( + "testLoadMissingCheckInterval.yaml", + "failed to validate config: check interval must be set when mode is 'Scheduled'", + )) + t.Run("Invalid mode", testFactoryFileRelatedError( + "testLoadInvalidMode.yaml", + "failed to validate config: mode must be one of 'RunOnce' or 'Scheduled'", + )) + t.Run("Invalid YAML", testFactoryFileRelatedError( + "testLoadInvalidYAML.yaml", + "failed to unmarshal config file: yaml: line 2: did not find expected key", + )) } func testLoadCanFindFile() func(t *testing.T) { return func(t *testing.T) { - c := Config{} err := c.Load("./__mocks__/testLoadCanFindFile.yaml") - want := c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain" + want := err == nil && c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain" && c.Mode == "RunOnce" if !want || err != nil { - t.Fatalf("DnsProviderName couldn't be properly loaded or unmarshaled, Load() = %v, want %v", err, want) - } - } -} - -func testLoadCannotFindFile() func(t *testing.T) { - return func(t *testing.T) { - c := Config{} - err := c.Load("nonexistent.yaml") - want := err != nil - - if !want { - t.Fatalf("Config didn't throw an error") - } - } -} - -func testLoadUnmarshalsFromDirectInput() func(t *testing.T) { - return func(t *testing.T) { - c := Config{} - err := c.Load(`--- -ip_provider: - type: plain - config: - url: https://ifconfig.me -dns_provider: - type: ionos - config: - api_key: exampleAPIKey - base_url: https://example.com -notification_provider: - type: gotify - config: - url: https://example.com -domains: - - tld: example.com - subdomains: - - "@" - - www -check_interval: 0 0 0/6 * * * *`) - - want := c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain" && c.NotificationProvider.Type == "gotify" - - if !want || err != nil { - t.Fatalf("DnsProviderName couldn't be properly loaded or unmarshaled, Load() = %v, want %v", err, want) + t.Fatalf("Failed to load config file, expected no errors but got: %s", err) } } } diff --git a/pkg/changeDetector/changeDetector.go b/pkg/realDynDns/realDynDns.go similarity index 70% rename from pkg/changeDetector/changeDetector.go rename to pkg/realDynDns/realDynDns.go index 10d0c8e..c08c800 100644 --- a/pkg/changeDetector/changeDetector.go +++ b/pkg/realDynDns/realDynDns.go @@ -1,11 +1,15 @@ -package changeDetector +package realDynDns import ( "fmt" + "time" + "realdnydns/pkg/config" "realdnydns/pkg/dnsProvider" "realdnydns/pkg/externalIpProvider" "realdnydns/pkg/notificationProvider" + + "github.com/go-co-op/gocron" ) type ChangeDetector struct { @@ -29,7 +33,31 @@ func New( } } -func (c *ChangeDetector) DetectAndApplyChanges() (int, error) { +func (c *ChangeDetector) RunWithSchedule(checkInterval string) (*gocron.Scheduler, *gocron.Job, error) { + s := gocron.NewScheduler(time.UTC) + s.SingletonMode() + s.CronWithSeconds(checkInterval) + + job, err := s.DoWithJobDetails(func(job gocron.Job) { + numberChanged, err := c.detectAndApplyChanges() + if err != nil { + panic(err) + } + fmt.Printf("Number of changes: %d\n", numberChanged) + fmt.Println("Next run:", job.NextRun()) + }) + if err != nil { + return nil, nil, err + } + + return s, job, nil +} + +func (c *ChangeDetector) RunOnce() (int, error) { + return c.detectAndApplyChanges() +} + +func (c *ChangeDetector) detectAndApplyChanges() (int, error) { externalIp, err := c.externalIpProvider.GetExternalIp() if err != nil { return 0, err diff --git a/pkg/changeDetector/changeDetector_test.go b/pkg/realDynDns/realDynDns_test.go similarity index 94% rename from pkg/changeDetector/changeDetector_test.go rename to pkg/realDynDns/realDynDns_test.go index 87e6988..22eb603 100644 --- a/pkg/changeDetector/changeDetector_test.go +++ b/pkg/realDynDns/realDynDns_test.go @@ -1,4 +1,4 @@ -package changeDetector +package realDynDns import ( "net" @@ -74,7 +74,7 @@ func testDetectAndApplyChangesWithChanges() func(t *testing.T) { }, }) - numberUpdated, err := changeDetector.DetectAndApplyChanges() + numberUpdated, err := changeDetector.RunOnce() if err != nil { t.Errorf("expected no error, got %v", err) } @@ -103,7 +103,7 @@ func testDetectAndApplyChangesWithoutChanges() func(t *testing.T) { }, }) - numberUpdated, err := changeDetector.DetectAndApplyChanges() + numberUpdated, err := changeDetector.RunOnce() if err != nil { t.Errorf("expected no error, got %v", err) }