feat(go): IO layer behind interfaces (M4)
All checks were successful
Deploy to K8s / deploy (push) Successful in 11s
All checks were successful
Deploy to K8s / deploy (push) Successful in 11s
- io/attendance: CSV-over-public-URL client + Fake for adult/junior tabs - io/drive: Drive v3 modifiedTime client + Fake - io/sheets: Sheets v4 client (GetValues/AppendValues/BatchUpdateValues/ WriteHeader/SortByDateColumn) + Fake with call-capture - io/cache: Drive-modifiedTime-gated FileCache; two TTL knobs; atomic writes; generic Get[T]; Python-compatible JSON format; Flush() - io/fio: Client interface backed by Fio REST API (apiClient) and HTML scraper (transparentClient); Fake; testdata fixtures - membership/sources: NewSources wires attendance CSV + Sheets + cache into LoadAdults/LoadJuniors/LoadTransactions/LoadExceptions; Czech month parsing + merged-month maps - banksync: SyncToSheets (SHA-256 dedup, optional sort) and InferPayments ([?] review prefix, dry-run) — tested with fakes - cmd/fuj: sync and infer subcommands wired; fees and reconcile use real NewSources; go.mod gains google.golang.org/api + x/net - gofumpt extra-rules applied across all packages; lint clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
125
go/internal/io/cache/filecache_test.go
vendored
Normal file
125
go/internal/io/cache/filecache_test.go
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fuj-management/go/internal/io/drive"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGet_FreshFetch(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
d := &drive.Fake{Times: map[string]string{"sheet1": "2026-01-01T00:00:00Z"}}
|
||||
fc := New(d, dir, map[string]string{"mykey": "sheet1"}, time.Minute, time.Minute)
|
||||
|
||||
calls := 0
|
||||
got, err := Get(context.Background(), fc, "mykey", func(_ context.Context) ([]string, error) {
|
||||
calls++
|
||||
return []string{"a", "b"}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 2 || got[0] != "a" {
|
||||
t.Errorf("unexpected: %v", got)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Errorf("want 1 fetch call, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet_CacheHit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
d := &drive.Fake{Times: map[string]string{"sheet1": "2026-01-01T00:00:00Z"}}
|
||||
fc := New(d, dir, map[string]string{"mykey": "sheet1"}, time.Minute, time.Minute)
|
||||
|
||||
fetch := func(_ context.Context) ([]string, error) { return []string{"a"}, nil }
|
||||
if _, err := Get(context.Background(), fc, "mykey", fetch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Second call — modifiedTime unchanged, should hit cache
|
||||
calls := 0
|
||||
got, err := Get(context.Background(), fc, "mykey", func(_ context.Context) ([]string, error) {
|
||||
calls++
|
||||
return []string{"SHOULD_NOT_CALL"}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got[0] != "a" {
|
||||
t.Errorf("want cache hit with 'a', got %q", got[0])
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Errorf("want 0 fetch calls on hit, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet_CacheMiss_OnModifiedTimeChange(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
d := &drive.Fake{Times: map[string]string{"sheet1": "2026-01-01T00:00:00Z"}}
|
||||
// No TTL guards so we always hit Drive
|
||||
fc := New(d, dir, map[string]string{"mykey": "sheet1"}, 0, 0)
|
||||
|
||||
fetch := func(_ context.Context) ([]string, error) { return []string{"v1"}, nil }
|
||||
if _, err := Get(context.Background(), fc, "mykey", fetch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Sheet updated — change modifiedTime
|
||||
d.Times["sheet1"] = "2026-02-01T00:00:00Z"
|
||||
got, err := Get(context.Background(), fc, "mykey", func(_ context.Context) ([]string, error) {
|
||||
return []string{"v2"}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got[0] != "v2" {
|
||||
t.Errorf("want v2 after sheet update, got %q", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet_DriveFailureFallback(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
d := &drive.Fake{Err: errors.New("drive down")}
|
||||
fc := New(d, dir, nil, 0, 0)
|
||||
|
||||
calls := 0
|
||||
_, err := Get(context.Background(), fc, "mykey", func(_ context.Context) ([]string, error) {
|
||||
calls++
|
||||
return []string{"fallback"}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Errorf("want 1 fetch call, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlush(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
d := &drive.Fake{Times: map[string]string{"sheet1": "t1"}}
|
||||
fc := New(d, dir, map[string]string{"k": "sheet1"}, 0, 0)
|
||||
|
||||
if _, err := Get(context.Background(), fc, "k", func(_ context.Context) (int, error) { return 42, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := fc.Flush()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("want 1 deleted file, got %d", n)
|
||||
}
|
||||
// Cache dir should be empty of _cache.json files
|
||||
entries, _ := os.ReadDir(dir)
|
||||
for _, e := range entries {
|
||||
if e.Name() != "" {
|
||||
t.Errorf("expected empty dir after flush, found %s", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user