accounts/api/xworkmate_vault_live_test.go

358 lines
13 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"account/internal/auth"
"account/internal/store"
)
func TestXWorkmateVaultLiveIntegration(t *testing.T) {
vaultAddr := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_ADDR"))
vaultToken := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_TOKEN"))
vaultMount := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_MOUNT"))
if vaultAddr == "" || vaultToken == "" {
t.Skip("live vault integration requires XWORKMATE_VAULT_ADDR and XWORKMATE_VAULT_TOKEN")
}
if vaultMount == "" {
vaultMount = "kv"
}
vaultService, err := NewXWorkmateVaultService(XWorkmateVaultConfig{
Address: vaultAddr,
Token: vaultToken,
Namespace: strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
Mount: vaultMount,
})
if err != nil {
t.Fatalf("create vault service: %v", err)
}
router, _, token := newXWorkmateTestHarnessWithVault(t, nil, vaultService)
profileBody, err := json.Marshal(map[string]any{
"profile": map[string]any{
"openclawUrl": "wss://gateway.example.com",
"openclawOrigin": "https://gateway.example.com",
"vaultUrl": vaultAddr,
"vaultNamespace": strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
"apisixUrl": "https://apigw.example.com",
},
})
if err != nil {
t.Fatalf("marshal profile payload: %v", err)
}
putProfileReq := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/profile", bytes.NewReader(profileBody))
putProfileReq.Header.Set("Content-Type", "application/json")
putProfileReq.Header.Set("Authorization", "Bearer "+token)
putProfileReq.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
putProfileRec := httptest.NewRecorder()
router.ServeHTTP(putProfileRec, putProfileReq)
if putProfileRec.Code != http.StatusOK {
t.Fatalf("expected profile update success, got %d: %s", putProfileRec.Code, putProfileRec.Body.String())
}
targets := []string{
store.XWorkmateSecretLocatorTargetOpenclawGatewayToken,
store.XWorkmateSecretLocatorTargetVaultRootToken,
store.XWorkmateSecretLocatorTargetAIGatewayAccessToken,
}
secretValuePrefix := "live-vault-check-" + time.Now().UTC().Format("20060102T150405.000000000")
for _, target := range targets {
body, err := json.Marshal(map[string]any{"value": secretValuePrefix + "-" + target})
if err != nil {
t.Fatalf("marshal secret payload for %s: %v", target, err)
}
req := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/secrets/"+target, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected live secret write success for %s, got %d: %s", target, rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), secretValuePrefix) {
t.Fatalf("expected live secret write response to hide raw value for %s", target)
}
}
getSecretsReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/secrets", nil)
getSecretsReq.Header.Set("Authorization", "Bearer "+token)
getSecretsReq.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
getSecretsRec := httptest.NewRecorder()
router.ServeHTTP(getSecretsRec, getSecretsReq)
if getSecretsRec.Code != http.StatusOK {
t.Fatalf("expected live secret status fetch success, got %d: %s", getSecretsRec.Code, getSecretsRec.Body.String())
}
if strings.Contains(getSecretsRec.Body.String(), secretValuePrefix) {
t.Fatalf("expected live secret status response to hide raw values")
}
var getSecretsResp struct {
TokenConfigured struct {
Openclaw bool `json:"openclaw"`
Vault bool `json:"vault"`
Apisix bool `json:"apisix"`
} `json:"tokenConfigured"`
}
if err := json.Unmarshal(getSecretsRec.Body.Bytes(), &getSecretsResp); err != nil {
t.Fatalf("decode live secret status response: %v", err)
}
if !getSecretsResp.TokenConfigured.Openclaw || !getSecretsResp.TokenConfigured.Vault || !getSecretsResp.TokenConfigured.Apisix {
t.Fatalf("expected all live tokenConfigured statuses true, got %#v", getSecretsResp.TokenConfigured)
}
for _, target := range targets {
req := httptest.NewRequest(http.MethodDelete, "/api/auth/xworkmate/secrets/"+target, nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected live secret delete success for %s, got %d: %s", target, rec.Code, rec.Body.String())
}
}
getProfileReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile", nil)
getProfileReq.Header.Set("Authorization", "Bearer "+token)
getProfileReq.Header.Set("X-Forwarded-Host", store.SharedXWorkmateDomain)
getProfileRec := httptest.NewRecorder()
router.ServeHTTP(getProfileRec, getProfileReq)
if getProfileRec.Code != http.StatusOK {
t.Fatalf("expected live profile fetch success, got %d: %s", getProfileRec.Code, getProfileRec.Body.String())
}
var profileResp struct {
TokenConfigured struct {
Openclaw bool `json:"openclaw"`
Vault bool `json:"vault"`
Apisix bool `json:"apisix"`
} `json:"tokenConfigured"`
}
if err := json.Unmarshal(getProfileRec.Body.Bytes(), &profileResp); err != nil {
t.Fatalf("decode live profile response: %v", err)
}
if profileResp.TokenConfigured.Openclaw || profileResp.TokenConfigured.Vault || profileResp.TokenConfigured.Apisix {
t.Fatalf("expected live profile tokenConfigured statuses to reset after cleanup, got %#v", profileResp.TokenConfigured)
}
}
func TestXWorkmateVaultLiveIntegrationPrivateScope(t *testing.T) {
vaultAddr := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_ADDR"))
vaultToken := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_TOKEN"))
vaultMount := strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_MOUNT"))
if vaultAddr == "" || vaultToken == "" {
t.Skip("live vault integration requires XWORKMATE_VAULT_ADDR and XWORKMATE_VAULT_TOKEN")
}
if vaultMount == "" {
vaultMount = "kv"
}
vaultService, err := NewXWorkmateVaultService(XWorkmateVaultConfig{
Address: vaultAddr,
Token: vaultToken,
Namespace: strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
Mount: vaultMount,
})
if err != nil {
t.Fatalf("create vault service: %v", err)
}
ctx := context.Background()
st := store.NewMemoryStore()
tenantID := "sandbox-live-" + time.Now().UTC().Format("20060102t150405000000000")
tenantDomain := tenantID + ".svc.plus"
tenant := &store.Tenant{
ID: tenantID,
Name: "Sandbox Live Tenant",
Edition: store.TenantPrivateEdition,
}
if err := st.EnsureTenant(ctx, tenant); err != nil {
t.Fatalf("ensure tenant: %v", err)
}
if err := st.EnsureTenantDomain(ctx, &store.TenantDomain{
TenantID: tenant.ID,
Domain: tenantDomain,
Kind: store.TenantDomainKindGenerated,
IsPrimary: true,
Status: store.TenantDomainStatusVerified,
}); err != nil {
t.Fatalf("ensure tenant domain: %v", err)
}
user := &store.User{
Name: "Vault Sandbox Operator",
Email: "vault-sandbox-operator@example.com",
EmailVerified: true,
Role: store.RoleAdmin,
Level: store.LevelAdmin,
Active: true,
}
if err := st.CreateUser(ctx, user); err != nil {
t.Fatalf("create user: %v", err)
}
if err := st.UpsertTenantMembership(ctx, &store.TenantMembership{
TenantID: tenant.ID,
UserID: user.ID,
Role: store.TenantMembershipRoleAdmin,
}); err != nil {
t.Fatalf("upsert tenant membership: %v", err)
}
token := "sandbox-live-token"
if err := st.CreateSession(ctx, token, user.ID, time.Now().Add(time.Hour)); err != nil {
t.Fatalf("create session: %v", err)
}
engine := newPrivateScopeLiveRouter(t, st, vaultService)
profileBody, err := json.Marshal(map[string]any{
"profile": map[string]any{
"openclawUrl": "wss://gateway.example.com",
"openclawOrigin": "https://gateway.example.com",
"vaultUrl": vaultAddr,
"vaultNamespace": strings.TrimSpace(os.Getenv("XWORKMATE_VAULT_NAMESPACE")),
"apisixUrl": "https://apigw.example.com",
},
})
if err != nil {
t.Fatalf("marshal profile payload: %v", err)
}
putProfileReq := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/profile", bytes.NewReader(profileBody))
putProfileReq.Header.Set("Content-Type", "application/json")
putProfileReq.Header.Set("Authorization", "Bearer "+token)
putProfileReq.Header.Set("X-Forwarded-Host", tenantDomain)
putProfileRec := httptest.NewRecorder()
engine.ServeHTTP(putProfileRec, putProfileReq)
if putProfileRec.Code != http.StatusOK {
t.Fatalf("expected private-scope profile update success, got %d: %s", putProfileRec.Code, putProfileRec.Body.String())
}
targets := []string{
store.XWorkmateSecretLocatorTargetOpenclawGatewayToken,
store.XWorkmateSecretLocatorTargetVaultRootToken,
store.XWorkmateSecretLocatorTargetAIGatewayAccessToken,
}
secretValuePrefix := "private-live-vault-check-" + time.Now().UTC().Format("20060102T150405.000000000")
for _, target := range targets {
body, err := json.Marshal(map[string]any{"value": secretValuePrefix + "-" + target})
if err != nil {
t.Fatalf("marshal secret payload for %s: %v", target, err)
}
req := httptest.NewRequest(http.MethodPut, "/api/auth/xworkmate/secrets/"+target, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Forwarded-Host", tenantDomain)
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected private-scope secret write success for %s, got %d: %s", target, rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), secretValuePrefix) {
t.Fatalf("expected private-scope secret write response to hide raw value for %s", target)
}
}
getSecretsReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/secrets", nil)
getSecretsReq.Header.Set("Authorization", "Bearer "+token)
getSecretsReq.Header.Set("X-Forwarded-Host", tenantDomain)
getSecretsRec := httptest.NewRecorder()
engine.ServeHTTP(getSecretsRec, getSecretsReq)
if getSecretsRec.Code != http.StatusOK {
t.Fatalf("expected private-scope secret status fetch success, got %d: %s", getSecretsRec.Code, getSecretsRec.Body.String())
}
var getSecretsResp struct {
ProfileScope string `json:"profileScope"`
TokenConfigured struct {
Openclaw bool `json:"openclaw"`
Vault bool `json:"vault"`
Apisix bool `json:"apisix"`
} `json:"tokenConfigured"`
}
if err := json.Unmarshal(getSecretsRec.Body.Bytes(), &getSecretsResp); err != nil {
t.Fatalf("decode private-scope secret status response: %v", err)
}
if getSecretsResp.ProfileScope != store.XWorkmateProfileScopeUserPrivate {
t.Fatalf("expected private profile scope, got %q", getSecretsResp.ProfileScope)
}
if !getSecretsResp.TokenConfigured.Openclaw || !getSecretsResp.TokenConfigured.Vault || !getSecretsResp.TokenConfigured.Apisix {
t.Fatalf("expected all private tokenConfigured statuses true, got %#v", getSecretsResp.TokenConfigured)
}
for _, target := range targets {
req := httptest.NewRequest(http.MethodDelete, "/api/auth/xworkmate/secrets/"+target, nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Forwarded-Host", tenantDomain)
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected private-scope secret delete success for %s, got %d: %s", target, rec.Code, rec.Body.String())
}
}
getProfileReq := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile", nil)
getProfileReq.Header.Set("Authorization", "Bearer "+token)
getProfileReq.Header.Set("X-Forwarded-Host", tenantDomain)
getProfileRec := httptest.NewRecorder()
engine.ServeHTTP(getProfileRec, getProfileReq)
if getProfileRec.Code != http.StatusOK {
t.Fatalf("expected private-scope profile fetch success, got %d: %s", getProfileRec.Code, getProfileRec.Body.String())
}
var profileResp struct {
ProfileScope string `json:"profileScope"`
TokenConfigured struct {
Openclaw bool `json:"openclaw"`
Vault bool `json:"vault"`
Apisix bool `json:"apisix"`
} `json:"tokenConfigured"`
}
if err := json.Unmarshal(getProfileRec.Body.Bytes(), &profileResp); err != nil {
t.Fatalf("decode private-scope profile response: %v", err)
}
if profileResp.ProfileScope != store.XWorkmateProfileScopeUserPrivate {
t.Fatalf("expected private profile scope after cleanup, got %q", profileResp.ProfileScope)
}
if profileResp.TokenConfigured.Openclaw || profileResp.TokenConfigured.Vault || profileResp.TokenConfigured.Apisix {
t.Fatalf("expected private profile tokenConfigured statuses to reset after cleanup, got %#v", profileResp.TokenConfigured)
}
}
func newPrivateScopeLiveRouter(t *testing.T, st store.Store, vaultService xworkmateVaultService) *gin.Engine {
t.Helper()
router := gin.New()
RegisterRoutes(
router,
WithStore(st),
WithEmailVerification(false),
WithTokenService(auth.NewTokenService(auth.TokenConfig{
PublicToken: "public-token",
RefreshSecret: "refresh-secret",
AccessSecret: "access-secret",
AccessExpiry: time.Hour,
RefreshExpiry: time.Hour,
Store: st,
})),
WithXWorkmateVaultService(vaultService),
)
return router
}