feat: add htsy CLI tool for Haitun private domain API
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
/bin/
|
||||||
|
/dist/
|
||||||
|
/.htsy/
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Added by aicommit after detecting protected files
|
||||||
|
/htsy-cli
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BINARY ?= htsy
|
||||||
|
VERSION ?= dev
|
||||||
|
LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||||
|
PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64
|
||||||
|
|
||||||
|
.PHONY: test build release clean
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
build:
|
||||||
|
mkdir -p bin
|
||||||
|
CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/$(BINARY) .
|
||||||
|
|
||||||
|
release: clean test
|
||||||
|
mkdir -p dist
|
||||||
|
for platform in $(PLATFORMS); do \
|
||||||
|
GOOS=$${platform%/*}; \
|
||||||
|
GOARCH=$${platform#*/}; \
|
||||||
|
EXT=""; \
|
||||||
|
if [ "$$GOOS" = "windows" ]; then EXT=".exe"; fi; \
|
||||||
|
OUT="dist/$(BINARY)_$${GOOS}_$${GOARCH}$${EXT}"; \
|
||||||
|
echo "building $$OUT"; \
|
||||||
|
CGO_ENABLED=0 GOOS=$$GOOS GOARCH=$$GOARCH go build -trimpath -ldflags "$(LDFLAGS)" -o "$$OUT" .; \
|
||||||
|
done
|
||||||
|
cd dist && shasum -a 256 * > checksums.txt
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf bin dist
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# htsy-cli
|
||||||
|
|
||||||
|
海豚私域接口命令行工具。
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./htsy-cli login --username USER --password PASS
|
||||||
|
./htsy-cli status
|
||||||
|
./htsy-cli call /api/account/user/info -d '{}'
|
||||||
|
./htsy-cli call /api/bot/wx/list -d '{"page":1,"pageSize":20,"botType":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows 下二进制文件名是 `htsy-cli.exe`,命令示例里的 `./htsy-cli` 对应替换为 `htsy-cli.exe`。
|
||||||
|
|
||||||
|
`login` 会按用户名是否包含 `@` 自动分流:
|
||||||
|
|
||||||
|
- `ldc@18030042216` -> `/api/account/user/login/in`
|
||||||
|
- `18030042216` -> `/api/account/login/in`
|
||||||
|
|
||||||
|
登录成功后,CLI 会把登录信息保存到当前用户主目录下的 `~/.htsy/config.json`。Linux、macOS、Windows 均支持。
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
make release VERSION=v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
`make release` 会构建:
|
||||||
|
|
||||||
|
- `linux/amd64`
|
||||||
|
- `linux/arm64`
|
||||||
|
- `darwin/amd64`
|
||||||
|
- `darwin/arm64`
|
||||||
|
- `windows/amd64`
|
||||||
|
- `windows/arm64`
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configDirName = ".htsy"
|
||||||
|
configFileName = "config.json"
|
||||||
|
defaultBaseURL = "https://htsy-gateway.cpshelp.cn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BaseURL string `json:"baseUrl,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
LoginKind string `json:"loginKind,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfigPath() (string, error) {
|
||||||
|
if value := os.Getenv("HTSY_CONFIG"); value != "" {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("resolve home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(home, configDirName, configFileName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveConfigPath(path string) (string, error) {
|
||||||
|
if path != "" {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
return defaultConfigPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) (Config, error) {
|
||||||
|
resolved, err := resolveConfigPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(resolved)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return Config{BaseURL: defaultBaseURL}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("read config %s: %w", resolved, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("parse config %s: %w", resolved, err)
|
||||||
|
}
|
||||||
|
if cfg.BaseURL == "" {
|
||||||
|
cfg.BaseURL = defaultBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig(path string, cfg Config) error {
|
||||||
|
resolved, err := resolveConfigPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.BaseURL == "" {
|
||||||
|
cfg.BaseURL = defaultBaseURL
|
||||||
|
}
|
||||||
|
cfg.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
|
dir := filepath.Dir(resolved)
|
||||||
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
|
return fmt.Errorf("create config dir %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encode config: %w", err)
|
||||||
|
}
|
||||||
|
data = append(data, '\n')
|
||||||
|
|
||||||
|
if err := os.WriteFile(resolved, data, 0o600); err != nil {
|
||||||
|
return fmt.Errorf("write config %s: %w", resolved, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearLogin(path string) error {
|
||||||
|
cfg, err := loadConfig(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg.Token = ""
|
||||||
|
cfg.Username = ""
|
||||||
|
cfg.LoginKind = ""
|
||||||
|
return saveConfig(path, cfg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type requestOptions struct {
|
||||||
|
BaseURL string
|
||||||
|
Path string
|
||||||
|
Body []byte
|
||||||
|
Token string
|
||||||
|
NoAuth bool
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseData struct {
|
||||||
|
StatusCode int
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBaseURL(raw string) (string, error) {
|
||||||
|
if raw == "" {
|
||||||
|
raw = defaultBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse base url: %w", err)
|
||||||
|
}
|
||||||
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||||
|
return "", fmt.Errorf("base url must start with http:// or https://")
|
||||||
|
}
|
||||||
|
if parsed.Host == "" {
|
||||||
|
return "", fmt.Errorf("base url must include a host")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimRight(parsed.String(), "/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointURL(baseURL, path string) (string, error) {
|
||||||
|
base, err := normalizeBaseURL(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
return "", fmt.Errorf("endpoint path is required")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
return base + path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postJSON(ctx context.Context, opts requestOptions) (responseData, error) {
|
||||||
|
target, err := endpointURL(opts.BaseURL, opts.Path)
|
||||||
|
if err != nil {
|
||||||
|
return responseData{}, err
|
||||||
|
}
|
||||||
|
if len(opts.Body) == 0 {
|
||||||
|
opts.Body = []byte("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(opts.Body))
|
||||||
|
if err != nil {
|
||||||
|
return responseData{}, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if !opts.NoAuth && opts.Token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+opts.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := opts.Timeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return responseData{}, fmt.Errorf("http post %s: %w", target, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return responseData{}, fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return responseData{StatusCode: resp.StatusCode, Body: body}, httpStatusError{StatusCode: resp.StatusCode, Body: body}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData{StatusCode: resp.StatusCode, Body: body}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpStatusError struct {
|
||||||
|
StatusCode int
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e httpStatusError) Error() string {
|
||||||
|
body := strings.TrimSpace(string(e.Body))
|
||||||
|
if body == "" {
|
||||||
|
return fmt.Sprintf("request failed with status %d", e.StatusCode)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("request failed with status %d: %s", e.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readJSONData(value string) ([]byte, error) {
|
||||||
|
if value == "" {
|
||||||
|
return []byte("{}"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case value == "-":
|
||||||
|
data, err = io.ReadAll(os.Stdin)
|
||||||
|
case strings.HasPrefix(value, "@"):
|
||||||
|
path := strings.TrimPrefix(value, "@")
|
||||||
|
if path == "" {
|
||||||
|
return nil, fmt.Errorf("missing file after @")
|
||||||
|
}
|
||||||
|
cleanPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolve json file: %w", err)
|
||||||
|
}
|
||||||
|
data, err = os.ReadFile(cleanPath)
|
||||||
|
default:
|
||||||
|
data = []byte(value)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read json body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
data = []byte("{}")
|
||||||
|
}
|
||||||
|
if !json.Valid(data) {
|
||||||
|
return nil, fmt.Errorf("request body must be valid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSONResponse(w io.Writer, body []byte, raw bool) error {
|
||||||
|
body = bytes.TrimSpace(body)
|
||||||
|
if len(body) == 0 {
|
||||||
|
_, err := fmt.Fprintln(w, "{}")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if raw {
|
||||||
|
_, err := w.Write(append(body, '\n'))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := json.Indent(&out, body, "", " "); err != nil {
|
||||||
|
_, writeErr := w.Write(append(body, '\n'))
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
out.WriteByte('\n')
|
||||||
|
_, err := w.Write(out.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenFromResponse(body []byte) (string, error) {
|
||||||
|
var decoded map[string]any
|
||||||
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
||||||
|
return "", fmt.Errorf("login response is not JSON: %w", err)
|
||||||
|
}
|
||||||
|
if token, ok := decoded["token"].(string); ok && token != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, ok := decoded["data"].(map[string]any); ok {
|
||||||
|
if token, ok := data["token"].(string); ok && token != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("login response does not contain token")
|
||||||
|
}
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
printUsage()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "login":
|
||||||
|
return runLogin(args[1:], false)
|
||||||
|
case "account-login":
|
||||||
|
return runLogin(args[1:], true)
|
||||||
|
case "register":
|
||||||
|
return runRegister(args[1:])
|
||||||
|
case "send-code":
|
||||||
|
return runSendCode(args[1:])
|
||||||
|
case "call":
|
||||||
|
return runCall(args[1:])
|
||||||
|
case "status":
|
||||||
|
return runStatus(args[1:])
|
||||||
|
case "logout":
|
||||||
|
return runLogout(args[1:])
|
||||||
|
case "config":
|
||||||
|
return runConfig(args[1:])
|
||||||
|
case "version", "--version", "-v":
|
||||||
|
fmt.Printf("htsy %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
return nil
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
printUsage()
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown command %q", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogin(args []string, accountLogin bool) error {
|
||||||
|
fs := flag.NewFlagSet("login", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
var username, phone, password, code, baseURL, configPath string
|
||||||
|
var timeout time.Duration
|
||||||
|
var printToken bool
|
||||||
|
fs.StringVar(&username, "username", "", "user login username")
|
||||||
|
fs.StringVar(&phone, "phone", "", "account login phone")
|
||||||
|
fs.StringVar(&password, "password", "", "password; defaults to HTSY_PASSWORD when omitted")
|
||||||
|
fs.StringVar(&code, "code", "", "sms code for account phone login")
|
||||||
|
fs.StringVar(&baseURL, "base-url", "", "API base URL")
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
fs.DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
|
||||||
|
fs.BoolVar(&printToken, "print-token", false, "print token in command output")
|
||||||
|
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if password == "" && code == "" {
|
||||||
|
password = os.Getenv("HTSY_PASSWORD")
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = os.Getenv("HTSY_BASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
loginID := username
|
||||||
|
if loginID == "" {
|
||||||
|
loginID = phone
|
||||||
|
}
|
||||||
|
if loginID == "" && fs.NArg() > 0 {
|
||||||
|
loginID = fs.Arg(0)
|
||||||
|
}
|
||||||
|
if password == "" && fs.NArg() > 1 {
|
||||||
|
password = fs.Arg(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginID == "" {
|
||||||
|
if accountLogin {
|
||||||
|
return fmt.Errorf("--phone is required")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("--username or --phone is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, payloadKey, kind := loginRoute(loginID, accountLogin)
|
||||||
|
loginName := loginID
|
||||||
|
payload := map[string]string{payloadKey: loginID}
|
||||||
|
if accountLogin {
|
||||||
|
if password == "" && code == "" {
|
||||||
|
return fmt.Errorf("--password or --code is required")
|
||||||
|
}
|
||||||
|
} else if password == "" {
|
||||||
|
return fmt.Errorf("--password is required or set HTSY_PASSWORD")
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
payload["password"] = password
|
||||||
|
}
|
||||||
|
if code != "" {
|
||||||
|
payload["code"] = code
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resolvedBaseURL, err := baseURLFromConfigOrFlag(configPath, baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := postJSON(context.Background(), requestOptions{
|
||||||
|
BaseURL: resolvedBaseURL,
|
||||||
|
Path: endpoint,
|
||||||
|
Body: body,
|
||||||
|
NoAuth: true,
|
||||||
|
Timeout: timeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := tokenFromResponse(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: resolvedBaseURL,
|
||||||
|
Token: token,
|
||||||
|
Username: loginName,
|
||||||
|
LoginKind: kind,
|
||||||
|
}
|
||||||
|
if err := saveConfig(configPath, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := resolveConfigPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out := map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"baseUrl": resolvedBaseURL,
|
||||||
|
"configPath": path,
|
||||||
|
"loginKind": kind,
|
||||||
|
"username": loginName,
|
||||||
|
"tokenSaved": true,
|
||||||
|
}
|
||||||
|
if printToken {
|
||||||
|
out["token"] = token
|
||||||
|
}
|
||||||
|
return printObject(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRegister(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("register", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
var username, password, smsCode, inviteToken, baseURL, configPath string
|
||||||
|
var timeout time.Duration
|
||||||
|
var printToken bool
|
||||||
|
fs.StringVar(&username, "username", "", "username, normally the phone number")
|
||||||
|
fs.StringVar(&password, "password", "", "password; defaults to HTSY_PASSWORD when omitted")
|
||||||
|
fs.StringVar(&smsCode, "sms-code", "", "sms verification code")
|
||||||
|
fs.StringVar(&inviteToken, "invite-token", "", "optional invite token")
|
||||||
|
fs.StringVar(&baseURL, "base-url", "", "API base URL")
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
fs.DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
|
||||||
|
fs.BoolVar(&printToken, "print-token", false, "print token in command output")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if username == "" && fs.NArg() > 0 {
|
||||||
|
username = fs.Arg(0)
|
||||||
|
}
|
||||||
|
if password == "" && fs.NArg() > 1 {
|
||||||
|
password = fs.Arg(1)
|
||||||
|
}
|
||||||
|
if smsCode == "" && fs.NArg() > 2 {
|
||||||
|
smsCode = fs.Arg(2)
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
password = os.Getenv("HTSY_PASSWORD")
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
return fmt.Errorf("--username is required")
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
return fmt.Errorf("--password is required or set HTSY_PASSWORD")
|
||||||
|
}
|
||||||
|
if smsCode == "" {
|
||||||
|
return fmt.Errorf("--sms-code is required")
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = os.Getenv("HTSY_BASE_URL")
|
||||||
|
}
|
||||||
|
resolvedBaseURL, err := baseURLFromConfigOrFlag(configPath, baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"smsCode": smsCode,
|
||||||
|
}
|
||||||
|
if inviteToken != "" {
|
||||||
|
payload["inviteToken"] = inviteToken
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := postJSON(context.Background(), requestOptions{
|
||||||
|
BaseURL: resolvedBaseURL,
|
||||||
|
Path: "/api/account/user/register",
|
||||||
|
Body: body,
|
||||||
|
NoAuth: true,
|
||||||
|
Timeout: timeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
token, err := tokenFromResponse(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: resolvedBaseURL,
|
||||||
|
Token: token,
|
||||||
|
Username: username,
|
||||||
|
LoginKind: "user",
|
||||||
|
}
|
||||||
|
if err := saveConfig(configPath, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path, err := resolveConfigPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out := map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"baseUrl": resolvedBaseURL,
|
||||||
|
"configPath": path,
|
||||||
|
"loginKind": "user",
|
||||||
|
"username": username,
|
||||||
|
"tokenSaved": true,
|
||||||
|
}
|
||||||
|
if printToken {
|
||||||
|
out["token"] = token
|
||||||
|
}
|
||||||
|
return printObject(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSendCode(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("send-code", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
var phone, scene, baseURL, configPath string
|
||||||
|
var timeout time.Duration
|
||||||
|
var raw bool
|
||||||
|
fs.StringVar(&phone, "phone", "", "phone number")
|
||||||
|
fs.StringVar(&scene, "scene", "", "scene: register, resetPwd, login, bind")
|
||||||
|
fs.StringVar(&baseURL, "base-url", "", "API base URL")
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
fs.DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
|
||||||
|
fs.BoolVar(&raw, "raw", false, "print raw response")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if phone == "" && fs.NArg() > 0 {
|
||||||
|
phone = fs.Arg(0)
|
||||||
|
}
|
||||||
|
if scene == "" && fs.NArg() > 1 {
|
||||||
|
scene = fs.Arg(1)
|
||||||
|
}
|
||||||
|
if phone == "" {
|
||||||
|
return fmt.Errorf("--phone is required")
|
||||||
|
}
|
||||||
|
if scene == "" {
|
||||||
|
return fmt.Errorf("--scene is required")
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = os.Getenv("HTSY_BASE_URL")
|
||||||
|
}
|
||||||
|
resolvedBaseURL, err := baseURLFromConfigOrFlag(configPath, baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(map[string]string{"phone": phone, "scene": scene})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := postJSON(context.Background(), requestOptions{
|
||||||
|
BaseURL: resolvedBaseURL,
|
||||||
|
Path: "/api/account/user/phone/sendCode",
|
||||||
|
Body: body,
|
||||||
|
NoAuth: true,
|
||||||
|
Timeout: timeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeJSONResponse(os.Stdout, resp.Body, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCall(args []string) error {
|
||||||
|
path, flagArgs, err := splitCallArgs(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("call", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
var data, baseURL, configPath, token string
|
||||||
|
var timeout time.Duration
|
||||||
|
var noAuth, raw bool
|
||||||
|
fs.StringVar(&data, "d", "{}", "JSON request body, @file, or - for stdin")
|
||||||
|
fs.StringVar(&data, "data", "{}", "JSON request body, @file, or - for stdin")
|
||||||
|
fs.StringVar(&baseURL, "base-url", "", "API base URL")
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
fs.StringVar(&token, "token", "", "override bearer token")
|
||||||
|
fs.DurationVar(&timeout, "timeout", 30*time.Second, "request timeout")
|
||||||
|
fs.BoolVar(&noAuth, "no-auth", false, "do not attach bearer token")
|
||||||
|
fs.BoolVar(&raw, "raw", false, "print raw response")
|
||||||
|
if err := fs.Parse(flagArgs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fs.NArg() > 0 {
|
||||||
|
return fmt.Errorf("unexpected call arguments: %s", strings.Join(fs.Args(), " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = os.Getenv("HTSY_BASE_URL")
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = cfg.BaseURL
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
token = os.Getenv("HTSY_TOKEN")
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
token = cfg.Token
|
||||||
|
}
|
||||||
|
if !noAuth && token == "" {
|
||||||
|
return fmt.Errorf("not logged in; run htsy login first or pass --token")
|
||||||
|
}
|
||||||
|
body, err := readJSONData(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := postJSON(context.Background(), requestOptions{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Path: path,
|
||||||
|
Body: body,
|
||||||
|
Token: token,
|
||||||
|
NoAuth: noAuth,
|
||||||
|
Timeout: timeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeJSONResponse(os.Stdout, resp.Body, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitCallArgs(args []string) (string, []string, error) {
|
||||||
|
var endpoint string
|
||||||
|
var flagArgs []string
|
||||||
|
expectValue := false
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if expectValue {
|
||||||
|
flagArgs = append(flagArgs, arg)
|
||||||
|
expectValue = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCallValueFlag(arg) {
|
||||||
|
flagArgs = append(flagArgs, arg)
|
||||||
|
if !strings.Contains(arg, "=") {
|
||||||
|
expectValue = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCallBoolFlag(arg) || strings.HasPrefix(arg, "-") {
|
||||||
|
flagArgs = append(flagArgs, arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = arg
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil, fmt.Errorf("unexpected call argument %q", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectValue {
|
||||||
|
return "", nil, fmt.Errorf("missing value for call flag")
|
||||||
|
}
|
||||||
|
if endpoint == "" {
|
||||||
|
return "", nil, fmt.Errorf("endpoint path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint, flagArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCallValueFlag(arg string) bool {
|
||||||
|
for _, name := range []string{"-d", "--data", "-data", "--base-url", "-base-url", "--config", "-config", "--token", "-token", "--timeout", "-timeout"} {
|
||||||
|
if arg == name || strings.HasPrefix(arg, name+"=") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCallBoolFlag(arg string) bool {
|
||||||
|
for _, name := range []string{"--no-auth", "-no-auth", "--raw", "-raw"} {
|
||||||
|
if arg == name || strings.HasPrefix(arg, name+"=") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStatus(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("status", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
var configPath string
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg, err := loadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path, err := resolveConfigPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tokenPrefix := ""
|
||||||
|
if cfg.Token != "" {
|
||||||
|
if len(cfg.Token) <= 8 {
|
||||||
|
tokenPrefix = cfg.Token
|
||||||
|
} else {
|
||||||
|
tokenPrefix = cfg.Token[:8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return printObject(map[string]any{
|
||||||
|
"baseUrl": cfg.BaseURL,
|
||||||
|
"configPath": path,
|
||||||
|
"loggedIn": cfg.Token != "",
|
||||||
|
"loginKind": cfg.LoginKind,
|
||||||
|
"username": cfg.Username,
|
||||||
|
"tokenPrefix": tokenPrefix,
|
||||||
|
"updatedAt": cfg.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogout(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("logout", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
var configPath string
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := clearLogin(configPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printObject(map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfig(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("config subcommand is required: path or set-base-url")
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "path":
|
||||||
|
fs := flag.NewFlagSet("config path", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
var configPath string
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
if err := fs.Parse(args[1:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path, err := resolveConfigPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(path)
|
||||||
|
return nil
|
||||||
|
case "set-base-url":
|
||||||
|
fs := flag.NewFlagSet("config set-base-url", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
var configPath string
|
||||||
|
fs.StringVar(&configPath, "config", "", "config file path")
|
||||||
|
if err := fs.Parse(args[1:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fs.NArg() < 1 {
|
||||||
|
return fmt.Errorf("base url is required")
|
||||||
|
}
|
||||||
|
baseURL, err := normalizeBaseURL(fs.Arg(0))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg, err := loadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg.BaseURL = baseURL
|
||||||
|
if err := saveConfig(configPath, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printObject(map[string]any{"ok": true, "baseUrl": baseURL})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown config subcommand %q", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseURLFromConfigOrFlag(configPath, baseURL string) (string, error) {
|
||||||
|
if baseURL == "" {
|
||||||
|
cfg, err := loadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
baseURL = cfg.BaseURL
|
||||||
|
}
|
||||||
|
return normalizeBaseURL(baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printObject(value any) error {
|
||||||
|
data, err := json.MarshalIndent(value, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Println(strings.TrimSpace(`
|
||||||
|
htsy 是海豚私域接口命令行工具。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
htsy login --username USER --password PASS [--base-url URL]
|
||||||
|
htsy account-login --phone PHONE --password PASS
|
||||||
|
htsy send-code --phone PHONE --scene register
|
||||||
|
htsy register --username PHONE --password PASS --sms-code CODE [--invite-token TOKEN]
|
||||||
|
htsy call /api/account/user/info -d '{}'
|
||||||
|
htsy status
|
||||||
|
htsy logout
|
||||||
|
htsy config path
|
||||||
|
htsy config set-base-url https://htsy-gateway.cpshelp.cn
|
||||||
|
|
||||||
|
配置:
|
||||||
|
登录信息会保存到当前用户主目录下的 ~/.htsy/config.json。
|
||||||
|
可用 HTSY_CONFIG 覆盖配置文件路径。
|
||||||
|
自动化场景可使用 HTSY_BASE_URL、HTSY_TOKEN、HTSY_PASSWORD。
|
||||||
|
|
||||||
|
登录规则:
|
||||||
|
login 命令里带 @ 的用户名会走子账号登录,调用 /api/account/user/login/in。
|
||||||
|
不带 @ 的用户名会走主账号登录,调用 /api/account/login/in。
|
||||||
|
account-login 始终走主账号登录,调用 /api/account/login/in。
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginRoute(loginID string, accountLogin bool) (endpoint string, payloadKey string, kind string) {
|
||||||
|
if accountLogin {
|
||||||
|
return "/api/account/login/in", "phone", "account"
|
||||||
|
}
|
||||||
|
if strings.Contains(loginID, "@") {
|
||||||
|
return "/api/account/user/login/in", "username", "user"
|
||||||
|
}
|
||||||
|
return "/api/account/login/in", "phone", "account"
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeBaseURL(t *testing.T) {
|
||||||
|
got, err := normalizeBaseURL("https://mpbot.cpshelp.cn/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != "https://mpbot.cpshelp.cn" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := normalizeBaseURL("mpbot.cpshelp.cn"); err == nil {
|
||||||
|
t.Fatal("expected invalid base url error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpointURL(t *testing.T) {
|
||||||
|
got, err := endpointURL("https://mpbot.cpshelp.cn/", "api/account/user/info")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := "https://mpbot.cpshelp.cn/api/account/user/info"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadJSONDataLiteralAndFile(t *testing.T) {
|
||||||
|
if _, err := readJSONData(`{"ok":true}`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := readJSONData(`{"ok":`); err == nil {
|
||||||
|
t.Fatal("expected invalid json error")
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "body.json")
|
||||||
|
if err := os.WriteFile(path, []byte(`{"from":"file"}`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := readJSONData("@" + path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRoundTrip(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), ".htsy", "config.json")
|
||||||
|
cfg := Config{
|
||||||
|
BaseURL: "https://mpbot.cpshelp.cn",
|
||||||
|
Token: "token-value",
|
||||||
|
Username: "user",
|
||||||
|
LoginKind: "user",
|
||||||
|
}
|
||||||
|
if err := saveConfig(path, cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
loaded, err := loadConfig(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if loaded.Token != cfg.Token || loaded.Username != cfg.Username || loaded.BaseURL != cfg.BaseURL {
|
||||||
|
t.Fatalf("loaded config mismatch: %+v", loaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenFromResponse(t *testing.T) {
|
||||||
|
token, err := tokenFromResponse([]byte(`{"token":"top-level"}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if token != "top-level" {
|
||||||
|
t.Fatalf("got %q", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err = tokenFromResponse([]byte(`{"code":0,"data":{"token":"nested"}}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if token != "nested" {
|
||||||
|
t.Fatalf("got %q", token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitCallArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantPath string
|
||||||
|
wantFlags []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "path first",
|
||||||
|
args: []string{"/api/account/user/info", "-d", "{}"},
|
||||||
|
wantPath: "/api/account/user/info",
|
||||||
|
wantFlags: []string{"-d", "{}"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flags first",
|
||||||
|
args: []string{"-d", "{}", "--raw", "/api/account/user/info"},
|
||||||
|
wantPath: "/api/account/user/info",
|
||||||
|
wantFlags: []string{"-d", "{}", "--raw"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equals flag",
|
||||||
|
args: []string{"--data={}", "/api/account/user/info"},
|
||||||
|
wantPath: "/api/account/user/info",
|
||||||
|
wantFlags: []string{"--data={}"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing endpoint",
|
||||||
|
args: []string{"-d", "{}"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing flag value",
|
||||||
|
args: []string{"/api/account/user/info", "-d"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotPath, gotFlags, err := splitCallArgs(tt.args)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if gotPath != tt.wantPath {
|
||||||
|
t.Fatalf("path got %q want %q", gotPath, tt.wantPath)
|
||||||
|
}
|
||||||
|
if len(gotFlags) != len(tt.wantFlags) {
|
||||||
|
t.Fatalf("flags got %#v want %#v", gotFlags, tt.wantFlags)
|
||||||
|
}
|
||||||
|
for i := range gotFlags {
|
||||||
|
if gotFlags[i] != tt.wantFlags[i] {
|
||||||
|
t.Fatalf("flags got %#v want %#v", gotFlags, tt.wantFlags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user