HAMLOG.online is a seventh external service: one API key, one ADIF record per QSO, immediate / delayed / on-close like the rest. They publish no API documentation, so the protocol is read from THEIR OWN client — the HAMLOG Agent (github.com/hamlogonline/Agent), which is the authoritative source short of asking them: POST https://hamlog.online/api/agent/ {"ADIFADD": {"APIKEY": k, "ADIFDATA": record}} → {"STATUS":"OK"} {"KEYSTATUS": {"APIKEY": k}} → {"STATUS":"OK","CALLSIGN":…} Success is STATUS == OK, not "no ERROR field": their failure carries ERROR and no STATUS, and reading an unknown reply — a proxy page, a maintenance notice — as an acceptance is how a contact goes missing without anyone noticing. KEYSTATUS buys something no other service here offers: the key can be checked BEFORE the first QSO, and the answer names the account. A key pasted from another callsign is caught in the settings panel rather than through a week of silent refusals. Their confirmations are also an award source now, ticked like LoTW rather than expressed through "custom". It reads the ADIF extras, not a column: the standard names a field for hamlog.EU and none for hamlog.ONLINE, and borrowing the other site's field would write a falsehood into every exported log. Three plausible key names from their own export are accepted too, so nobody has to rename a column by hand after an export. Yaesu gains antenna selection (AN), remembered per band — the antenna picked on a band comes back with it, with no table to fill in anywhere. Rigs with one socket never answer AN and never show the row; the log says which case it is. And a serial port that is refused now names its likely holder. OmniRig stays resident once activated and keeps the port of the rig configured in it, so a native backend never gets it — "Serial port busy" alone accused nobody, and an FTDX10 spent a morning being blamed for it.
98 lines
3.9 KiB
Go
98 lines
3.9 KiB
Go
package award
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"hamlog/internal/qso"
|
|
)
|
|
|
|
// Every source the editor offers must actually do something. "qrzcom" and
|
|
// "custom" were listed in the UI and in Def's doc comment but had no case in the
|
|
// switch, so ticking either marked nothing as confirmed — a checkbox that is
|
|
// trusted and silently inert.
|
|
func TestConfirmedSources(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
q qso.QSO
|
|
d Def
|
|
sources []string
|
|
want bool
|
|
}{
|
|
{"lotw", qso.QSO{LOTWRcvd: "Y"}, Def{}, []string{"lotw"}, true},
|
|
{"qsl", qso.QSO{QSLRcvd: "Y"}, Def{}, []string{"qsl"}, true},
|
|
{"eqsl", qso.QSO{EQSLRcvd: "Y"}, Def{}, []string{"eqsl"}, true},
|
|
|
|
// QRZ confirms on the DOWNLOAD status. The upload one is us telling QRZ
|
|
// about the QSO, which is not a confirmation of anything.
|
|
{"qrz download", qso.QSO{QRZComDownloadStatus: "Y"}, Def{}, []string{"qrzcom"}, true},
|
|
{"qrz upload only", qso.QSO{QRZComUploadStatus: "Y"}, Def{}, []string{"qrzcom"}, false},
|
|
|
|
// Custom, no value required: any non-empty value counts. This is the
|
|
// OpsLog card case — the marker holds a timestamp, not a flag.
|
|
{"custom any value",
|
|
qso.QSO{Extras: map[string]string{"APP_OPSLOG_QSL_RCVD": "2026-08-09T01:00:00Z"}},
|
|
Def{ConfirmField: "APP_OPSLOG_QSL_RCVD"}, []string{"custom"}, true},
|
|
{"custom field empty",
|
|
qso.QSO{Extras: map[string]string{"APP_OPSLOG_QSL_RCVD": ""}},
|
|
Def{ConfirmField: "APP_OPSLOG_QSL_RCVD"}, []string{"custom"}, false},
|
|
|
|
// Custom with an explicit value list, matched case-insensitively.
|
|
{"custom value match",
|
|
qso.QSO{Extras: map[string]string{"APP_CLUB_CONF": "v"}},
|
|
Def{ConfirmField: "APP_CLUB_CONF", ConfirmValue: "Y,V"}, []string{"custom"}, true},
|
|
{"custom value mismatch",
|
|
qso.QSO{Extras: map[string]string{"APP_CLUB_CONF": "N"}},
|
|
Def{ConfirmField: "APP_CLUB_CONF", ConfirmValue: "Y,V"}, []string{"custom"}, false},
|
|
|
|
// A custom source naming no field must confirm NOTHING — never everything.
|
|
{"custom without a field",
|
|
qso.QSO{Extras: map[string]string{"APP_OPSLOG_QSL_RCVD": "x"}},
|
|
Def{}, []string{"custom"}, false},
|
|
|
|
// Known QSO fields resolve too, not just extras.
|
|
{"custom on a known field", qso.QSO{State: "TX"},
|
|
Def{ConfirmField: "state", ConfirmValue: "TX"}, []string{"custom"}, true},
|
|
|
|
// Any source in the list is enough.
|
|
{"first source misses, second hits", qso.QSO{EQSLRcvd: "Y"}, Def{},
|
|
[]string{"lotw", "eqsl"}, true},
|
|
{"no source set", qso.QSO{LOTWRcvd: "Y"}, Def{}, nil, false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := Confirmed(&c.q, c.d, c.sources); got != c.want {
|
|
t.Errorf("%s: Confirmed = %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HAMLOG.online is a named confirmation source, not something to express through
|
|
// "custom": an award ticks it the way it ticks LoTW.
|
|
func TestHamlogConfirmationSource(t *testing.T) {
|
|
def := Def{Confirm: []string{"hamlog"}, Validate: []string{"hamlog"}}
|
|
yes := func(extras map[string]string) bool {
|
|
return Confirmed(&qso.QSO{Extras: extras}, def, def.Confirm)
|
|
}
|
|
if !yes(map[string]string{HamlogQSLKey: "Y"}) {
|
|
t.Error("our own key did not confirm")
|
|
}
|
|
// Their export's field name is unpublished, so the plausible shapes count too.
|
|
if !yes(map[string]string{"APP_HAMLOG_QSL": "20260823"}) {
|
|
t.Error("a date in an alternative key did not confirm")
|
|
}
|
|
// A value is not a confirmation when it says no.
|
|
if yes(map[string]string{HamlogQSLKey: "N"}) {
|
|
t.Error(`"N" was taken as a confirmation`)
|
|
}
|
|
if yes(map[string]string{HamlogQSLKey: " "}) {
|
|
t.Error("blank was taken as a confirmation")
|
|
}
|
|
if yes(nil) || yes(map[string]string{"SOMETHING_ELSE": "Y"}) {
|
|
t.Error("confirmed with nothing to confirm it")
|
|
}
|
|
// And it does not leak into an award that never asked for it.
|
|
other := Def{Confirm: []string{"lotw"}}
|
|
if Confirmed(&qso.QSO{Extras: map[string]string{HamlogQSLKey: "Y"}}, other, other.Confirm) {
|
|
t.Error("a hamlog confirmation counted for an award that only accepts LoTW")
|
|
}
|
|
}
|