Files
bvandeusen 46c8edfa82 feat(playlists): gocron-based per-user scheduler
Builds the per-user daily-at-03:00-local scheduler for #392 Half B.
Uses github.com/go-co-op/gocron/v2 with WithLocation(userTZ) for
each user's job. Hourly reconciliation pass keeps the in-memory
job set in sync with the active-users query.

Start sequence: clear stale in_flight rows; register a daily job
for every active user at their stored timezone; fire a one-shot
runOnce to catch up missed schedules during downtime; start gocron.

Stop drains and shuts down the gocron loop.

Refresh(ctx, userID) removes the user's existing job (if any) and
registers a fresh one at their current timezone — wired into the
PUT /api/me/timezone handler and new-user registration in the next
commit.

Server struct gains PlaylistScheduler; main.go constructs it after
the eventbus and threads it through to api.Mount. The existing
StartSystemPlaylistCron call stays for one more commit so we don't
have a window where no scheduler is running; Task 3 deletes it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:57:13 -04:00

52 lines
1.4 KiB
Go

package playlists
import (
"testing"
)
func TestValidateTimezone_Valid(t *testing.T) {
cases := []string{"UTC", "America/New_York", "Europe/London", "Asia/Tokyo"}
for _, tz := range cases {
t.Run(tz, func(t *testing.T) {
loc, err := validateTimezone(tz)
if err != nil {
t.Fatalf("validateTimezone(%q): %v", tz, err)
}
if loc == nil {
t.Fatalf("validateTimezone(%q): nil location", tz)
}
})
}
}
func TestValidateTimezone_Invalid(t *testing.T) {
cases := []string{"", "Not/A/Zone", "GMT+5", "EST5EDT-not-iana"}
for _, tz := range cases {
t.Run(tz, func(t *testing.T) {
_, err := validateTimezone(tz)
if err == nil {
t.Fatalf("validateTimezone(%q): expected error, got nil", tz)
}
})
}
}
func TestValidateTimezoneOrUTC_FallbackToUTC(t *testing.T) {
// validateTimezoneOrUTC returns UTC for any unparseable input.
// Used by the scheduler when reading stored timezones — corrupted
// DB values should not crash; they should fall back to UTC.
loc := validateTimezoneOrUTC("Not/A/Zone")
if loc.String() != "UTC" {
t.Errorf("expected UTC fallback for invalid input, got %s", loc)
}
// Valid input returns the named location, not UTC.
loc = validateTimezoneOrUTC("America/New_York")
if loc.String() == "UTC" {
t.Errorf("valid input incorrectly fell back to UTC")
}
if loc.String() != "America/New_York" {
t.Errorf("expected America/New_York, got %s", loc)
}
}