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
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user