8e08c73f60
Full-stack Go monolith with embedded React frontend for orchestrating rsync-over-SSH file synchronization with Wake-on-LAN support. Features: - JWT auth (HS256) with bcrypt password hashing - CRUD for machines (with WoL config) and sync_pairs - Ed25519 SSH key generation and known_hosts management - WoL magic packet sender + TCP-connect waiter with backoff - Sync engine: rsync subprocess, per-pair job queue, progress parsing - Homebrew cron parser for scheduled syncs - SSE stream for live job status (queued/waking_up/running/success/failed) - React+TS+Vite+Tailwind SPA embedded via embed.FS - Debian packaging with systemd unit, postinst/prerm/postrm Tech stack: - Go 1.22+ (CGO_ENABLED=0, pure SQLite via modernc.org/sqlite) - chi router for HTTP API - TypeScript + React 18 + Tailwind CSS frontend - Cross-compiled to Linux amd64 for Proxmox LXC deployment Tests: wol (MAC parsing, magic packet), syncengine/queue, scheduler/cron
98 lines
1.9 KiB
Go
98 lines
1.9 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseCron(t *testing.T) {
|
|
tests := []struct {
|
|
expr string
|
|
wantErr bool
|
|
}{
|
|
{"* * * * *", false},
|
|
{"0 * * * *", false},
|
|
{"*/5 * * * *", false},
|
|
{"0,30 * * * *", false},
|
|
{"0-30 * * * *", false},
|
|
{"*/15 9-17 * * *", false},
|
|
{"0 0 1 * *", false},
|
|
{"0 0 * * 0", false},
|
|
{"0 0 1,15 * *", false},
|
|
{"invalid", true},
|
|
{"* * * *", true},
|
|
{"60 * * * *", true},
|
|
{"* 24 * * *", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
_, err := ParseCron(tt.expr)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseCron(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCronMatches(t *testing.T) {
|
|
expr, err := ParseCron("*/5 * * * *")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
minute int
|
|
want bool
|
|
}{
|
|
{0, true},
|
|
{5, true},
|
|
{10, true},
|
|
{15, true},
|
|
{1, false},
|
|
{2, false},
|
|
{7, false},
|
|
}
|
|
|
|
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
for _, tt := range tests {
|
|
tm := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), tt.minute, 0, 0, time.UTC)
|
|
if got := expr.Matches(tm); got != tt.want {
|
|
t.Errorf("Matches(minute=%d) = %v, want %v", tt.minute, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCronMatchesSpecific(t *testing.T) {
|
|
expr, err := ParseCron("30 9 15 * *")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
matches := time.Date(2024, 6, 15, 9, 30, 0, 0, time.UTC)
|
|
if !expr.Matches(matches) {
|
|
t.Error("should match 9:30 on 15th of month")
|
|
}
|
|
|
|
notMatch := time.Date(2024, 6, 16, 9, 30, 0, 0, time.UTC)
|
|
if expr.Matches(notMatch) {
|
|
t.Error("should not match 9:30 on 16th of month")
|
|
}
|
|
}
|
|
|
|
func TestNextRun(t *testing.T) {
|
|
expr, err := ParseCron("*/5 * * * *")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
from := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
next := NextRun(expr, from)
|
|
|
|
if next.Minute() != 5 || next.Hour() != 12 {
|
|
t.Errorf("NextRun = %v, want 12:05", next)
|
|
}
|
|
|
|
if !next.After(from) {
|
|
t.Error("NextRun should be after from time")
|
|
}
|
|
}
|