Merge pull request #1 from kun775/cloudflare-admin-grok2api
feat(cloudflare): 支持管理员创建临时邮箱并兼容 grok2api 管理接口
This commit is contained in:
@@ -86,14 +86,40 @@ cp config.example.json config.json
|
|||||||
| `proxy` | 代理地址,可留空 |
|
| `proxy` | 代理地址,可留空 |
|
||||||
| `enable_nsfw` | 注册后是否尝试开启 NSFW |
|
| `enable_nsfw` | 注册后是否尝试开启 NSFW |
|
||||||
| `cloudflare_api_base` | Cloudflare 临时邮箱 API 地址 |
|
| `cloudflare_api_base` | Cloudflare 临时邮箱 API 地址 |
|
||||||
| `cloudflare_auth_mode` | Cloudflare API 鉴权模式:`none`、`bearer`、`x-api-key`、`query-key` |
|
| `cloudflare_api_key` | Cloudflare 临时邮箱接口密钥;admin 模式填 `ADMIN_PASSWORD` |
|
||||||
|
| `cloudflare_auth_mode` | Cloudflare API 鉴权模式:`none`、`bearer`、`x-api-key`、`x-admin-auth`、`query-key` |
|
||||||
|
| `cloudflare_path_accounts` | Cloudflare 创建邮箱路径;匿名模式用 `/api/new_address`,admin 模式用 `/admin/new_address` |
|
||||||
| `defaultDomains` | Cloudflare 临时邮箱默认域名 |
|
| `defaultDomains` | Cloudflare 临时邮箱默认域名 |
|
||||||
| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 |
|
| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 |
|
||||||
| `grok2api_local_token_file` | 本地 grok2api token 文件路径 |
|
| `grok2api_local_token_file` | 本地 grok2api token 文件路径 |
|
||||||
| `grok2api_auto_add_remote` | 是否写入远端 grok2api |
|
| `grok2api_auto_add_remote` | 是否写入远端 grok2api |
|
||||||
| `grok2api_remote_base` | 远端 grok2api 管理 API 地址 |
|
| `grok2api_remote_base` | 远端 grok2api 地址,可填站点根地址或 `/admin/api` 管理 API 地址 |
|
||||||
| `grok2api_remote_app_key` | 远端 grok2api app key |
|
| `grok2api_remote_app_key` | 远端 grok2api app key |
|
||||||
|
|
||||||
|
### Cloudflare 临时邮箱 admin 模式
|
||||||
|
|
||||||
|
如果使用 `dreamhunter2333/cloudflare_temp_email` 且匿名 `/api/new_address` 开启了 Turnstile,可以改用 admin 创建邮箱接口:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email_provider": "cloudflare",
|
||||||
|
"cloudflare_api_base": "https://你的-worker-api-域名",
|
||||||
|
"cloudflare_api_key": "你的 ADMIN_PASSWORD",
|
||||||
|
"cloudflare_auth_mode": "x-admin-auth",
|
||||||
|
"cloudflare_path_accounts": "/admin/new_address",
|
||||||
|
"cloudflare_path_messages": "/api/mails",
|
||||||
|
"defaultDomains": "你的收信域名.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
创建邮箱会使用 `x-admin-auth` 调用 `/admin/new_address`,后续收件仍使用接口返回的地址 JWT 调用 `/api/mails`。
|
||||||
|
|
||||||
|
可先用调试脚本验证 admin 创建接口:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python cf_mail_debug.py --api-base "https://你的-worker-api-域名" --auth-mode x-admin-auth --api-key "你的 ADMIN_PASSWORD" --create-path /admin/new_address --domain "你的收信域名.com"
|
||||||
|
```
|
||||||
|
|
||||||
`config.json` 包含个人配置和密钥,不要提交到 Git。
|
`config.json` 包含个人配置和密钥,不要提交到 Git。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|||||||
+72
-7
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
@@ -36,21 +38,72 @@ def json_or_text(resp: requests.Response) -> Tuple[Optional[Dict[str, Any]], str
|
|||||||
return None, (resp.text or "")[:400]
|
return None, (resp.text or "")[:400]
|
||||||
|
|
||||||
|
|
||||||
def create_address(api_base: str) -> Tuple[str, str]:
|
def generate_username(length: int = 10) -> str:
|
||||||
|
"""生成 cloudflare_temp_email admin API 需要的随机邮箱名称。"""
|
||||||
|
chars = string.ascii_lowercase + string.digits
|
||||||
|
return "".join(secrets.choice(chars) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_path(path: str, default_path: str) -> str:
|
||||||
|
"""标准化 API 路径,避免漏写开头斜杠。"""
|
||||||
|
raw = (path or default_path).strip() or default_path
|
||||||
|
return raw if raw.startswith("/") else f"/{raw}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_auth_headers(auth_mode: str, api_key: str, content_type: bool = False) -> Dict[str, str]:
|
||||||
|
"""按调试参数构造 Cloudflare 临时邮箱接口鉴权请求头。"""
|
||||||
|
headers = {"Content-Type": "application/json"} if content_type else {}
|
||||||
|
key = (api_key or "").strip()
|
||||||
|
mode = (auth_mode or "none").strip().lower()
|
||||||
|
if not key:
|
||||||
|
return headers
|
||||||
|
if mode == "x-admin-auth":
|
||||||
|
headers["x-admin-auth"] = key
|
||||||
|
elif mode == "x-api-key":
|
||||||
|
headers["X-API-Key"] = key
|
||||||
|
elif mode == "bearer":
|
||||||
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def create_address(
|
||||||
|
api_base: str,
|
||||||
|
auth_mode: str = "none",
|
||||||
|
api_key: str = "",
|
||||||
|
create_path: str = "/api/new_address",
|
||||||
|
domain: str = "",
|
||||||
|
name: str = "",
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
"""创建 Cloudflare 临时邮箱地址,支持匿名 API 和 admin API。"""
|
||||||
|
path = normalize_path(create_path, "/api/new_address")
|
||||||
|
is_admin_create = path.rstrip("/").lower() == "/admin/new_address"
|
||||||
|
if is_admin_create:
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"name": name.strip() if name.strip() else generate_username(),
|
||||||
|
"enablePrefix": True,
|
||||||
|
}
|
||||||
|
if domain.strip():
|
||||||
|
payload["domain"] = domain.strip()
|
||||||
|
headers = build_auth_headers(auth_mode, api_key, content_type=True)
|
||||||
|
else:
|
||||||
|
payload = {}
|
||||||
|
if domain.strip():
|
||||||
|
payload["domain"] = domain.strip()
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
f"{api_base.rstrip('/')}/api/new_address",
|
f"{api_base.rstrip('/')}{path}",
|
||||||
json={},
|
json=payload,
|
||||||
headers={"Content-Type": "application/json"},
|
headers=headers,
|
||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data, raw = json_or_text(resp)
|
data, raw = json_or_text(resp)
|
||||||
if not data:
|
if not data:
|
||||||
raise RuntimeError(f"/api/new_address 非JSON: {raw}")
|
raise RuntimeError(f"{path} 非JSON: {raw}")
|
||||||
address = str(data.get("address", "")).strip()
|
address = str(data.get("address", "")).strip()
|
||||||
jwt = str(data.get("jwt", "")).strip()
|
jwt = str(data.get("jwt", "")).strip()
|
||||||
if not address or not jwt:
|
if not address or not jwt:
|
||||||
raise RuntimeError(f"/api/new_address 缺少 address/jwt: {data}")
|
raise RuntimeError(f"{path} 缺少 address/jwt: {data}")
|
||||||
return address, jwt
|
return address, jwt
|
||||||
|
|
||||||
|
|
||||||
@@ -131,6 +184,11 @@ def main():
|
|||||||
ap.add_argument("--api-base", required=True)
|
ap.add_argument("--api-base", required=True)
|
||||||
ap.add_argument("--address", default="")
|
ap.add_argument("--address", default="")
|
||||||
ap.add_argument("--credential", default="")
|
ap.add_argument("--credential", default="")
|
||||||
|
ap.add_argument("--auth-mode", default="none", choices=["none", "bearer", "x-api-key", "x-admin-auth"])
|
||||||
|
ap.add_argument("--api-key", default="")
|
||||||
|
ap.add_argument("--create-path", default="/api/new_address")
|
||||||
|
ap.add_argument("--domain", default="")
|
||||||
|
ap.add_argument("--name", default="")
|
||||||
ap.add_argument("--timeout", type=int, default=180)
|
ap.add_argument("--timeout", type=int, default=180)
|
||||||
ap.add_argument("--interval", type=int, default=3)
|
ap.add_argument("--interval", type=int, default=3)
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
@@ -138,7 +196,14 @@ def main():
|
|||||||
address = args.address.strip()
|
address = args.address.strip()
|
||||||
credential = args.credential.strip()
|
credential = args.credential.strip()
|
||||||
if not credential:
|
if not credential:
|
||||||
address, credential = create_address(args.api_base)
|
address, credential = create_address(
|
||||||
|
args.api_base,
|
||||||
|
auth_mode=args.auth_mode,
|
||||||
|
api_key=args.api_key,
|
||||||
|
create_path=args.create_path,
|
||||||
|
domain=args.domain,
|
||||||
|
name=args.name,
|
||||||
|
)
|
||||||
print(f"[NEW] address={address}")
|
print(f"[NEW] address={address}")
|
||||||
print(f"[NEW] credential(jwt)={credential}")
|
print(f"[NEW] credential(jwt)={credential}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"duckmail_api_key": "",
|
"duckmail_api_key": "",
|
||||||
"cloudflare_api_base": "https://cloudflare.pages.dev",
|
"cloudflare_api_base": "https://temp-mail.example.com",
|
||||||
"cloudflare_api_key": "",
|
"cloudflare_api_key": "",
|
||||||
"cloudflare_auth_mode": "none",
|
"cloudflare_auth_mode": "none",
|
||||||
"cloudflare_path_domains": "/api/domains",
|
"cloudflare_path_domains": "/api/domains",
|
||||||
|
|||||||
+86
-32
@@ -174,6 +174,8 @@ def cloudflare_build_headers(content_type=False):
|
|||||||
if key:
|
if key:
|
||||||
if mode == "x-api-key":
|
if mode == "x-api-key":
|
||||||
headers["X-API-Key"] = key
|
headers["X-API-Key"] = key
|
||||||
|
elif mode == "x-admin-auth":
|
||||||
|
headers["x-admin-auth"] = key
|
||||||
elif mode != "none":
|
elif mode != "none":
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
return headers
|
return headers
|
||||||
@@ -188,6 +190,22 @@ def cloudflare_apply_auth_params(params=None):
|
|||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def cloudflare_next_default_domain():
|
||||||
|
"""按配置轮换选择 Cloudflare 临时邮箱域名。"""
|
||||||
|
global _cf_domain_index
|
||||||
|
domains = [x.strip() for x in str(config.get("defaultDomains", "") or "").split(",") if x.strip()]
|
||||||
|
if not domains:
|
||||||
|
return ""
|
||||||
|
domain = domains[_cf_domain_index % len(domains)]
|
||||||
|
_cf_domain_index += 1
|
||||||
|
return domain
|
||||||
|
|
||||||
|
|
||||||
|
def cloudflare_is_admin_create_path(path):
|
||||||
|
"""判断当前创建邮箱路径是否为 cloudflare_temp_email 管理员创建接口。"""
|
||||||
|
return str(path or "").rstrip("/").lower() == "/admin/new_address"
|
||||||
|
|
||||||
|
|
||||||
def _pick_list_payload(data):
|
def _pick_list_payload(data):
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
return data
|
return data
|
||||||
@@ -208,19 +226,22 @@ def _pick_list_payload(data):
|
|||||||
|
|
||||||
|
|
||||||
def cloudflare_create_temp_address(api_base):
|
def cloudflare_create_temp_address(api_base):
|
||||||
"""适配 cloudflare_temp_email v1.8.x: POST /api/new_address -> {address,jwt}"""
|
"""适配 cloudflare_temp_email 新建地址接口并兼容 admin 创建模式。"""
|
||||||
global _cf_domain_index
|
path = get_cloudflare_path("cloudflare_path_accounts", "/api/new_address")
|
||||||
url = f"{api_base}/api/new_address"
|
url = f"{api_base}{path}"
|
||||||
payload = {}
|
domain = cloudflare_next_default_domain()
|
||||||
try:
|
is_admin_create = cloudflare_is_admin_create_path(path)
|
||||||
# 在多个域名之间轮换,降低单域偶发不收件导致的失败率
|
if is_admin_create:
|
||||||
domains = [x.strip() for x in str(config.get("defaultDomains", "") or "").split(",") if x.strip()]
|
payload = {"name": generate_username(10), "enablePrefix": True}
|
||||||
if domains:
|
if domain:
|
||||||
payload["domain"] = domains[_cf_domain_index % len(domains)]
|
payload["domain"] = domain
|
||||||
_cf_domain_index += 1
|
headers = cloudflare_build_headers(content_type=True)
|
||||||
except Exception:
|
else:
|
||||||
pass
|
payload = {}
|
||||||
resp = http_post(url, json=payload, headers={"Content-Type": "application/json"})
|
if domain:
|
||||||
|
payload["domain"] = domain
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
resp = http_post(url, json=payload, headers=headers)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
try:
|
try:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
@@ -295,6 +316,35 @@ def add_token_to_grok2api_local_pool(raw_token, email="", log_callback=None):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_grok2api_remote_api_bases(base):
|
||||||
|
"""生成 grok2api 管理 API 候选根路径。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- base str: 用户配置的 grok2api 远端地址
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[str]: 依次尝试的管理 API 根路径
|
||||||
|
"""
|
||||||
|
normalized = str(base or "").strip().rstrip("/")
|
||||||
|
if not normalized:
|
||||||
|
return []
|
||||||
|
lower = normalized.lower()
|
||||||
|
candidates = [normalized]
|
||||||
|
if lower.endswith("/admin/api"):
|
||||||
|
return candidates
|
||||||
|
if lower.endswith("/admin"):
|
||||||
|
candidates.append(f"{normalized}/api")
|
||||||
|
else:
|
||||||
|
candidates.append(f"{normalized}/admin/api")
|
||||||
|
seen = set()
|
||||||
|
unique = []
|
||||||
|
for item in candidates:
|
||||||
|
if item not in seen:
|
||||||
|
unique.append(item)
|
||||||
|
seen.add(item)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
def add_token_to_grok2api_remote_pool(raw_token, email="", log_callback=None):
|
def add_token_to_grok2api_remote_pool(raw_token, email="", log_callback=None):
|
||||||
token = _normalize_sso_token(raw_token)
|
token = _normalize_sso_token(raw_token)
|
||||||
if not token:
|
if not token:
|
||||||
@@ -310,24 +360,29 @@ def add_token_to_grok2api_remote_pool(raw_token, email="", log_callback=None):
|
|||||||
query = {"app_key": app_key}
|
query = {"app_key": app_key}
|
||||||
pool_map = {"ssoBasic": "basic", "ssoSuper": "super"}
|
pool_map = {"ssoBasic": "basic", "ssoSuper": "super"}
|
||||||
remote_pool = pool_map.get(pool_name, "basic")
|
remote_pool = pool_map.get(pool_name, "basic")
|
||||||
|
api_bases = get_grok2api_remote_api_bases(base)
|
||||||
|
add_errors = []
|
||||||
# 优先使用 add 接口,避免全量覆盖远端池
|
# 优先使用 add 接口,避免全量覆盖远端池
|
||||||
try:
|
add_payload = {"tokens": [token], "pool": remote_pool, "tags": ["auto-register"]}
|
||||||
add_payload = {"tokens": [token], "pool": remote_pool, "tags": ["auto-register"]}
|
for api_base in api_bases:
|
||||||
resp_add = http_post(
|
endpoint = f"{api_base}/tokens/add"
|
||||||
f"{base}/tokens/add",
|
try:
|
||||||
headers=headers,
|
resp_add = http_post(
|
||||||
params=query,
|
endpoint,
|
||||||
json=add_payload,
|
headers=headers,
|
||||||
timeout=30,
|
params=query,
|
||||||
proxies={},
|
json=add_payload,
|
||||||
)
|
timeout=30,
|
||||||
resp_add.raise_for_status()
|
proxies={},
|
||||||
if log_callback:
|
)
|
||||||
log_callback(f"[+] 已写入 grok2api 远端池: {pool_name} ({base}/tokens/add)")
|
resp_add.raise_for_status()
|
||||||
return True
|
if log_callback:
|
||||||
except Exception as add_exc:
|
log_callback(f"[+] 已写入 grok2api 远端池: {pool_name} ({endpoint})")
|
||||||
if log_callback:
|
return True
|
||||||
log_callback(f"[Debug] /tokens/add 写入失败,尝试 /tokens 全量模式: {add_exc}")
|
except Exception as add_exc:
|
||||||
|
add_errors.append(f"{endpoint}: {add_exc}")
|
||||||
|
if log_callback:
|
||||||
|
log_callback(f"[Debug] /tokens/add 写入失败,尝试 /tokens 全量模式: {'; '.join(add_errors)}")
|
||||||
|
|
||||||
# 兜底:旧版全量保存接口
|
# 兜底:旧版全量保存接口
|
||||||
current = {}
|
current = {}
|
||||||
@@ -2480,7 +2535,7 @@ class GrokRegisterGUI:
|
|||||||
add_label(2, 2, "Cloudflare 鉴权模式:")
|
add_label(2, 2, "Cloudflare 鉴权模式:")
|
||||||
self.cloudflare_auth_mode_var = tk.StringVar(value=config.get("cloudflare_auth_mode", "bearer"))
|
self.cloudflare_auth_mode_var = tk.StringVar(value=config.get("cloudflare_auth_mode", "bearer"))
|
||||||
self.cloudflare_auth_mode_combo = tk_option_menu(
|
self.cloudflare_auth_mode_combo = tk_option_menu(
|
||||||
config_frame, self.cloudflare_auth_mode_var, ["query-key", "bearer", "x-api-key", "none"], width=12
|
config_frame, self.cloudflare_auth_mode_var, ["query-key", "bearer", "x-api-key", "x-admin-auth", "none"], width=12
|
||||||
)
|
)
|
||||||
add_field(self.cloudflare_auth_mode_combo, 2, 3, sticky=tk.W)
|
add_field(self.cloudflare_auth_mode_combo, 2, 3, sticky=tk.W)
|
||||||
|
|
||||||
@@ -2989,4 +3044,3 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import cf_mail_debug
|
||||||
|
import grok_register_ttk as app
|
||||||
|
|
||||||
|
|
||||||
|
class DummyResponse:
|
||||||
|
def __init__(self, payload):
|
||||||
|
self._payload = payload
|
||||||
|
self.text = ""
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class CloudflareAdminCreateTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.original_config = app.config.copy()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
app.config = self.original_config
|
||||||
|
|
||||||
|
def test_app_uses_admin_new_address_with_x_admin_auth(self):
|
||||||
|
app.config.update({
|
||||||
|
"cloudflare_api_key": "admin-secret",
|
||||||
|
"cloudflare_auth_mode": "x-admin-auth",
|
||||||
|
"cloudflare_path_accounts": "/admin/new_address",
|
||||||
|
"defaultDomains": "vitassk.com",
|
||||||
|
})
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_post(url, **kwargs):
|
||||||
|
captured["url"] = url
|
||||||
|
captured.update(kwargs)
|
||||||
|
return DummyResponse({"address": "adminuser@vitassk.com", "jwt": "address-jwt"})
|
||||||
|
|
||||||
|
with patch.object(app, "generate_username", return_value="adminuser"), \
|
||||||
|
patch.object(app, "http_post", side_effect=fake_post):
|
||||||
|
address, jwt = app.cloudflare_create_temp_address("https://temp-mail.ikun.day")
|
||||||
|
|
||||||
|
self.assertEqual(address, "adminuser@vitassk.com")
|
||||||
|
self.assertEqual(jwt, "address-jwt")
|
||||||
|
self.assertEqual(captured["url"], "https://temp-mail.ikun.day/admin/new_address")
|
||||||
|
self.assertEqual(captured["json"], {
|
||||||
|
"name": "adminuser",
|
||||||
|
"domain": "vitassk.com",
|
||||||
|
"enablePrefix": True,
|
||||||
|
})
|
||||||
|
self.assertEqual(captured["headers"]["Content-Type"], "application/json")
|
||||||
|
self.assertEqual(captured["headers"]["x-admin-auth"], "admin-secret")
|
||||||
|
|
||||||
|
def test_app_keeps_anonymous_new_address_with_none_auth(self):
|
||||||
|
app.config.update({
|
||||||
|
"cloudflare_api_key": "",
|
||||||
|
"cloudflare_auth_mode": "none",
|
||||||
|
"cloudflare_path_accounts": "/api/new_address",
|
||||||
|
"defaultDomains": "vitassk.com",
|
||||||
|
})
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_post(url, **kwargs):
|
||||||
|
captured["url"] = url
|
||||||
|
captured.update(kwargs)
|
||||||
|
return DummyResponse({"address": "anon@vitassk.com", "jwt": "anon-jwt"})
|
||||||
|
|
||||||
|
with patch.object(app, "http_post", side_effect=fake_post):
|
||||||
|
address, jwt = app.cloudflare_create_temp_address("https://temp-mail.ikun.day")
|
||||||
|
|
||||||
|
self.assertEqual(address, "anon@vitassk.com")
|
||||||
|
self.assertEqual(jwt, "anon-jwt")
|
||||||
|
self.assertEqual(captured["url"], "https://temp-mail.ikun.day/api/new_address")
|
||||||
|
self.assertEqual(captured["json"], {"domain": "vitassk.com"})
|
||||||
|
self.assertEqual(captured["headers"], {"Content-Type": "application/json"})
|
||||||
|
|
||||||
|
def test_debug_tool_can_create_address_through_admin_api(self):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_post(url, **kwargs):
|
||||||
|
captured["url"] = url
|
||||||
|
captured.update(kwargs)
|
||||||
|
return DummyResponse({"address": "debuguser@vitassk.com", "jwt": "debug-jwt"})
|
||||||
|
|
||||||
|
with patch.object(cf_mail_debug.requests, "post", side_effect=fake_post):
|
||||||
|
address, jwt = cf_mail_debug.create_address(
|
||||||
|
"https://temp-mail.ikun.day",
|
||||||
|
auth_mode="x-admin-auth",
|
||||||
|
api_key="admin-secret",
|
||||||
|
create_path="/admin/new_address",
|
||||||
|
domain="vitassk.com",
|
||||||
|
name="debuguser",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(address, "debuguser@vitassk.com")
|
||||||
|
self.assertEqual(jwt, "debug-jwt")
|
||||||
|
self.assertEqual(captured["url"], "https://temp-mail.ikun.day/admin/new_address")
|
||||||
|
self.assertEqual(captured["json"], {
|
||||||
|
"name": "debuguser",
|
||||||
|
"domain": "vitassk.com",
|
||||||
|
"enablePrefix": True,
|
||||||
|
})
|
||||||
|
self.assertEqual(captured["headers"]["Content-Type"], "application/json")
|
||||||
|
self.assertEqual(captured["headers"]["x-admin-auth"], "admin-secret")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import grok_register_ttk as app
|
||||||
|
|
||||||
|
|
||||||
|
class DummyResponse:
|
||||||
|
def __init__(self, payload=None, status_code=200, reason=""):
|
||||||
|
self._payload = payload or {}
|
||||||
|
self.status_code = status_code
|
||||||
|
self.reason = reason
|
||||||
|
self.text = ""
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
if self.status_code >= 400:
|
||||||
|
raise RuntimeError(f"HTTP Error {self.status_code}: {self.reason}")
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class Grok2ApiRemotePoolTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.original_config = app.config.copy()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
app.config = self.original_config
|
||||||
|
|
||||||
|
def test_remote_pool_falls_back_to_admin_api_prefix_when_root_tokens_add_is_404(self):
|
||||||
|
app.config.update({
|
||||||
|
"grok2api_remote_base": "https://grok.example.com",
|
||||||
|
"grok2api_remote_app_key": "app-secret",
|
||||||
|
"grok2api_pool_name": "ssoBasic",
|
||||||
|
})
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_post(url, **kwargs):
|
||||||
|
calls.append((url, kwargs))
|
||||||
|
if url == "https://grok.example.com/tokens/add":
|
||||||
|
return DummyResponse(status_code=404)
|
||||||
|
return DummyResponse({"status": "success", "count": 1})
|
||||||
|
|
||||||
|
with patch.object(app, "http_post", side_effect=fake_post):
|
||||||
|
ok = app.add_token_to_grok2api_remote_pool("sso=abc123", email="a@example.com")
|
||||||
|
|
||||||
|
self.assertTrue(ok)
|
||||||
|
self.assertEqual([url for url, _ in calls], [
|
||||||
|
"https://grok.example.com/tokens/add",
|
||||||
|
"https://grok.example.com/admin/api/tokens/add",
|
||||||
|
])
|
||||||
|
self.assertEqual(calls[-1][1]["params"], {"app_key": "app-secret"})
|
||||||
|
self.assertEqual(calls[-1][1]["json"], {
|
||||||
|
"tokens": ["abc123"],
|
||||||
|
"pool": "basic",
|
||||||
|
"tags": ["auto-register"],
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_remote_pool_does_not_duplicate_admin_api_prefix_when_base_already_points_to_admin_api(self):
|
||||||
|
app.config.update({
|
||||||
|
"grok2api_remote_base": "https://grok.example.com/admin/api",
|
||||||
|
"grok2api_remote_app_key": "app-secret",
|
||||||
|
"grok2api_pool_name": "ssoSuper",
|
||||||
|
})
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_post(url, **kwargs):
|
||||||
|
calls.append((url, kwargs))
|
||||||
|
return DummyResponse({"status": "success", "count": 1})
|
||||||
|
|
||||||
|
with patch.object(app, "http_post", side_effect=fake_post):
|
||||||
|
ok = app.add_token_to_grok2api_remote_pool("sso=super123", email="a@example.com")
|
||||||
|
|
||||||
|
self.assertTrue(ok)
|
||||||
|
self.assertEqual([url for url, _ in calls], [
|
||||||
|
"https://grok.example.com/admin/api/tokens/add",
|
||||||
|
])
|
||||||
|
self.assertEqual(calls[0][1]["json"]["pool"], "super")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user