Files
go-onlyoffice/docx_comment_test.go
eSlider 00b55caff8
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go 1.25) (pull_request) Successful in 22s
Tests / Test (Go stable) (pull_request) Successful in 37s
fix(docs): pair comment ids with the document order, not the spec order (#92)
2026-10-07 18:45:16 +01:00

182 lines
5.9 KiB
Go

package onlyoffice
import (
"archive/zip"
"bytes"
"io"
"regexp"
"strings"
"testing"
)
// buildTestDocx returns a minimal valid .docx with two paragraphs.
func buildTestDocx(t *testing.T) []byte {
t.Helper()
const (
contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`
document = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:pPr><w:jc w:val="both"/></w:pPr><w:r><w:t>The term is two years, ending 8 October 2028.</w:t></w:r></w:p><w:p><w:r><w:t>Nothing relevant here.</w:t></w:r></w:p></w:body></w:document>`
rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`
)
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for name, body := range map[string]string{
"[Content_Types].xml": contentTypes,
"word/document.xml": document,
"word/_rels/document.xml.rels": rels,
} {
w, err := zw.Create(name)
if err != nil {
t.Fatalf("zip create: %v", err)
}
if _, err := w.Write([]byte(body)); err != nil {
t.Fatalf("zip write: %v", err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("zip close: %v", err)
}
return buf.Bytes()
}
func unzipPart(t *testing.T, data []byte, name string) string {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
t.Fatalf("zip reader: %v", err)
}
for _, f := range zr.File {
if f.Name != name {
continue
}
rc, err := f.Open()
if err != nil {
t.Fatalf("open part: %v", err)
}
defer rc.Close()
b, err := io.ReadAll(rc)
if err != nil {
t.Fatalf("read part: %v", err)
}
return string(b)
}
t.Fatalf("part %s not found", name)
return ""
}
func TestInjectDocxComments(t *testing.T) {
src := buildTestDocx(t)
comments := []DocxComment{
{Anchor: "8 October 2028", Text: "Change to two years."},
{Anchor: "two years", Text: "second anchor, same paragraph"},
{Anchor: "no such text anywhere", Text: "skipped"},
}
// First anchor matches the first paragraph; the second also matches that
// paragraph but must be skipped (one comment per paragraph).
out, applied, err := InjectDocxComments(src, "Andriy Oblivantsev", comments)
if err != nil {
t.Fatalf("inject: %v", err)
}
if applied != 1 {
t.Fatalf("applied = %d, want 1", applied)
}
doc := unzipPart(t, out, "word/document.xml")
for _, want := range []string{"commentRangeStart", "commentRangeEnd", "commentReference"} {
if !strings.Contains(doc, want) {
t.Errorf("document.xml missing %s", want)
}
}
if strings.Contains(doc, `<w:pPr><w:commentRangeStart`) {
t.Error("commentRangeStart inserted inside pPr")
}
cmts := unzipPart(t, out, "word/comments.xml")
if strings.Count(cmts, "<w:comment ") != 1 {
t.Fatalf("comments count = %d, want 1", strings.Count(cmts, "<w:comment "))
}
if !strings.Contains(cmts, `w:author="Andriy Oblivantsev"`) {
t.Error("author missing in comments.xml")
}
if !strings.Contains(cmts, "Change to two years.") {
t.Error("comment text missing")
}
ct := unzipPart(t, out, "[Content_Types].xml")
if !strings.Contains(ct, "/word/comments.xml") {
t.Error("content types missing comments override")
}
rels := unzipPart(t, out, "word/_rels/document.xml.rels")
if !strings.Contains(rels, "relationships/comments") {
t.Error("document rels missing comments relationship")
}
}
func TestInjectDocxCommentsNoMatch(t *testing.T) {
src := buildTestDocx(t)
out, applied, err := InjectDocxComments(src, "X", []DocxComment{{Anchor: "absent", Text: "t"}})
if err != nil {
t.Fatalf("inject: %v", err)
}
if applied != 0 {
t.Fatalf("applied = %d, want 0", applied)
}
if out == nil {
t.Fatal("output nil")
}
}
// TestInjectDocxCommentsOrder guards the marker-id ↔ comments.xml pairing when
// the spec order differs from the document order.
func TestInjectDocxCommentsOrder(t *testing.T) {
src := buildTestDocx(t)
spec := []DocxComment{
{Anchor: "Nothing relevant", Text: "comment B"},
{Anchor: "The term is two years", Text: "comment A"},
}
out, applied, err := InjectDocxComments(src, "A", spec)
if err != nil || applied != 2 {
t.Fatalf("inject: applied=%d err=%v", applied, err)
}
doc := unzipPart(t, out, "word/document.xml")
cmts := unzipPart(t, out, "word/comments.xml")
// id -> text from comments.xml
byID := map[string]string{}
for _, m := range regexp.MustCompile(`(?s)<w:comment w:id="(\d+)"[^>]*>(.*?)</w:comment>`).FindAllStringSubmatch(cmts, -1) {
txt := strings.TrimSpace(stripTags(m[2]))
byID[m[1]] = txt
}
// paragraph that starts with the term text must point at "comment A"
for _, p := range regexp.MustCompile(`(?s)<w:p\b[^>]*>.*?</w:p>`).FindAllString(doc, -1) {
if !strings.Contains(stripTags(p), "The term is two years") {
continue
}
m := regexp.MustCompile(`<w:commentRangeStart w:id="(\d+)"/>`).FindStringSubmatch(p)
if m == nil {
t.Fatal("no commentRangeStart in the term paragraph")
}
if got := byID[m[1]]; got != "comment A" {
t.Fatalf("term paragraph points at id %s = %q, want \"comment A\"", m[1], got)
}
return
}
t.Fatal("term paragraph not found")
}
func stripTags(s string) string {
return regexp.MustCompile(`<[^>]*>`).ReplaceAllString(s, "")
}
func TestParseDocxComments(t *testing.T) {
got, err := ParseDocxComments([]byte(`[{"anchor":"a","text":"b"}]`))
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(got) != 1 || got[0].Anchor != "a" || got[0].Text != "b" {
t.Fatalf("unexpected: %+v", got)
}
if _, err := ParseDocxComments([]byte(`{"anchor":"a"}`)); err == nil {
t.Fatal("expected error for object input")
}
}