test(scripts): harden remote43 managed-probe validation
This commit is contained in:
@@ -65,6 +65,29 @@ func TestServiceCloseAssignsSubscriptionsAndProbesGateway(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCloseSubscriptionManagedKeyOverridesExplicitProbeAPIKey(t *testing.T) {
|
||||
host := &fakeClosureHost{
|
||||
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
||||
managedAccess: map[string]sub2api.SubscriptionAccessRef{
|
||||
"user-1": {UserID: "host-user-1", APIKey: "managed-user-key"},
|
||||
},
|
||||
}
|
||||
service := NewService(host)
|
||||
_, err := service.Close(context.Background(), ClosureRequest{
|
||||
Mode: "subscription",
|
||||
ProbeAPIKey: "caller-supplied-key",
|
||||
GroupID: "group-1",
|
||||
ExpectedModel: "deepseek-chat",
|
||||
Subscriptions: []SubscriptionTarget{{UserID: "user-1", DurationDays: 30}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Close() error = %v", err)
|
||||
}
|
||||
if host.gatewayProbe.APIKey != "managed-user-key" {
|
||||
t.Fatalf("gateway probe api key = %q, want managed-user-key override", host.gatewayProbe.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCloseReturnsSubscriptionErrorBeforeGatewayProbe(t *testing.T) {
|
||||
host := &fakeClosureHost{assignErr: errors.New("assign failed")}
|
||||
service := NewService(host)
|
||||
|
||||
@@ -592,10 +592,10 @@ func TestCreateGroupWithMock(t *testing.T) {
|
||||
func TestCreateChannelWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
GroupIDs []int64 `json:"group_ids"`
|
||||
ModelMapping map[string]map[string]string `json:"model_mapping"`
|
||||
ModelPricing []struct {
|
||||
Name string `json:"name"`
|
||||
GroupIDs []int64 `json:"group_ids"`
|
||||
ModelMapping map[string]map[string]string `json:"model_mapping"`
|
||||
ModelPricing []struct {
|
||||
Platform string `json:"platform"`
|
||||
Models []string `json:"models"`
|
||||
BillingMode string `json:"billing_mode"`
|
||||
@@ -607,8 +607,8 @@ func TestCreateChannelWithMock(t *testing.T) {
|
||||
PerRequestPrice *float64 `json:"per_request_price"`
|
||||
Intervals []any `json:"intervals"`
|
||||
} `json:"model_pricing"`
|
||||
RestrictModels bool `json:"restrict_models"`
|
||||
BillingModelSource string `json:"billing_model_source"`
|
||||
RestrictModels bool `json:"restrict_models"`
|
||||
BillingModelSource string `json:"billing_model_source"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
|
||||
@@ -276,6 +276,83 @@ func TestRuntimeImportServiceRepeatedImportReusesManagedResources(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeImportServiceImportReconcilesExistingChannelConfiguration(t *testing.T) {
|
||||
store := openProvisionTestStore(t)
|
||||
defer closeProvisionTestStore(t, store)
|
||||
|
||||
seedProvisionHost(t, store, "host-1", "https://sub2api.example.com")
|
||||
|
||||
host := &fakeHostAdapter{
|
||||
batchAccounts: []sub2api.AccountRef{{ID: "account_1", Name: "minimax-01"}},
|
||||
testResults: map[string]sub2api.ProbeResult{
|
||||
"account_1": {OK: true, Status: "passed"},
|
||||
},
|
||||
models: map[string][]sub2api.AccountModel{
|
||||
"account_1": {{ID: "MiniMax-M2.7-highspeed"}},
|
||||
},
|
||||
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"MiniMax-M2.5-highspeed", "MiniMax-M2.7-highspeed"}},
|
||||
managedSnapshot: sub2api.ManagedResourceSnapshot{
|
||||
Groups: []sub2api.NamedResource{{ID: "group_existing", Name: "MiniMax 默认分组-self-service"}},
|
||||
Channels: []sub2api.NamedResource{{ID: "channel_existing", Name: "MiniMax 默认渠道-self-service"}},
|
||||
Plans: []sub2api.NamedResource{{ID: "plan_existing", Name: "MiniMax 默认套餐-self-service"}},
|
||||
},
|
||||
}
|
||||
|
||||
provider := sampleProviderManifest()
|
||||
provider.ProviderID = "minimax"
|
||||
provider.DisplayName = "MiniMax OpenAI Compatible"
|
||||
provider.BaseURL = "https://v2.aicodee.com/v1"
|
||||
provider.DefaultModels = []string{"MiniMax-M2.5-highspeed", "MiniMax-M2.7-highspeed"}
|
||||
provider.SmokeTestModel = "MiniMax-M2.7-highspeed"
|
||||
provider.GroupTemplate.Name = "MiniMax 默认分组"
|
||||
provider.ChannelTemplate = pack.ChannelTemplate{
|
||||
Name: "MiniMax 默认渠道",
|
||||
ModelMapping: map[string]string{"MiniMax-M2.5-highspeed": "MiniMax-M2.5-highspeed", "MiniMax-M2.7-highspeed": "MiniMax-M2.7-highspeed"},
|
||||
}
|
||||
provider.PlanTemplate.Name = "MiniMax 默认套餐"
|
||||
|
||||
svc := NewRuntimeImportService(store, host)
|
||||
result, err := svc.Import(context.Background(), RuntimeImportRequest{
|
||||
HostID: "host-1",
|
||||
HostBaseURL: "https://sub2api.example.com",
|
||||
Pack: pack.LoadedPack{
|
||||
Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", MinHostVersion: "0.1.126", MaxHostVersion: "0.2.x"},
|
||||
Checksum: "checksum-1",
|
||||
},
|
||||
Provider: provider,
|
||||
Mode: ImportModePartial,
|
||||
Keys: []string{"key-1"},
|
||||
Access: AccessRequest{
|
||||
Mode: AccessModeSelfService,
|
||||
ProbeAPIKey: "user-key",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RuntimeImportService.Import() error = %v", err)
|
||||
}
|
||||
if result.Report.Channel.ID != "channel_existing" {
|
||||
t.Fatalf("Channel.ID = %q, want reused channel_existing", result.Report.Channel.ID)
|
||||
}
|
||||
if host.createChannelCalls != 0 {
|
||||
t.Fatalf("CreateChannel() calls = %d, want 0 when channel already exists", host.createChannelCalls)
|
||||
}
|
||||
if host.updateChannelCalls != 1 {
|
||||
t.Fatalf("UpdateChannel() calls = %d, want 1", host.updateChannelCalls)
|
||||
}
|
||||
if host.updateChannelID != "channel_existing" {
|
||||
t.Fatalf("UpdateChannel() id = %q, want channel_existing", host.updateChannelID)
|
||||
}
|
||||
if len(host.updateChannelReq.ModelPricing) != 1 {
|
||||
t.Fatalf("UpdateChannel().ModelPricing len = %d, want 1", len(host.updateChannelReq.ModelPricing))
|
||||
}
|
||||
if got := host.updateChannelReq.ModelPricing[0].Models; len(got) != 2 || got[0] != "MiniMax-M2.5-highspeed" || got[1] != "MiniMax-M2.7-highspeed" {
|
||||
t.Fatalf("UpdateChannel().ModelPricing[0].Models = %v, want minimax default models", got)
|
||||
}
|
||||
if host.updateChannelReq.ModelPricing[0].BillingMode != "token" {
|
||||
t.Fatalf("UpdateChannel().ModelPricing[0].BillingMode = %q, want token", host.updateChannelReq.ModelPricing[0].BillingMode)
|
||||
}
|
||||
}
|
||||
|
||||
func openProvisionTestStore(t *testing.T) *sqlite.DB {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ HOST_NAME="${HOST_NAME:-remote43-current-host}"
|
||||
REMOTE_HOST_ENV_FILE="${REMOTE_HOST_ENV_FILE:-/home/ubuntu/sub2api-host-validation-fresh-deepseek-20260519_115244/.env}"
|
||||
REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-sub2api-relaymgr-pg}"
|
||||
REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-sub2api-relaymgr-redis}"
|
||||
PACK_PATH="${PACK_PATH:-/home/ubuntu/sub2api-cn-relay-manager/packs/openai-cn-pack}"
|
||||
PACK_PATH="${PACK_PATH:-$ROOT_DIR/packs/openai-cn-pack}"
|
||||
ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}"
|
||||
ART="${ART:-$ROOT/$(date +%Y%m%d_%H%M%S)_remote43_${provider_id}_key_import}"
|
||||
MIN_BALANCE="${MIN_BALANCE:-10}"
|
||||
@@ -46,6 +46,53 @@ ssh_cmd() {
|
||||
ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd"
|
||||
}
|
||||
|
||||
build_managed_subscription_identity_json() {
|
||||
local selector="$1"
|
||||
local group_id="$2"
|
||||
python3 - "$selector" "$group_id" <<'PY'
|
||||
import hashlib, json, sys
|
||||
|
||||
selector, group_id = sys.argv[1:3]
|
||||
|
||||
def sanitize(value: str) -> str:
|
||||
value = value.strip().lower()
|
||||
chars = []
|
||||
last_dash = False
|
||||
for ch in value:
|
||||
if ('a' <= ch <= 'z') or ('0' <= ch <= '9'):
|
||||
chars.append(ch)
|
||||
last_dash = False
|
||||
elif not last_dash:
|
||||
chars.append('-')
|
||||
last_dash = True
|
||||
return ''.join(chars).strip('-')
|
||||
|
||||
def truncate(value: str, max_len: int) -> str:
|
||||
if len(value) <= max_len:
|
||||
return value
|
||||
return value[:max_len].strip('-')
|
||||
|
||||
normalized = selector.strip().lower() + '|' + group_id.strip()
|
||||
digest = hashlib.sha256(normalized.encode('utf-8')).hexdigest()
|
||||
prefix = sanitize(selector) or 'relay-sub'
|
||||
prefix = truncate(prefix, 24)
|
||||
short_hash = digest[:16]
|
||||
key_hash = digest[:32]
|
||||
username = truncate(f"{prefix}-{short_hash[:8]}", 32)
|
||||
print(json.dumps({
|
||||
'email': f"{prefix}-{short_hash}@sub2api.local",
|
||||
'username': username,
|
||||
'custom_key': 'sk-relay-' + key_hash,
|
||||
'key_name': truncate(username + '-key', 48),
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
}
|
||||
|
||||
remote_lookup_managed_subscription_user_id() {
|
||||
local email="$1"
|
||||
remote_pg_query "select id from users where email = $(sql_literal "$email") order by id desc limit 1;"
|
||||
}
|
||||
|
||||
crm_curl_json() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
@@ -344,6 +391,11 @@ for item in batch_obj.get('managed_resources', []):
|
||||
raise SystemExit('missing managed group in import response and batch detail')
|
||||
PY
|
||||
)"
|
||||
managed_identity_json="$(build_managed_subscription_identity_json "$sub_uid" "$subscription_group_id")"
|
||||
managed_user_email="$(printf '%s' "$managed_identity_json" | python3 -c 'import json,sys; print(json.load(sys.stdin)["email"])')"
|
||||
managed_probe_key="$(printf '%s' "$managed_identity_json" | python3 -c 'import json,sys; print(json.load(sys.stdin)["custom_key"])')"
|
||||
managed_user_id="$(remote_lookup_managed_subscription_user_id "$managed_user_email")"
|
||||
managed_user_id="${managed_user_id##*$'\n'}"
|
||||
auth_cache_key="$(build_api_key_auth_cache_key "$sub_key")"
|
||||
balance_cache_key="$(build_user_balance_cache_key "$sub_uid")"
|
||||
subscription_cache_key="$(build_subscription_billing_cache_key "$sub_uid" "$subscription_group_id")"
|
||||
@@ -360,11 +412,15 @@ remote_pg_exec "$prep_sql" > "$ART/06-subscription-access-prep.psql.txt"
|
||||
printf 'subscription_cache_key=%s\n' "$subscription_cache_key"
|
||||
ssh_cmd "sudo -n docker exec $REMOTE_REDIS_CONTAINER_Q redis-cli DEL $auth_cache_key $balance_cache_key $subscription_cache_key"
|
||||
} > "$ART/07-redis-targeted-invalidation.txt"
|
||||
remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json"
|
||||
if [[ -n "$managed_user_id" ]]; then
|
||||
remote_fetch_group_state "$subscription_group_id" "$managed_user_id" "$managed_probe_key" "$ART/08-subscription-group-state.json"
|
||||
else
|
||||
remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json"
|
||||
fi
|
||||
|
||||
python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" <<'PY'
|
||||
python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" "$managed_user_email" "$managed_probe_key" "$managed_user_id" <<'PY'
|
||||
import json, sys, pathlib
|
||||
path, crm, host, crm_host, provider_id, sub_uid, sub_key, group_id, admin_uid = sys.argv[1:10]
|
||||
path, crm, host, crm_host, provider_id, sub_uid, sub_key, group_id, admin_uid, managed_user_email, managed_probe_key, managed_user_id = sys.argv[1:13]
|
||||
pathlib.Path(path).write_text(json.dumps({
|
||||
'crm_base': crm,
|
||||
'host_base': host,
|
||||
@@ -374,6 +430,9 @@ pathlib.Path(path).write_text(json.dumps({
|
||||
'subscription_user_key_prefix': sub_key[:12],
|
||||
'subscription_group_id': group_id,
|
||||
'admin_user_id': admin_uid,
|
||||
'managed_user_email': managed_user_email,
|
||||
'managed_user_id': managed_user_id,
|
||||
'managed_probe_key_prefix': managed_probe_key[:18],
|
||||
}, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
PY
|
||||
|
||||
@@ -387,11 +446,11 @@ print(json.dumps({
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $sub_key' $HOST_BASE/v1/models"
|
||||
ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $managed_probe_key' $HOST_BASE/v1/models"
|
||||
ssh_cmd "cat /tmp/models_headers.txt" > "$ART/09-models.headers.txt"
|
||||
ssh_cmd "cat /tmp/models_body.json" > "$ART/10-models.body.json"
|
||||
|
||||
ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $sub_key' -H 'Content-Type: application/json' $HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")"
|
||||
ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $managed_probe_key' -H 'Content-Type: application/json' $HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")"
|
||||
ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/11-chat.headers.txt"
|
||||
ssh_cmd "cat /tmp/chat_body.json" > "$ART/12-chat.body.json"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user