Files
go-onlyoffice/httpx_test.go
T
eSlider 43e385ce9d feat: merge oo-cli and extend library with Calendar/CRM/subtasks/files
- Add Calendar helpers (ListCalendars/ListEvents/AddEvent/DeleteEvent)
- Add CRM helpers: contacts, opportunities, cases, CRM tasks, history notes
- Add project-task extras (ListTasks/ListAllTasks/GetTaskByID/AddTask/
  AddSubtask/UpdateTaskStatus/DeleteTask, GetProjectByID)
- Add UploadOpportunityFile (multipart) and SelfUserID caching
- Introduce Defaults struct, SetDefaults, GetEnvironmentDefaults;
  GetEnvironmentCredentials now accepts ONLYOFFICE_HOST/NAME/PASSWORD aliases
- Add public Authenticate() helper that eagerly primes the token
- Bundle Cobra CLI at cmd/oo-cli with full command surface
  (calendar, tasks, subtasks, CRM, applications-sync); CLI-only deps
  (cobra, godotenv) kept out of the library surface
- Add httptest-based unit tests for form/multipart helpers
- Docs: CHANGELOG, AGENTS, .env.example, README updates for v0.3 features

Consolidates the standalone eSlider/oo-cli tool into this canonical Go
module (see eSlider/inventar ASR-0008).

Made-with: Cursor
2026-04-24 11:30:11 +01:00

195 lines
5.9 KiB
Go

package onlyoffice
// Unit tests for the form/multipart helpers and a few CRM/Calendar/Subtask
// methods. These do not require a real OnlyOffice server — they spin up a
// net/http/httptest.Server that emulates the expected endpoints.
import (
"context"
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
)
// fakeServer wires a minimal OnlyOffice-like API: it always returns the same
// token on /api/2.0/authentication.json and dispatches remaining requests via
// the caller-supplied handler.
func fakeServer(t *testing.T, h http.HandlerFunc) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/api/2.0/authentication.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"response":{"token":"TEST_TOKEN","expires":"2099-01-01T00:00:00Z"}}`)
})
mux.HandleFunc("/", h)
return httptest.NewServer(mux)
}
// newTestClient builds a Client pointed at the test server with a primed token
// so helpers skip the auth round-trip when the test does not exercise it.
func newTestClient(srv *httptest.Server) *Client {
c := NewClient(Credentials{Url: srv.URL, User: "u", Password: "p"})
c.token = &Token{Value: "TEST_TOKEN", Expires: Time(time.Now().Add(time.Hour))}
return c
}
func TestAddSubtaskFormEncoded(t *testing.T) {
var gotPath, gotAuth, gotCT, gotBody string
srv := fakeServer(t, func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
gotCT = r.Header.Get("Content-Type")
b, _ := io.ReadAll(r.Body)
gotBody = string(b)
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"response":{"id":99,"title":"child"}}`)
})
defer srv.Close()
c := newTestClient(srv)
out, err := c.AddSubtask(context.Background(), "42", "child")
if err != nil {
t.Fatalf("AddSubtask: %v", err)
}
if gotPath != "/api/2.0/project/task/42.json" {
t.Errorf("path = %q", gotPath)
}
if gotAuth != "TEST_TOKEN" {
t.Errorf("auth header = %q", gotAuth)
}
if !strings.HasPrefix(gotCT, "application/x-www-form-urlencoded") {
t.Errorf("content-type = %q", gotCT)
}
if gotBody != "title=child" {
t.Errorf("body = %q", gotBody)
}
if fm, ok := out["title"].(string); !ok || fm != "child" {
t.Errorf("out.title = %v", out["title"])
}
}
func TestAddEventUsesDefaultCalendar(t *testing.T) {
var gotPath, gotBody string
srv := fakeServer(t, func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
b, _ := io.ReadAll(r.Body)
gotBody = string(b)
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"response":[{"id":"e1","name":"hi"}]}`)
})
defer srv.Close()
c := newTestClient(srv)
c.SetDefaults(Defaults{CalendarID: "7"})
ev, err := c.AddEvent(context.Background(), "", "hi", "2025-01-01T10:00:00Z", "2025-01-01T11:00:00Z", "desc", false)
if err != nil {
t.Fatalf("AddEvent: %v", err)
}
if gotPath != "/api/2.0/calendar/7/event.json" {
t.Errorf("path = %q", gotPath)
}
if !strings.Contains(gotBody, "name=hi") || !strings.Contains(gotBody, "isAllDayLong=false") {
t.Errorf("body = %q", gotBody)
}
if ev["id"] != "e1" {
t.Errorf("event id = %v", ev["id"])
}
}
func TestAddEventRequiresCalendar(t *testing.T) {
srv := fakeServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("server must not be hit without calendar id")
})
defer srv.Close()
c := newTestClient(srv)
if _, err := c.AddEvent(context.Background(), "", "t", "s", "e", "", false); err == nil {
t.Fatal("expected error when no calendar id and no default")
}
}
func TestListContactsTotal(t *testing.T) {
srv := fakeServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/2.0/crm/contact/filter.json" {
t.Errorf("path = %q", r.URL.Path)
}
if r.URL.Query().Get("filterValue") != "acme" {
t.Errorf("filterValue = %q", r.URL.Query().Get("filterValue"))
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"response":[{"id":1,"displayName":"ACME"}],"total":7}`)
})
defer srv.Close()
c := newTestClient(srv)
items, total, err := c.ListContacts(context.Background(), 50, 0, "acme")
if err != nil {
t.Fatalf("ListContacts: %v", err)
}
if total != 7 {
t.Errorf("total = %d", total)
}
if len(items) != 1 {
t.Fatalf("len(items) = %d", len(items))
}
}
func TestUploadOpportunityFileMultipart(t *testing.T) {
tmp := t.TempDir() + "/a.txt"
if err := writeFile(tmp, "hello"); err != nil {
t.Fatal(err)
}
var gotField, gotFilename, gotBody string
srv := fakeServer(t, func(w http.ResponseWriter, r *http.Request) {
ct := r.Header.Get("Content-Type")
if !strings.HasPrefix(ct, "multipart/form-data") {
t.Errorf("content-type = %q", ct)
}
_, params, err := mime.ParseMediaType(ct)
if err != nil {
t.Fatal(err)
}
mr := multipart.NewReader(r.Body, params["boundary"])
part, err := mr.NextPart()
if err != nil {
t.Fatal(err)
}
gotField = part.FormName()
gotFilename = part.FileName()
b, _ := io.ReadAll(part)
gotBody = string(b)
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"response":{"id":1,"title":"a.txt"}}`)
})
defer srv.Close()
c := newTestClient(srv)
out, err := c.UploadOpportunityFile(context.Background(), "123", tmp)
if err != nil {
t.Fatalf("UploadOpportunityFile: %v", err)
}
if gotField != "file" || gotFilename != "a.txt" || gotBody != "hello" {
t.Errorf("multipart = field=%q filename=%q body=%q", gotField, gotFilename, gotBody)
}
if out["title"] != "a.txt" {
t.Errorf("out.title = %v", out["title"])
}
}
func writeFile(path, content string) error {
return os.WriteFile(path, []byte(content), 0o600)
}
func TestResponseFieldMissing(t *testing.T) {
_, err := responseField(json.RawMessage(`{"other":1}`), "response")
if err == nil {
t.Fatal("expected error on missing field")
}
}