228 lines
6.3 KiB
Go
228 lines
6.3 KiB
Go
package syncer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"usoscal/internal/seal"
|
|
"usoscal/internal/store"
|
|
"usoscal/internal/usos"
|
|
"usoscal/internal/usostest"
|
|
)
|
|
|
|
type fixture struct {
|
|
t *testing.T
|
|
fake *usostest.Fake
|
|
store *store.Store
|
|
syncer *Syncer
|
|
now time.Time
|
|
}
|
|
|
|
func newFixture(t *testing.T) *fixture {
|
|
t.Helper()
|
|
fake := usostest.New()
|
|
t.Cleanup(fake.Close)
|
|
st, err := store.Open(filepath.Join(t.TempDir(), "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
box, err := seal.New(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{3}, 32)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := usos.New(fake.URL, usostest.ConsumerKey, usostest.ConsumerSecret)
|
|
f := &fixture{t: t, fake: fake, store: st, now: time.Date(2026, 10, 11, 2, 30, 0, 0, usos.Location)}
|
|
f.syncer = New(st, client, box, 24*time.Hour)
|
|
f.syncer.Pause = 0
|
|
f.syncer.Now = func() time.Time { return f.now }
|
|
|
|
ctx := context.Background()
|
|
request, err := client.RequestToken(ctx, "http://app.example/callback")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
access, err := client.AccessToken(ctx, request, "verifier-of-"+request.Key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id := usostest.UserID
|
|
if err := st.Connect(ctx, id, "Ada Nowak", box.Seal(access.Key, id+TokenPurpose), box.Seal(access.Secret, id+SecretPurpose)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (f *fixture) sync() {
|
|
f.t.Helper()
|
|
if err := f.syncer.Sync(context.Background(), usostest.UserID); err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (f *fixture) events() []store.Event {
|
|
f.t.Helper()
|
|
events, err := f.store.Events(context.Background(), usostest.UserID)
|
|
if err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
return events
|
|
}
|
|
|
|
func (f *fixture) user() store.User {
|
|
f.t.Helper()
|
|
user, err := f.store.User(context.Background(), usostest.UserID)
|
|
if err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func day(year int, month time.Month, date int) time.Time {
|
|
return time.Date(year, month, date, 0, 0, 0, 0, usos.Location)
|
|
}
|
|
|
|
func TestLiveSyncFetchesTheSemesterAhead(t *testing.T) {
|
|
f := newFixture(t)
|
|
f.sync()
|
|
|
|
events := f.events()
|
|
if len(events) == 0 {
|
|
t.Fatal("no events after sync")
|
|
}
|
|
first, last := events[0], events[len(events)-1]
|
|
if want := time.Date(2026, 10, 14, 13, 30, 0, 0, usos.Location); !first.Start.Equal(want) {
|
|
t.Errorf("first event at %v, want %v", first.Start, want)
|
|
}
|
|
if want := time.Date(2027, 1, 29, 11, 45, 0, 0, usos.Location); !last.Start.Equal(want) {
|
|
t.Errorf("last event at %v, want %v", last.Start, want)
|
|
}
|
|
if first.Title != "Algebra liniowa z elementami matematyki dyskretnej" || first.Kind != "Ćwiczenia" || first.Room != "303" ||
|
|
first.Building != "A1 - BM - Budowa Maszyn, budynek z zegarem" || first.Address != "ul. Piotrowo 3, 61-138 Poznań, Polska" ||
|
|
first.Lecturers != "dr inż. Jan Kowalski" || first.Group != "1" || first.UID != "sm-20261014" {
|
|
t.Errorf("unexpected event fields: %+v", first)
|
|
}
|
|
if windows := f.fake.WindowCount(); windows != 21 {
|
|
t.Errorf("%d timetable requests, want 21 weekly windows up to the end of the term", windows)
|
|
}
|
|
if user := f.user(); user.SyncedAt.IsZero() || user.SyncError != "" {
|
|
t.Errorf("sync state not recorded: %+v", user)
|
|
}
|
|
}
|
|
|
|
func TestWithoutArchiveThePastIsDropped(t *testing.T) {
|
|
f := newFixture(t)
|
|
f.sync()
|
|
f.now = f.now.AddDate(0, 0, 7)
|
|
f.sync()
|
|
if first := f.events()[0]; first.Start.Before(day(2026, 10, 18)) {
|
|
t.Errorf("event from %v survived without the archive", first.Start)
|
|
}
|
|
}
|
|
|
|
func TestArchiveBackfillsAndFreezesThePast(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
if err := f.store.SetArchive(ctx, usostest.UserID, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.sync()
|
|
|
|
events := f.events()
|
|
if want := time.Date(2025, 10, 1, 13, 30, 0, 0, usos.Location); !events[0].Start.Equal(want) {
|
|
t.Fatalf("archive starts at %v, want %v", events[0].Start, want)
|
|
}
|
|
if !f.user().Backfilled {
|
|
t.Fatal("backfill not marked as done")
|
|
}
|
|
backfillWindows := f.fake.WindowCount() - 21
|
|
if backfillWindows < 54 || backfillWindows > 54+backfillQuietRun {
|
|
t.Errorf("%d backfill requests, want the weeks back to October 2025 plus the quiet run", backfillWindows)
|
|
}
|
|
total := len(events)
|
|
|
|
f.fake.SetSchedule(func(day time.Time) []usostest.Class {
|
|
classes := usostest.Weekly(day)
|
|
switch day.Format("2006-01-02") {
|
|
case "2026-10-23":
|
|
return nil
|
|
case "2026-10-30":
|
|
classes[0].Room = "105"
|
|
}
|
|
if day.Before(f.now) {
|
|
return nil
|
|
}
|
|
return classes
|
|
})
|
|
before := f.fake.WindowCount()
|
|
f.now = f.now.AddDate(0, 0, 7)
|
|
f.sync()
|
|
|
|
if again := f.fake.WindowCount() - before; again > 21 {
|
|
t.Errorf("%d requests on the second run, the backfill must not repeat", again)
|
|
}
|
|
events = f.events()
|
|
if len(events) != total-1 {
|
|
t.Errorf("%d events, want %d: only the cancelled future class goes away", len(events), total-1)
|
|
}
|
|
byUID := map[string]store.Event{}
|
|
for _, event := range events {
|
|
byUID[event.UID] = event
|
|
}
|
|
if _, kept := byUID["sm-20261014"]; !kept {
|
|
t.Error("a class that passed since the last run left the archive")
|
|
}
|
|
if _, kept := byUID["sm-20261023"]; kept {
|
|
t.Error("a future class removed from the plan is still in the feed")
|
|
}
|
|
if got := byUID["sm-20261030"].Room; got != "105" {
|
|
t.Errorf("room of the changed class = %q, want 105", got)
|
|
}
|
|
}
|
|
|
|
func TestTurningTheArchiveOffAllowsANewBackfill(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
for _, on := range []bool{true, false, true} {
|
|
if err := f.store.SetArchive(ctx, usostest.UserID, on); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.sync()
|
|
first := f.events()[0].Start
|
|
if on && !first.Before(day(2025, 10, 2)) {
|
|
t.Errorf("archive on: earliest event %v", first)
|
|
}
|
|
if !on && first.Before(day(2026, 10, 11)) {
|
|
t.Errorf("archive off: earliest event %v", first)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRevokedAccessStopsSyncAndKeepsTheData(t *testing.T) {
|
|
f := newFixture(t)
|
|
f.sync()
|
|
kept := len(f.events())
|
|
f.fake.Revoke()
|
|
f.now = f.now.AddDate(0, 0, 1)
|
|
f.sync()
|
|
|
|
user := f.user()
|
|
if user.Connected() {
|
|
t.Error("tokens kept after USOS rejected them")
|
|
}
|
|
if user.SyncError == "" {
|
|
t.Error("no message for the user about the lost access")
|
|
}
|
|
if len(f.events()) != kept {
|
|
t.Error("stored events were lost with the access")
|
|
}
|
|
due, err := f.store.DueUsers(context.Background(), f.now.Add(48*time.Hour))
|
|
if err != nil || len(due) != 0 {
|
|
t.Errorf("disconnected user still scheduled: %v %v", due, err)
|
|
}
|
|
}
|