chore: release v0.21.3

This commit is contained in:
2026-07-26 16:57:19 +02:00
parent 4fd70f6a9d
commit 91b5af1c7b
46 changed files with 2491 additions and 355 deletions
+114
View File
@@ -0,0 +1,114 @@
package qso
import (
"context"
"database/sql"
"encoding/json"
"path/filepath"
"testing"
_ "modernc.org/sqlite"
)
// openBulkTestDB builds the minimum of the qso table this needs. It does not go
// through db.Open (that would pull the whole migration set and an import cycle);
// the columns BulkSetExtra touches are extras_json and updated_at.
func openBulkTestDB(t *testing.T) *sql.DB {
t.Helper()
conn, err := sql.Open("sqlite", "file:"+filepath.Join(t.TempDir(), "t.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { conn.Close() })
if _, err := conn.Exec(`CREATE TABLE qso (
id INTEGER PRIMARY KEY AUTOINCREMENT,
callsign TEXT NOT NULL,
extras_json TEXT,
updated_at TEXT
)`); err != nil {
t.Fatal(err)
}
return conn
}
func extras(t *testing.T, conn *sql.DB, id int64) map[string]any {
t.Helper()
var raw sql.NullString
if err := conn.QueryRow(`SELECT extras_json FROM qso WHERE id = ?`, id).Scan(&raw); err != nil {
t.Fatal(err)
}
if !raw.Valid || raw.String == "" {
return map[string]any{}
}
var m map[string]any
if err := json.Unmarshal([]byte(raw.String), &m); err != nil {
t.Fatalf("extras_json is not valid JSON (%q): %v", raw.String, err)
}
return m
}
// OWNER_CALLSIGN has no promoted column, so bulk-editing it means merging a key
// into extras_json. The thing that must not happen is collateral damage: the
// other ADIF extras on the same QSO have to survive.
func TestBulkSetExtraPreservesOtherExtras(t *testing.T) {
conn := openBulkTestDB(t)
r := &Repo{db: conn}
ctx := context.Background()
// Two QSOs with existing extras, one with none at all (NULL column).
conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('F5LIT', '{"SILENT_KEY":"Y","ANT_PATH":"S"}')`)
conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('PA3EYF', '{"ANT_PATH":"L"}')`)
conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('F4BPO', NULL)`)
n, err := r.BulkSetExtra(ctx, []int64{1, 2, 3}, "OWNER_CALLSIGN", "TM2Q")
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("updated %d rows, want 3", n)
}
e1 := extras(t, conn, 1)
if e1["OWNER_CALLSIGN"] != "TM2Q" {
t.Errorf("row 1 OWNER_CALLSIGN = %v, want TM2Q", e1["OWNER_CALLSIGN"])
}
if e1["SILENT_KEY"] != "Y" || e1["ANT_PATH"] != "S" {
t.Errorf("row 1 lost its other extras: %v", e1)
}
// A NULL extras_json must become a valid object, not stay null or hold "null".
if e3 := extras(t, conn, 3); e3["OWNER_CALLSIGN"] != "TM2Q" {
t.Errorf("row 3 (extras_json was NULL) = %v, want OWNER_CALLSIGN=TM2Q", e3)
}
}
// Clearing the field must REMOVE the key: a blank extra would otherwise be
// carried into every ADIF export from then on.
func TestBulkSetExtraEmptyRemovesKey(t *testing.T) {
conn := openBulkTestDB(t)
r := &Repo{db: conn}
conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('F5LIT', '{"OWNER_CALLSIGN":"TM2Q","ANT_PATH":"S"}')`)
if _, err := r.BulkSetExtra(context.Background(), []int64{1}, "OWNER_CALLSIGN", ""); err != nil {
t.Fatal(err)
}
e := extras(t, conn, 1)
if _, present := e["OWNER_CALLSIGN"]; present {
t.Errorf("OWNER_CALLSIGN should be gone, got %v", e)
}
if e["ANT_PATH"] != "S" {
t.Errorf("clearing one extra removed another: %v", e)
}
}
// The frontend field id must resolve to the ADIF key, and nothing else must slip
// through — this map is the whitelist guarding a spliced JSON path.
func TestBulkExtraKeyWhitelist(t *testing.T) {
if got := BulkExtraKey("owner_callsign"); got != "OWNER_CALLSIGN" {
t.Errorf(`BulkExtraKey("owner_callsign") = %q, want "OWNER_CALLSIGN"`, got)
}
for _, bad := range []string{"", "callsign", "notes", "OWNER_CALLSIGN", "owner_callsign'"} {
if got := BulkExtraKey(bad); got != "" {
t.Errorf("BulkExtraKey(%q) = %q, want empty", bad, got)
}
}
}