|
|
@@ -0,0 +1,208 @@
|
|
|
+/*
|
|
|
+Copyright © The ESO Authors
|
|
|
+
|
|
|
+Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+you may not use this file except in compliance with the License.
|
|
|
+You may obtain a copy of the License at
|
|
|
+
|
|
|
+ https://www.apache.org/licenses/LICENSE-2.0
|
|
|
+
|
|
|
+Unless required by applicable law or agreed to in writing, software
|
|
|
+distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+See the License for the specific language governing permissions and
|
|
|
+limitations under the License.
|
|
|
+*/
|
|
|
+
|
|
|
+package externalsecret
|
|
|
+
|
|
|
+import (
|
|
|
+ "testing"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ robfigcron "github.com/robfig/cron/v3"
|
|
|
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
+
|
|
|
+ esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
|
|
+)
|
|
|
+
|
|
|
+func mustParseCron(expr string) robfigcron.Schedule {
|
|
|
+ sched, err := cronParser.Parse(expr)
|
|
|
+ if err != nil {
|
|
|
+ panic(expr + ": " + err.Error())
|
|
|
+ }
|
|
|
+ return sched
|
|
|
+}
|
|
|
+
|
|
|
+// TestIsWithinSyncWindow exercises the half-open / closed window boundaries
|
|
|
+// using a daily schedule that fires at 22:00 UTC with a 2-hour duration
|
|
|
+// (window open 22:00-00:00 UTC).
|
|
|
+func TestIsWithinSyncWindow(t *testing.T) {
|
|
|
+ sched := mustParseCron("0 22 * * *")
|
|
|
+ dur := 2 * time.Hour
|
|
|
+
|
|
|
+ // Reference firing: 2026-06-01 22:00 UTC (Monday).
|
|
|
+ open := time.Date(2026, 6, 1, 22, 0, 0, 0, time.UTC)
|
|
|
+ closeTime := open.Add(dur) // 2026-06-02 00:00 UTC
|
|
|
+
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ at time.Time
|
|
|
+ want bool
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "at window open (inclusive)",
|
|
|
+ at: open,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "inside window",
|
|
|
+ at: open.Add(30 * time.Minute),
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "at window close (inclusive)",
|
|
|
+ at: closeTime,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "one second past window close",
|
|
|
+ at: closeTime.Add(time.Second),
|
|
|
+ want: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "one hour before window opens",
|
|
|
+ at: open.Add(-1 * time.Hour),
|
|
|
+ want: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "between two consecutive occurrences",
|
|
|
+ // 03:00 UTC next day -- well past the 22:00+2h window.
|
|
|
+ at: open.Add(5 * time.Hour),
|
|
|
+ want: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "inside the second occurrence (next day)",
|
|
|
+ at: open.Add(24*time.Hour + 30*time.Minute),
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ got := isWithinSyncWindow(sched, dur, tt.at)
|
|
|
+ if got != tt.want {
|
|
|
+ t.Errorf("at=%v: got %v, want %v", tt.at.Format(time.RFC3339), got, tt.want)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestIsPeriodicRefreshAllowedByWindows covers the nil / empty / allow / deny /
|
|
|
+// invalid-schedule paths and the unknown-kind default.
|
|
|
+func TestIsPeriodicRefreshAllowedByWindows(t *testing.T) {
|
|
|
+ // inWindow: 2026-06-01 23:00 UTC -- inside the 22:00+2h window.
|
|
|
+ inWindow := time.Date(2026, 6, 1, 23, 0, 0, 0, time.UTC)
|
|
|
+ // outWindow: 2026-06-01 20:00 UTC -- outside any window.
|
|
|
+ outWindow := time.Date(2026, 6, 1, 20, 0, 0, 0, time.UTC)
|
|
|
+
|
|
|
+ // Shared window definitions.
|
|
|
+ validEntry := esv1.ExternalSecretSyncWindowEntry{
|
|
|
+ Schedule: "0 22 * * *",
|
|
|
+ Duration: metav1.Duration{Duration: 2 * time.Hour},
|
|
|
+ }
|
|
|
+ invalidEntry := esv1.ExternalSecretSyncWindowEntry{
|
|
|
+ Schedule: "not-a-cron",
|
|
|
+ Duration: metav1.Duration{Duration: 2 * time.Hour},
|
|
|
+ }
|
|
|
+
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ sw *esv1.ExternalSecretSyncWindows
|
|
|
+ at time.Time
|
|
|
+ want bool
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "nil SyncWindows always permits",
|
|
|
+ sw: nil,
|
|
|
+ at: inWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "empty Windows list always permits",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow},
|
|
|
+ at: inWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ // allow kind
|
|
|
+ {
|
|
|
+ name: "allow: at inside window -- permit",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
|
|
|
+ at: inWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "allow: at outside window -- block",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
|
|
|
+ at: outWindow,
|
|
|
+ want: false,
|
|
|
+ },
|
|
|
+ // deny kind
|
|
|
+ {
|
|
|
+ name: "deny: at inside window -- block",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowDeny, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
|
|
|
+ at: inWindow,
|
|
|
+ want: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "deny: at outside window -- permit",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowDeny, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
|
|
|
+ at: outWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ // invalid schedule handling
|
|
|
+ {
|
|
|
+ name: "allow: only invalid schedule -- block (no window is active)",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow, Windows: []esv1.ExternalSecretSyncWindowEntry{invalidEntry}},
|
|
|
+ at: inWindow,
|
|
|
+ want: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "deny: only invalid schedule -- permit (no window is active)",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowDeny, Windows: []esv1.ExternalSecretSyncWindowEntry{invalidEntry}},
|
|
|
+ at: inWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "allow: one invalid + one valid active window -- permit",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{
|
|
|
+ Kind: esv1.SyncWindowAllow,
|
|
|
+ Windows: []esv1.ExternalSecretSyncWindowEntry{invalidEntry, validEntry},
|
|
|
+ },
|
|
|
+ at: inWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ // unknown kind falls back to always-permit
|
|
|
+ {
|
|
|
+ name: "unknown kind -- always permit",
|
|
|
+ sw: &esv1.ExternalSecretSyncWindows{
|
|
|
+ Kind: "unknown",
|
|
|
+ Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry},
|
|
|
+ },
|
|
|
+ at: inWindow,
|
|
|
+ want: true,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ es := &esv1.ExternalSecret{
|
|
|
+ Spec: esv1.ExternalSecretSpec{SyncWindows: tt.sw},
|
|
|
+ }
|
|
|
+ got := isPeriodicRefreshAllowedByWindows(es, tt.at)
|
|
|
+ if got != tt.want {
|
|
|
+ t.Errorf("got %v, want %v", got, tt.want)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|