feat(platform): add cmsctl and document newsletter and shell updates
Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
1 parent
0f8ed97878
commit
fea6eb51b1
7 files changed
+2408
No files matched your search
@@ -0,0 +1,295 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user