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 = ``
document = `The term is two years, ending 8 October 2028.Nothing relevant here.`
rels = ``
)
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, ` text from comments.xml
byID := map[string]string{}
for _, m := range regexp.MustCompile(`(?s)]*>(.*?)`).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)]*>.*?`).FindAllString(doc, -1) {
if !strings.Contains(stripTags(p), "The term is two years") {
continue
}
m := regexp.MustCompile(``).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")
}
}