Files
darroyo 8e08c73f60 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
2026-07-07 15:03:22 -04:00

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")
}
}