Files
Maven/cmd/mavcaldav/main_test.go
T
kami 1c94df76b7 mavcaldav: reconcile the render collection on startup
Withdrawal read published, which is in-memory, so the second loop only
ever withdrew reminders this process had published. Fire a reminder,
restart mavcaldav, and its event stayed in the collection forever with
nothing left to revisit it. "Losing it costs nothing, the next tick
rebuilds it" holds for events that should be there and not for the ones
that should not.

The first tick now PROPFINDs the collection and reconciles what it finds
against what is pending. Only hrefs carrying ReminderUIDPrefix are read
back, so the pass can never propose deleting a file maven did not create.
A failed read is retried on the next tick rather than skipped for the
life of the process.

Two smaller things from the same review. checkRenderTarget takes the
whole read set, so a second calendar to read cannot quietly fall outside
the guarantee the package comment makes. writeIfChanged loses its
confidence parameter, which every caller passed 1.0 and nothing read.
Found in review of #56.
2026-08-01 14:11:07 +04:00

215 lines
6.0 KiB
Go

package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/kami/maven/internal/ipc"
)
type fakeCore struct {
ipc.UnimplementedCoreAPI
facts map[string]ipc.Fact // composite key "key|source" → Fact
writeLog []ipc.WriteFactReq
writeErr error
readErr error
}
func (f *fakeCore) LatestFactBySource(_ context.Context, key, source string) (ipc.Fact, error) {
if f.readErr != nil {
return ipc.Fact{}, f.readErr
}
if f.facts == nil {
return ipc.Fact{}, ipc.ErrNoFact
}
fk := key + "|" + source
fact, ok := f.facts[fk]
if !ok {
return ipc.Fact{}, ipc.ErrNoFact
}
return fact, nil
}
func (f *fakeCore) WriteFact(_ context.Context, req ipc.WriteFactReq) (int64, error) {
if f.writeErr != nil {
return 0, f.writeErr
}
if f.facts == nil {
f.facts = make(map[string]ipc.Fact)
}
fk := req.Key + "|" + req.Source
f.facts[fk] = ipc.Fact{
Key: req.Key,
Value: req.Value,
Source: req.Source,
}
f.writeLog = append(f.writeLog, req)
return int64(len(f.writeLog)), nil
}
// ---------------------------------------------------------------------------
// Core logic tests
// ---------------------------------------------------------------------------
func TestWriteIfChanged(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC)
t.Run("no previous fact writes", func(t *testing.T) {
fc := &fakeCore{}
p := &poller{core: fc}
err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "hello", now)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fc.writeLog) != 1 {
t.Fatalf("expected 1 write, got %d", len(fc.writeLog))
}
if fc.writeLog[0].Value != "hello" {
t.Errorf("value = %q, want %q", fc.writeLog[0].Value, "hello")
}
if fc.writeLog[0].Key != "test_key" {
t.Errorf("key = %q, want %q", fc.writeLog[0].Key, "test_key")
}
if fc.writeLog[0].Source != "poll:caldav" {
t.Errorf("source = %q, want %q", fc.writeLog[0].Source, "poll:caldav")
}
if fc.writeLog[0].Kind != "env" {
t.Errorf("kind = %q, want %q", fc.writeLog[0].Kind, "env")
}
})
t.Run("same value skips write", func(t *testing.T) {
fc := &fakeCore{
facts: map[string]ipc.Fact{
"test_key|poll:caldav": {Value: "hello"},
},
}
p := &poller{core: fc}
err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "hello", now)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fc.writeLog) != 0 {
t.Errorf("expected 0 writes, got %d", len(fc.writeLog))
}
})
t.Run("different value writes", func(t *testing.T) {
fc := &fakeCore{
facts: map[string]ipc.Fact{
"test_key|poll:caldav": {Value: "old"},
},
}
p := &poller{core: fc}
err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "new", now)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fc.writeLog) != 1 {
t.Fatalf("expected 1 write, got %d", len(fc.writeLog))
}
if fc.writeLog[0].Value != "new" {
t.Errorf("value = %q, want %q", fc.writeLog[0].Value, "new")
}
})
t.Run("read error other than ErrNoFact returns error", func(t *testing.T) {
fc := &fakeCore{readErr: fmt.Errorf("connection refused")}
p := &poller{core: fc}
err := p.writeIfChanged(ctx, "fail_key", "poll:caldav", "x", now)
if err == nil {
t.Fatal("expected error, got nil")
}
})
t.Run("write error returns error", func(t *testing.T) {
fc := &fakeCore{
facts: map[string]ipc.Fact{},
writeErr: fmt.Errorf("disk full"),
}
p := &poller{core: fc}
err := p.writeIfChanged(ctx, "test_key", "poll:caldav", "hello", now)
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
// ---------------------------------------------------------------------------
// Poll cycle test
// ---------------------------------------------------------------------------
func TestPollOnce(t *testing.T) {
now := time.Now().UTC()
start := now.Add(-2 * time.Hour).Truncate(time.Second)
end := now.Add(2 * time.Hour).Truncate(time.Second)
ical := fmt.Sprintf("BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:%sT%sZ\nDTEND:%sT%sZ\nSUMMARY:Current meeting\nEND:VEVENT\nEND:VCALENDAR",
start.Format("20060102"), start.Format("150405"),
end.Format("20060102"), end.Format("150405"))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(ical))
}))
defer srv.Close()
fc := &fakeCore{}
p := &poller{
core: fc,
http: srv.Client(),
url: srv.URL,
}
p.pollOnce(context.Background())
if len(fc.writeLog) != 2 {
t.Fatalf("expected 2 writes (calendar_busy + calendar_event), got %d", len(fc.writeLog))
}
// First write: calendar_busy = "true"
busyReq := fc.writeLog[0]
if busyReq.Key != "calendar_busy" {
t.Errorf("first write key = %q, want %q", busyReq.Key, "calendar_busy")
}
if busyReq.Value != "true" {
t.Errorf("calendar_busy value = %q, want %q", busyReq.Value, "true")
}
if busyReq.Source != "poll:caldav" {
t.Errorf("source = %q, want %q", busyReq.Source, "poll:caldav")
}
if busyReq.Kind != "env" {
t.Errorf("kind = %q, want %q", busyReq.Kind, "env")
}
if busyReq.Ts.IsZero() {
t.Errorf("calendar_busy ts is zero")
}
// Second write: calendar_event_<date>_<summary> = "<summary> @ HH:MM-HH:MM".
// The iCal states the event in UTC and the fact is stamped on the owner's
// clock, so the expected key date and times are the local reading of it.
eventReq := fc.writeLog[1]
expectedKey := "calendar_event_" + start.Local().Format("20060102") + "_Current-meeting"
if eventReq.Key != expectedKey {
t.Errorf("event key = %q, want %q", eventReq.Key, expectedKey)
}
expectedVal := "Current meeting @ " + start.Local().Format("15:04") + "-" + end.Local().Format("15:04")
if eventReq.Value != expectedVal {
t.Errorf("event value = %q, want %q", eventReq.Value, expectedVal)
}
if eventReq.Source != "poll:caldav" {
t.Errorf("event source = %q, want %q", eventReq.Source, "poll:caldav")
}
if eventReq.Kind != "env" {
t.Errorf("event kind = %q, want %q", eventReq.Kind, "env")
}
if !eventReq.Ts.Equal(start) {
t.Errorf("event ts = %v, want %v", eventReq.Ts, start)
}
}