Files
opsxapi/apps/server/internal/mocksrv/mocksrv_test.go
T
logan d9ccccd280 本地 Mock 服务 /mock + 限流 20QPS/60QPM + 服务条款
- Go Mock 引擎(与前端同语法 @cname/@integer/... 每次请求随机生成,JSON 叶子保持类型)
- mock_endpoints 表 + 发布 API(PUT /api/workspaces/:id/mock,全量替换,单空间 200 个)
- 路由 /mock/:wsid/*path:状态码/延迟(≤5s)/?_method 覆盖;未发布 404
- 限流(IP+空间 滑动窗口):20 QPS / 60 QPM → 429 + Retry-After + 条款提示
- 官网 /terms 服务条款(七节:含禁止滥用——未经授权或滥用有权采取措施、技术手段规避限流属违规)
- UI:HTTP 编辑器「Mock 链接」弹窗(发布/状态码/延迟/模板预览/复制/试一试);
  发布自动烘焙智能 Mock 表达式;集合端点去重 + INSERT OR REPLACE
- docs:14-mock 增本地 Mock 服务与限流章节;页脚/429 响应均指向 /terms
- 单测:mocksrv 引擎/限流/路径 5 用例;curl E2E(随机×2、201+延迟、404、连发 30 次触发 429);浏览器发布+试一试通过
2026-09-18 12:29:49 +08:00

138 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mocksrv
import (
"encoding/json"
"regexp"
"strings"
"testing"
"time"
)
func TestGenerateFormats(t *testing.T) {
cases := []struct {
expr string
re string
}{
{"@integer(3, 7), extra", ""},
}
_ = cases
checks := map[string]*regexp.Regexp{
"@integer(3, 7)": regexp.MustCompile(`^[3-7]$`),
"@float(1, 9, 2)": regexp.MustCompile(`^\d\.\d{2}$`),
"@email": regexp.MustCompile(`^[\w.]+@[\w.]+\.\w+$`),
"@phone": regexp.MustCompile(`^1[3-9]\d{9}$`),
"@guid": regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`),
"@id": regexp.MustCompile(`^\d{17}[\dX]$`),
"@datetime(YYYY-MM-DD HH:mm:ss)": regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`),
"@timestamp": regexp.MustCompile(`^\d{12,}$`),
"@boolean": regexp.MustCompile(`^(true|false)$`),
"@cname": regexp.MustCompile(`^\p{Han}{2,3}$`),
"@county(true)": regexp.MustCompile(`\p{Han}+(市)?\p{Han}+区$`),
"@pick(\"男\", \"女\")": regexp.MustCompile(`^(男|女)$`),
"@notExistThing": regexp.MustCompile(`^@notExistThing$`), // 未知表达式原样
"普通文本": regexp.MustCompile(`^普通文本$`),
}
for expr, re := range checks {
for i := 0; i < 30; i++ {
out := Generate(expr)
if !re.MatchString(out) {
t.Fatalf("%s → %q 不匹配 %v", expr, out, re)
}
}
}
}
func TestGenerateJSONLeaves(t *testing.T) {
tpl := `{"username":"@cname","age":"@integer(18,65)","vip":"@boolean","keep":"固定值"}`
var v map[string]interface{}
if err := json.Unmarshal([]byte(GenerateJSON(tpl)), &v); err != nil {
t.Fatalf("生成结果非 JSON: %v", err)
}
age, ok := v["age"].(float64)
if !ok || age < 18 || age > 65 {
t.Fatalf("age 应为 18-65 的数字,得到 %v", v["age"])
}
if _, ok := v["vip"].(bool); !ok {
t.Fatalf("vip 应为布尔,得到 %T", v["vip"])
}
if v["keep"] != "固定值" {
t.Fatalf("非表达式叶子应原样保留")
}
if s, _ := v["username"].(string); len(s) < 2 {
t.Fatalf("username 生成异常: %v", v["username"])
}
}
func TestGenerateJSONTopLevelAndInvalid(t *testing.T) {
if out := GenerateJSON("@cname"); len([]rune(out)) < 2 {
t.Fatalf("顶层表达式应生成: %q", out)
}
if out := GenerateJSON("not-json"); out != "not-json" {
t.Fatalf("非法 JSON 原样返回: %q", out)
}
}
func TestLimiter(t *testing.T) {
l := newLimiter()
now := time.Now()
// 同一秒内第 21 次拒绝(QPS)
pass := 0
for i := 0; i < 25; i++ {
if l.allow("k", now) {
pass++
}
}
if pass != LimitQPS {
t.Fatalf("QPS 应放行 %d,实际 %d", LimitQPS, pass)
}
// 后续秒窗重置但 60 秒窗累计:S2 放 20、S3 放 20(累计 60),S4 拒绝(QPM)
for _, off := range []time.Duration{2 * time.Second, 3 * time.Second} {
pass2 := 0
for i := 0; i < 25; i++ {
if l.allow("k", now.Add(off)) {
pass2++
}
}
if pass2 != 20 {
t.Fatalf("+%v 应放行 20(QPS),实际 %d", off, pass2)
}
}
if l.allow("k", now.Add(4*time.Second)) {
t.Fatalf("60 秒窗口内累计达 60 应拒绝")
}
// 不同 key 相互独立
if !l.allow("other", now.Add(3*time.Second)) {
t.Fatal("不同 key 不应被牵连")
}
// 滑动窗口过期后恢复
l2 := newLimiter()
t0 := time.Now()
for i := 0; i < LimitQPM; i++ {
if !l2.allow("k2", t0.Add(time.Duration(i)*time.Second)) {
t.Fatalf("分散 60 秒内应全部放行(第 %d 次被拒)", i+1)
}
}
if l2.allow("k2", t0.Add(59*time.Second)) {
t.Fatal("60 次命中仍在窗口内应被拒")
}
if !l2.allow("k2", t0.Add(130*time.Second)) {
t.Fatal("窗口滑过后应恢复放行")
}
}
func TestNormalizePath(t *testing.T) {
if normalizePath("api/users/") != "/api/users" {
t.Fatalf("got %q", normalizePath("api/users/"))
}
if normalizePath("") != "/" {
t.Fatalf("空路径 → /")
}
if normalizePath("/x/") != "/x" {
t.Fatalf("去尾斜杠")
}
_ = strings.TrimSpace
}