syncwindow_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package externalsecret
  14. import (
  15. "testing"
  16. "time"
  17. robfigcron "github.com/robfig/cron/v3"
  18. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  19. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  20. )
  21. func mustParseCron(expr string) robfigcron.Schedule {
  22. sched, err := cronParser.Parse(expr)
  23. if err != nil {
  24. panic(expr + ": " + err.Error())
  25. }
  26. return sched
  27. }
  28. // TestIsWithinSyncWindow exercises the half-open / closed window boundaries
  29. // using a daily schedule that fires at 22:00 UTC with a 2-hour duration
  30. // (window open 22:00-00:00 UTC).
  31. func TestIsWithinSyncWindow(t *testing.T) {
  32. sched := mustParseCron("0 22 * * *")
  33. dur := 2 * time.Hour
  34. // Reference firing: 2026-06-01 22:00 UTC (Monday).
  35. open := time.Date(2026, 6, 1, 22, 0, 0, 0, time.UTC)
  36. closeTime := open.Add(dur) // 2026-06-02 00:00 UTC
  37. tests := []struct {
  38. name string
  39. at time.Time
  40. want bool
  41. }{
  42. {
  43. name: "at window open (inclusive)",
  44. at: open,
  45. want: true,
  46. },
  47. {
  48. name: "inside window",
  49. at: open.Add(30 * time.Minute),
  50. want: true,
  51. },
  52. {
  53. name: "at window close (inclusive)",
  54. at: closeTime,
  55. want: true,
  56. },
  57. {
  58. name: "one second past window close",
  59. at: closeTime.Add(time.Second),
  60. want: false,
  61. },
  62. {
  63. name: "one hour before window opens",
  64. at: open.Add(-1 * time.Hour),
  65. want: false,
  66. },
  67. {
  68. name: "between two consecutive occurrences",
  69. // 03:00 UTC next day -- well past the 22:00+2h window.
  70. at: open.Add(5 * time.Hour),
  71. want: false,
  72. },
  73. {
  74. name: "inside the second occurrence (next day)",
  75. at: open.Add(24*time.Hour + 30*time.Minute),
  76. want: true,
  77. },
  78. }
  79. for _, tt := range tests {
  80. t.Run(tt.name, func(t *testing.T) {
  81. got := isWithinSyncWindow(sched, dur, tt.at)
  82. if got != tt.want {
  83. t.Errorf("at=%v: got %v, want %v", tt.at.Format(time.RFC3339), got, tt.want)
  84. }
  85. })
  86. }
  87. }
  88. // TestIsPeriodicRefreshAllowedByWindows covers the nil / empty / allow / deny /
  89. // invalid-schedule paths and the unknown-kind default.
  90. func TestIsPeriodicRefreshAllowedByWindows(t *testing.T) {
  91. // inWindow: 2026-06-01 23:00 UTC -- inside the 22:00+2h window.
  92. inWindow := time.Date(2026, 6, 1, 23, 0, 0, 0, time.UTC)
  93. // outWindow: 2026-06-01 20:00 UTC -- outside any window.
  94. outWindow := time.Date(2026, 6, 1, 20, 0, 0, 0, time.UTC)
  95. // Shared window definitions.
  96. validEntry := esv1.ExternalSecretSyncWindowEntry{
  97. Schedule: "0 22 * * *",
  98. Duration: metav1.Duration{Duration: 2 * time.Hour},
  99. }
  100. invalidEntry := esv1.ExternalSecretSyncWindowEntry{
  101. Schedule: "not-a-cron",
  102. Duration: metav1.Duration{Duration: 2 * time.Hour},
  103. }
  104. tests := []struct {
  105. name string
  106. sw *esv1.ExternalSecretSyncWindows
  107. at time.Time
  108. want bool
  109. }{
  110. {
  111. name: "nil SyncWindows always permits",
  112. sw: nil,
  113. at: inWindow,
  114. want: true,
  115. },
  116. {
  117. name: "empty Windows list always permits",
  118. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow},
  119. at: inWindow,
  120. want: true,
  121. },
  122. // allow kind
  123. {
  124. name: "allow: at inside window -- permit",
  125. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
  126. at: inWindow,
  127. want: true,
  128. },
  129. {
  130. name: "allow: at outside window -- block",
  131. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
  132. at: outWindow,
  133. want: false,
  134. },
  135. // deny kind
  136. {
  137. name: "deny: at inside window -- block",
  138. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowDeny, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
  139. at: inWindow,
  140. want: false,
  141. },
  142. {
  143. name: "deny: at outside window -- permit",
  144. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowDeny, Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry}},
  145. at: outWindow,
  146. want: true,
  147. },
  148. // invalid schedule handling
  149. {
  150. name: "allow: only invalid schedule -- block (no window is active)",
  151. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowAllow, Windows: []esv1.ExternalSecretSyncWindowEntry{invalidEntry}},
  152. at: inWindow,
  153. want: false,
  154. },
  155. {
  156. name: "deny: only invalid schedule -- permit (no window is active)",
  157. sw: &esv1.ExternalSecretSyncWindows{Kind: esv1.SyncWindowDeny, Windows: []esv1.ExternalSecretSyncWindowEntry{invalidEntry}},
  158. at: inWindow,
  159. want: true,
  160. },
  161. {
  162. name: "allow: one invalid + one valid active window -- permit",
  163. sw: &esv1.ExternalSecretSyncWindows{
  164. Kind: esv1.SyncWindowAllow,
  165. Windows: []esv1.ExternalSecretSyncWindowEntry{invalidEntry, validEntry},
  166. },
  167. at: inWindow,
  168. want: true,
  169. },
  170. // unknown kind falls back to always-permit
  171. {
  172. name: "unknown kind -- always permit",
  173. sw: &esv1.ExternalSecretSyncWindows{
  174. Kind: "unknown",
  175. Windows: []esv1.ExternalSecretSyncWindowEntry{validEntry},
  176. },
  177. at: inWindow,
  178. want: true,
  179. },
  180. }
  181. for _, tt := range tests {
  182. t.Run(tt.name, func(t *testing.T) {
  183. es := &esv1.ExternalSecret{
  184. Spec: esv1.ExternalSecretSpec{SyncWindows: tt.sw},
  185. }
  186. got := isPeriodicRefreshAllowedByWindows(es, tt.at)
  187. if got != tt.want {
  188. t.Errorf("got %v, want %v", got, tt.want)
  189. }
  190. })
  191. }
  192. }