296 lines
10 KiB
Go
296 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestConfigRoundTripProtectsToken(t *testing.T) {
|
|
configFile := filepath.Join(t.TempDir(), "nested", "config.json")
|
|
want := Config{
|
|
BaseURL: "https://shop.example",
|
|
AccessToken: "secret-token",
|
|
TokenType: "Bearer",
|
|
ExpiresAt: time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
if err := saveConfig(configFile, want); err != nil {
|
|
t.Fatalf("saveConfig: %v", err)
|
|
}
|
|
got, err := loadConfig(configFile)
|
|
if err != nil {
|
|
t.Fatalf("loadConfig: %v", err)
|
|
}
|
|
if got.AccessToken != want.AccessToken || !got.ExpiresAt.Equal(want.ExpiresAt) || got.BaseURL != want.BaseURL {
|
|
t.Fatalf("round trip = %+v, want %+v", got, want)
|
|
}
|
|
info, err := os.Stat(configFile)
|
|
if err != nil {
|
|
t.Fatalf("stat config: %v", err)
|
|
}
|
|
if mode := info.Mode().Perm(); mode != 0o600 {
|
|
t.Fatalf("config mode = %o, want 600", mode)
|
|
}
|
|
}
|
|
|
|
func TestConfigPathHonorsXDGConfigHome(t *testing.T) {
|
|
xdg := t.TempDir()
|
|
t.Setenv("CMSCTL_CONFIG", "")
|
|
t.Setenv("XDG_CONFIG_HOME", xdg)
|
|
got, err := configPath("")
|
|
if err != nil {
|
|
t.Fatalf("configPath: %v", err)
|
|
}
|
|
want := filepath.Join(xdg, "cmsctl", "config.json")
|
|
if got != want {
|
|
t.Fatalf("configPath = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDeviceLoginPersistsTokenAndBaseURL(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/auth/device":
|
|
_, _ = io.WriteString(w, `{"device_code":"device-1","user_code":"ABCD-EFGH","verification_uri_complete":"http://example/device?code=ABCD-EFGH","expires_in":10,"interval":1}`)
|
|
case "/api/auth/device/token":
|
|
_, _ = io.WriteString(w, `{"access_token":"persisted-token","token_type":"Bearer","expires_in":3600}`)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
configFile := filepath.Join(t.TempDir(), "config.json")
|
|
var stdout, stderr bytes.Buffer
|
|
err := runWithStdin(
|
|
[]string{"--config", configFile, "--base-url", server.URL, "auth", "login", "--scope", "read"},
|
|
strings.NewReader(""),
|
|
&stdout,
|
|
&stderr,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("login: %v (stderr=%s)", err, stderr.String())
|
|
}
|
|
cfg, err := loadConfig(configFile)
|
|
if err != nil {
|
|
t.Fatalf("load persisted config: %v", err)
|
|
}
|
|
if cfg.AccessToken != "persisted-token" {
|
|
t.Fatalf("access token = %q", cfg.AccessToken)
|
|
}
|
|
if cfg.BaseURL != server.URL {
|
|
t.Fatalf("base URL = %q, want %q", cfg.BaseURL, server.URL)
|
|
}
|
|
}
|
|
|
|
func TestClientSendsBearerToken(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/nodes" {
|
|
t.Errorf("path = %q, want /api/nodes", r.URL.Path)
|
|
}
|
|
if got := r.Header.Get("Authorization"); got != "Bearer token-123" {
|
|
t.Errorf("authorization = %q", got)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"items":[],"total":0,"offset":0,"limit":50}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newClient(Config{BaseURL: server.URL, TokenType: "Bearer"}, "token-123", "", time.Second)
|
|
body, err := client.get(context.Background(), client.cmsURL("/nodes"))
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if !json.Valid(body) {
|
|
t.Fatalf("response is not JSON: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestFinderImportUsesBackofficeBaseURL(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/catalog/admin/items/batch" {
|
|
t.Errorf("finder import path = %q, want /catalog/admin/items/batch", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"status":"ok","inserts":1}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newClient(Config{BaseURL: server.URL, FinderURL: "http://reader.invalid"}, "device-token", "", time.Second)
|
|
var stdout bytes.Buffer
|
|
if err := runFinder(context.Background(), client, []string{"import", "-"}, strings.NewReader(`{"id":1,"title":"Piped"}`), &stdout); err != nil {
|
|
t.Fatalf("finder import: %v", err)
|
|
}
|
|
if !strings.Contains(stdout.String(), `"inserts":1`) {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunCMSCreateFromStdin(t *testing.T) {
|
|
var gotAuth, gotBody string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAuth = r.Header.Get("Authorization")
|
|
data, _ := io.ReadAll(r.Body)
|
|
gotBody = string(data)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"id":"node-1","dataType":"page","data":{"title":"Piped"}}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
configFile := filepath.Join(t.TempDir(), "config.json")
|
|
var stdout, stderr bytes.Buffer
|
|
err := runWithStdin(
|
|
[]string{"--config", configFile, "--base-url", server.URL, "--token", "device-token", "cms", "nodes", "create", "--input", "-"},
|
|
strings.NewReader(`{"dataType":"page","data":{"title":"Piped"}}`),
|
|
&stdout,
|
|
&stderr,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("run: %v (stderr=%s)", err, stderr.String())
|
|
}
|
|
if gotAuth != "Bearer device-token" {
|
|
t.Fatalf("authorization = %q", gotAuth)
|
|
}
|
|
if gotBody != `{"dataType":"page","data":{"title":"Piped"}}` {
|
|
t.Fatalf("request body = %q", gotBody)
|
|
}
|
|
if !strings.Contains(stdout.String(), `"node-1"`) {
|
|
t.Fatalf("stdout = %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestScanJSONLinesAcceptsArrayAndJSONL(t *testing.T) {
|
|
for _, input := range []string{
|
|
`[{"id":1},{"id":2}]`,
|
|
"{\"id\":1}\n{\"id\":2}\n",
|
|
} {
|
|
items, err := scanJSONLines([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("scanJSONLines(%q): %v", input, err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Fatalf("scanJSONLines(%q) = %d items, want 2", input, len(items))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCMSPatchAndEdgeCreateUseNativeEndpoints(t *testing.T) {
|
|
var methods, paths []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
methods = append(methods, r.Method)
|
|
paths = append(paths, r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.Method == http.MethodPost {
|
|
_, _ = io.WriteString(w, `{"id":"edge-1"}`)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, `{"id":"node-1","data":{"title":"merged"}}`)
|
|
}))
|
|
defer server.Close()
|
|
client := newClient(Config{BaseURL: server.URL}, "token", "", time.Second)
|
|
var output bytes.Buffer
|
|
if err := runCMSNodes(context.Background(), client, []string{"patch", "node-1", "--input", "-"}, strings.NewReader(`{"data":{"title":"merged"}}`), &output); err != nil {
|
|
t.Fatalf("patch: %v", err)
|
|
}
|
|
if err := runCMSEdges(context.Background(), client, []string{"create", "--from", "page-1", "--to", "node-1"}, strings.NewReader(""), &output); err != nil {
|
|
t.Fatalf("edge create: %v", err)
|
|
}
|
|
if strings.Join(methods, ",") != "PATCH,POST" {
|
|
t.Fatalf("methods = %v", methods)
|
|
}
|
|
if strings.Join(paths, ",") != "/api/nodes/node-1,/api/edges" {
|
|
t.Fatalf("paths = %v", paths)
|
|
}
|
|
}
|
|
|
|
func TestFinderSearchPassesFiltersAndStreamItemsUsesPipe(t *testing.T) {
|
|
var searchRequest map[string]any
|
|
var streamBody string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/find/api/stream":
|
|
if err := json.NewDecoder(r.Body).Decode(&searchRequest); err != nil {
|
|
t.Errorf("decode search request: %v", err)
|
|
}
|
|
_, _ = io.WriteString(w, `{"totalHits":1}`)
|
|
case "/find/api/stream-items":
|
|
data, _ := io.ReadAll(r.Body)
|
|
streamBody = string(data)
|
|
_, _ = io.WriteString(w, `{"id":42}`)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
client := newClient(Config{BaseURL: server.URL, FinderURL: server.URL + "/find"}, "token", "", time.Second)
|
|
var output bytes.Buffer
|
|
if err := runFinder(context.Background(), client, []string{"search", "--q", "drill", "--filters", `{"string":[{"id":"brand","value":["Dala"]}]}`, "--stock", "se", "--disable-grouping"}, strings.NewReader(""), &output); err != nil {
|
|
t.Fatalf("finder search: %v", err)
|
|
}
|
|
if searchRequest["query"] != "drill" || searchRequest["disableGrouping"] != true {
|
|
t.Fatalf("search request = %#v", searchRequest)
|
|
}
|
|
if _, ok := searchRequest["string"]; !ok {
|
|
t.Fatalf("search request omitted string filters: %#v", searchRequest)
|
|
}
|
|
if err := runFinder(context.Background(), client, []string{"stream-items", "-"}, strings.NewReader("41\n42\n"), &output); err != nil {
|
|
t.Fatalf("stream items: %v", err)
|
|
}
|
|
if streamBody != "41\n42\n" {
|
|
t.Fatalf("stream body = %q", streamBody)
|
|
}
|
|
}
|
|
|
|
func TestCMSReadCommandsUseBackofficeBaseURL(t *testing.T) {
|
|
var paths []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
paths = append(paths, r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{}`)
|
|
}))
|
|
defer server.Close()
|
|
client := newClient(Config{BaseURL: server.URL, FinderURL: "http://reader.invalid"}, "token", "", time.Second)
|
|
var output bytes.Buffer
|
|
if err := runCMSSchemas(context.Background(), client, []string{"get", "page"}, strings.NewReader(""), &output); err != nil {
|
|
t.Fatalf("schema get: %v", err)
|
|
}
|
|
if err := runCMSTree(context.Background(), client, []string{"root", "--depth", "2"}, &output); err != nil {
|
|
t.Fatalf("tree: %v", err)
|
|
}
|
|
if strings.Join(paths, ",") != "/api/schemas/page,/api/tree/root" {
|
|
t.Fatalf("paths = %v", paths)
|
|
}
|
|
}
|
|
|
|
func TestFinderAdminAlwaysUsesBackofficeBaseURL(t *testing.T) {
|
|
var methods, paths []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
methods = append(methods, r.Method)
|
|
paths = append(paths, r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"status":"ok"}`)
|
|
}))
|
|
defer server.Close()
|
|
client := newClient(Config{BaseURL: server.URL, FinderURL: "http://reader.invalid"}, "token", "", time.Second)
|
|
var output bytes.Buffer
|
|
if err := runFinder(context.Background(), client, []string{"admin", "settings", "get"}, strings.NewReader(""), &output); err != nil {
|
|
t.Fatalf("admin settings: %v", err)
|
|
}
|
|
if err := runFinder(context.Background(), client, []string{"admin", "facets", "delete", "brand", "--yes"}, strings.NewReader(""), &output); err != nil {
|
|
t.Fatalf("admin facet delete: %v", err)
|
|
}
|
|
if strings.Join(methods, ",") != "GET,DELETE" || strings.Join(paths, ",") != "/catalog/admin/settings,/catalog/admin/facets/brand" {
|
|
t.Fatalf("requests = %v %v", methods, paths)
|
|
}
|
|
}
|