162 lines
4.4 KiB
Go
162 lines
4.4 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const mig0024 = "0024_normalise_callsign.sql"
|
|
|
|
// openWithUnappliedRewrite builds a logbook that already holds QSOs and has not
|
|
// yet had the callsign-normalising migration applied — i.e. exactly what an
|
|
// existing installation looks like the moment the auto-updater relaunches it.
|
|
func openWithUnappliedRewrite(t *testing.T, rows [][2]string) string {
|
|
t.Helper()
|
|
p := filepath.Join(t.TempDir(), "logbook.db")
|
|
conn, err := Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range rows {
|
|
if _, err := conn.Exec(
|
|
`INSERT INTO qso (callsign, qso_date, band, mode) VALUES (?, ?, '20m', 'SSB')`, r[0], r[1]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Rewind so the migration runs against real data on the next Open.
|
|
if _, err := conn.Exec(`DELETE FROM schema_migrations WHERE name = ?`, mig0024); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := conn.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func callsigns(t *testing.T, path string) []string {
|
|
t.Helper()
|
|
conn, err := sql.Open("sqlite", "file:"+path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
rows, err := conn.Query(`SELECT callsign FROM qso ORDER BY id`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
var out []string
|
|
for rows.Next() {
|
|
var c string
|
|
if err := rows.Scan(&c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// The auto-updater migrates before the operator ever sees the changelog, so the
|
|
// app has to take the safety copy itself. The copy must hold the data as it was
|
|
// BEFORE the rewrite — a copy of the already-migrated rows would be worthless.
|
|
func TestRewriteMigrationBacksUpOriginalData(t *testing.T) {
|
|
p := openWithUnappliedRewrite(t, [][2]string{
|
|
{"f5lit", "2026-07-01"},
|
|
{" Pa3Eyf ", "2026-07-02"},
|
|
{"F4BPO", "2026-07-03"},
|
|
})
|
|
|
|
var logged []string
|
|
LogSink = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
|
|
defer func() { LogSink = nil }()
|
|
|
|
conn, err := Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close()
|
|
|
|
// The live logbook is normalised.
|
|
if got, want := callsigns(t, p), []string{"F5LIT", "PA3EYF", "F4BPO"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Errorf("logbook callsigns = %v, want %v", got, want)
|
|
}
|
|
|
|
// The backup exists, next to the logbook, named after the migration.
|
|
backup := p + ".pre-0024_normalise_callsign.bak"
|
|
if _, err := os.Stat(backup); err != nil {
|
|
t.Fatalf("no safety copy at %s: %v\nlogged:\n%s", backup, err, strings.Join(logged, "\n"))
|
|
}
|
|
// …and it holds the ORIGINAL rows, untouched.
|
|
if got, want := callsigns(t, backup), []string{"f5lit", " Pa3Eyf ", "F4BPO"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Errorf("backup callsigns = %v, want the pre-migration values %v", got, want)
|
|
}
|
|
|
|
var sawBackup bool
|
|
for _, l := range logged {
|
|
if strings.Contains(l, "backed up 3 QSO(s)") {
|
|
sawBackup = true
|
|
}
|
|
}
|
|
if !sawBackup {
|
|
t.Errorf("the backup was not logged; got:\n%s", strings.Join(logged, "\n"))
|
|
}
|
|
}
|
|
|
|
// No QSOs, nothing to protect: the settings database and a freshly created
|
|
// logbook must not litter the folder with pointless copies.
|
|
func TestRewriteMigrationSkipsBackupWhenNoQSOs(t *testing.T) {
|
|
p := openWithUnappliedRewrite(t, nil)
|
|
conn, err := Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close()
|
|
if _, err := os.Stat(p + ".pre-0024_normalise_callsign.bak"); err == nil {
|
|
t.Error("an empty database should not be backed up")
|
|
}
|
|
}
|
|
|
|
// schema_migrations is what makes "runs once" a guarantee rather than a promise:
|
|
// a second launch must neither re-run the rewrite nor take a second copy.
|
|
func TestRewriteMigrationRunsOnce(t *testing.T) {
|
|
p := openWithUnappliedRewrite(t, [][2]string{{"f5lit", "2026-07-01"}})
|
|
conn, err := Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close()
|
|
|
|
backup := p + ".pre-0024_normalise_callsign.bak"
|
|
first, err := os.Stat(backup)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var logged []string
|
|
LogSink = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
|
|
defer func() { LogSink = nil }()
|
|
|
|
conn2, err := Open(p) // second launch
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn2.Close()
|
|
|
|
for _, l := range logged {
|
|
if strings.Contains(l, mig0024) {
|
|
t.Errorf("migration ran again on the second launch: %s", l)
|
|
}
|
|
}
|
|
second, err := os.Stat(backup)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !first.ModTime().Equal(second.ModTime()) {
|
|
t.Error("the existing safety copy was overwritten on the second launch")
|
|
}
|
|
}
|