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

597 lines
25 KiB
Go

package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// pdfOf is a document that is a PDF to the sniff and size bytes long, its bytes varying so that a part out of
// place changes the join.
func pdfOf(size int) []byte {
b := make([]byte, size)
copy(b, "%PDF-1.4\n")
for i := 9; i < size; i++ {
b[i] = byte(i*7 + i/251)
}
return b
}
// 1453/AC10: a document is cut into parts of at most PartSize, under the limit, and the parts joined in
// order are the document, with the SHA-256 checked; a wrong SHA-256 or a part missing is refused; a part out of
// range is refused naming the range.
func TestADocumentIsCutIntoPartsAndJoinedWithItsSha256Checked(t *testing.T) {
doc := pdfOf(2*AnswerPartSize + 1234)
n := answerPartCount(len(doc))
if n != 3 {
t.Fatalf("%d parts, want 3", n)
}
var parts [][]byte
for i := 1; i <= n; i++ {
p, total, err := cutPart(doc, i)
if err != nil || total != n {
t.Fatalf("part %d: %v (of %d)", i, err, total)
}
if len(p) > AnswerPartSize {
t.Errorf("part %d is %d bytes, over %d", i, len(p), AnswerPartSize)
}
parts = append(parts, p)
}
got, err := joinParts(parts, sha256Hex(doc))
if err != nil || !bytes.Equal(got, doc) {
t.Fatalf("the join is not the document: %v", err)
}
if _, err := joinParts(parts, sha256Hex([]byte("other"))); err == nil || !strings.Contains(err.Error(), sha256Hex(doc)) {
t.Errorf("a wrong SHA-256 was accepted or the error does not name the joined one: %v", err)
}
if _, err := joinParts(parts[:2], sha256Hex(doc)); err == nil {
t.Errorf("a join with a part missing passed its SHA-256")
}
for _, bad := range []int{0, -1, n + 1} {
if _, _, err := cutPart(doc, bad); err == nil || !strings.Contains(err.Error(), "out of range") {
t.Errorf("part %d: error = %v", bad, err)
}
}
if partCount(0) != 1 {
t.Errorf("an empty document has one part")
}
}
// 1453/AC10: scanner.document gives each part of a kept scan with the part count, the size and the SHA-256, each
// answer within the bus's 1 MiB; the parts joined are the scan. A part out of range, or a key that is no scan's, is
// refused, the first before anything is queued.
func TestScannerDocumentGivesEachPartOfAKeptScan(t *testing.T) {
r := newRig(t, `{}`, "pw")
doc := pdfOf(AnswerPartSize + 5000)
key := sha256Hex(doc) + ".pdf"
r.objects.Put(t.Context(), key, "application/pdf", doc)
var joined []byte
for i := 1; i <= 2; i++ {
got, err := r.call(t, "scanner.document", map[string]any{"object": key, "part": float64(i)})
if err != nil {
t.Fatalf("part %d: %v", i, err)
}
m := got.(map[string]any)
if m["parts"] != float64(2) || m["size"] != float64(len(doc)) || m["sha256"] != sha256Hex(doc) || m["part"] != float64(i) {
t.Errorf("part %d answer = %v", i, m)
}
raw, err := base64.StdEncoding.DecodeString(m["data"].(string))
if err != nil {
t.Fatal(err)
}
joined = append(joined, raw...)
}
if !bytes.Equal(joined, doc) {
t.Errorf("the parts fetched are not the scan")
}
before := r.bus.asks()
if _, err := r.call(t, "scanner.document", map[string]any{"object": key, "part": float64(0)}); err == nil {
t.Errorf("part 0 was asked for")
}
if r.bus.asks() != before {
t.Errorf("an ask was queued for a part the tool could refuse")
}
if _, err := r.call(t, "scanner.document", map[string]any{"object": key, "part": float64(3)}); err == nil || !strings.Contains(err.Error(), "out of range") {
t.Errorf("part 3: error = %v", err)
}
for _, k := range []string{"../etc/passwd", "uploads/x.pdf", strings.ToUpper(key)} {
if _, err := r.call(t, "scanner.document", map[string]any{"object": k, "part": float64(1)}); err == nil || !strings.Contains(err.Error(), "not a scan's key") {
t.Errorf("object %q: error = %v", k, err)
}
}
if _, err := r.call(t, "scanner.document", map[string]any{"object": sha256Hex([]byte("none")) + ".pdf", "part": float64(1)}); err == nil {
t.Errorf("a scan that is not kept was given")
}
}
// 1453/AC10: a scan of more than one part is fetched whole by scanner.scan, joined, checked and filed.
func TestScanNowFetchesAScanOfSeveralParts(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, _ := scanRig(t, f, Settings{Crop: false})
// Everything a scan is made of but its size is the real one; the stored object is what the parts carry, so a
// bigger object stands for a bigger scan: replace what the device keeps with a three-part document.
big := pdfOf(2*AnswerPartSize + 99)
bigKey := sha256Hex(big) + ".pdf"
r.objects.Put(t.Context(), bigKey, "application/pdf", big)
got := make([][]byte, 0, 3)
for i := 1; i <= 3; i++ {
p, err := r.worker.document(t.Context(), Ask{Op: OpDocument, Object: bigKey, Part: i})
if err != nil {
t.Fatal(err)
}
raw, _ := base64.StdEncoding.DecodeString(p["data"].(string))
got = append(got, raw)
}
if joined, err := joinParts(got, sha256Hex(big)); err != nil || !bytes.Equal(joined, big) {
t.Fatalf("three parts do not join: %v", err)
}
body, err := r.scan(t)
if err != nil {
t.Fatal(err)
}
if body["path"] == nil {
t.Errorf("no path: %v", body)
}
}
func upload(t *testing.T, r *printerRig, id string, doc []byte, sha string, part, parts int, piece []byte) (any, error) {
t.Helper()
return r.call(t, "printer.upload", map[string]any{"upload": id, "part": float64(part), "parts": float64(parts), "sha256": sha,
"data": base64.StdEncoding.EncodeToString(piece)})
}
// 1453/AC11: an upload in parts, in any order, is stored as uploads/<id>.pdf once all have come and the joined
// SHA-256 matches; until then it says how many came and stores nothing.
func TestAnUploadIsStoredOnceAllItsPartsCameAndTheSha256Matches(t *testing.T) {
r := newRig(t, `{}`, "pw")
doc := pdfOf(PartSize + 700)
sum := sha256Hex(doc)
p1, _, _ := cutPartOf(doc, 1, PartSize)
p2, _, _ := cutPartOf(doc, 2, PartSize)
got, err := upload(t, r, "up-1", doc, sum, 2, 2, p2)
if err != nil {
t.Fatal(err)
}
if m := got.(map[string]any); m["complete"] != false || m["received"] != float64(1) {
t.Errorf("after one part: %v", m)
}
if len(r.objects.keys()) != 0 {
t.Errorf("a document was stored before its parts all came: %v", r.objects.keys())
}
got, err = upload(t, r, "up-1", doc, sum, 1, 2, p1)
if err != nil {
t.Fatal(err)
}
m := got.(map[string]any)
if m["complete"] != true || m["object"] != "uploads/up-1.pdf" || m["sha256"] != sum || m["size"] != float64(len(doc)) {
t.Errorf("after the last part: %v", m)
}
if kept, err := r.objects.Get(t.Context(), "uploads/up-1.pdf"); err != nil || !bytes.Equal(kept, doc) {
t.Errorf("the stored document is not the upload (%v)", err)
}
// A JPEG is stored with its own extension.
jpg := append([]byte{0xFF, 0xD8, 0xFF, 0xE0}, bytes.Repeat([]byte{7}, 100)...)
if _, err := upload(t, r, "pic", jpg, sha256Hex(jpg), 1, 1, jpg); err != nil {
t.Fatal(err)
}
if _, err := r.objects.Get(t.Context(), "uploads/pic.jpg"); err != nil {
t.Errorf("the JPEG is not stored as uploads/pic.jpg: %v", err)
}
}
// 1453/AC11: a wrong SHA-256 on the joined document stores nothing and is said; a document that is neither PDF nor
// JPEG stores nothing; a part out of range, a part larger than the limit, a part that is not base64 and parts that
// disagree are refused; the upload can then be sent again whole.
func TestAnUploadWithAWrongSha256OrAnOutOfRangePartStoresNothing(t *testing.T) {
r := newRig(t, `{}`, "pw")
doc := pdfOf(300)
wrong := sha256Hex([]byte("not it"))
if _, err := upload(t, r, "bad", doc, wrong, 1, 1, doc); err == nil || !strings.Contains(err.Error(), "refused") || !strings.Contains(err.Error(), sha256Hex(doc)) {
t.Errorf("a wrong SHA-256: error = %v", err)
}
if len(r.objects.keys()) != 0 || r.objects.puts != 0 {
t.Errorf("a document with a wrong SHA-256 was stored: %v", r.objects.keys())
}
text := []byte("plain text, no PDF")
if _, err := upload(t, r, "txt", text, sha256Hex(text), 1, 1, text); err == nil || !strings.Contains(err.Error(), "neither a PDF nor a JPEG") {
t.Errorf("a text upload: error = %v", err)
}
for _, c := range []struct{ part, parts int }{{0, 1}, {2, 1}, {1, 0}, {1, MostParts + 1}} {
if _, err := upload(t, r, "range", doc, sha256Hex(doc), c.part, c.parts, doc); err == nil {
t.Errorf("part %d of %d was taken", c.part, c.parts)
}
}
if _, err := upload(t, r, "range", doc, sha256Hex(doc), 2, 1, doc); err == nil || !strings.Contains(err.Error(), "out of range") {
t.Errorf("part 2 of 1: error = %v", err)
}
if _, err := r.call(t, "printer.upload", map[string]any{"upload": "x", "part": float64(1), "parts": float64(1), "sha256": sha256Hex(doc), "data": "!!"}); err == nil {
t.Errorf("a part that is not base64 was taken")
}
if _, err := upload(t, r, "big", doc, sha256Hex(doc), 1, 1, make([]byte, MaxPart+1)); err == nil || !strings.Contains(err.Error(), "at most") {
t.Errorf("a part over the limit: error = %v", err)
}
if r.bus.asks() != 2 {
// Only the two uploads that were complete in form reached the device process; every refusal above them
// was the tool's.
t.Errorf("%d asks queued, want the 2 whose form was right", r.bus.asks())
}
// Parts that disagree about the whole.
p := pdfOf(PartSize + 10)
p1, _, _ := cutPartOf(p, 1, PartSize)
if _, err := upload(t, r, "mix", p, sha256Hex(p), 1, 2, p1); err != nil {
t.Fatal(err)
}
if _, err := upload(t, r, "mix", p, sha256Hex(p), 2, 3, p1); err == nil || !strings.Contains(err.Error(), "all say the same") {
t.Errorf("parts that disagree: error = %v", err)
}
if len(r.objects.keys()) != 0 {
t.Errorf("objects stored: %v", r.objects.keys())
}
}
// 1453/AC11: an incomplete upload's parts are dropped after fifteen minutes.
func TestAnIncompleteUploadIsDroppedAfterAWhile(t *testing.T) {
r := newRig(t, `{}`, "pw")
doc := pdfOf(PartSize + 10)
p1, _, _ := cutPartOf(doc, 1, PartSize)
p2, _, _ := cutPartOf(doc, 2, PartSize)
now := fixedNow
r.worker.Now = func() time.Time { return now }
if _, err := upload(t, r, "late", doc, sha256Hex(doc), 1, 2, p1); err != nil {
t.Fatal(err)
}
now = now.Add(pendingFor + time.Minute)
// Asked of the worker directly: the tool's own clock is the fixed one, and an ask this old would be stale.
got, err := r.worker.upload(t.Context(), Ask{Op: OpUpload, Upload: "late", Part: 2, Parts: 2, SHA256: sha256Hex(doc),
Data: base64.StdEncoding.EncodeToString(p2)})
if err != nil {
t.Fatal(err)
}
if got["complete"] != false {
t.Errorf("the dropped part was still there: %v", got)
}
}
// ippFake is a printer's IPP port: it records the request and answers a Print-Job answer with a job id and state.
type ippFake struct {
url string
body []byte
status int
}
func newIPPFake(t *testing.T, jobID, jobState int) *ippFake {
t.Helper()
f := &ippFake{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f.body, _ = io.ReadAll(r.Body)
ans := []byte{0x02, 0x00, byte(f.status >> 8), byte(f.status), 0, 0, 0, 1, ippOperation}
ans = appendAttr(ans, ippCharset, "attributes-charset", "utf-8")
ans = append(ans, ippJob)
ans = appendAttr(ans, ippInteger, "job-id", string(binary.BigEndian.AppendUint32(nil, uint32(jobID))))
ans = appendAttr(ans, ippEnum, "job-state", string(binary.BigEndian.AppendUint32(nil, uint32(jobState))))
ans = appendAttr(ans, ippKeyword, "job-state-reasons", "none")
ans = append(ans, ippEnd)
w.Header().Set("Content-Type", "application/ipp")
w.Write(ans)
}))
t.Cleanup(srv.Close)
f.url = srv.URL
old := ippURL
ippURL = func(string) string { return srv.URL }
t.Cleanup(func() { ippURL = old })
return f
}
// request splits what the fake was sent into its attributes and its document.
func (f *ippFake) request(t *testing.T) (IPPAnswer, []byte) {
t.Helper()
a, off, err := decodeIPP(f.body)
if err != nil {
t.Fatal(err)
}
return a, f.body[off:]
}
// 1453/AC12: printer.print sends a kept scan to the printer as an IPP Print-Job (operation 0x0002): the document
// bytes after the attributes, document-format by sniffing, copies, sides and media as asked, to the printer's own
// IPP address; the answer is the job's id and state.
func TestPrinterPrintSendsAPrintJobAndAnswersTheJob(t *testing.T) {
fake := newIPPFake(t, 77, 5)
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
doc := pdfOf(5000)
key := sha256Hex(doc) + ".pdf"
r.objects.Put(t.Context(), key, "application/pdf", doc)
got, err := r.call(t, "printer.print", map[string]any{"object": key, "copies": float64(2), "sides": "two-sided-long-edge", "media": "na_letter_8.5x11in"})
if err != nil {
t.Fatal(err)
}
m := got.(map[string]any)
if m["job-id"] != float64(77) || m["job-state"] != "processing" || m["format"] != "application/pdf" {
t.Errorf("answer = %v", m)
}
if binary.BigEndian.Uint16(fake.body[2:4]) != 0x0002 {
t.Errorf("operation = 0x%04x, want Print-Job 0x0002", binary.BigEndian.Uint16(fake.body[2:4]))
}
attrs, sent := fake.request(t)
if !bytes.Equal(sent, doc) {
t.Errorf("the document sent is not the kept one (%d bytes, want %d)", len(sent), len(doc))
}
text := func(n string) string { return strings.Join(attrs.texts(n), ",") }
if text("document-format") != "application/pdf" || text("sides") != "two-sided-long-edge" || text("media") != "na_letter_8.5x11in" ||
text("printer-uri") != "ipp://192.0.2.10/ipp/port1" {
t.Errorf("attributes = %v", attrs.Attributes)
}
if c := attrs.Attributes["copies"]; len(c) != 1 || c[0].Integer != 2 {
t.Errorf("copies = %v", c)
}
}
// 1453/AC12: the defaults are one copy, one-sided, A4; a JPEG is sent as image/jpeg; an upload is printed by its id.
func TestPrinterPrintDefaultsAndFormatsAndUploads(t *testing.T) {
fake := newIPPFake(t, 8, 3)
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
jpg := append([]byte{0xFF, 0xD8, 0xFF, 0xE0}, bytes.Repeat([]byte{9}, 200)...)
if _, err := upload(t, r, "photo", jpg, sha256Hex(jpg), 1, 1, jpg); err != nil {
t.Fatal(err)
}
got, err := r.call(t, "printer.print", map[string]any{"upload": "photo"})
if err != nil {
t.Fatal(err)
}
if got.(map[string]any)["job-state"] != "pending" {
t.Errorf("answer = %v", got)
}
attrs, sent := fake.request(t)
if !bytes.Equal(sent, jpg) || strings.Join(attrs.texts("document-format"), "") != "image/jpeg" ||
strings.Join(attrs.texts("sides"), "") != "one-sided" || strings.Join(attrs.texts("media"), "") != DefaultMedia || attrs.Attributes["copies"][0].Integer != 1 {
t.Errorf("attributes = %v, %d bytes sent", attrs.Attributes, len(sent))
}
if _, err := r.call(t, "printer.print", map[string]any{"upload": "never-uploaded"}); err == nil || !strings.Contains(err.Error(), "not complete") {
t.Errorf("an upload never made: error = %v", err)
}
}
// 1453/AC12: a print that is not well formed is refused before anything is queued; the printer's refusal is an
// error naming its IPP status; a printer not set is said by name.
func TestPrinterPrintRefusesWhatIsNotWellFormed(t *testing.T) {
fake := newIPPFake(t, 1, 3)
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
key := sha256Hex([]byte("x")) + ".pdf"
for name, args := range map[string]map[string]any{
"neither": {},
"both": {"object": key, "upload": "u"},
"copies 0": {"object": key, "copies": float64(-1)},
"copies 100": {"object": key, "copies": float64(100)},
"sides": {"object": key, "sides": "sideways"},
"media": {"object": key, "media": "A4 paper"},
"not a key": {"object": "../x.pdf"},
"copies float": {"object": key, "copies": 1.5},
} {
if _, err := r.call(t, "printer.print", args); err == nil {
t.Errorf("%s: accepted", name)
}
}
if r.bus.asks() != 0 || fake.body != nil {
t.Errorf("%d asks queued, the printer was sent %d bytes", r.bus.asks(), len(fake.body))
}
doc := pdfOf(100)
dk := sha256Hex(doc) + ".pdf"
r.objects.Put(t.Context(), dk, "application/pdf", doc)
fake.status = 0x0400
if _, err := r.call(t, "printer.print", map[string]any{"object": dk}); err == nil || !strings.Contains(err.Error(), "0x0400") {
t.Errorf("a refused job: error = %v", err)
}
// A kept object that is not a document the printer takes is not sent.
fake.status, fake.body = 0, nil
r.objects.Put(t.Context(), key, "text/plain", []byte("hello"))
if _, err := r.call(t, "printer.print", map[string]any{"object": key}); err == nil || !strings.Contains(err.Error(), "neither a PDF nor a JPEG") || fake.body != nil {
t.Errorf("a text object: error = %v, sent %d bytes", err, len(fake.body))
}
un := newRig(t, `{}`, "pw")
if _, err := un.call(t, "printer.print", map[string]any{"object": key}); err == nil || !strings.Contains(err.Error(), "printer is not set") || un.bus.asks() != 0 {
t.Errorf("no printer: error = %v", err)
}
}
// The Print-Job request is the one RFC 8011 gives: header, operation attributes in order, the job's own, the
// end-of-attributes tag, then the document.
func TestThePrintJobRequestIsWellFormed(t *testing.T) {
doc := []byte("%PDF-1.4 hello")
b := EncodePrintJob("ipp://192.0.2.10/ipp/port1", PrintOptions{Format: "application/pdf", Copies: 3, Sides: "one-sided", Media: DefaultMedia, Name: "n"}, doc)
if !bytes.Equal(b[:8], []byte{2, 0, 0, 2, 0, 0, 0, 1}) || b[8] != ippOperation {
t.Errorf("header = %x", b[:9])
}
a, off, err := decodeIPP(b)
if err != nil || !bytes.Equal(b[off:], doc) {
t.Fatalf("decode: %v, document %q", err, b[off:])
}
if first := string(b[9:]); !strings.HasPrefix(first[3:], "attributes-charset") {
t.Errorf("the first attribute is not attributes-charset")
}
if len(a.Attributes["copies"]) != 1 || a.Attributes["copies"][0].Integer != 3 {
t.Errorf("copies = %v", a.Attributes["copies"])
}
}
// 1453: a scan and a print are acknowledged before the work (a slow one is not delivered again and done twice);
// the other operations are acknowledged after it.
func TestAScanAndAPrintAreAcknowledgedBeforeTheWork(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, _ := scanRig(t, f, Settings{Crop: true})
body, _ := json.Marshal(Ask{ID: "s1", Op: OpScan, At: fixedNow})
calls := 0
var postsAtAck int
act, _ := r.worker.Handle(body, func() { calls++; postsAtAck = f.posts })
if calls != 1 || postsAtAck != 0 || act != actionSettled || f.posts != 1 {
t.Errorf("early called %d times with %d scans begun; action %v; %d scans in all", calls, postsAtAck, act, f.posts)
}
body, _ = json.Marshal(Ask{ID: "s2", Op: OpSettings, Page: DefaultPage, At: fixedNow})
calls = 0
if act, _ := r.worker.Handle(body, func() { calls++ }); calls != 0 || act != actionAck {
t.Errorf("settings: early called %d times, action %v", calls, act)
}
}
// 1453/AC11: a part of 700 KiB is taken and one of a byte more is refused, by the module's own message naming
// 700 KiB, before it is queued; the limit and the size the module cuts are one.
func TestAPartOverSevenHundredKiBIsRefusedByTheModulesOwnMessage(t *testing.T) {
if MaxPart != PartSize || PartSize != 700<<10 {
t.Fatalf("MaxPart %d, PartSize %d: both are 700 KiB", MaxPart, PartSize)
}
r := newRig(t, `{}`, "pw")
big := make([]byte, 700<<10+1)
_, err := upload(t, r, "over", big, sha256Hex(big), 1, 1, big)
if err == nil || !strings.Contains(err.Error(), "700 KiB") {
t.Fatalf("a part of 700 KiB + 1 byte: error = %v, want the module's own message naming 700 KiB", err)
}
if r.bus.asks() != 0 {
t.Errorf("an oversized part was queued")
}
ok := pdfOf(700 << 10)
if _, err := upload(t, r, "exact", ok, sha256Hex(ok), 1, 1, ok); err != nil {
t.Errorf("a part of exactly 700 KiB: %v", err)
}
}
// 1535: a scan larger than the state's value limit (256 KiB) is filed whole, its SHA-256 equal, and the answer to
// the scan itself is small: it carries the event's body and never the document. Every part answer fits the state.
func TestAScanLargerThanTheStatesValueLimitIsFiledWholeAndItsAnswerIsSmall(t *testing.T) {
f := newFakeScanner(t, fixture(t, "scan.jpg"))
r, folder := scanRig(t, f, Settings{Crop: false})
// The real capture is small; the worker keeps a stand-in of 3 parts' size under the key the scan names.
big := pdfOf(222388 + 2*AnswerPartSize)
if len(big) <= StateMaxValue {
t.Fatal("the stand-in is not larger than the state's value limit")
}
r.worker.Capture = func(ctx context.Context, req ScanRequest) (Capture, error) {
return Capture{PDF: big, SHA256: sha256Hex(big), Resolution: 300, At: fixedNow}, nil
}
body, err := r.scan(t)
if err != nil {
t.Fatal(err)
}
path, _ := body["path"].(string)
got, err := os.ReadFile(path)
if err != nil || sha256Hex(got) != sha256Hex(big) || filepath.Dir(path) != folder {
t.Fatalf("filed %s: %v, SHA-256 equal %v", path, err, err == nil && sha256Hex(got) == sha256Hex(big))
}
for id, raw := range r.answers.m {
if len(raw) > StateMaxValue {
t.Errorf("answer %s is %d bytes, over the state's %d", id, len(raw), StateMaxValue)
}
}
for _, a := range r.bus.published {
if a.Op == OpScan {
if raw := r.answers.get(a.ID); len(raw) > 4<<10 || bytes.Contains(raw, []byte(`"data"`)) {
t.Errorf("the scan's answer is %d bytes and may carry the document: %s", len(raw), raw)
}
}
}
}
// 1535: an answer the state refuses ends the ask: a small error answer says why, and the queued message is
// terminated, never redelivered into the same failure; a scan's message, acknowledged early, stays settled.
func TestARefusedAnswerEndsTheAskWithAnErrorAndNoRedelivery(t *testing.T) {
r := newRig(t, `{}`, "pw")
big := pdfOf(300 << 10)
key := sha256Hex(big) + ".pdf"
r.objects.Put(t.Context(), key, "application/pdf", big)
// A part answer of 700 KiB, as the old cut made it, is over the state's limit; the worker must not loop on it.
r.worker.answerPart = PartSize
ask, _ := json.Marshal(Ask{ID: "big1", Op: OpDocument, Object: key, Part: 1, At: fixedNow})
act, _ := r.worker.Handle(ask, nil)
if act != actionTerm {
t.Errorf("action = %v, want term", act)
}
raw := r.answers.get("big1")
if raw == nil || !bytes.Contains(raw, []byte(`"error"`)) || len(raw) > 4<<10 {
t.Errorf("no small error answer: %s", raw)
}
}
// 1535: only a refused value (error code 10054, message too large) ends the ask; a store that is away, a timeout
// or a cut connection is redelivered, a document fetch being safe to repeat.
func TestOnlyATooLargeAnswerEndsTheAskATransientFailureIsRetried(t *testing.T) {
r := newRig(t, `{}`, "pw")
big := pdfOf(300 << 10)
key := sha256Hex(big) + ".pdf"
r.objects.Put(t.Context(), key, "application/pdf", big)
ask, _ := json.Marshal(Ask{ID: "t1", Op: OpDocument, Object: key, Part: 1, At: fixedNow})
r.answers.fail = fmt.Errorf("nats: timeout")
if act, d := r.worker.Handle(ask, nil); act != actionRetry || d <= 0 {
t.Errorf("a timeout: action %v delay %v, want retry", act, d)
}
r.answers.fail = nil
r.worker.answerPart = PartSize
ask2, _ := json.Marshal(Ask{ID: "t2", Op: OpDocument, Object: key, Part: 1, At: fixedNow})
if act, _ := r.worker.Handle(ask2, nil); act != actionTerm {
t.Errorf("a too large answer: action %v, want term", act)
}
}
// 1551: printer.print with a path reads the file, uploads it in parts and prints it in one call; a document of
// several parts arrives whole, and the answer has the job's id and state.
func TestPrinterPrintTakesAPathAndUploadsAndPrintsInOneCall(t *testing.T) {
fake := newIPPFake(t, 31, 5)
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
doc := pdfOf(2*PartSize + 1234)
file := filepath.Join(t.TempDir(), "letter.bin") // the name says nothing: the bytes are read
if err := os.WriteFile(file, doc, 0o600); err != nil {
t.Fatal(err)
}
got, err := r.call(t, "printer.print", map[string]any{"path": file, "copies": float64(2)})
if err != nil {
t.Fatal(err)
}
m := got.(map[string]any)
if m["job-id"] != float64(31) || m["job-state"] != "processing" || m["format"] != "application/pdf" || m["bytes"] != len(doc) {
t.Errorf("answer = %v", m)
}
attrs, sent := fake.request(t)
if !bytes.Equal(sent, doc) || attrs.Attributes["copies"][0].Integer != 2 {
t.Errorf("sent %d bytes (want %d), attributes %v", len(sent), len(doc), attrs.Attributes)
}
ops := r.bus.ops()
if len(ops) != 4 || ops[0] != OpUpload || ops[2] != OpUpload || ops[3] != OpPrint {
t.Errorf("asks = %v, want three upload parts and a print", ops)
}
}
// 1551: a file that is neither a PDF nor a JPEG by its bytes is refused whatever its name, and a path that is
// relative, missing, a folder, or named together with an object is refused; nothing is queued.
func TestPrinterPrintRefusesAPathThatIsNotAPDFOrJPEG(t *testing.T) {
fake := newIPPFake(t, 1, 5)
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
dir := t.TempDir()
text := filepath.Join(dir, "fake.pdf")
os.WriteFile(text, []byte("just text, named .pdf"), 0o600)
for name, args := range map[string]map[string]any{
"text named pdf": {"path": text},
"relative": {"path": "fake.pdf"},
"missing": {"path": filepath.Join(dir, "none.pdf")},
"folder": {"path": dir},
"with object": {"path": text, "object": sha256Hex([]byte("x")) + ".pdf"},
} {
if _, err := r.call(t, "printer.print", args); err == nil {
t.Errorf("%s: printed", name)
}
}
if _, err := r.call(t, "printer.print", map[string]any{"path": text}); err == nil || !strings.Contains(err.Error(), "neither a PDF nor a JPEG") {
t.Errorf("error = %v", err)
}
if r.bus.asks() != 0 || fake.body != nil {
t.Errorf("%d asks queued, printer sent %d bytes", r.bus.asks(), len(fake.body))
}
}