Files
gotenberg/pkg/modules/qpdf/qpdf_test.go
T
2026-04-16 17:28:16 +02:00

272 lines
7.9 KiB
Go

package qpdf
import (
"encoding/json"
"log/slog"
"os"
"testing"
)
func TestStripQpdfStringPrefix(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"unicode prefix", "u:factur-x.xml", "factur-x.xml"},
{"binary prefix", "b:binary.bin", "binary.bin"},
{"encoded prefix", "e:encoded.txt", "encoded.txt"},
{"no prefix", "plain.xml", "plain.xml"},
{"empty string", "", ""},
{"prefix only", "u:", ""},
{"colon in value", "u:file:name.xml", "file:name.xml"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := stripQpdfStringPrefix(tt.input)
if got != tt.expected {
t.Errorf("stripQpdfStringPrefix(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
func TestParsePdfObjects(t *testing.T) {
tests := []struct {
name string
input string
wantKeys []string
wantError bool
}{
{
name: "valid QPDF JSON v2",
input: `{"qpdf":[{"jsonversion":2},{"obj:1 0 R":{"value":{"/Type":"/Catalog"}}}]}`,
wantKeys: []string{"obj:1 0 R"},
},
{
name: "invalid JSON",
input: `not json`,
wantError: true,
},
{
name: "empty qpdf array",
input: `{"qpdf":[]}`,
wantError: true,
},
{
name: "only header element",
input: `{"qpdf":[{"jsonversion":2}]}`,
wantError: true,
},
{
name: "multiple objects",
input: `{"qpdf":[{},{"obj:1 0 R":{"value":{}},"obj:2 0 R":{"value":{}}}]}`,
wantKeys: []string{"obj:1 0 R", "obj:2 0 R"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objects, err := parsePdfObjects([]byte(tt.input))
if tt.wantError {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, key := range tt.wantKeys {
if _, ok := objects[key]; !ok {
t.Errorf("expected key %q in objects", key)
}
}
})
}
}
func TestPatchFilespecMetadata(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
t.Run("sets AFRelationship on matching Filespec", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Catalog"}}`),
"obj:2 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:factur-x.xml"}}`),
}
metadata := map[string]map[string]string{
"factur-x.xml": {"relationship": "Data"},
}
catalogRef, _, filespecRefs, updateObjects := patchFilespecMetadata(logger, objects, metadata)
if catalogRef != "obj:1 0 R" {
t.Errorf("catalogRef = %q, want %q", catalogRef, "obj:1 0 R")
}
if len(filespecRefs) != 1 || filespecRefs[0] != "obj:2 0 R" {
t.Errorf("filespecRefs = %v, want [obj:2 0 R]", filespecRefs)
}
updated, ok := updateObjects["obj:2 0 R"]
if !ok {
t.Fatal("expected obj:2 0 R in updateObjects")
}
value := updated.(map[string]any)["value"].(map[string]any)
if value["/AFRelationship"] != "/Data" {
t.Errorf("/AFRelationship = %v, want /Data", value["/AFRelationship"])
}
})
t.Run("skips Filespec with no matching metadata", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:other.xml"}}`),
}
metadata := map[string]map[string]string{
"factur-x.xml": {"relationship": "Data"},
}
_, _, filespecRefs, _ := patchFilespecMetadata(logger, objects, metadata)
if len(filespecRefs) != 0 {
t.Errorf("filespecRefs = %v, want empty", filespecRefs)
}
})
t.Run("falls back to /F when /UF is absent", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/F":"u:factur-x.xml"}}`),
}
metadata := map[string]map[string]string{
"factur-x.xml": {"relationship": "Alternative"},
}
_, _, filespecRefs, updateObjects := patchFilespecMetadata(logger, objects, metadata)
if len(filespecRefs) != 1 {
t.Fatalf("filespecRefs = %v, want 1 entry", filespecRefs)
}
value := updateObjects["obj:1 0 R"].(map[string]any)["value"].(map[string]any)
if value["/AFRelationship"] != "/Alternative" {
t.Errorf("/AFRelationship = %v, want /Alternative", value["/AFRelationship"])
}
})
t.Run("sets stream Subtype via EF reference", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:2 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:factur-x.xml","/EF":{"/F":"3 0 R"}}}`),
"obj:3 0 R": json.RawMessage(`{"stream":{"dict":{"/Type":"/EmbeddedFile"}}}`),
}
metadata := map[string]map[string]string{
"factur-x.xml": {"mimeType": "text/xml"},
}
_, _, _, updateObjects := patchFilespecMetadata(logger, objects, metadata)
streamObj, ok := updateObjects["obj:3 0 R"]
if !ok {
t.Fatal("expected obj:3 0 R in updateObjects")
}
stream := streamObj.(map[string]any)["stream"].(map[string]any)
dict := stream["dict"].(map[string]any)
if dict["/Subtype"] != "/text/xml" {
t.Errorf("/Subtype = %v, want /text/xml", dict["/Subtype"])
}
})
}
func TestPatchCatalogAF(t *testing.T) {
t.Run("adds filespec refs to AF array", func(t *testing.T) {
catalogValue := map[string]any{"/Type": "/Catalog"}
updateObjects := make(map[string]any)
patchCatalogAF("obj:1 0 R", catalogValue, []string{"obj:2 0 R", "obj:3 0 R"}, updateObjects)
af, ok := catalogValue["/AF"].([]any)
if !ok {
t.Fatal("expected /AF to be []any")
}
if len(af) != 2 {
t.Fatalf("/AF has %d entries, want 2", len(af))
}
if af[0] != "2 0 R" || af[1] != "3 0 R" {
t.Errorf("/AF = %v, want [2 0 R, 3 0 R]", af)
}
})
t.Run("does not duplicate existing refs", func(t *testing.T) {
catalogValue := map[string]any{
"/Type": "/Catalog",
"/AF": []any{"2 0 R"},
}
updateObjects := make(map[string]any)
patchCatalogAF("obj:1 0 R", catalogValue, []string{"obj:2 0 R", "obj:3 0 R"}, updateObjects)
af := catalogValue["/AF"].([]any)
if len(af) != 2 {
t.Fatalf("/AF has %d entries, want 2", len(af))
}
})
t.Run("no-op when catalogRef is empty", func(t *testing.T) {
updateObjects := make(map[string]any)
patchCatalogAF("", nil, []string{"obj:2 0 R"}, updateObjects)
if len(updateObjects) != 0 {
t.Error("expected no updates for empty catalogRef")
}
})
}
func TestSetStreamSubtype(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
t.Run("sets Subtype in stream dict", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:3 0 R": json.RawMessage(`{"stream":{"dict":{"/Type":"/EmbeddedFile"}}}`),
}
updateObjects := make(map[string]any)
setStreamSubtype(logger, objects, updateObjects, "obj:3 0 R", "text/xml")
streamObj := updateObjects["obj:3 0 R"].(map[string]any)["stream"].(map[string]any)
dict := streamObj["dict"].(map[string]any)
if dict["/Subtype"] != "/text/xml" {
t.Errorf("/Subtype = %v, want /text/xml", dict["/Subtype"])
}
})
t.Run("auto-adds obj: prefix to ref", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:5 0 R": json.RawMessage(`{"stream":{"dict":{}}}`),
}
updateObjects := make(map[string]any)
setStreamSubtype(logger, objects, updateObjects, "5 0 R", "application/pdf")
if _, ok := updateObjects["obj:5 0 R"]; !ok {
t.Error("expected obj:5 0 R in updateObjects")
}
})
t.Run("warns on missing object", func(t *testing.T) {
objects := map[string]json.RawMessage{}
updateObjects := make(map[string]any)
setStreamSubtype(logger, objects, updateObjects, "obj:99 0 R", "text/xml")
if len(updateObjects) != 0 {
t.Error("expected no updates for missing object")
}
})
t.Run("warns on object without stream key", func(t *testing.T) {
objects := map[string]json.RawMessage{
"obj:3 0 R": json.RawMessage(`{"value":{"/Type":"/Page"}}`),
}
updateObjects := make(map[string]any)
setStreamSubtype(logger, objects, updateObjects, "obj:3 0 R", "text/xml")
if len(updateObjects) != 0 {
t.Error("expected no updates for non-stream object")
}
})
}