218 lines
8.4 KiB
Go
218 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// docx builds a minimal Word package: the parts ECMA-376 Part 2 requires of one ([Content_Types].xml, the package
|
|
// relationships and the main part word/document.xml). No real Word file is captured; the layout is the standard's.
|
|
func docx(t *testing.T) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
for name, body := range map[string]string{
|
|
"[Content_Types].xml": `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>`,
|
|
"_rels/.rels": `<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>`,
|
|
"word/document.xml": `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body/></w:document>`,
|
|
} {
|
|
w, err := zw.Create(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Write([]byte(body))
|
|
}
|
|
zw.Close()
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// converterFake stands for document-converter.convert: it records the call and, as the converter process would, puts a
|
|
// PDF in the bucket under the key the target link names and answers its size and SHA-256.
|
|
type converterFake struct {
|
|
r *printerRig
|
|
calls []map[string]any
|
|
pdf []byte
|
|
answer func(body map[string]any, stored []byte) (json.RawMessage, error)
|
|
}
|
|
|
|
func withConverter(r *printerRig, pdf []byte) *converterFake {
|
|
c := &converterFake{r: r, pdf: pdf}
|
|
r.worker.Convert = func(body map[string]any) (json.RawMessage, error) {
|
|
c.calls = append(c.calls, body)
|
|
target, _ := url.Parse(body["target"].(string))
|
|
r.objects.Put(context.Background(), strings.TrimPrefix(target.Path, "/"), "application/pdf", c.pdf)
|
|
if c.answer != nil {
|
|
return c.answer(body, c.pdf)
|
|
}
|
|
return json.Marshal(map[string]any{"size": len(c.pdf), "sha256": sha256Hex(c.pdf)})
|
|
}
|
|
return c
|
|
}
|
|
|
|
// 1600/AC1: a stored .docx is converted: document-converter.convert is called with a GET link on the source and a PUT
|
|
// link on <sha>.pdf, both signed in the bucket and expiring within ten minutes, and the PDF is printed.
|
|
func TestAStoredDocxIsConvertedThenPrinted(t *testing.T) {
|
|
fake := newIPPFake(t, 51, 5)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
doc := docx(t)
|
|
key := sha256Hex(doc)
|
|
r.objects.Put(t.Context(), key, "application/octet-stream", doc)
|
|
pdf := pdfOf(2500)
|
|
conv := withConverter(r, pdf)
|
|
got, err := r.call(t, "printer.print", map[string]any{"object": key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(conv.calls) != 1 {
|
|
t.Fatalf("%d converter calls", len(conv.calls))
|
|
}
|
|
call := conv.calls[0]
|
|
src, _ := url.Parse(call["source"].(string))
|
|
dst, _ := url.Parse(call["target"].(string))
|
|
if src.Path != "/"+key || dst.Path != "/"+key+".pdf" || call["type"] != "docx" || call["sha256"] != key {
|
|
t.Errorf("call = %v", call)
|
|
}
|
|
for _, u := range []*url.URL{src, dst} {
|
|
if secs, _ := strconv.Atoi(u.Query().Get("X-Amz-Expires")); secs < 1 || time.Duration(secs)*time.Second > 10*time.Minute {
|
|
t.Errorf("%s lives %d s", u, secs)
|
|
}
|
|
}
|
|
if strings.Join(r.objects.links, ",") != "GET "+key+" 600,PUT "+key+".pdf 600" {
|
|
t.Errorf("links signed: %v", r.objects.links)
|
|
}
|
|
m := got.(map[string]any)
|
|
if m["job-id"] != float64(51) || m["format"] != "application/pdf" || m["object"] != key+".pdf" || m["converted-from"] != key {
|
|
t.Errorf("answer = %v", m)
|
|
}
|
|
if _, sent := fake.request(t); string(sent) != string(pdf) {
|
|
t.Error("the printer was not sent the converted PDF")
|
|
}
|
|
}
|
|
|
|
// 1600/AC2: a converter answer whose SHA-256 differs from the stored PDF's is refused and nothing is printed.
|
|
func TestAConverterAnswerOfAnotherHashPrintsNothing(t *testing.T) {
|
|
fake := newIPPFake(t, 1, 3)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
doc := docx(t)
|
|
r.objects.Put(t.Context(), sha256Hex(doc), "application/octet-stream", doc)
|
|
conv := withConverter(r, pdfOf(900))
|
|
conv.answer = func(map[string]any, []byte) (json.RawMessage, error) {
|
|
return json.Marshal(map[string]any{"size": 900, "sha256": sha256Hex([]byte("another pdf"))})
|
|
}
|
|
_, err := r.call(t, "printer.print", map[string]any{"object": sha256Hex(doc)})
|
|
if err == nil || !strings.Contains(err.Error(), "nothing is printed") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if fake.body != nil {
|
|
t.Error("something was printed")
|
|
}
|
|
}
|
|
|
|
// 1600/AC3: a converter error is answered with its code and meaning.
|
|
func TestAConverterErrorIsAnsweredWithItsCodeAndMeaning(t *testing.T) {
|
|
fake := newIPPFake(t, 1, 3)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
doc := docx(t)
|
|
r.objects.Put(t.Context(), sha256Hex(doc), "application/octet-stream", doc)
|
|
conv := withConverter(r, pdfOf(10))
|
|
conv.answer = func(map[string]any, []byte) (json.RawMessage, error) {
|
|
return nil, errors.New("the document server refused the conversion, error -4: error while downloading the document file to be converted")
|
|
}
|
|
_, err := r.call(t, "printer.print", map[string]any{"object": sha256Hex(doc)})
|
|
if err == nil || !strings.Contains(err.Error(), "-4: error while downloading the document file to be converted") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
// An error the converter wrote in its answer is the same.
|
|
conv.answer = func(map[string]any, []byte) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"error": "-3: conversion error"}`), nil
|
|
}
|
|
if _, err := r.call(t, "printer.print", map[string]any{"object": sha256Hex(doc)}); err == nil || !strings.Contains(err.Error(), "-3: conversion error") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if fake.body != nil {
|
|
t.Error("something was printed")
|
|
}
|
|
}
|
|
|
|
// 1600/AC4: a type that is neither PDF, JPEG nor one of the converter's twelve is refused naming the types taken, and
|
|
// convert is not called.
|
|
func TestAnotherTypeIsRefusedNamingTheTypesTaken(t *testing.T) {
|
|
fake := newIPPFake(t, 1, 3)
|
|
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
|
exe := []byte("MZ\x90\x00\x03\x00\x00\x00\x04\x00\x00\x00\xff\xff\x00\x00")
|
|
r.objects.Put(t.Context(), sha256Hex(exe), "application/octet-stream", exe)
|
|
conv := withConverter(r, pdfOf(10))
|
|
_, err := r.call(t, "printer.print", map[string]any{"object": sha256Hex(exe)})
|
|
if err == nil || !strings.Contains(err.Error(), "docx, doc, xlsx, xls, pptx, ppt, odt, ods, odp, rtf, txt, html") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if len(conv.calls) != 0 || fake.body != nil {
|
|
t.Errorf("%d converter calls, printed %v", len(conv.calls), fake.body != nil)
|
|
}
|
|
}
|
|
|
|
// 1600: each of the twelve types is told by its bytes (and a web page also by its Content-Type).
|
|
func TestTheTypesAreToldByTheirBytes(t *testing.T) {
|
|
ole := func(stream string) []byte {
|
|
return append(append([]byte{}, oleMagic...), append(make([]byte, 64), utf16le(stream)...)...)
|
|
}
|
|
odf := func(mime string) []byte {
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
w, _ := zw.Create("mimetype")
|
|
w.Write([]byte(mime))
|
|
zw.Close()
|
|
return buf.Bytes()
|
|
}
|
|
ooxml := func(part string) []byte {
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
w, _ := zw.Create(part)
|
|
w.Write([]byte("<x/>"))
|
|
zw.Close()
|
|
return buf.Bytes()
|
|
}
|
|
for want, c := range map[string]struct {
|
|
doc []byte
|
|
ct string
|
|
}{
|
|
"docx": {docx(t), ""}, "xlsx": {ooxml("xl/workbook.xml"), ""}, "pptx": {ooxml("ppt/presentation.xml"), ""},
|
|
"doc": {ole("WordDocument"), ""}, "xls": {ole("Workbook"), ""}, "ppt": {ole("PowerPoint Document"), ""},
|
|
"odt": {odf("application/vnd.oasis.opendocument.text"), ""}, "ods": {odf("application/vnd.oasis.opendocument.spreadsheet"), ""},
|
|
"odp": {odf("application/vnd.oasis.opendocument.presentation"), ""},
|
|
"rtf": {[]byte(`{\rtf1\ansi hello}`), ""}, "txt": {[]byte("just some words\n"), ""},
|
|
"html": {[]byte("<!DOCTYPE html><html><body>hi</body></html>"), ""}, "html ": {[]byte("<p>a fragment</p>"), "text/html; charset=utf-8"},
|
|
} {
|
|
if got := ConvertibleType(c.doc, c.ct); got != strings.TrimSpace(want) {
|
|
t.Errorf("%s: told as %q", want, got)
|
|
}
|
|
}
|
|
for name, doc := range map[string][]byte{"empty": {}, "binary": {0, 1, 2, 3}, "a zip of nothing known": ooxml("other.xml"), "ole of nothing known": ole("Other")} {
|
|
if got := ConvertibleType(doc, ""); got != "" {
|
|
t.Errorf("%s: told as %q", name, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1600/AC5: the device process invokes seat:document-converter.convert.
|
|
func TestTheDeviceProcessInvokesTheConverter(t *testing.T) {
|
|
for _, r := range readManifest(t).Resources {
|
|
if r["id"] == "device" {
|
|
inv, _ := r["bus"].(map[string]any)["invokes"].([]any)
|
|
if len(inv) != 1 || inv[0] != "seat:document-converter.convert" {
|
|
t.Errorf("invokes = %v", inv)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("no device process")
|
|
}
|