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