283 lines
11 KiB
Go
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))
|
|
}
|
|
}
|