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
182 lines
5.9 KiB
Go
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")
|
|
}
|
|
}
|