Repository: favonia/cloudflare-ddns
Pull request #45 (2021-08-01 19:10:45)

test(cron): 100% coverage of cron


Patch:
diff --git a/internal/cron/printer.go b/internal/cron/printer.go
index fb87115..2100282 100644
--- a/internal/cron/printer.go
+++ b/internal/cron/printer.go
@@ -34,6 +34,7 @@ func describeOffset(offset int) string {
 	}
 }
 
+// DescribeLocation gives a description of loc that combines loc.String() and time offset.
 func DescribeLocation(loc *time.Location) string {
 	_, offset := time.Now().In(loc).Zone()
 
diff --git a/internal/cron/schedule.go b/internal/cron/schedule.go
index 87a432f..420241e 100644
--- a/internal/cron/schedule.go
+++ b/internal/cron/schedule.go
@@ -1,48 +1,52 @@
+// Package cron handles anything related to time.
 package cron
 
 import (
 	"fmt"
-	"log"
 	"time"
 
 	"github.com/robfig/cron/v3"
 )
 
+// Schedule tells the next time a scheduled event should happen.
 type Schedule = interface {
 	Next() time.Time
 	String() string
 }
 
-type Cron struct {
+// cronSchedule holds a parsed cron expression and its original input.
+type cronSchedule struct {
 	spec     string
 	schedule cron.Schedule
 }
 
-func New(spec string) (*Cron, error) {
+// New creates a new Schedule.
+func New(spec string) (Schedule, error) {
 	sche, err := cron.ParseStandard(spec)
 	if err != nil {
 		return nil, fmt.Errorf("parsing %q: %w", spec, err)
 	}
 
-	return &Cron{
+	return &cronSchedule{
 		spec:     spec,
 		schedule: sche,
 	}, nil
 }
 
-func MustNew(spec string) *Cron {
+// MustNew creates a new Schedule, and panics if it fails to parse the input.
+func MustNew(spec string) Schedule {
 	cron, err := New(spec)
 	if err != nil {
-		log.Fatalf(`🤯 schedule.MustNew failed: %v`, err)
+		panic(fmt.Errorf(`🤯 schedule.MustNew failed: %w`, err))
 	}
 
 	return cron
 }
 
-func (s *Cron) Next() time.Time {
+func (s *cronSchedule) Next() time.Time {
 	return s.schedule.Next(time.Now())
 }
 
-func (s *Cron) String() string {
+func (s *cronSchedule) String() string {
 	return s.spec
 }


Test patch:
diff --git a/internal/cron/printer_test.go b/internal/cron/printer_test.go
index 4defb5a..72a5c37 100644
--- a/internal/cron/printer_test.go
+++ b/internal/cron/printer_test.go
@@ -9,11 +9,6 @@ import (
 	"github.com/favonia/cloudflare-ddns-go/internal/cron"
 )
 
-type descriptionTest struct {
-	input  *time.Location
-	output string
-}
-
 func mustLoadLocation(name string) *time.Location {
 	loc, err := time.LoadLocation(name)
 	if err != nil {
@@ -26,15 +21,18 @@ func mustLoadLocation(name string) *time.Location {
 func TestDescribeLocation(t *testing.T) {
 	t.Parallel()
 
-	for _, p := range [...]descriptionTest{
-		{input: time.UTC, output: "UTC (UTC+00 now)"},
-		{input: mustLoadLocation("Asia/Thimphu"), output: "Asia/Thimphu (UTC+06 now)"},
-		{input: mustLoadLocation("Asia/Seoul"), output: "Asia/Seoul (UTC+09 now)"},
-		{input: mustLoadLocation("Asia/Shanghai"), output: "Asia/Shanghai (UTC+08 now)"},
-		{input: mustLoadLocation("Asia/Kolkata"), output: "Asia/Kolkata (UTC+05:30 now)"},
-		{input: mustLoadLocation("America/Port_of_Spain"), output: "America/Port_of_Spain (UTC−04 now)"},
-		{input: time.FixedZone("Dublin Mean Time", -1521), output: "Dublin Mean Time (UTC−00:25:21 now)"},
-		{input: time.FixedZone("Bangkok Mean Time", 24124), output: "Bangkok Mean Time (UTC+06:42:04 now)"},
+	for _, p := range [...]struct {
+		input  *time.Location
+		output string
+	}{
+		{time.UTC, "UTC (UTC+00 now)"},
+		{mustLoadLocation("Asia/Thimphu"), "Asia/Thimphu (UTC+06 now)"},
+		{mustLoadLocation("Asia/Seoul"), "Asia/Seoul (UTC+09 now)"},
+		{mustLoadLocation("Asia/Shanghai"), "Asia/Shanghai (UTC+08 now)"},
+		{mustLoadLocation("Asia/Kolkata"), "Asia/Kolkata (UTC+05:30 now)"},
+		{mustLoadLocation("America/Port_of_Spain"), "America/Port_of_Spain (UTC−04 now)"},
+		{time.FixedZone("Dublin Mean Time", -1521), "Dublin Mean Time (UTC−00:25:21 now)"},
+		{time.FixedZone("Bangkok Mean Time", 24124), "Bangkok Mean Time (UTC+06:42:04 now)"},
 	} {
 		assert.Equalf(t, p.output, cron.DescribeLocation(p.input), "Timezone descriptions should match.")
 	}
diff --git a/internal/cron/schedule_test.go b/internal/cron/schedule_test.go
new file mode 100644
index 0000000..f02f815
--- /dev/null
+++ b/internal/cron/schedule_test.go
@@ -0,0 +1,50 @@
+package cron_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/favonia/cloudflare-ddns-go/internal/cron"
+)
+
+func TestMustNewSuccessful(t *testing.T) {
+	t.Parallel()
+
+	for _, s := range [...]string{
+		"*/4 * * * *",
+		"@every 5h0s",
+		"@yearly",
+	} {
+		assert.Equal(t, s, cron.MustNew(s).String(), "Cron.String() should return the original spec.")
+	}
+}
+
+func TestMustNewPanicking(t *testing.T) {
+	t.Parallel()
+
+	for _, s := range [...]string{
+		"*/4 * * * * *",
+		"@every 5ss",
+		"@cool",
+	} {
+		s := s // redefine s to avoid capturing of the same variable
+		assert.Panics(t, func() { cron.MustNew(s) })
+	}
+}
+
+func TestNext(t *testing.T) {
+	t.Parallel()
+
+	const delta = time.Second * 5
+	for _, c := range [...]struct {
+		spec     string
+		interval time.Duration
+	}{
+		{"@every 1h", time.Hour},
+		{"@every 4h", time.Hour * 4},
+	} {
+		assert.WithinDuration(t, time.Now().Add(c.interval), cron.MustNew(c.spec).Next(), delta)
+	}
+}