fix: read GOOGLE_SHEETS_ID and default range to current-year tab

LoadConfig read SPREADSHEET_ID, but the .env defines GOOGLE_SHEETS_ID,
so config loading failed. The default SHEET_RANGE was also a hardcoded
'Dyżury!A1:F100', but the spreadsheet tabs are named per year (e.g.
'2026'), producing a 400 'Unable to parse range'. Read GOOGLE_SHEETS_ID
and derive the default range from the current year.

Add a RUN_LIVE-gated integration test that drives the full
LoadConfig -> Service -> Syncer.Sync -> Store path against the live
Sheet, and gitignore the generated rota.json cache.
This commit is contained in:
Kamil 'Kamaś' Bruchal 2026-06-17 21:16:25 +02:00
parent 572701106c
commit cb0cc679c8
4 changed files with 67 additions and 10 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.env .env
rota.json

50
live_test.go Normal file
View File

@ -0,0 +1,50 @@
package dyzurbot
import (
"context"
"os"
"path/filepath"
"testing"
)
// TestLiveFetch exercises the full production path — LoadConfig reads the
// environment/.env, NewService wires the Store + Syncer, and Syncer.Sync hits
// the live Google Sheets API, parses the result, and persists it to the cache
// file via Store.Replace. The resulting JSON is left on disk (CachePath,
// default rota.json) for inspection.
//
// It is skipped unless RUN_LIVE=1, so the normal `go test ./...` run stays
// offline and fast.
//
// RUN_LIVE=1 go test -run TestLiveFetch -v .
//
// Requires GOOGLE_API_KEY and GOOGLE_SHEETS_ID in the environment/.env.
func TestLiveFetch(t *testing.T) {
if os.Getenv("RUN_LIVE") != "1" {
t.Skip("set RUN_LIVE=1 to run the live Google Sheets fetch")
}
cfg, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
t.Logf("config: spreadsheet=%s range=%q cache=%s",
cfg.SpreadsheetID, cfg.SheetRange, cfg.CachePath)
svc := NewService(cfg)
if err := svc.Syncer.Sync(context.Background()); err != nil {
t.Fatalf("sync: %v", err)
}
rota, lastSynced := svc.Store.Snapshot()
t.Logf("synced %d weeks, %d people at %s",
len(rota.Weeks), len(rota.People), lastSynced.Format("2006-01-02 15:04:05"))
// Confirm the JSON actually landed on disk and show where.
written, err := os.ReadFile(cfg.CachePath)
if err != nil {
t.Fatalf("read cache %q: %v", cfg.CachePath, err)
}
abs, _ := filepath.Abs(cfg.CachePath)
t.Logf("wrote cache JSON (%d bytes) to %s", len(written), abs)
}

View File

@ -22,13 +22,17 @@ type Config struct {
func LoadConfig() (Config, error) { func LoadConfig() (Config, error) {
_ = godotenv.Load() _ = godotenv.Load()
// Default range targets the current year's tab (sheets are named per year,
// e.g. "2026"), matching the spreadsheet layout.
defaultRange := fmt.Sprintf("%d!A1:F100", time.Now().Year())
cfg := Config{ cfg := Config{
SpreadsheetID: os.Getenv("SPREADSHEET_ID"), SpreadsheetID: os.Getenv("GOOGLE_SHEETS_ID"),
SheetRange: getenvDefault("SHEET_RANGE", "Dyżury!A1:F100"), SheetRange: getenvDefault("SHEET_RANGE", defaultRange),
CachePath: getenvDefault("ROTA_CACHE_PATH", "rota.json"), CachePath: getenvDefault("ROTA_CACHE_PATH", "rota.json"),
} }
if cfg.SpreadsheetID == "" { if cfg.SpreadsheetID == "" {
return Config{}, fmt.Errorf("missing SPREADSHEET_ID in environment/.env") return Config{}, fmt.Errorf("missing GOOGLE_SHEETS_ID in environment/.env")
} }
raw := getenvDefault("SYNC_INTERVAL", "30m") raw := getenvDefault("SYNC_INTERVAL", "30m")

View File

@ -1,12 +1,13 @@
package dyzurbot package dyzurbot
import ( import (
"fmt"
"testing" "testing"
"time" "time"
) )
func TestLoadConfigDefaults(t *testing.T) { func TestLoadConfigDefaults(t *testing.T) {
t.Setenv("SPREADSHEET_ID", "sheet-123") t.Setenv("GOOGLE_SHEETS_ID", "sheet-123")
t.Setenv("SHEET_RANGE", "") t.Setenv("SHEET_RANGE", "")
t.Setenv("ROTA_CACHE_PATH", "") t.Setenv("ROTA_CACHE_PATH", "")
t.Setenv("SYNC_INTERVAL", "") t.Setenv("SYNC_INTERVAL", "")
@ -18,8 +19,9 @@ func TestLoadConfigDefaults(t *testing.T) {
if cfg.SpreadsheetID != "sheet-123" { if cfg.SpreadsheetID != "sheet-123" {
t.Errorf("SpreadsheetID: got %q", cfg.SpreadsheetID) t.Errorf("SpreadsheetID: got %q", cfg.SpreadsheetID)
} }
if cfg.SheetRange != "Dyżury!A1:F100" { wantRange := fmt.Sprintf("%d!A1:F100", time.Now().Year())
t.Errorf("SheetRange default: got %q", cfg.SheetRange) if cfg.SheetRange != wantRange {
t.Errorf("SheetRange default: got %q, want %q", cfg.SheetRange, wantRange)
} }
if cfg.CachePath != "rota.json" { if cfg.CachePath != "rota.json" {
t.Errorf("CachePath default: got %q", cfg.CachePath) t.Errorf("CachePath default: got %q", cfg.CachePath)
@ -30,14 +32,14 @@ func TestLoadConfigDefaults(t *testing.T) {
} }
func TestLoadConfigRequiresSpreadsheetID(t *testing.T) { func TestLoadConfigRequiresSpreadsheetID(t *testing.T) {
t.Setenv("SPREADSHEET_ID", "") t.Setenv("GOOGLE_SHEETS_ID", "")
if _, err := LoadConfig(); err == nil { if _, err := LoadConfig(); err == nil {
t.Fatal("expected error when SPREADSHEET_ID is missing") t.Fatal("expected error when GOOGLE_SHEETS_ID is missing")
} }
} }
func TestLoadConfigRejectsBadInterval(t *testing.T) { func TestLoadConfigRejectsBadInterval(t *testing.T) {
t.Setenv("SPREADSHEET_ID", "sheet-123") t.Setenv("GOOGLE_SHEETS_ID", "sheet-123")
t.Setenv("SYNC_INTERVAL", "not-a-duration") t.Setenv("SYNC_INTERVAL", "not-a-duration")
if _, err := LoadConfig(); err == nil { if _, err := LoadConfig(); err == nil {
t.Fatal("expected error for unparseable SYNC_INTERVAL") t.Fatal("expected error for unparseable SYNC_INTERVAL")
@ -45,7 +47,7 @@ func TestLoadConfigRejectsBadInterval(t *testing.T) {
} }
func TestLoadConfigRejectsNonPositiveInterval(t *testing.T) { func TestLoadConfigRejectsNonPositiveInterval(t *testing.T) {
t.Setenv("SPREADSHEET_ID", "sheet-123") t.Setenv("GOOGLE_SHEETS_ID", "sheet-123")
t.Setenv("SYNC_INTERVAL", "0s") t.Setenv("SYNC_INTERVAL", "0s")
if _, err := LoadConfig(); err == nil { if _, err := LoadConfig(); err == nil {
t.Fatal("expected error for non-positive SYNC_INTERVAL") t.Fatal("expected error for non-positive SYNC_INTERVAL")