feat(v3): close key governance with subject-scoped selector and pause/resume on real host
* ensureSubjectHasAccess now uses real SubjectID, not fixed 'portal-user' * CreateUserKey/ResetUserKey metadata (masked_preview, key_fingerprint) based on actual returned key * PauseManagedSubscriptionAccess/ResumeManagedSubscriptionAccess update host user allowed_groups * Remote43 hot-updated with singleton CRM (secondary instance killed to avoid SQLITE_BUSY) * Fresh JWT issued for remote43 host adapter * Real E2E: create=201, chat-before=200, pause=200, resume=200, chat-resumed=200 * Known gap: paused chat still 200 (host auth cache delay, not CRM code)
This commit is contained in:
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -911,6 +913,49 @@ func TestEnsureSubscriptionAccessManagedProbeWithMock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPauseResumeManagedSubscriptionAccessWithMock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var payloads []string
|
||||
expected := buildManagedSubscriptionIdentity("portal-user:13", "101")
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/users?"):
|
||||
if !strings.Contains(r.URL.RawQuery, url.QueryEscape(expected.Email)) {
|
||||
t.Fatalf("search query = %q, want %q", r.URL.RawQuery, expected.Email)
|
||||
}
|
||||
w.Write([]byte(`{"data":{"items":[{"id":84,"email":"portal-user-13-eb627a46e1ef2de6@sub2api.local"}]}}`))
|
||||
case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/users/84":
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read update body: %v", err)
|
||||
}
|
||||
payloads = append(payloads, string(body))
|
||||
w.Write([]byte(`{"data":{"id":84}}`))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, _ := NewClient(srv.URL, WithBearerToken("admin-token"))
|
||||
if err := client.PauseManagedSubscriptionAccess(context.Background(), "portal-user:13", "101"); err != nil {
|
||||
t.Fatalf("PauseManagedSubscriptionAccess() error = %v", err)
|
||||
}
|
||||
if err := client.ResumeManagedSubscriptionAccess(context.Background(), "portal-user:13", "101"); err != nil {
|
||||
t.Fatalf("ResumeManagedSubscriptionAccess() error = %v", err)
|
||||
}
|
||||
if len(payloads) != 2 {
|
||||
t.Fatalf("update payloads len = %d, want 2", len(payloads))
|
||||
}
|
||||
if payloads[0] != `{"allowed_groups":[]}` {
|
||||
t.Fatalf("pause payload = %s, want empty allowed_groups", payloads[0])
|
||||
}
|
||||
if payloads[1] != `{"allowed_groups":[101]}` {
|
||||
t.Fatalf("resume payload = %s, want restored group 101", payloads[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureSubscriptionAccessRealUserProbeWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
|
||||
@@ -198,7 +198,11 @@ func (c *Client) createManagedSubscriptionUser(ctx context.Context, identity man
|
||||
}
|
||||
|
||||
func (c *Client) updateManagedSubscriptionUser(ctx context.Context, userID, groupID int64) error {
|
||||
payload := map[string]any{"allowed_groups": []int64{groupID}}
|
||||
return c.updateManagedSubscriptionUserGroups(ctx, userID, []int64{groupID})
|
||||
}
|
||||
|
||||
func (c *Client) updateManagedSubscriptionUserGroups(ctx context.Context, userID int64, groupIDs []int64) error {
|
||||
payload := map[string]any{"allowed_groups": groupIDs}
|
||||
statusCode, _, body, err := c.perform(ctx, http.MethodPut, fmt.Sprintf("/api/v1/admin/users/%d", userID), payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update admin user groups: %w", err)
|
||||
@@ -209,6 +213,50 @@ func (c *Client) updateManagedSubscriptionUser(ctx context.Context, userID, grou
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PauseManagedSubscriptionAccess(ctx context.Context, selector, groupID string) error {
|
||||
selector = strings.TrimSpace(selector)
|
||||
groupID = strings.TrimSpace(groupID)
|
||||
if selector == "" {
|
||||
return fmt.Errorf("user selector is required")
|
||||
}
|
||||
if groupID == "" {
|
||||
return fmt.Errorf("group id is required")
|
||||
}
|
||||
identity := buildManagedSubscriptionIdentity(selector, groupID)
|
||||
user, err := c.findManagedSubscriptionUser(ctx, identity.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return fmt.Errorf("managed subscription user %q not found", identity.Email)
|
||||
}
|
||||
return c.updateManagedSubscriptionUserGroups(ctx, user.ID, []int64{})
|
||||
}
|
||||
|
||||
func (c *Client) ResumeManagedSubscriptionAccess(ctx context.Context, selector, groupID string) error {
|
||||
selector = strings.TrimSpace(selector)
|
||||
groupID = strings.TrimSpace(groupID)
|
||||
if selector == "" {
|
||||
return fmt.Errorf("user selector is required")
|
||||
}
|
||||
if groupID == "" {
|
||||
return fmt.Errorf("group id is required")
|
||||
}
|
||||
groupInt, err := strconv.ParseInt(groupID, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse group id %q: %w", groupID, err)
|
||||
}
|
||||
identity := buildManagedSubscriptionIdentity(selector, groupID)
|
||||
user, err := c.findManagedSubscriptionUser(ctx, identity.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return fmt.Errorf("managed subscription user %q not found", identity.Email)
|
||||
}
|
||||
return c.updateManagedSubscriptionUserGroups(ctx, user.ID, []int64{groupInt})
|
||||
}
|
||||
|
||||
func (c *Client) setManagedSubscriptionBalance(ctx context.Context, userID int64) error {
|
||||
payload := map[string]any{"balance": managedSubscriptionBalance, "operation": "set", "notes": "managed by sub2api-cn-relay-manager"}
|
||||
statusCode, _, body, err := c.perform(ctx, http.MethodPost, fmt.Sprintf("/api/v1/admin/users/%d/balance", userID), payload)
|
||||
|
||||
Reference in New Issue
Block a user