Files
OpsLog/bulk_fields_test.go
T
rouggy a13ac7d917 test: the bulk-edit guard learns the integer My-station fields
my_dxcc/my_cq_zone/my_itu_zone go through BulkSetIntField, not the string
column map — the guard flagged them as unwritable ever since they were
added, which is the drift it exists to catch, one package over.
2026-08-30 21:03:03 +02:00

82 lines
3.0 KiB
Go

package main
import (
"os"
"regexp"
"strings"
"testing"
"hamlog/internal/qso"
)
// Every field the bulk-edit dialog OFFERS must be one the repository accepts.
//
// These two lists live in different packages — a TypeScript field table on one
// side, a Go whitelist on the other — and they drifted apart the day the
// confirmation dates were added: the new columns went into the QSL Manager's
// filter whitelist instead of the bulk-edit one, so the dialog listed fields it
// could not write and the operator got "field … is not bulk-editable" only after
// selecting 54 QSOs and clicking Apply. Nothing in the build caught it.
//
// The test reads the ACTUAL field ids out of the .tsx rather than a copy, so a
// field added to the UI alone fails here immediately.
func TestBulkEditFieldsAreWritable(t *testing.T) {
src, err := os.ReadFile("frontend/src/components/BulkEditModal.tsx")
if err != nil {
t.Skipf("frontend source not available: %v", err)
}
ids := regexp.MustCompile(`\{ id: '([a-z0-9_]+)'`).FindAllStringSubmatch(string(src), -1)
if len(ids) < 10 {
t.Fatalf("only %d field ids found — the parse is wrong, not the data", len(ids))
}
for _, m := range ids {
id := m[1]
// Handled before the column map: freq takes a numeric path (freq_hz +
// band together) and the extras live in extras_json.
// The integer My-station fields take their own numeric path
// (BulkSetIntField + bulkEditableIntCols in the repo), added after this
// guard was written — which is exactly the drift it exists to catch.
if id == "my_dxcc" || id == "my_cq_zone" || id == "my_itu_zone" {
continue
}
if id == "freq" || qso.IsBulkEditableExtra(id) {
continue
}
col, ok := bulkFieldColumns[id]
if !ok {
t.Errorf("bulk-edit UI offers %q, which maps to no column (bulkFieldColumns)", id)
continue
}
// Extras live outside the qso table (owner_callsign and friends): they are
// written into extras_json, not a column, so the column whitelist does not
// apply to them.
if strings.HasPrefix(col, "extras.") {
continue
}
if !qso.IsBulkEditable(col) && !qso.IsBulkEditableExtra(col) {
t.Errorf("bulk-edit UI offers %q → column %q, which the repository refuses", id, col)
}
}
}
// Same contract for the filter builder: every field it offers must be one the
// query layer will accept.
func TestFilterFieldsAreQueryable(t *testing.T) {
src, err := os.ReadFile("frontend/src/components/FilterBuilder.tsx")
if err != nil {
t.Skipf("frontend source not available: %v", err)
}
// Anchored on `type:` — the FIELDS entries carry one, the OPERATOR list does
// not, and without it every operator (eq, contains, …) was read as a field.
ids := regexp.MustCompile(`\{ value: '([a-z0-9_]+)', label: '[^']*', type:`).FindAllStringSubmatch(string(src), -1)
if len(ids) < 10 {
t.Fatalf("only %d field ids found — the parse is wrong, not the data", len(ids))
}
for _, m := range ids {
col := m[1]
if !qso.IsFilterable(col) && !qso.IsFilterableExtra(col) {
t.Errorf("filter builder offers %q, which the query layer refuses", col)
}
}
}