package database import ( "net/netip" "reflect" "testing" "time" "git.eeqj.de/sneak/routewatch/internal/routetable" ) // TestOriginChangesWriteThenRead writes three origin changes and reads them // back newest first, honouring the limit. func TestOriginChangesWriteThenRead(t *testing.T) { t.Parallel() db := newRoutesTestDatabase(t) prefix := netip.MustParsePrefix(tablePrefixV4) changes := []routetable.OriginChange{ {Prefix: prefix, OriginASN: asA, Announced: true, PeerCount: 1, Time: tableTime(10)}, {Prefix: prefix, OriginASN: asA, Announced: false, PeerCount: 0, Time: tableTime(20)}, {Prefix: prefix, OriginASN: asB, Announced: true, PeerCount: 1, Time: tableTime(30)}, } err := db.WriteOriginChanges(changes) if err != nil { t.Fatalf("WriteOriginChanges: %v", err) } got, err := db.GetOriginChanges(t.Context(), prefix, 2) if err != nil { t.Fatalf("GetOriginChanges: %v", err) } want := []routetable.OriginChange{ {Prefix: prefix, OriginASN: asB, Announced: true, PeerCount: 1, Time: tableTime(30)}, {Prefix: prefix, OriginASN: asA, Announced: false, PeerCount: 0, Time: tableTime(20)}, } if !reflect.DeepEqual(got, want) { t.Errorf("GetOriginChanges = %+v, want %+v", got, want) } } // TestOriginChangesPrune checks that the prune keeps, for a prefix with more // than 500 rows, its newest 500 rows and every row newer than 72 hours, and // deletes the rest. It checks which rows survive by their times, not only how // many. func TestOriginChangesPrune(t *testing.T) { t.Parallel() db := newRoutesTestDatabase(t) oldPrefix := netip.MustParsePrefix(tablePrefixV4) straddling := netip.MustParsePrefix(tablePrefixShortV4) busy := netip.MustParsePrefix(tablePrefixBusyV4) other := netip.MustParsePrefix("10.20.0.0/16") // Whole seconds, so the stored times round-trip exactly. now := time.Now().Truncate(time.Second) // 600 rows for oldPrefix, all older than 72 hours, oldest first. The // prune keeps the newest 500 and deletes the oldest 100. changes := rows(oldPrefix, now.Add(-100*time.Hour), 600) // 600 rows for straddling: 400 older than 72 hours, then 200 newer. The // prune keeps all 200 newer and the newest 300 of the 400 older. changes = append(changes, rows(straddling, now.Add(-100*time.Hour), 400)...) changes = append(changes, rows(straddling, now.Add(-71*time.Hour), 200)...) // 100 rows older than 72 hours and minOriginChangesPerPrefix+100 newer // rows for busy. The prune keeps all the newer rows and none of the older, // because the newer rows alone are more than minOriginChangesPerPrefix. changes = append(changes, rows(busy, now.Add(-100*time.Hour), 100)...) changes = append(changes, rows(busy, now.Add(-71*time.Hour), minOriginChangesPerPrefix+100)...) // One row for other, which the prune leaves alone. changes = append(changes, rows(other, now.Add(-100*time.Hour), 1)...) err := db.WriteOriginChanges(changes) if err != nil { t.Fatalf("WriteOriginChanges: %v", err) } err = db.PruneOriginChanges(t.Context()) if err != nil { t.Fatalf("PruneOriginChanges: %v", err) } // The newest row survives, and the oldest survivor is the 100th oldest, // so a prune that kept the oldest 500 instead would fail. checkSurvivors(t, db, oldPrefix, 500, now.Add(-100*time.Hour).Add(599*time.Minute), now.Add(-100*time.Hour).Add(100*time.Minute)) // The newest of the newer-than-72-hours rows survives, and the oldest // survivor is the 100th oldest of the older-than-72-hours rows. checkSurvivors(t, db, straddling, 500, now.Add(-71*time.Hour).Add(199*time.Minute), now.Add(-100*time.Hour).Add(100*time.Minute)) // Every newer-than-72-hours row survives, so the oldest survivor is the // first of them and the newest is minOriginChangesPerPrefix+99 minutes // after it. checkSurvivors(t, db, busy, minOriginChangesPerPrefix+100, now.Add(-71*time.Hour). Add(time.Duration(minOriginChangesPerPrefix+99)*time.Minute), now.Add(-71*time.Hour)) gotOther, err := db.GetOriginChanges(t.Context(), other, 10) if err != nil { t.Fatalf("GetOriginChanges: %v", err) } if len(gotOther) != 1 { t.Errorf("GetOriginChanges(other) = %d rows, want 1", len(gotOther)) } } // rows returns n origin changes for prefix, at times from plus one minute per // step, oldest first. func rows(prefix netip.Prefix, from time.Time, n int) []routetable.OriginChange { changes := make([]routetable.OriginChange, 0, n) for i := range n { changes = append(changes, routetable.OriginChange{ Prefix: prefix, OriginASN: asA, Announced: true, PeerCount: 1, Time: from.Add(time.Duration(i) * time.Minute), }) } return changes } // survivorTimes returns the times of prefix's rows after the prune, newest // first. func survivorTimes( t *testing.T, db *Database, prefix netip.Prefix, limit int, ) []time.Time { t.Helper() rows, err := db.GetOriginChanges(t.Context(), prefix, limit) if err != nil { t.Fatalf("GetOriginChanges: %v", err) } times := make([]time.Time, len(rows)) for i, row := range rows { times[i] = row.Time } return times } // checkSurvivors checks that prefix kept exactly wantCount rows, with the // newest survivor at wantNewest and the oldest at wantOldest. func checkSurvivors( t *testing.T, db *Database, prefix netip.Prefix, wantCount int, wantNewest, wantOldest time.Time, ) { t.Helper() times := survivorTimes(t, db, prefix, wantCount+1) if len(times) != wantCount { t.Errorf("GetOriginChanges(%s) = %d rows, want %d", prefix, len(times), wantCount) return } if !times[0].Equal(wantNewest) { t.Errorf("the newest surviving row's time = %v, want %v", times[0], wantNewest) } if !times[wantCount-1].Equal(wantOldest) { t.Errorf("the oldest surviving row's time = %v, want %v", times[wantCount-1], wantOldest) } } // TestOriginChangesSurviveRouteDeletion checks that deleting a routes row // leaves the prefix's origin_changes rows. func TestOriginChangesSurviveRouteDeletion(t *testing.T) { t.Parallel() db := newRoutesTestDatabase(t) prefix := netip.MustParsePrefix(tablePrefixV4) err := db.WriteOriginChanges([]routetable.OriginChange{{ Prefix: prefix, OriginASN: asA, Announced: true, PeerCount: 1, Time: tableTime(10), }}) if err != nil { t.Fatalf("WriteOriginChanges: %v", err) } // Write a route, then delete it. table := routetable.New(routetable.Params{}) announceTo(table, tablePrefixV4, asA, peerA, 10) writeTableChanges(t, db, table) table.Withdraw(prefix, peerA) writeTableChanges(t, db, table) got, err := db.GetOriginChanges(t.Context(), prefix, 10) if err != nil { t.Fatalf("GetOriginChanges: %v", err) } if len(got) != 1 { t.Errorf("GetOriginChanges = %d rows, want 1", len(got)) } }