Files
mesh-catalog/modules/brother/cmd/brother/printer_test.go
T

283 lines
11 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"brother/cmd/brother/internal/secret"
)
// 1453/AC3: printer.settings logs in by the field name the form gives, sends the password from the secret
// file, follows the session cookie, and returns the page's title, links and form fields; a password field's
// value is never returned, and the session is logged out. The page is SYNTHETIC (testdata/admin/
// synthetic-settings.html): the real pages arrive in round 2.
func TestPrinterSettingsReadsAPageBehindTheLogin(t *testing.T) {
f := newFakePrinter(t, "Qw9z3", "p4ss word")
_, run := printerTools(t, f, "p4ss word")
got, err := run(map[string]any{"page": "/general/other.html"})
if err != nil {
t.Fatal(err)
}
if len(f.posted) != 1 || f.posted[0].Get("Qw9z3") != "p4ss word" {
t.Errorf("the login posted %v", f.posted)
}
raw, _ := json.Marshal(got)
out := string(raw)
var a struct {
Title string
Links []Link
Forms []Form
Logout string
}
json.Unmarshal(raw, &a)
if a.Title != "Synthetic settings" {
t.Errorf("title = %q", a.Title)
}
if len(a.Links) != 0 {
t.Errorf("links in the default answer: %v", a.Links)
}
if len(a.Forms) != 1 || a.Forms[0].Action != "/general/other.html" || a.Forms[0].Method != "post" {
t.Fatalf("forms = %+v", a.Forms)
}
want := map[string]Field{
"lock": {Name: "lock", Type: "checkbox", Value: "on", Label: "Scan lock"},
"node": {Name: "node", Type: "text", Value: "BRN-SYNTH", Label: "Name"},
"pw": {Name: "pw", Type: "password", Value: "", Label: "Secret"},
"note": {Name: "note", Type: "textarea", Value: "hello & bye"},
}
for _, fl := range a.Forms[0].Fields {
if fl.Name == "mode" {
if fl.Type != "select" || fl.Value != "m" || fl.Label != "Mode" || len(fl.Options) != 2 || fl.Options[0] != (Option{Value: "a", Text: "Auto"}) || fl.Options[1] != (Option{Value: "m", Text: "Manual"}) {
t.Errorf("mode = %+v", fl)
}
continue
}
w, ok := want[fl.Name]
if !ok || fl.Name != w.Name || fl.Type != w.Type || fl.Value != w.Value || fl.Label != w.Label {
t.Errorf("field %+v, want %+v", fl, w)
}
delete(want, fl.Name)
}
if len(want) != 0 {
t.Errorf("fields missing: %v", want)
}
if strings.Contains(out, "shouldnotshow") {
t.Errorf("a password field's value was returned: %s", out)
}
if a.Logout != "logged out" || f.loggedInNow() != 0 {
t.Errorf("logout = %q, sessions open %d", a.Logout, f.loggedInNow())
}
// Read-only: the only form posted is the login.
if len(f.posted) != 1 {
t.Errorf("forms posted: %v", f.posted)
}
}
// 1453/AC3: with no page asked for, the status page is read.
func TestPrinterSettingsDefaultsToTheStatusPage(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
_, run := printerTools(t, f, "pw")
got, err := run(nil)
if err != nil {
t.Fatal(err)
}
if got.(map[string]any)["title"] != "Synthetic status" {
t.Errorf("answer = %v", got)
}
}
// 1453/AC4: a wrong password is said as a failed login.
func TestPrinterSettingsSaysAFailedLogin(t *testing.T) {
f := newFakePrinter(t, "B1891", "right")
_, run := printerTools(t, f, "wrong")
if _, err := run(nil); err == nil || !strings.Contains(err.Error(), "login failed") {
t.Fatalf("error = %v", err)
}
}
// 1453/AC4: printer or printer-certificate not set is said by name by the tool, nothing is queued and the
// printer is not contacted.
func TestPrinterSettingsSaysWhichConfigIsUnsetAndQueuesNothing(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
for _, c := range []struct{ name, settings, want string }{
{"no printer", `{"printer-certificate":"` + f.print() + `"}`, "printer is not set"},
{"no certificate", `{"printer":"` + f.host() + `"}`, "printer-certificate is not set"},
{"neither", `{}`, "printer and printer-certificate"},
} {
r := newRig(t, c.settings, "pw")
_, err := r.run(nil)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: error = %v, want it to say %q", c.name, err, c.want)
}
if r.bus.asks() != 0 {
t.Errorf("%s: an ask was queued", c.name)
}
}
if f.connsNow() != 0 || len(f.requests) != 0 {
t.Errorf("the printer was contacted: %d connections, %v", f.connsNow(), f.requests)
}
}
// 1453/AC4: the secret not given is said by name, by the device process, and the printer is not contacted.
func TestPrinterSettingsSaysTheSecretIsNotGivenAndContactsNothing(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
_, run := printerTools(t, f, "")
if _, err := run(nil); err == nil || !strings.Contains(err.Error(), "admin-password is not given") {
t.Errorf("no secret file: error = %v", err)
}
// An empty file is not given either.
r := newRig(t, printerSettingsJSON(f), "")
r.worker.Password = func() (string, error) {
p := filepath.Join(t.TempDir(), "e")
os.WriteFile(p, []byte("\n"), 0o600)
return secret.Read(p)
}
if _, err := r.run(nil); err == nil || !strings.Contains(err.Error(), "admin-password is not given") {
t.Errorf("empty secret: error = %v", err)
}
if f.connsNow() != 0 || len(f.requests) != 0 {
t.Errorf("the printer was contacted: %d connections, %v", f.connsNow(), f.requests)
}
}
// 1453: a page that is not a page of the admin site is refused before anything is queued, and by the admin
// process too if one reaches it.
func TestPrinterSettingsRefusesAPageOutsideTheSite(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
r := newRig(t, printerSettingsJSON(f), "pw")
for _, p := range []string{"general/status.html", "/general/status", "//evil.example/x.html", "/a/../b.html", "/a.html?x=1", "/a b.html"} {
if _, err := r.run(map[string]any{"page": p}); err == nil || !strings.Contains(err.Error(), "not a page") {
t.Errorf("page %q: error = %v", p, err)
}
if _, err := r.worker.read(t.Context(), p, false, false); err == nil || !strings.Contains(err.Error(), "not a page") {
t.Errorf("the device process, page %q: error = %v", p, err)
}
}
if _, err := r.run(map[string]any{"page": 5}); err == nil {
t.Errorf("a page that is no string was accepted")
}
if r.bus.asks() != 0 || f.connsNow() != 0 {
t.Errorf("%d asks queued, the printer contacted %d times", r.bus.asks(), f.connsNow())
}
}
// 1453: nobody consuming the queue is said as the device process not answering, naming the wait.
func TestPrinterSettingsSaysWhenTheAdminProcessDoesNotAnswer(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
r := newRig(t, printerSettingsJSON(f), "pw")
r.bus.worker = nil
answerWait = 100 * time.Millisecond
_, err := r.run(nil)
if err == nil || !strings.Contains(err.Error(), "device process did not answer") {
t.Fatalf("error = %v", err)
}
if r.bus.asks() != 1 {
t.Errorf("%d asks queued", r.bus.asks())
}
}
// 1453: the device process terminates an ask it cannot read, drops one nobody waits for any more without
// contacting the printer, acknowledges one already answered, and asks for redelivery when it cannot write its
// answer.
func TestTheAdminWorkerSettlesEachAsk(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
r := newRig(t, printerSettingsJSON(f), "pw")
w := r.worker
if act, _ := w.Handle([]byte("not json"), nil); act != actionTerm {
t.Errorf("an unreadable ask: action %v, want term", act)
}
old, _ := json.Marshal(Ask{ID: "old", Op: OpSettings, Page: DefaultPage, At: fixedNow.Add(-StaleAfter - time.Second)})
if act, _ := w.Handle(old, nil); act != actionAck || r.answers.get("old") != nil || f.connsNow() != 0 {
t.Errorf("a stale ask: action %v, answer %s, %d connections", act, r.answers.get("old"), f.connsNow())
}
fresh, _ := json.Marshal(Ask{ID: "a1", Op: OpSettings, Page: DefaultPage, At: fixedNow})
if act, _ := w.Handle(fresh, nil); act != actionAck || r.answers.get("a1") == nil {
t.Errorf("a fresh ask: action %v", act)
}
conns := f.connsNow()
if act, _ := w.Handle(fresh, nil); act != actionAck {
t.Errorf("an ask already answered: action %v, want ack", act)
}
_ = conns
r.answers.fail = fmt.Errorf("the bus is away")
again, _ := json.Marshal(Ask{ID: "a2", Op: OpSettings, Page: DefaultPage, At: fixedNow})
if act, delay := w.Handle(again, nil); act != actionRetry || delay <= 0 {
t.Errorf("an answer that cannot be written for want of the bus: action %v delay %v, want retry", act, delay)
}
}
// 1514: printer.settings with no page, and with /home/status.html, logs in from the page the printer serves its
// login form on (the root redirects to it) and reads that page. The printer has no /general/status.html behind
// the login: the fake, like the captured printer, answers it a 404 once logged in, and a redirect to the login
// page before.
func TestPrinterSettingsDefaultPageAndHomeStatusAreReadAfterTheLogin(t *testing.T) {
for name, args := range map[string]map[string]any{"default": {}, "explicit": {"page": "/home/status.html"}} {
f := newFakePrinter(t, "B1891", "pw")
_, run := printerTools(t, f, "pw")
got, err := run(args)
if err != nil {
t.Fatalf("%s: %v", name, err)
}
raw, _ := json.Marshal(got)
if !strings.Contains(string(raw), "Synthetic status") || !strings.Contains(string(raw), "/home/status.html") {
t.Errorf("%s: answer %.200s", name, raw)
}
if len(f.posted) != 1 || f.posted[0].Get("loginurl") != "/home/status.html" {
t.Errorf("%s: posted %v, want loginurl /home/status.html", name, f.posted)
}
if f.loggedInNow() != 0 {
t.Errorf("%s: the session is still open", name)
}
}
if DefaultPage != "/home/status.html" {
t.Errorf("DefaultPage = %s", DefaultPage)
}
}
// 1530: the printer's menu links are left out of the answer unless links:true is asked, and hidden fields
// (the CSRF token) unless hidden:true is asked; a password field's value is never given.
func TestPrinterSettingsLeavesOutLinksAndHiddenFieldsUnlessAsked(t *testing.T) {
f := newFakePrinter(t, "B1891", "pw")
f.pages["/scan/scanfrompc.html"] = adminFixture(t, "reconstructed-scanfrompc.html")
_, run := printerTools(t, f, "pw")
read := func(args map[string]any) map[string]any {
t.Helper()
got, err := run(args)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
var m map[string]any
json.Unmarshal(raw, &m)
return m
}
names := func(m map[string]any) string {
var n []string
for _, fm := range m["forms"].([]any) {
for _, fl := range fm.(map[string]any)["fields"].([]any) {
n = append(n, fl.(map[string]any)["name"].(string))
}
}
return strings.Join(n, ",")
}
plain := read(map[string]any{"page": "/scan/scanfrompc.html"})
if _, has := plain["links"]; has {
t.Errorf("links in the default answer: %v", plain["links"])
}
if names(plain) != "B117d" {
t.Errorf("default fields = %s, want B117d alone", names(plain))
}
more := read(map[string]any{"page": "/scan/scanfrompc.html", "links": true, "hidden": true})
if l, _ := more["links"].([]any); len(l) != 3 {
t.Errorf("links:true gave %v", more["links"])
}
if names(more) != "CSRFToken,B1892,pageid,CSRFToken,postif_registration_reject,B117d" {
t.Errorf("hidden:true fields = %s", names(more))
}
}