feat: HAMLOG.online upload and confirmations, Yaesu antenna, a named port holder
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.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package extsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The verb, the key and the record must arrive in the shape HAMLOG's own agent
|
||||
// sends — this is read from their client, not from documentation, so the test
|
||||
// pins it rather than trusting a memory of it.
|
||||
func TestUploadHamlogRequestShape(t *testing.T) {
|
||||
var got map[string]map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("method %s, want POST", r.Method)
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") {
|
||||
t.Errorf("Content-Type %q", ct)
|
||||
}
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
_ = json.Unmarshal(b, &got)
|
||||
_, _ = w.Write([]byte(`{"STATUS":"OK"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := uploadHamlogTo(context.Background(), nil, srv.URL, ServiceConfig{APIKey: "KEY123"}, "<call:5>F4BPO <eor>")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.OK {
|
||||
t.Fatalf("upload not OK: %+v", res)
|
||||
}
|
||||
add, ok := got["ADIFADD"]
|
||||
if !ok {
|
||||
t.Fatalf("no ADIFADD verb in %v", got)
|
||||
}
|
||||
if add["APIKEY"] != "KEY123" {
|
||||
t.Errorf("APIKEY = %v", add["APIKEY"])
|
||||
}
|
||||
if add["ADIFDATA"] != "<call:5>F4BPO <eor>" {
|
||||
t.Errorf("ADIFDATA = %v", add["ADIFDATA"])
|
||||
}
|
||||
}
|
||||
|
||||
// A refusal must be reported as a refusal. Their failure shape carries ERROR
|
||||
// and no STATUS, so "no error field" would have read an unknown reply as an
|
||||
// accepted QSO — which is how a contact goes missing without anyone noticing.
|
||||
func TestHamlogFailureIsNotSuccess(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
body string
|
||||
wantOK bool
|
||||
wantSaid string
|
||||
}{
|
||||
{`{"STATUS":"OK"}`, true, ""},
|
||||
{`{"ERROR":"Invalid API key"}`, false, "Invalid API key"},
|
||||
{`{"STATUS":"FAILED"}`, false, "rejected"},
|
||||
{`{}`, false, "rejected"}, // an empty object is not an acceptance
|
||||
} {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(tc.body))
|
||||
}))
|
||||
res, err := uploadHamlogTo(context.Background(), nil, srv.URL, ServiceConfig{APIKey: "K"}, "<eor>")
|
||||
srv.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tc.body, err)
|
||||
}
|
||||
if res.OK != tc.wantOK {
|
||||
t.Errorf("%s → OK=%v, want %v", tc.body, res.OK, tc.wantOK)
|
||||
}
|
||||
if !tc.wantOK && res.Message != tc.wantSaid {
|
||||
t.Errorf("%s → message %q, want %q", tc.body, res.Message, tc.wantSaid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing leaves without a key, and the message says where to get one.
|
||||
func TestUploadHamlogNeedsAKey(t *testing.T) {
|
||||
_, err := UploadHamlog(context.Background(), nil, ServiceConfig{}, "<eor>")
|
||||
if err == nil || !strings.Contains(err.Error(), hamlogKeyPage) {
|
||||
t.Fatalf("err = %v, want it to point at %s", err, hamlogKeyPage)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user