feat: complete SyncServer implementation
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
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user