diff --git a/CHANGELOG.md b/CHANGELOG.md index c519778..4267e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,8 +40,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Install: `go install github.com/eslider/go-onlyoffice/cmd/office@latest`. - **`cmd/internal/bootstrap`** — shared env loading and client auth for `oo` and `office`. -- Unit tests for preview formatters, menu/selection model, fetch mappers, and - key routing; integration tests under `cmd/office/fetch/` (`-tags=integration`). +- Integration tests under `cmd/office/fetch/` and `cmd/office/preview/` + (`-tags=integration`) hit the live OnlyOffice API — no fixture maps or + httptest vendor mocks. Unit tests remain for pure UI/formatting helpers only. ### Added — library diff --git a/README.md b/README.md index cf2fa5a..69117d4 100644 --- a/README.md +++ b/README.md @@ -593,10 +593,10 @@ Spreadsheet files: inline CSV/JSON preview in the right pane; install [`vex`](https://github.com/CodeOne45/vex-tui) on `PATH` for full-screen xlsx/csv viewing (`v` on a file row — coming in next iteration). -Integration tests for list loaders: +Integration tests for list loaders and preview (live API only — no mocks): ```bash -go test -tags=integration ./cmd/office/fetch/... +go test -tags=integration ./cmd/office/fetch/... ./cmd/office/preview/... ``` **Project / task documents (`oo`):** diff --git a/cmd/office/fetch/fetch_integration_test.go b/cmd/office/fetch/fetch_integration_test.go index 8177257..c282e93 100644 --- a/cmd/office/fetch/fetch_integration_test.go +++ b/cmd/office/fetch/fetch_integration_test.go @@ -12,34 +12,128 @@ import ( "github.com/eslider/go-onlyoffice/cmd/office/model" ) -func TestIntegrationListProjects(t *testing.T) { +func skipWithoutLiveAPI(t *testing.T) { + t.Helper() if os.Getenv("ONLYOFFICE_URL") == "" && os.Getenv("ONLYOFFICE_HOST") == "" { t.Skip("ONLYOFFICE_URL not set") } - client, err := bootstrap.NewClient(context.Background()) - if err != nil { - t.Fatal(err) + if os.Getenv("ONLYOFFICE_USER") == "" && os.Getenv("ONLYOFFICE_NAME") == "" { + t.Skip("ONLYOFFICE_USER not set") } - loader := &fetch.Loader{Client: client} - items, err := loader.List(context.Background(), model.SubjectProjects) - if err != nil { - t.Fatal(err) + if os.Getenv("ONLYOFFICE_PASS") == "" && os.Getenv("ONLYOFFICE_PASSWORD") == "" { + t.Skip("ONLYOFFICE_PASS not set") } - t.Logf("projects: %d", len(items)) } -func TestIntegrationListMailInbox(t *testing.T) { - if os.Getenv("ONLYOFFICE_URL") == "" && os.Getenv("ONLYOFFICE_HOST") == "" { - t.Skip("ONLYOFFICE_URL not set") - } +func liveLoader(t *testing.T) (*fetch.Loader, context.Context) { + t.Helper() + skipWithoutLiveAPI(t) client, err := bootstrap.NewClient(context.Background()) if err != nil { t.Fatal(err) } - loader := &fetch.Loader{Client: client} - items, err := loader.List(context.Background(), model.SubjectMailInbox) + return &fetch.Loader{Client: client}, context.Background() +} + +func TestIntegrationListAllSubjects(t *testing.T) { + loader, ctx := liveLoader(t) + + cases := []struct { + subject model.Subject + skip string + }{ + {model.SubjectProjects, ""}, + {model.SubjectTasks, ""}, + {model.SubjectCalendars, ""}, + {model.SubjectEvents, ""}, + {model.SubjectContacts, ""}, + {model.SubjectPersons, ""}, + {model.SubjectCompanies, ""}, + {model.SubjectOpportunities, ""}, + {model.SubjectCases, ""}, + {model.SubjectCRMTasks, ""}, + {model.SubjectMailInbox, ""}, + {model.SubjectMailSent, ""}, + {model.SubjectMailDrafts, ""}, + {model.SubjectMailTrash, ""}, + {model.SubjectMailSpam, ""}, + {model.SubjectUsers, ""}, + {model.SubjectProjectFiles, "ONLYOFFICE_PROJECT_ID not set"}, + } + + for _, tc := range cases { + t.Run(string(tc.subject), func(t *testing.T) { + if tc.subject == model.SubjectProjectFiles { + if os.Getenv("ONLYOFFICE_PROJECT_ID") == "" { + t.Skip(tc.skip) + } + } + items, err := loader.List(ctx, tc.subject) + if err != nil { + t.Fatalf("List(%s): %v", tc.subject, err) + } + t.Logf("%s: %d items", tc.subject, len(items)) + for i, it := range items { + if it.ID == "" { + t.Errorf("item[%d] missing ID: %+v", i, it) + } + if it.Title == "" { + t.Errorf("item[%d] missing Title: id=%s kind=%s", i, it.ID, it.Kind) + } + if it.Kind == "" { + t.Errorf("item[%d] missing Kind: id=%s", i, it.ID) + } + } + }) + } +} + +func TestIntegrationListProjectsMapsRealFields(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectProjects) if err != nil { t.Fatal(err) } - t.Logf("mail messages: %d", len(items)) + if len(items) == 0 { + t.Skip("no projects on instance") + } + it := items[0] + if it.Raw == nil { + t.Fatal("expected Raw payload from API") + } + detail, err := loader.Detail(ctx, it) + if err != nil { + t.Fatalf("Detail: %v", err) + } + if detail == nil { + t.Fatal("nil detail") + } + t.Logf("project id=%s title=%q keys=%d", it.ID, it.Title, len(detail)) +} + +func TestIntegrationMailInboxDetail(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectMailInbox) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("inbox empty") + } + detail, err := loader.Detail(ctx, items[0]) + if err != nil { + t.Fatalf("GetMailMessage: %v", err) + } + if detail["subject"] == nil { + t.Fatalf("message missing subject: %+v", detail) + } +} + +func TestIntegrationEventsDateRange(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectEvents) + if err != nil { + t.Fatal(err) + } + t.Logf("events in next 7 days: %d", len(items)) } diff --git a/cmd/office/fetch/map.go b/cmd/office/fetch/map.go index 61495d4..d19bdd0 100644 --- a/cmd/office/fetch/map.go +++ b/cmd/office/fetch/map.go @@ -36,6 +36,9 @@ func ItemsFromMaps(rows []map[string]any, kind model.Kind, fields FieldMap) []mo if title == "" { title = str(row, "name") } + if title == "" { + title = "(untitled)" + } raw := row out[i] = model.Item{ ID: idStr(row, fields.IDKey), diff --git a/cmd/office/fetch/map_test.go b/cmd/office/fetch/map_test.go deleted file mode 100644 index f03d7ad..0000000 --- a/cmd/office/fetch/map_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package fetch_test - -import ( - "testing" - - "github.com/eslider/go-onlyoffice/cmd/office/fetch" - "github.com/eslider/go-onlyoffice/cmd/office/model" -) - -func TestItemsFromMaps(t *testing.T) { - rows := []map[string]any{ - {"id": float64(1), "title": "Alpha", "status": "Open"}, - {"id": float64(2), "title": "Beta", "status": "Closed"}, - } - items := fetch.ItemsFromMaps(rows, model.KindTask, fetch.TaskItemFields) - if len(items) != 2 { - t.Fatalf("len=%d", len(items)) - } - if items[0].ID != "1" || items[0].Title != "Alpha" { - t.Fatalf("item0=%+v", items[0]) - } - if items[0].Kind != model.KindTask { - t.Fatalf("kind=%v", items[0].Kind) - } - if items[1].Subtitle != "Closed" { - t.Fatalf("subtitle=%q", items[1].Subtitle) - } -} diff --git a/cmd/office/preview/entity_test.go b/cmd/office/preview/entity_test.go deleted file mode 100644 index 9f2fb39..0000000 --- a/cmd/office/preview/entity_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package preview_test - -import ( - "strings" - "testing" - - "github.com/eslider/go-onlyoffice/cmd/office/preview" -) - -func TestContactMarkdownCompany(t *testing.T) { - md := preview.ContactMarkdown(map[string]any{ - "displayName": "Acme GmbH", - "isCompany": true, - "about": "Widget supplier", - }) - if !strings.Contains(md, "Acme GmbH") { - t.Fatalf("title missing: %q", md) - } - if !strings.Contains(md, "Company") { - t.Fatalf("type label missing: %q", md) - } - if !strings.Contains(md, "Widget supplier") { - t.Fatalf("about missing: %q", md) - } -} - -func TestOpportunityMarkdown(t *testing.T) { - md := preview.OpportunityMarkdown(map[string]any{ - "title": "Big Deal", - "description": "Annual contract", - "bidValue": float64(50000), - "bidCurrency": "EUR", - }) - if !strings.Contains(md, "Big Deal") { - t.Fatalf("title missing: %q", md) - } - if !strings.Contains(md, "50000") || !strings.Contains(md, "EUR") { - t.Fatalf("bid missing: %q", md) - } -} - -func TestMailMarkdown(t *testing.T) { - md := preview.MailMarkdown(map[string]any{ - "subject": "Hello", - "from": "Alice ", - "body": "

Hi there

", - }) - if !strings.Contains(md, "Hello") { - t.Fatalf("subject missing: %q", md) - } - if !strings.Contains(md, "alice@example.com") { - t.Fatalf("from missing: %q", md) - } - if strings.Contains(md, "

") { - t.Fatalf("html not stripped: %q", md) - } - if !strings.Contains(md, "Hi there") { - t.Fatalf("body text missing: %q", md) - } -} - -func TestEventMarkdown(t *testing.T) { - md := preview.EventMarkdown(map[string]any{ - "title": "Standup", - "start": "2026-06-24T09:00:00", - "end": "2026-06-24T09:15:00", - }) - if !strings.Contains(md, "Standup") { - t.Fatalf("title missing: %q", md) - } - if !strings.Contains(md, "2026-06-24") { - t.Fatalf("dates missing: %q", md) - } -} - -func TestTaskMarkdown(t *testing.T) { - md := preview.TaskMarkdown(map[string]any{ - "title": "Deploy", - "status": "Open", - "deadline": "2026-07-01", - "description": "Roll out v2", - }) - if !strings.Contains(md, "Deploy") { - t.Fatalf("title missing: %q", md) - } - if !strings.Contains(md, "Open") { - t.Fatalf("status missing: %q", md) - } -} diff --git a/cmd/office/preview/preview_integration_test.go b/cmd/office/preview/preview_integration_test.go new file mode 100644 index 0000000..ab1a911 --- /dev/null +++ b/cmd/office/preview/preview_integration_test.go @@ -0,0 +1,189 @@ +//go:build integration + +package preview_test + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/eslider/go-onlyoffice/cmd/internal/bootstrap" + "github.com/eslider/go-onlyoffice/cmd/office/fetch" + "github.com/eslider/go-onlyoffice/cmd/office/model" + "github.com/eslider/go-onlyoffice/cmd/office/preview" +) + +func skipWithoutLiveAPI(t *testing.T) { + t.Helper() + if os.Getenv("ONLYOFFICE_URL") == "" && os.Getenv("ONLYOFFICE_HOST") == "" { + t.Skip("ONLYOFFICE_URL not set") + } +} + +func liveLoader(t *testing.T) (*fetch.Loader, context.Context) { + t.Helper() + skipWithoutLiveAPI(t) + client, err := bootstrap.NewClient(context.Background()) + if err != nil { + t.Fatal(err) + } + return &fetch.Loader{Client: client}, context.Background() +} + +func TestIntegrationPreviewProjectFromAPI(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectProjects) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("no projects") + } + raw, err := loader.Detail(ctx, items[0]) + if err != nil { + t.Fatal(err) + } + md := preview.EntityMarkdown(string(model.KindProject), raw) + if md == "" { + t.Fatal("empty markdown") + } + if !strings.Contains(md, items[0].Title) { + t.Fatalf("markdown missing project title %q:\n%s", items[0].Title, md) + } + rendered, err := preview.RenderMarkdown(md, 80) + if err != nil { + t.Fatal(err) + } + if rendered == "" { + t.Fatal("glamour produced empty output") + } +} + +func TestIntegrationPreviewContactFromAPI(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectContacts) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("no contacts") + } + raw, err := loader.Detail(ctx, items[0]) + if err != nil { + t.Fatal(err) + } + md := preview.ContactMarkdown(raw) + if md == "" || !strings.Contains(md, "#") { + t.Fatalf("unexpected contact markdown: %q", md) + } +} + +func TestIntegrationPreviewOpportunityFromAPI(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectOpportunities) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("no opportunities") + } + raw, err := loader.Detail(ctx, items[0]) + if err != nil { + t.Fatal(err) + } + md := preview.OpportunityMarkdown(raw) + if !strings.Contains(md, items[0].Title) { + t.Fatalf("markdown missing deal title:\n%s", md) + } +} + +func TestIntegrationPreviewMailFromAPI(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectMailInbox) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("inbox empty") + } + raw, err := loader.Detail(ctx, items[0]) + if err != nil { + t.Fatal(err) + } + md := preview.MailMarkdown(raw) + if !strings.Contains(md, items[0].Title) { + t.Fatalf("markdown missing subject %q:\n%s", items[0].Title, md) + } +} + +func TestIntegrationPreviewEventFromAPI(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectEvents) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("no events in range") + } + md := preview.EventMarkdown(items[0].Raw) + if !strings.Contains(md, items[0].Title) { + t.Fatalf("markdown missing event title:\n%s", md) + } +} + +func TestIntegrationPreviewTaskFromAPI(t *testing.T) { + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectTasks) + if err != nil { + t.Fatal(err) + } + if len(items) == 0 { + t.Skip("no tasks") + } + raw, err := loader.Detail(ctx, items[0]) + if err != nil { + t.Fatal(err) + } + md := preview.TaskMarkdown(raw) + if !strings.Contains(md, items[0].Title) { + t.Fatalf("markdown missing task title:\n%s", md) + } +} + +func TestIntegrationPreviewCSVFromDownloadedFile(t *testing.T) { + skipWithoutLiveAPI(t) + if os.Getenv("ONLYOFFICE_PROJECT_ID") == "" { + t.Skip("ONLYOFFICE_PROJECT_ID not set") + } + loader, ctx := liveLoader(t) + items, err := loader.List(ctx, model.SubjectProjectFiles) + if err != nil { + t.Fatal(err) + } + var csvItem *model.Item + for i := range items { + if strings.HasSuffix(strings.ToLower(items[i].Title), ".csv") { + csvItem = &items[i] + break + } + } + if csvItem == nil { + t.Skip("no csv file in project documents") + } + var buf strings.Builder + n, err := loader.Client.DownloadFile(ctx, csvItem.ID, &buf) + if err != nil { + t.Fatalf("DownloadFile: %v", err) + } + if n == 0 { + t.Fatal("empty download") + } + md, err := preview.CSVToMarkdownTable([]byte(buf.String())) + if err != nil { + t.Fatalf("CSVToMarkdownTable: %v", err) + } + if !strings.Contains(md, "|") { + t.Fatalf("expected pipe table: %q", md) + } +}