Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
Expand Down Expand Up @@ -254,7 +255,12 @@ func (c *Client) Delete(ctx context.Context, endpoint string, v any) (*http.Resp

// NewRequest handles JSON marshaling, context attachment, and header setup.
func (c *Client) NewRequest(ctx context.Context, method, endpoint string, body any) (*http.Request, error) {
u := c.serverURL.JoinPath("api", c.apiVersion, endpoint)
// Split path and query if present to avoid escaping the '?' by JoinPath
path, query, found := strings.Cut(endpoint, "?")
u := c.serverURL.JoinPath("api", c.apiVersion, path)
if found {
u.RawQuery = query
}

var buf io.ReadSeeker
if body != nil {
Expand Down Expand Up @@ -312,9 +318,14 @@ func (e *Error) Error() string {
}

if e.Response != nil && e.Response.Request != nil {
// Strip query parameters and fragment for a cleaner error message
u := *e.Response.Request.URL
u.RawQuery = ""
u.Fragment = ""

return fmt.Sprintf("openrelik: %s %s: %s%s%s",
e.Response.Request.Method,
e.Response.Request.URL,
u.String(),
e.Response.Status,
msg,
cause)
Expand Down
41 changes: 34 additions & 7 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestNewClient(t *testing.T) {
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

transport, ok := client.httpClient.Transport.(*tokenRefreshTransport)
if !ok {
t.Fatal("Expected tokenRefreshTransport")
Expand Down Expand Up @@ -222,7 +222,7 @@ func TestWithHTTPClient_SideEffects(t *testing.T) {
t.Run("Scheme Mismatch (HTTPS -> HTTP)", func(t *testing.T) {
// Configure client for HTTPS
client, _ := NewClient("https://openrelik.local", "test-key")

var lastReq *http.Request
recorder := RoundTripFunc(func(req *http.Request) (*http.Response, error) {
lastReq = req
Expand Down Expand Up @@ -264,7 +264,7 @@ func TestRoundTrip_RedirectLeakage(t *testing.T) {
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

ctx := context.Background()
req, _ := client.NewRequest(ctx, http.MethodGet, "/redirect", nil)
resp, err := client.Do(req, nil)
Expand Down Expand Up @@ -448,7 +448,7 @@ func TestDo(t *testing.T) {
if string(apiErr.Body) != "error detail" {
t.Errorf("Expected Body 'error detail', got %q", string(apiErr.Body))
}

expectedMsgPrefix := "openrelik: GET http://"
if !strings.HasPrefix(apiErr.Error(), expectedMsgPrefix) {
t.Errorf("Expected error message to start with %q, got %q", expectedMsgPrefix, apiErr.Error())
Expand All @@ -466,7 +466,7 @@ func TestDo(t *testing.T) {

req, _ := client.NewRequest(ctx, http.MethodGet, "/structured-error", nil)
_, err := client.Do(req, nil)

var apiErr *Error
if !errors.As(err, &apiErr) {
t.Fatalf("Expected *Error, got %T", err)
Expand All @@ -479,6 +479,33 @@ func TestDo(t *testing.T) {
}
})

t.Run("Readable API Error (Stripped Query)", func(t *testing.T) {
mux.HandleFunc("/api/v1/query-error", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, `{"detail": "invalid input"}`)
})

// Use a URL with query parameters
req, _ := client.NewRequest(ctx, http.MethodGet, "/query-error?long_param=very_long_value&other=123", nil)
_, err := client.Do(req, nil)

var apiErr *Error
if !errors.As(err, &apiErr) {
t.Fatalf("Expected *Error, got %T", err)
}

errStr := apiErr.Error()
if strings.Contains(errStr, "very_long_value") {
t.Errorf("Error message should NOT contain query parameters: %q", errStr)
}
if !strings.Contains(errStr, "/api/v1/query-error") {
t.Errorf("Error message should contain the path: %q", errStr)
}
if !strings.Contains(errStr, "invalid input") {
t.Errorf("Error message should contain API error message: %q", errStr)
}
})

t.Run("Decode Error with Unwrap", func(t *testing.T) {
mux.HandleFunc("/api/v1/bad-json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand All @@ -499,7 +526,7 @@ func TestDo(t *testing.T) {
if !errors.As(err, &apiErr) {
t.Fatalf("Expected *Error wrapping the decode error, got %T", err)
}

if apiErr.Cause == nil {
t.Fatal("Expected Cause to be set on decode error")
}
Expand Down Expand Up @@ -629,7 +656,7 @@ func TestClient_LowLevelMethods(t *testing.T) {

t.Run("Patch", func(t *testing.T) {
var res testResource
_ , err := client.Patch(ctx, "/test", map[string]string{"name": "patch"}, &res)
_, err := client.Patch(ctx, "/test", map[string]string{"name": "patch"}, &res)
if err != nil {
t.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/openrelik/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/openrelik/openrelik-go-client/cmd/cli
go 1.24.2

require (
github.com/google/uuid v1.6.0
github.com/openrelik/openrelik-go-client v0.0.0-00010101000000-000000000000
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.30.0
Expand Down
2 changes: 2 additions & 0 deletions cmd/openrelik/go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
18 changes: 11 additions & 7 deletions cmd/openrelik/internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ func NewRootCmd() *cobra.Command {
Long: `A command line tool to interact with the OpenRelik API`,
TraverseChildren: true,
SilenceErrors: true,
SilenceUsage: true,
}

cmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text, json)")
cmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Suppress all output")
cmd.CompletionOptions.DisableDefaultCmd = true

cmd.Flags().StringVarP(&outputFormat, "format", "f", "text", "Output format (text, json)")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Suppress all output")

cmd.AddCommand(newAuthCmd())
cmd.AddCommand(newUsersCmd())
cmd.AddCommand(newFoldersCmd())
cmd.AddCommand(newFilesCmd())
cmd.AddCommand(newWorkersCmd())
cmd.AddCommand(newUserCmd())
cmd.AddCommand(newFolderCmd())
cmd.AddCommand(newFileCmd())
cmd.AddCommand(newWorkerCmd())
cmd.AddCommand(newWorkflowCmd())
cmd.AddCommand(newRunCmd())

return cmd
}
Expand Down Expand Up @@ -90,4 +95,3 @@ func Execute() {
os.Exit(1)
}
}

12 changes: 8 additions & 4 deletions cmd/openrelik/internal/cli/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ var (
chunkSize int
)

func newFilesCmd() *cobra.Command {
func newFileCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "files",
Use: "file",
Short: "Manage files",
}

Expand Down Expand Up @@ -146,7 +146,11 @@ func newFileDownloadCmd() *cobra.Command {

var r io.Reader = body
if !quiet {
r = util.NewProgressReader(body, file.Filesize, cmd.OutOrStdout())
tracker := util.NewProgressTracker(cmd.OutOrStdout(), file.Filesize, "Download: "+file.DisplayName)
r = &util.ProgressReader{
Reader: body,
Tracker: tracker,
}
}

_, err = io.Copy(out, r)
Expand Down Expand Up @@ -186,7 +190,7 @@ func newFileUploadCmd() *cobra.Command {

var tracker *util.ProgressTracker
if !quiet {
tracker = util.NewProgressTracker(cmd.OutOrStdout(), fileInfo.Size(), "Uploading")
tracker = util.NewProgressTracker(cmd.OutOrStdout(), fileInfo.Size(), "Upload: "+filepath.Base(filePath))
// Calculate total chunks for initial display
totalChunks := int(fileInfo.Size() / int64(chunkSize))
if fileInfo.Size()%int64(chunkSize) != 0 {
Expand Down
28 changes: 14 additions & 14 deletions cmd/openrelik/internal/cli/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"testing"
)

func TestFilesListCmd(t *testing.T) {
func TestFileListCmd(t *testing.T) {
// Mock API server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/folders/123/files/" {
Expand All @@ -38,7 +38,7 @@ func TestFilesListCmd(t *testing.T) {
}{
{
name: "list files in folder",
args: []string{"files", "list", "123"},
args: []string{"file", "list", "123"},
expectedOutput: []string{
"ID : 1",
"DisplayName : file1.txt",
Expand All @@ -50,7 +50,7 @@ func TestFilesListCmd(t *testing.T) {
},
{
name: "missing folder ID",
args: []string{"files", "list"},
args: []string{"file", "list"},
expectError: true,
},
}
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestFilesListCmd(t *testing.T) {
}
}

func TestFilesDownloadCmd(t *testing.T) {
func TestFileDownloadCmd(t *testing.T) {
// Mock API server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/files/789" {
Expand Down Expand Up @@ -123,7 +123,7 @@ func TestFilesDownloadCmd(t *testing.T) {
root := NewRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetArgs([]string{"files", "download", "789"})
root.SetArgs([]string{"file", "download", "789"})

if err := root.Execute(); err != nil {
t.Fatalf("Execute() failed: %v", err)
Expand All @@ -145,7 +145,7 @@ func TestFilesDownloadCmd(t *testing.T) {
root := NewRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetArgs([]string{"files", "download", "789", tmpDir})
root.SetArgs([]string{"file", "download", "789", tmpDir})

if err := root.Execute(); err != nil {
t.Fatalf("Execute() failed: %v", err)
Expand All @@ -165,7 +165,7 @@ func TestFilesDownloadCmd(t *testing.T) {
root := NewRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetArgs([]string{"files", "download", "789", customPath})
root.SetArgs([]string{"file", "download", "789", customPath})

if err := root.Execute(); err != nil {
t.Fatalf("Execute() failed: %v", err)
Expand All @@ -182,7 +182,7 @@ func TestFilesDownloadCmd(t *testing.T) {
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs([]string{"files", "download", "789", "/non/existent/path/file.txt"})
root.SetArgs([]string{"file", "download", "789", "/non/existent/path/file.txt"})

err := root.Execute()
if err == nil {
Expand All @@ -204,7 +204,7 @@ func TestFilesDownloadCmd(t *testing.T) {
root.SetOut(buf)
// Mock "y" input
root.SetIn(strings.NewReader("y\n"))
root.SetArgs([]string{"files", "download", "789", tmpFile.Name()})
root.SetArgs([]string{"file", "download", "789", tmpFile.Name()})

if err := root.Execute(); err != nil {
t.Fatalf("Execute() failed: %v", err)
Expand All @@ -228,7 +228,7 @@ func TestFilesDownloadCmd(t *testing.T) {
root.SetErr(buf)
// Mock empty input (just newline)
root.SetIn(strings.NewReader("\n"))
root.SetArgs([]string{"files", "download", "789", tmpFile.Name()})
root.SetArgs([]string{"file", "download", "789", tmpFile.Name()})

err := root.Execute()
if err == nil {
Expand All @@ -245,7 +245,7 @@ func TestFilesDownloadCmd(t *testing.T) {
})
}

func TestFilesUploadCmd(t *testing.T) {
func TestFileUploadCmd(t *testing.T) {
// Mock API server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.URL.Path == "/api/v1/files/upload" {
Expand Down Expand Up @@ -273,15 +273,15 @@ func TestFilesUploadCmd(t *testing.T) {
root := NewRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetArgs([]string{"files", "upload", tmpFile.Name(), "123", "--chunk-size", "10"})
root.SetArgs([]string{"file", "upload", tmpFile.Name(), "123", "--chunk-size", "10"})

if err := root.Execute(); err != nil {
t.Fatalf("Execute() failed: %v", err)
}

output := buf.String()
if !strings.Contains(output, "Uploading [") {
t.Errorf("expected output to contain progress bar, but it was %q", output)
if !strings.Contains(output, "↑") && !strings.Contains(output, "upload.txt") {
t.Errorf("expected output to contain upload icon and filename, but it was %q", output)
}
if !strings.Contains(output, "chunks") {
t.Errorf("expected output to contain chunk info, but it was %q", output)
Expand Down
4 changes: 2 additions & 2 deletions cmd/openrelik/internal/cli/folders.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ var (
displayName string
)

func newFoldersCmd() *cobra.Command {
func newFolderCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "folders",
Use: "folder",
Short: "Manage folders",
}

Expand Down
Loading
Loading