fix: resolve merge conflicts with main, preserve PAT functionality
- Resolve conflicts in CLAUDE.md, client.ts, settings/page.tsx - Migrate PAT types and API methods to @/shared/types + @/shared/api architecture - Restore simplified login flow (login page, auth store, tests) - Fix issue detail comment submit test (use fireEvent + useRef for mock) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f70b34a50f
45 changed files with 2044 additions and 261 deletions
|
|
@ -20,27 +20,36 @@ import (
|
|||
type APIClient struct {
|
||||
BaseURL string
|
||||
WorkspaceID string
|
||||
Token string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewAPIClient creates a new API client for ctrl commands.
|
||||
func NewAPIClient(baseURL, workspaceID string) *APIClient {
|
||||
func NewAPIClient(baseURL, workspaceID, token string) *APIClient {
|
||||
return &APIClient{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
WorkspaceID: workspaceID,
|
||||
Token: token,
|
||||
HTTPClient: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *APIClient) setHeaders(req *http.Request) {
|
||||
if c.Token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
}
|
||||
if c.WorkspaceID != "" {
|
||||
req.Header.Set("X-Workspace-ID", c.WorkspaceID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetJSON performs a GET request and decodes the JSON response.
|
||||
func (c *APIClient) GetJSON(ctx context.Context, path string, out any) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.WorkspaceID != "" {
|
||||
req.Header.Set("X-Workspace-ID", c.WorkspaceID)
|
||||
}
|
||||
c.setHeaders(req)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -64,9 +73,7 @@ func (c *APIClient) DeleteJSON(ctx context.Context, path string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.WorkspaceID != "" {
|
||||
req.Header.Set("X-Workspace-ID", c.WorkspaceID)
|
||||
}
|
||||
c.setHeaders(req)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -81,6 +88,36 @@ func (c *APIClient) DeleteJSON(ctx context.Context, path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// PostJSON performs a POST request with a JSON body.
|
||||
func (c *APIClient) PostJSON(ctx context.Context, path string, body any, out any) error {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
c.setHeaders(req)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
respData, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
return fmt.Errorf("POST %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(respData)))
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
// PutJSON performs a PUT request with a JSON body.
|
||||
func (c *APIClient) PutJSON(ctx context.Context, path string, body any, out any) error {
|
||||
data, err := json.Marshal(body)
|
||||
|
|
@ -93,9 +130,7 @@ func (c *APIClient) PutJSON(ctx context.Context, path string, body any, out any)
|
|||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.WorkspaceID != "" {
|
||||
req.Header.Set("X-Workspace-ID", c.WorkspaceID)
|
||||
}
|
||||
c.setHeaders(req)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
|||
104
server/internal/cli/client_test.go
Normal file
104
server/internal/cli/client_test.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPostJSON(t *testing.T) {
|
||||
type reqBody struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
type respBody struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("expected Content-Type application/json, got %s", ct)
|
||||
}
|
||||
if auth := r.Header.Get("Authorization"); auth != "Bearer test-token" {
|
||||
t.Errorf("expected Authorization Bearer test-token, got %s", auth)
|
||||
}
|
||||
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("failed to decode request body: %v", err)
|
||||
}
|
||||
if body.Name != "alice" || body.Age != 30 {
|
||||
t.Errorf("unexpected body: %+v", body)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(respBody{ID: "123"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewAPIClient(srv.URL, "", "test-token")
|
||||
var out respBody
|
||||
err := client.PostJSON(context.Background(), "/test", reqBody{Name: "alice", Age: 30}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out.ID != "123" {
|
||||
t.Errorf("expected ID 123, got %s", out.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error status", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
io.WriteString(w, "bad request")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewAPIClient(srv.URL, "", "test-token")
|
||||
err := client.PostJSON(context.Background(), "/test", reqBody{Name: "bob"}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if got := err.Error(); got != "POST /test returned 400: bad request" {
|
||||
t.Errorf("unexpected error message: %s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil output", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewAPIClient(srv.URL, "", "test-token")
|
||||
err := client.PostJSON(context.Background(), "/test", reqBody{Name: "charlie"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("workspace header", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ws := r.Header.Get("X-Workspace-ID"); ws != "ws-abc" {
|
||||
t.Errorf("expected X-Workspace-ID ws-abc, got %s", ws)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(respBody{ID: "456"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewAPIClient(srv.URL, "ws-abc", "test-token")
|
||||
var out respBody
|
||||
err := client.PostJSON(context.Background(), "/test", reqBody{}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ const defaultCLIConfigPath = ".multica/config.json"
|
|||
type CLIConfig struct {
|
||||
ServerURL string `json:"server_url,omitempty"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// CLIConfigPath returns the default path for the CLI config file.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue