From 652e1d670b4782211b50ef46527cdc2da306ad2d Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Wed, 15 Nov 2023 11:30:10 +0100
Subject: [PATCH] fix: date format (pauseuntil, time schedule) results in error
 #29

---
 persistence.go      | 33 ++++++++++++++++++++
 persistence_test.go | 49 ++++++++++++++++++++++++++++++
 scheduler.go        | 34 +++++++++++++++++++++
 scheduler_test.go   | 74 +++++++++++++++++++++++++++++++++++++++++++++
 time-formats.go     | 14 +++++++++
 5 files changed, 204 insertions(+)
 create mode 100644 time-formats.go

diff --git a/persistence.go b/persistence.go
index f464268..1921cb8 100644
--- a/persistence.go
+++ b/persistence.go
@@ -35,6 +35,39 @@ type JobPersistence struct {
 	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-" yaml:"-"`
 }
 
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (jp *JobPersistence) UnmarshalJSON(data []byte) error {
+	// Anonymous struct for unmarshalling with custom time format
+	type Alias JobPersistence
+	aux := &struct {
+		PauseUntil *string `json:"pauseUntil,omitempty"`
+		*Alias
+	}{
+		Alias: (*Alias)(jp),
+	}
+
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	if aux.PauseUntil != nil {
+		var t time.Time
+		var err error
+		for _, format := range SupportedTimeFormats {
+			t, err = time.Parse(format, *aux.PauseUntil)
+			if err == nil {
+				break
+			}
+		}
+		if err != nil {
+			return err
+		}
+		jp.PauseUntil = &t
+	}
+
+	return nil
+}
+
 func (jp JobPersistence) GetLogs() []JobLog {
 	return jp.Logs
 }
diff --git a/persistence_test.go b/persistence_test.go
index 9f07022..32094c4 100644
--- a/persistence_test.go
+++ b/persistence_test.go
@@ -1,6 +1,7 @@
 package jobqueue
 
 import (
+	"encoding/json"
 	"github.com/robfig/cron/v3"
 	"github.com/stretchr/testify/assert"
 	"io/ioutil"
@@ -132,3 +133,51 @@ func TestReadYAMLFile(t *testing.T) {
 		t.Errorf("Expected job with ID '1' and priority 1, got %+v", jobs)
 	}
 }
+
+func TestJobPersistenceUnmarshalJSON(t *testing.T) {
+	testCases := []struct {
+		name      string
+		jsonInput string
+		expected  *time.Time
+		wantErr   bool
+	}{
+		{
+			name:      "RFC3339 Format",
+			jsonInput: `{"pauseUntil":"2023-11-15T09:01:00Z"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15T09:01:00Z"),
+			wantErr:   false,
+		},
+		{
+			name:      "Date and Time",
+			jsonInput: `{"pauseUntil":"2023-11-15T09:01"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15T09:01"),
+			wantErr:   false,
+		},
+		{
+			name:      "Only Date",
+			jsonInput: `{"pauseUntil":"2023-11-15"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15"),
+			wantErr:   false,
+		},
+		{
+			name:      "Invalid Format",
+			jsonInput: `{"pauseUntil":"15. November 2023"}`,
+			expected:  nil,
+			wantErr:   true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var jp JobPersistence
+			err := json.Unmarshal([]byte(tc.jsonInput), &jp)
+
+			if tc.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.expected, jp.PauseUntil)
+			}
+		})
+	}
+}
diff --git a/scheduler.go b/scheduler.go
index 7bce73c..e40ee14 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -1,6 +1,7 @@
 package jobqueue
 
 import (
+	"encoding/json"
 	"fmt"
 	"github.com/robfig/cron/v3"
 	"time"
@@ -31,6 +32,39 @@ type SchedulerPersistence struct {
 	Executed bool          `yaml:"executed,omitempty" json:"executed,omitempty" gorm:"column:executed"`
 }
 
+// UnmarshalJSON implements the json.Unmarshaler interface
+func (sp *SchedulerPersistence) UnmarshalJSON(data []byte) error {
+	// Anonymous structure to avoid endless recursion
+	type Alias SchedulerPersistence
+	aux := &struct {
+		Time *string `json:"time,omitempty"`
+		*Alias
+	}{
+		Alias: (*Alias)(sp),
+	}
+
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	if aux.Time != nil {
+		var t time.Time
+		var err error
+		for _, format := range SupportedTimeFormats {
+			t, err = time.Parse(format, *aux.Time)
+			if err == nil {
+				break
+			}
+		}
+		if err != nil {
+			return err
+		}
+		sp.Time = &t
+	}
+
+	return nil
+}
+
 // IntervalScheduler is a scheduler that schedules a job at a fixed interval
 type IntervalScheduler struct {
 	Interval time.Duration
diff --git a/scheduler_test.go b/scheduler_test.go
index 167931e..0ed3861 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -1,6 +1,7 @@
 package jobqueue
 
 import (
+	"encoding/json"
 	"github.com/robfig/cron/v3"
 	"github.com/stretchr/testify/assert"
 	"sync/atomic"
@@ -292,3 +293,76 @@ func TestTimeScheduler_BasicFunctionality(t *testing.T) {
 	}
 
 }
+
+// TestSchedulerPersistenceUnmarshalJSON testet die Unmarshalling-Funktion mit verschiedenen Zeitformaten.
+func TestSchedulerPersistenceUnmarshalJSON(t *testing.T) {
+	testCases := []struct {
+		name      string
+		jsonInput string
+		expected  *time.Time
+		wantErr   bool
+	}{
+		{
+			name:      "RFC3339 Format",
+			jsonInput: `{"type":"Time","time":"2023-11-15T09:01:00Z"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15T09:01:00Z"),
+			wantErr:   false,
+		},
+		{
+			name:      "Date and Time",
+			jsonInput: `{"type":"Time","time":"2023-11-15T09:01"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15T09:01"),
+			wantErr:   false,
+		},
+		{
+			name:      "Date and Time with Space",
+			jsonInput: `{"type":"Time","time":"2023-11-15 09:01"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15T09:01"),
+			wantErr:   false,
+		},
+		{
+			name:      "Date and Time with Space and Seconds",
+			jsonInput: `{"type":"Time","time":"2023-11-15 09:01:02"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15T09:01:02"),
+			wantErr:   false,
+		},
+		{
+			name:      "Only Date",
+			jsonInput: `{"type":"Time","time":"2023-11-15"}`,
+			expected:  parseTimeForTesting(t, "2023-11-15"),
+			wantErr:   false,
+		},
+		{
+			name:      "Invalid Format",
+			jsonInput: `{"type":"Time","time":"15. November 2023"}`,
+			expected:  nil,
+			wantErr:   true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var sp SchedulerPersistence
+			err := json.Unmarshal([]byte(tc.jsonInput), &sp)
+
+			if tc.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.expected, sp.Time)
+			}
+		})
+	}
+}
+
+func parseTimeForTesting(t *testing.T, value string) *time.Time {
+
+	for _, format := range SupportedTimeFormats {
+		parsedTime, err := time.Parse(format, value)
+		if err == nil {
+			return &parsedTime
+		}
+	}
+	t.Fatalf("Failed to parse time '%s' in any known format", value)
+	return nil
+}
diff --git a/time-formats.go b/time-formats.go
new file mode 100644
index 0000000..cf80a68
--- /dev/null
+++ b/time-formats.go
@@ -0,0 +1,14 @@
+package jobqueue
+
+import "time"
+
+var SupportedTimeFormats = []string{
+	time.RFC3339,
+	"2006-01-02T15:04:05",
+	"2006-01-02T15:04",
+	"2006-01-02T15",
+	"2006-01-02 15:04:05",
+	"2006-01-02 15:04",
+	"2006-01-02 15",
+	"2006-01-02",
+}
-- 
GitLab