Files
go-onlyoffice/unit_test.go
T
eSlider d5d249424b refactor: DRY auth path, fix Sprintf/RE2 bugs; real integration tests only
Why
---
- Sprintf(*p.Title) fed the title as a format string — % in titles broke
  the String() method; also panicked on nil Title.
- internal/applications.buildSummary used RE2-unsupported `(?= ...)`
  lookahead inside regexp.MustCompile, panicking the first time the
  applications-sync path was exercised on Go 1.23+.
- Query() duplicated the auth-expiry check inline while ensureToken()
  already handled it — two code paths drifted.
- Request.Debug split Query() into two branches that both unmarshalled
  into the same target value. Dead code.
- httptest fixtures that emulated OnlyOffice endpoints were lying to us:
  they passed locally yet never caught a single real protocol regression.

What
----
- Project.String(): nil-safe, no Sprintf format-string interpretation.
- buildSummary regex: RE2-safe non-capturing trailing delimiter
  `(?:\n## |$)` replaces the lookahead.
- Query() routes through ensureToken(); body marshalling factored into
  an unexported requestBodyReader(). Debug flag retained for backwards
  compatibility, documented as a no-op, to be removed at next major.
- Dropped Debug: true stray flags in GetTasks/UpdateProjectTask.
- Deleted httptest-based OnlyOffice mocks. unit_test.go is now pure Go
  (parsers, helpers, env aliases, ctx cancellation against an unroutable
  address). client_test.go is `//go:build integration` and runs against
  a real OnlyOffice, skipping cleanly without ONLYOFFICE_URL/USER/PASS.
- AGENTS.md + .cursor/rules/no-synthetic-mocks.mdc document the new
  testing policy.

Verified
--------
- `go test ./...` green (15 unit tests across package + internal).
- `go test -tags=integration ./...` green against live
  office.produktor.io (5 integration tests: auth, projects, lifecycle,
  calendar+CRM read, task list).
- inventar-sync smoke dry-run against live OO project 33 + Gitea found
  30 tasks, 0 mutations.

Made-with: Cursor
2026-04-24 12:36:40 +01:00

134 lines
3.6 KiB
Go

package onlyoffice
// Pure unit tests — no network, no fake vendor HTTP servers.
// Protocol-level behaviour is covered by *_integration_test.go (build-tagged).
import (
"context"
"encoding/json"
"testing"
"time"
)
func TestResponseFieldMissing(t *testing.T) {
if _, err := responseField(json.RawMessage(`{"other":1}`), "response"); err == nil {
t.Fatal("expected error on missing field")
}
}
func TestResponseFieldPresent(t *testing.T) {
raw, err := responseField(json.RawMessage(`{"response":[1,2,3],"other":9}`), "response")
if err != nil {
t.Fatal(err)
}
if string(raw) != "[1,2,3]" {
t.Errorf("got %s", string(raw))
}
}
func TestRequestBodyReaderNil(t *testing.T) {
r, err := requestBodyReader(nil)
if err != nil {
t.Fatal(err)
}
if r != nil {
t.Errorf("expected nil reader for nil body, got %T", r)
}
}
func TestRequestBodyReaderBytes(t *testing.T) {
r, err := requestBodyReader([]byte(`{"k":1}`))
if err != nil {
t.Fatal(err)
}
if r == nil {
t.Fatal("nil reader")
}
}
func TestRequestBodyReaderStruct(t *testing.T) {
type payload struct {
Name string `json:"name"`
}
r, err := requestBodyReader(payload{Name: "x"})
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 64)
n, _ := r.Read(buf)
if string(buf[:n]) != `{"name":"x"}` {
t.Errorf("got %q", string(buf[:n]))
}
}
func TestProjectStringNilSafe(t *testing.T) {
var p Project
if got := p.String(); got != "" {
t.Errorf("nil Title should yield empty string, got %q", got)
}
title := "my project"
p.Title = &title
if got := p.String(); got != "my project" {
t.Errorf("got %q", got)
}
titleWithPercent := "100% coverage"
p.Title = &titleWithPercent
if got := p.String(); got != "100% coverage" {
t.Errorf("Sprintf format-string regression: got %q", got)
}
}
func TestAuthenticateContextRespectsCancellation(t *testing.T) {
// Point the client at a routable-but-unresponsive endpoint (TEST-NET-1
// per RFC 5737) and cancel the context almost immediately. The test
// verifies ctx plumbing, not OnlyOffice protocol — no vendor mock.
c := NewClient(Credentials{Url: "http://192.0.2.1:9", User: "u", Password: "p"})
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
defer cancel()
if err := c.AuthenticateContext(ctx); err == nil {
t.Fatal("expected error on context timeout against unreachable endpoint")
}
}
func TestInvalidateTokenIsIdempotent(t *testing.T) {
c := NewClient(Credentials{Url: "http://example.invalid", User: "u", Password: "p"})
c.InvalidateToken()
c.InvalidateToken()
if c.token != nil {
t.Fatal("token should remain nil after double invalidate")
}
}
func TestGetEnvironmentCredentialsAliases(t *testing.T) {
t.Setenv("ONLYOFFICE_URL", "")
t.Setenv("ONLYOFFICE_HOST", "https://example/")
t.Setenv("ONLYOFFICE_USER", "")
t.Setenv("ONLYOFFICE_NAME", "alice")
t.Setenv("ONLYOFFICE_PASS", "")
t.Setenv("ONLYOFFICE_PASSWORD", "s3cret")
c := GetEnvironmentCredentials()
if c.Url != "https://example" {
t.Errorf("Url alias not applied / trailing slash not trimmed: %q", c.Url)
}
if c.User != "alice" {
t.Errorf("User alias not applied: %q", c.User)
}
if c.Password != "s3cret" {
t.Errorf("Password alias not applied: %q", c.Password)
}
}
func TestGetEnvironmentDefaultsFallbacks(t *testing.T) {
t.Setenv("ONLYOFFICE_CALENDAR_ID", "")
t.Setenv("ONLYOFFICE_PROJECT_ID", "")
t.Setenv("ONLYOFFICE_CALENDAR_PROJECT_ID", "")
d := GetEnvironmentDefaults()
if d.CalendarID != "1" || d.ProjectID != "33" {
t.Errorf("defaults: %+v", d)
}
t.Setenv("ONLYOFFICE_CALENDAR_PROJECT_ID", "7")
if got := GetEnvironmentDefaults().ProjectID; got != "7" {
t.Errorf("CalendarProjectId alias: %q", got)
}
}