feat(bugseti): add AutoRegister via Forge token exchange
Exchange a Forge API token for a hub API key by POSTing to /api/bugseti/auth/forge. Skips if hub token already cached. Adds drainPendingOps() stub for future Task 7 use. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
50829dc3ba
commit
4134c58488
2 changed files with 146 additions and 0 deletions
|
|
@ -8,9 +8,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/forge"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HubService coordinates with the agentic portal for issue assignment and leaderboard.
|
// HubService coordinates with the agentic portal for issue assignment and leaderboard.
|
||||||
|
|
@ -207,3 +210,80 @@ func (h *HubService) loadPendingOps() {}
|
||||||
|
|
||||||
// savePendingOps is a no-op placeholder (disk persistence comes in Task 7).
|
// savePendingOps is a no-op placeholder (disk persistence comes in Task 7).
|
||||||
func (h *HubService) savePendingOps() {}
|
func (h *HubService) savePendingOps() {}
|
||||||
|
|
||||||
|
// drainPendingOps replays queued operations (no-op until Task 7).
|
||||||
|
func (h *HubService) drainPendingOps() {}
|
||||||
|
|
||||||
|
// AutoRegister exchanges a Forge API token for a hub API key.
|
||||||
|
// If a hub token is already configured, this is a no-op.
|
||||||
|
func (h *HubService) AutoRegister() error {
|
||||||
|
// Skip if already registered.
|
||||||
|
if h.config.GetHubToken() != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hubURL := h.config.GetHubURL()
|
||||||
|
if hubURL == "" {
|
||||||
|
return fmt.Errorf("hub URL not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve forge credentials from config/env.
|
||||||
|
forgeURL := h.config.GetForgeURL()
|
||||||
|
forgeToken := h.config.GetForgeToken()
|
||||||
|
if forgeToken == "" {
|
||||||
|
resolvedURL, resolvedToken, err := forge.ResolveConfig(forgeURL, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve forge config: %w", err)
|
||||||
|
}
|
||||||
|
forgeURL = resolvedURL
|
||||||
|
forgeToken = resolvedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if forgeToken == "" {
|
||||||
|
return fmt.Errorf("no forge token available (set FORGE_TOKEN or run: core forge config --token TOKEN)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request body.
|
||||||
|
payload := map[string]string{
|
||||||
|
"forge_url": forgeURL,
|
||||||
|
"forge_token": forgeToken,
|
||||||
|
"client_id": h.config.GetClientID(),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal auto-register body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST directly (no bearer token yet).
|
||||||
|
resp, err := h.client.Post(hubURL+"/api/bugseti/auth/forge", "application/json", bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
h.mu.Lock()
|
||||||
|
h.connected = false
|
||||||
|
h.mu.Unlock()
|
||||||
|
return fmt.Errorf("auto-register request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
h.connected = true
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("auto-register failed %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
APIKey string `json:"api_key"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return fmt.Errorf("decode auto-register response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.config.SetHubToken(result.APIKey); err != nil {
|
||||||
|
return fmt.Errorf("cache hub token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("BugSETI: auto-registered with hub, token cached")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -108,3 +109,68 @@ func TestDoRequest_Bad_NetworkError(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.False(t, h.IsConnected())
|
assert.False(t, h.IsConnected())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- AutoRegister ----
|
||||||
|
|
||||||
|
func TestAutoRegister_Good(t *testing.T) {
|
||||||
|
var gotBody map[string]string
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "/api/bugseti/auth/forge", r.URL.Path)
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&gotBody)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = w.Write([]byte(`{"api_key":"ak_test_12345"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
cfg := testConfigService(t, nil, nil)
|
||||||
|
cfg.config.HubURL = srv.URL
|
||||||
|
cfg.config.ForgeURL = "https://forge.example.com"
|
||||||
|
cfg.config.ForgeToken = "forge-tok-abc"
|
||||||
|
h := NewHubService(cfg)
|
||||||
|
|
||||||
|
err := h.AutoRegister()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify token was cached.
|
||||||
|
assert.Equal(t, "ak_test_12345", h.config.GetHubToken())
|
||||||
|
|
||||||
|
// Verify request body.
|
||||||
|
assert.Equal(t, "https://forge.example.com", gotBody["forge_url"])
|
||||||
|
assert.Equal(t, "forge-tok-abc", gotBody["forge_token"])
|
||||||
|
assert.NotEmpty(t, gotBody["client_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoRegister_Bad_NoForgeToken(t *testing.T) {
|
||||||
|
// Isolate from user's real ~/.core/config.yaml and env vars.
|
||||||
|
origHome := os.Getenv("HOME")
|
||||||
|
t.Setenv("HOME", t.TempDir())
|
||||||
|
t.Setenv("FORGE_TOKEN", "")
|
||||||
|
t.Setenv("FORGE_URL", "")
|
||||||
|
defer os.Setenv("HOME", origHome)
|
||||||
|
|
||||||
|
cfg := testConfigService(t, nil, nil)
|
||||||
|
cfg.config.HubURL = "https://hub.example.com"
|
||||||
|
// No forge token set, and env/config are empty in test.
|
||||||
|
h := NewHubService(cfg)
|
||||||
|
|
||||||
|
err := h.AutoRegister()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "no forge token available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoRegister_Good_SkipsIfAlreadyRegistered(t *testing.T) {
|
||||||
|
cfg := testConfigService(t, nil, nil)
|
||||||
|
cfg.config.HubURL = "https://hub.example.com"
|
||||||
|
cfg.config.HubToken = "existing-token"
|
||||||
|
h := NewHubService(cfg)
|
||||||
|
|
||||||
|
err := h.AutoRegister()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Token should remain unchanged.
|
||||||
|
assert.Equal(t, "existing-token", h.config.GetHubToken())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue