319 lines
12 KiB
Go
319 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// uploadFake stands for node-files.upload on another machine: it records the ask and answers a key, or refuses.
|
|
type uploadFake struct {
|
|
mu sync.Mutex
|
|
calls []string
|
|
key string
|
|
err error
|
|
}
|
|
|
|
// withUpload puts a fake node-files in the tool side's place, and the bucket file the mesh writes from the binding
|
|
// (files/bucket.json) where the tool reads it.
|
|
// seen is the uploads asked so far, read under the fake's lock (the print runs in the background).
|
|
func (f *uploadFake) seen() []string {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return append([]string(nil), f.calls...)
|
|
}
|
|
|
|
func withUpload(t *testing.T, key string, err error) *uploadFake {
|
|
t.Helper()
|
|
f := &uploadFake{key: key, err: err}
|
|
old := uploadVia
|
|
uploadVia = func(node, path, bucket string) (string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, node+"|"+path+"|"+bucket)
|
|
return f.key, f.err
|
|
}
|
|
t.Cleanup(func() { uploadVia = old })
|
|
file := filepath.Join(t.TempDir(), "bucket.json")
|
|
if err := os.WriteFile(file, []byte(`{"bucket": "brother-test"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("BROTHER_BUCKET", file)
|
|
return f
|
|
}
|
|
|
|
// 1598/AC1: none, or two, of object, path+node and url is refused naming the rule; path without node is refused.
|
|
func TestPrintTakesExactlyOneSource(t *testing.T) {
|
|
fake := newIPPFake(t, 1, 3)
|
|
up := withUpload(t, "k", nil)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
key := sha256Hex([]byte("x")) + ".pdf"
|
|
for name, args := range map[string]map[string]any{
|
|
"none": {},
|
|
"only options": {"copies": float64(2)},
|
|
"object and path": {"object": key, "path": "/home/ops/a.pdf", "node": "g14"},
|
|
"object and url": {"object": key, "url": "https://example.com/a.pdf"},
|
|
"path+node and url": {"path": "/home/ops/a.pdf", "node": "g14", "url": "https://example.com/a.pdf"},
|
|
"all three": {"object": key, "path": "/home/ops/a.pdf", "node": "g14", "url": "https://example.com/a.pdf"},
|
|
"path without node": {"path": "/home/ops/a.pdf"},
|
|
"node without path": {"node": "g14", "object": key},
|
|
"the retired upload": {"upload": "u1"},
|
|
"object and upload": {"object": key, "upload": "u1"},
|
|
"path and its object": {"path": "/home/ops/a.pdf", "node": "g14", "object": key},
|
|
} {
|
|
_, err := r.call(t, "printer.print", args)
|
|
if err == nil {
|
|
t.Errorf("%s: accepted", name)
|
|
continue
|
|
}
|
|
rule := strings.Contains(err.Error(), "exactly one of object") || strings.Contains(err.Error(), "path and node go together") ||
|
|
strings.Contains(err.Error(), "upload is no longer")
|
|
if !rule {
|
|
t.Errorf("%s: the refusal does not name the rule: %v", name, err)
|
|
}
|
|
}
|
|
if r.bus.asks() != 0 || len(up.seen()) != 0 || fake.body != nil {
|
|
t.Errorf("%d asks queued, %d uploads asked, printer sent %d bytes", r.bus.asks(), len(up.seen()), len(fake.body))
|
|
}
|
|
}
|
|
|
|
// 1598/AC2: with path and node g14, printer.print asks g14's node-files to upload the path into brother's bucket, and
|
|
// queues a print ask that carries the returned key and the options: under 4 KiB, and no path, node or bytes.
|
|
func TestPrintOfAFileOfAnotherMachineUploadsThenPrintsTheKey(t *testing.T) {
|
|
fake := newIPPFake(t, 31, 5)
|
|
doc := pdfOf(6000)
|
|
key := sha256Hex(doc)
|
|
up := withUpload(t, key, nil)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
r.objects.Put(t.Context(), key, "application/pdf", doc) // what g14's upload stored
|
|
got, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/letter.pdf", "node": "g14", "copies": float64(2)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(up.seen()) != 1 || up.seen()[0] != "g14|/home/ops/letter.pdf|brother-test" {
|
|
t.Fatalf("uploads asked: %v", up.seen())
|
|
}
|
|
if len(r.bus.published) != 1 {
|
|
t.Fatalf("%d asks queued", len(r.bus.published))
|
|
}
|
|
queued := r.bus.published[0]
|
|
raw, _ := json.Marshal(queued)
|
|
var fields map[string]any
|
|
json.Unmarshal(raw, &fields)
|
|
if queued.Op != OpPrint || queued.Object != key || queued.Copies != 2 || len(raw) >= 4096 {
|
|
t.Errorf("queued %s", raw)
|
|
}
|
|
for _, banned := range []string{"path", "node", "url", "data"} {
|
|
if _, has := fields[banned]; has {
|
|
t.Errorf("the queued ask carries %q: %s", banned, raw)
|
|
}
|
|
}
|
|
m := got.(map[string]any)
|
|
if m["job-id"] != float64(31) || m["node"] != "g14" || m["path"] != "/home/ops/letter.pdf" {
|
|
t.Errorf("answer = %v", m)
|
|
}
|
|
if _, sent := fake.request(t); string(sent) != string(doc) {
|
|
t.Errorf("the printer was not sent the stored document")
|
|
}
|
|
}
|
|
|
|
// 1598/AC3: an upload refused by node-files is answered with node-files' reason, and nothing is printed.
|
|
func TestARefusedUploadIsAnsweredWithItsReasonAndNothingIsPrinted(t *testing.T) {
|
|
fake := newIPPFake(t, 1, 3)
|
|
up := withUpload(t, "", errors.New("/home/ops/.ssh/id_ed25519 is refused by name: it is in .ssh, which holds an account's credentials"))
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
_, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "g14"})
|
|
if err == nil || !strings.Contains(err.Error(), "is refused by name: it is in .ssh") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if len(up.seen()) != 1 || r.bus.asks() != 0 || fake.body != nil {
|
|
t.Errorf("%d uploads, %d asks queued, printer sent %d bytes", len(up.seen()), r.bus.asks(), len(fake.body))
|
|
}
|
|
}
|
|
|
|
// 1598/AC4: the device reads document-format-supported before the job and refuses a format the printer does not
|
|
// list, naming the list; a printer that cannot be asked is sent nothing.
|
|
func TestTheDeviceChecksTheFormatsThePrinterListsBeforeTheJob(t *testing.T) {
|
|
fake := newIPPFake(t, 5, 3)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
doc := pdfOf(300)
|
|
key := sha256Hex(doc) + ".pdf"
|
|
r.objects.Put(t.Context(), key, "application/pdf", doc)
|
|
if _, err := r.call(t, "printer.print", map[string]any{"object": key}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fake.asked != 1 || fake.body == nil {
|
|
t.Fatalf("the formats were asked %d times, the job was sent: %v", fake.asked, fake.body != nil)
|
|
}
|
|
|
|
fake.body, fake.formats = nil, []string{"image/urf", "image/jpeg"}
|
|
_, err := r.call(t, "printer.print", map[string]any{"object": key})
|
|
if err == nil || !strings.Contains(err.Error(), "application/pdf") || !strings.Contains(err.Error(), "image/urf, image/jpeg") {
|
|
t.Fatalf("a format not listed: error = %v", err)
|
|
}
|
|
if fake.body != nil {
|
|
t.Error("a job was sent in a format the printer does not list")
|
|
}
|
|
|
|
fake.formats = nil
|
|
if _, err := r.call(t, "printer.print", map[string]any{"object": key}); err == nil || fake.body != nil {
|
|
t.Errorf("a printer that lists nothing: error = %v, sent %v", err, fake.body != nil)
|
|
}
|
|
}
|
|
|
|
// 1598/AC5: brother's s3-bucket requirement shares put with seat:node-files; brother invokes seat:node-files.upload;
|
|
// the bucket's name reaches the tool side through a file the mesh writes from the binding.
|
|
func TestManifestSharesPutWithNodeFilesAndInvokesItsUpload(t *testing.T) {
|
|
m := readManifest(t)
|
|
if len(m.Requires) != 1 || m.Requires[0].Name != "s3-bucket" || len(m.Requires[0].Shares) != 1 ||
|
|
m.Requires[0].Shares[0].To != "seat:node-files" || strings.Join(m.Requires[0].Shares[0].Methods, ",") != "put" {
|
|
t.Errorf("requires = %+v", m.Requires)
|
|
}
|
|
if len(m.Invokes) != 1 || m.Invokes[0] != "seat:node-files.upload" {
|
|
t.Errorf("invokes = %v", m.Invokes)
|
|
}
|
|
var bucket map[string]any
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "bucket" {
|
|
bucket = r
|
|
}
|
|
}
|
|
if bucket == nil || bucket["path"] != "${dir:state}/bucket.json" || bucket["template"] != "files/bucket.json" {
|
|
t.Fatalf("bucket resource = %v", bucket)
|
|
}
|
|
raw, _ := os.ReadFile("../../files/bucket.json")
|
|
if !strings.Contains(string(raw), "${bound:s3-bucket:bucket}") || strings.Contains(string(raw), "secret") {
|
|
t.Errorf("files/bucket.json = %s", raw)
|
|
}
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["name"] == "tools" && a["env"].(map[string]any)["BROTHER_BUCKET"] != "${dir:state}/bucket.json" {
|
|
t.Errorf("the tools bundle is not given BROTHER_BUCKET: %v", a["env"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// quickly makes a call wait this long before it answers started, for the test.
|
|
func quickly(t *testing.T, d time.Duration) {
|
|
t.Helper()
|
|
old := quickWait
|
|
quickWait = d
|
|
t.Cleanup(func() { quickWait = old })
|
|
}
|
|
|
|
// 1598/AC7: a device answer slower than the call's bound is answered as started with the ask id within the bound, and
|
|
// printer.print {ask} then returns the device's answer.
|
|
func TestASlowPrintIsAnsweredAsStartedAndReadWithItsAsk(t *testing.T) {
|
|
fake := newIPPFake(t, 71, 5)
|
|
fake.delay = 400 * time.Millisecond
|
|
quickly(t, 60*time.Millisecond)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
doc := pdfOf(300)
|
|
key := sha256Hex(doc) + ".pdf"
|
|
r.objects.Put(t.Context(), key, "application/pdf", doc)
|
|
began := time.Now()
|
|
got, err := r.call(t, "printer.print", map[string]any{"object": key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := got.(map[string]any)
|
|
id, _ := m["ask"].(string)
|
|
if m["state"] != "started" || id == "" || time.Since(began) > 300*time.Millisecond {
|
|
t.Fatalf("answer %v after %s", m, time.Since(began))
|
|
}
|
|
if QuickWait != 25*time.Second {
|
|
t.Errorf("a call waits %s, want 25s", QuickWait)
|
|
}
|
|
// Read while it goes on, and after: the same id.
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
read, err := r.call(t, "printer.print", map[string]any{"ask": id})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rm := read.(map[string]any)
|
|
if rm["state"] == nil {
|
|
if rm["job-id"] != float64(71) || rm["object"] != key {
|
|
t.Errorf("the outcome = %v", rm)
|
|
}
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("never done: %v", rm)
|
|
}
|
|
}
|
|
// A restarted tool process has no job: the device's answers are read.
|
|
r2 := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
r2.answers.m = map[string]json.RawMessage{"0123456789abcdef01234567": json.RawMessage(`{"job-id": 5}`)}
|
|
if read, err := r2.call(t, "printer.print", map[string]any{"ask": "0123456789abcdef01234567"}); err != nil || read.(map[string]any)["job-id"] != float64(5) {
|
|
t.Errorf("read from the answers: %v, %v", read, err)
|
|
}
|
|
if _, err := r.call(t, "printer.print", map[string]any{"ask": id, "copies": float64(2)}); err == nil {
|
|
t.Error("ask with another argument was accepted")
|
|
}
|
|
if _, err := r.call(t, "printer.print", map[string]any{"ask": "../x"}); err == nil {
|
|
t.Error("a made-up ask id was accepted")
|
|
}
|
|
}
|
|
|
|
// 1598/AC8: printer.print {path, node} whose upload and print together pass the bound answers started with the ask id,
|
|
// never a timeout, and the print follows.
|
|
func TestASlowUploadAndPrintAnswersStartedNeverATimeout(t *testing.T) {
|
|
fake := newIPPFake(t, 72, 5)
|
|
fake.delay = 100 * time.Millisecond
|
|
quickly(t, 60*time.Millisecond)
|
|
doc := pdfOf(300)
|
|
key := sha256Hex(doc)
|
|
up := withUpload(t, key, nil)
|
|
old := uploadVia
|
|
uploadVia = func(node, path, bucket string) (string, error) {
|
|
time.Sleep(150 * time.Millisecond) // node-files is slow
|
|
return old(node, path, bucket)
|
|
}
|
|
t.Cleanup(func() { uploadVia = old })
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
r.objects.Put(t.Context(), key, "application/pdf", doc)
|
|
got, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/letter.pdf", "node": "g14"})
|
|
if err != nil {
|
|
t.Fatalf("a slow upload failed the call: %v", err)
|
|
}
|
|
m := got.(map[string]any)
|
|
id, _ := m["ask"].(string)
|
|
if m["state"] != "started" || id == "" || len(up.seen()) > 1 {
|
|
t.Fatalf("answer %v", m)
|
|
}
|
|
for deadline := time.Now().Add(5 * time.Second); ; {
|
|
read, err := r.call(t, "printer.print", map[string]any{"ask": id})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rm := read.(map[string]any); rm["state"] == nil {
|
|
if rm["job-id"] != float64(72) || rm["path"] != "/home/ops/letter.pdf" || rm["node"] != "g14" {
|
|
t.Errorf("the outcome = %v", rm)
|
|
}
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("never done")
|
|
}
|
|
}
|
|
if len(up.seen()) != 1 || len(r.bus.published) != 1 || r.bus.published[0].Object != key {
|
|
t.Errorf("uploads %v, asks %v", up.seen(), r.bus.published)
|
|
}
|
|
// An upload that fails after the call answered is the outcome, read with the ask.
|
|
uploadVia = func(node, path, bucket string) (string, error) {
|
|
time.Sleep(150 * time.Millisecond)
|
|
return "", errors.New("/home/ops/.ssh/id_ed25519 is refused by name")
|
|
}
|
|
got, _ = r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "g14"})
|
|
id2, _ := got.(map[string]any)["ask"].(string)
|
|
time.Sleep(300 * time.Millisecond)
|
|
if _, err := r.call(t, "printer.print", map[string]any{"ask": id2}); err == nil || !strings.Contains(err.Error(), "refused by name") {
|
|
t.Errorf("the failed upload's outcome: %v", err)
|
|
}
|
|
}
|