Files

268 lines
7.2 KiB
Go

package ctl
import (
"strings"
"testing"
"time"
)
func TestEnvelopeShape(t *testing.T) {
stanza, sid, cid, err := Envelope("ctl@ecouser.net/r", "bot@155.ecorobot.net/atom", Outbound{
TD: "Clean",
Inner: []byte(`<clean type="auto" speed="standard" act="s"/>`),
})
if err != nil {
t.Fatalf("Envelope: %v", err)
}
s := string(stanza)
if sid == "" || cid == "" {
t.Fatalf("sid=%q cid=%q, want both", sid, cid)
}
if sid == cid {
t.Fatalf("sid %q must differ from cid", sid)
}
if len(cid) != 8 {
t.Fatalf("cid %q is not zero-padded to 8 digits", cid)
}
for _, want := range []string{
`<iq id="` + sid + `"`,
`type="set"`,
`<query xmlns="com:ctl">`,
`<ctl td="Clean" id="` + cid + `">`,
`<clean type="auto" speed="standard" act="s"/>`,
`</ctl></query></iq>`,
} {
if !strings.Contains(s, want) {
t.Errorf("stanza missing %q:\n%s", want, s)
}
}
}
func TestEnvelopeEscapesAttributes(t *testing.T) {
stanza, _, _, err := Envelope(`ctl@ecouser.net/"r"`, "bot@x/atom", Outbound{
TD: "Clean",
Attrs: []Attr{{Name: "speed", Value: `a"b<c&d`}},
})
if err != nil {
t.Fatalf("Envelope: %v", err)
}
s := string(stanza)
if strings.Contains(s, `a"b<c&d`) {
t.Fatalf("attribute value not escaped: %s", s)
}
if !strings.Contains(s, `a&#34;b&lt;c&amp;d`) {
t.Fatalf("escaped value missing: %s", s)
}
}
func TestMoveHasNoCtlID(t *testing.T) {
stanza, sid, cid, err := Envelope("ctl@ecouser.net/r", "bot@x/atom", Outbound{
TD: "Move",
OmitCtlID: true,
Inner: []byte(`<move action="forward"/>`),
})
if err != nil {
t.Fatalf("Envelope: %v", err)
}
if cid != "" {
t.Fatalf("Move cid = %q, want empty", cid)
}
in, err := Parse(stanza)
if err != nil {
t.Fatalf("Parse outbound Move: %v", err)
}
if in.TD != "Move" {
t.Fatalf("td = %q, want Move", in.TD)
}
if _, has := in.Attrs["id"]; has {
t.Fatalf("Move ctl must not carry an id attribute: %s", stanza)
}
// The correlator holds a sid waiter and no cid waiter: the stanza ack
// completes the command and no cid can ever complete it.
c := NewCorrelator(nil)
c.Register(sid, cid, "Move", false)
tr, ok := c.CompleteSID(sid)
if !ok {
t.Fatal("Move stanza ack did not complete the sid")
}
if tr.Phase != "ack" || tr.Command != "Move" {
t.Fatalf("trace = %+v, want ack phase for Move", tr)
}
if _, ok := c.CompleteSID(sid); ok {
t.Fatal("second ack completed again")
}
if traces := c.FailGeneration("connection-lost"); len(traces) != 0 {
t.Fatalf("completed Move left waiters: %+v", traces)
}
}
func TestParseKinds(t *testing.T) {
tests := []struct {
name string
stanza string
kind Kind
check func(t *testing.T, in Inbound)
}{
{
name: "ack",
stanza: `<iq to="c" type="result" id="42"/>`,
kind: KindAck,
check: func(t *testing.T, in Inbound) {
if in.SID != "42" {
t.Errorf("sid = %q", in.SID)
}
},
},
{
name: "result",
stanza: `<iq to="c" type="set" id="9"><query xmlns="com:ctl"><ctl id="00000042" ret="ok" errno=""><battery power="076"/></ctl></query></iq>`,
kind: KindResult,
check: func(t *testing.T, in Inbound) {
if in.CID != "00000042" || in.Ret != "ok" {
t.Errorf("cid/ret = %q/%q", in.CID, in.Ret)
}
if in.Errno == nil || *in.Errno != "" {
t.Errorf("errno = %v, want present empty", in.Errno)
}
if in.BatteryPower != "076" {
t.Errorf("battery = %q", in.BatteryPower)
}
},
},
{
name: "result errno omitted differs from empty",
stanza: `<iq type="set" id="9"><query><ctl id="1" ret="ok"/></query></iq>`,
kind: KindResult,
check: func(t *testing.T, in Inbound) {
if in.Errno != nil {
t.Errorf("errno = %q, want nil", *in.Errno)
}
},
},
{
name: "push",
stanza: `<iq to="c" type="set" id="9"><query xmlns="com:ctl"><ctl td="CleanReport"><clean type="auto" speed="strong" st=" " rsn=" "/></ctl></query></iq>`,
kind: KindPush,
check: func(t *testing.T, in Inbound) {
if in.TD != "CleanReport" {
t.Errorf("td = %q", in.TD)
}
if in.CleanAttrs["type"] != "auto" || in.CleanAttrs["speed"] != "strong" {
t.Errorf("clean attrs = %v", in.CleanAttrs)
}
},
},
{
name: "bare battery",
stanza: `<iq to="c" type="set" id="46"><query xmlns="com:ctl"><battery power="077"/></query></iq>`,
kind: KindBattery,
check: func(t *testing.T, in Inbound) {
if in.BatteryPower != "077" {
t.Errorf("battery = %q", in.BatteryPower)
}
},
},
{
name: "unknown child",
stanza: `<iq type="set" id="9"><query xmlns="com:ctl"><frobnicate x="1"/></query></iq>`,
kind: KindUnknown,
},
{
name: "unqualified elements parse by local name",
stanza: `<iq type="set"><query><ctl td="ChargeState"><charge type="going"/></ctl></query></iq>`,
kind: KindPush,
check: func(t *testing.T, in Inbound) {
if in.ChargeAttrs["type"] != "going" {
t.Errorf("charge = %v", in.ChargeAttrs)
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
in, err := Parse([]byte(tc.stanza))
if err != nil {
t.Fatalf("Parse: %v", err)
}
if in.Kind != tc.kind {
t.Fatalf("kind = %d, want %d", in.Kind, tc.kind)
}
if tc.check != nil {
tc.check(t, in)
}
})
}
}
func TestParseMalformed(t *testing.T) {
for _, s := range []string{
`<iq type="set"><query><ctl td="CleanReport"`,
`not xml at all`,
`<iq type="set"><query><ctl td="x"/></query></iq>junk<`,
} {
if _, err := Parse([]byte(s)); err == nil {
t.Errorf("Parse(%q) = nil error, want malformed", s)
}
}
}
func TestCorrelatorDuplicateLiveCID(t *testing.T) {
c := NewCorrelator(nil)
c.Register("1", "00000042", "GetCleanState", true)
c.Register("2", "00000042", "GetCleanState", true)
for _, sid := range []string{"1", "2"} {
tr, ok := c.CompleteSID(sid)
if !ok || tr.Phase != "ack" || tr.CID != "00000042" {
t.Fatalf("ack for sid %s = %+v, %v", sid, tr, ok)
}
}
tr, ok := c.CompleteCID("00000042")
if !ok || tr.Phase != "result" {
t.Fatalf("cid completion = %+v, %v", tr, ok)
}
if _, ok := c.CompleteCID("00000042"); ok {
t.Fatal("cid completed twice")
}
}
func TestCorrelatorExpireAndFail(t *testing.T) {
base := time.Unix(1790194386, 0)
now := base
c := NewCorrelator(func() time.Time { return now })
c.Register("1", "00000001", "Clean", true)
c.Register("2", "", "Move", false)
if traces := c.Expire(now.Add(CommandTimeout - time.Second)); len(traces) != 0 {
t.Fatalf("early expire produced %+v", traces)
}
traces := c.Expire(now.Add(CommandTimeout))
if len(traces) != 2 {
t.Fatalf("expire produced %d traces, want 2", len(traces))
}
byPhase := map[string]Trace{}
for _, tr := range traces {
byPhase[tr.Phase] = tr
if tr.Ret != "timeout" {
t.Errorf("expire ret = %q, want timeout", tr.Ret)
}
}
if _, ok := byPhase["result"]; !ok {
t.Error("expectResult command did not expire in result phase")
}
if _, ok := byPhase["ack"]; !ok {
t.Error("sid-only command did not expire in ack phase")
}
c.Register("3", "00000003", "GetSched", true)
traces = c.FailGeneration("connection-lost")
if len(traces) != 1 || traces[0].Ret != "connection-lost" || traces[0].Phase != "result" {
t.Fatalf("FailGeneration = %+v", traces)
}
if _, ok := c.CompleteSID("3"); ok {
t.Fatal("waiter survived FailGeneration")
}
}