feat/updates #1
2 changed files with 142 additions and 0 deletions
|
|
@ -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() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue