feat/updates #1

Merged
Snider merged 51 commits from feat/updates into dev 2026-02-16 05:54:07 +00:00
2 changed files with 142 additions and 0 deletions
Showing only changes of commit 50829dc3ba - Show all commits

View file

@ -2,9 +2,12 @@
package bugseti
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
@ -122,6 +125,83 @@ func generateClientID() string {
return hex.EncodeToString(b)
}
// doRequest builds and executes an HTTP request against the hub API.
// It returns the raw *http.Response and any transport-level error.
func (h *HubService) doRequest(method, path string, body interface{}) (*http.Response, error) {
hubURL := h.config.GetHubURL()
if hubURL == "" {
return nil, fmt.Errorf("hub URL not configured")
}
fullURL := hubURL + "/api/bugseti" + path
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("marshal request body: %w", err)
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, fullURL, bodyReader)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
token := h.config.GetHubToken()
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := h.client.Do(req)
if err != nil {
h.mu.Lock()
h.connected = false
h.mu.Unlock()
return nil, err
}
h.mu.Lock()
h.connected = true
h.mu.Unlock()
return resp, nil
}
// doJSON executes an HTTP request and decodes the JSON response into dest.
// It handles common error status codes with typed errors.
func (h *HubService) doJSON(method, path string, body, dest interface{}) error {
resp, err := h.doRequest(method, path, body)
if err != nil {
return err
}
defer resp.Body.Close()
switch {
case resp.StatusCode == http.StatusUnauthorized:
return fmt.Errorf("unauthorised")
case resp.StatusCode == http.StatusConflict:
return &ConflictError{StatusCode: resp.StatusCode}
case resp.StatusCode == http.StatusNotFound:
return &NotFoundError{StatusCode: resp.StatusCode}
case resp.StatusCode >= 400:
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("hub error %d: %s", resp.StatusCode, string(respBody))
}
if dest != nil {
if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
return fmt.Errorf("decode response: %w", err)
}
}
return nil
}
// loadPendingOps is a no-op placeholder (disk persistence comes in Task 7).
func (h *HubService) loadPendingOps() {}

View file

@ -1,6 +1,9 @@
package bugseti
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
@ -46,3 +49,62 @@ func TestNewHubService_Good_ReusesClientID(t *testing.T) {
h := NewHubService(cfg)
assert.Equal(t, "existing-client-id", h.GetClientID())
}
// ---- doRequest ----
func TestDoRequest_Good(t *testing.T) {
var gotAuth string
var gotContentType string
var gotAccept string
var gotBody map[string]string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
gotContentType = r.Header.Get("Content-Type")
gotAccept = r.Header.Get("Accept")
if r.Body != nil {
_ = json.NewDecoder(r.Body).Decode(&gotBody)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
cfg := testConfigService(t, nil, nil)
cfg.config.HubURL = srv.URL
cfg.config.HubToken = "test-token-123"
h := NewHubService(cfg)
body := map[string]string{"key": "value"}
resp, err := h.doRequest("POST", "/test", body)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "Bearer test-token-123", gotAuth)
assert.Equal(t, "application/json", gotContentType)
assert.Equal(t, "application/json", gotAccept)
assert.Equal(t, "value", gotBody["key"])
assert.True(t, h.IsConnected())
}
func TestDoRequest_Bad_NoHubURL(t *testing.T) {
h := testHubService(t, "")
resp, err := h.doRequest("GET", "/test", nil)
assert.Nil(t, resp)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hub URL not configured")
}
func TestDoRequest_Bad_NetworkError(t *testing.T) {
// Point to a port where nothing is listening.
h := testHubService(t, "http://127.0.0.1:1")
resp, err := h.doRequest("GET", "/test", nil)
assert.Nil(t, resp)
assert.Error(t, err)
assert.False(t, h.IsConnected())
}