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.
82 lines
3.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|