Files
PortRelay/server/readme_pages_test.go
DeepFal 3272e66247 docs: README 跟上授权延长与通道组,截图和动图改为一条命令重新生成
README
- 申请延长写清规则:授权有效期间也可申请,有效的从原到期时间顺延、已结束的从批准时起算,
  被撤销的不能延长;批准延长时通道会开到授权结束(与最长开启时长的关系)
- 修正通道组「一起开关」与「可以单独开关」的矛盾:闲置按整组计算,每个地址仍可单独开关
- 时长选项用界面上的「通道开启期间」;快速开始按实际流程写(保存后自动弹出邀请设置)
- 新增同事端申请延长的三张截图;流程图补上 30 分钟、自定义与 2 小时内可申请延长

截图与动图
- 授权管理、编辑通道、邀请文案、授权成功页、网页面板(含通道组)更新为当前界面
- 离屏渲染时控件按激活状态绘制,开关不再显示成灰色
- 新增 make readme-images:App 离屏渲染、动图合成、授权页与网页面板的无头 Chrome 截图一次完成,
  写入 docs/images;授权页 HTML 由服务端测试用模拟状态生成,网页面板用本机模拟 API

界面
- 计时文字第二个单位补零(27分05秒),卡片上的徽标不再随秒数左右跳动;App 与网页面板一致
- 待审批与延长申请的时间格式统一为不带秒;App 预览的分享文案补上延长提示
2026-10-14 14:51:07 +08:00

86 lines
3.2 KiB
Go
Raw Permalink 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 server
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/DeepFal/PortRelay/core/acl"
"github.com/DeepFal/PortRelay/core/protocol"
"github.com/DeepFal/PortRelay/core/traffic"
)
// TestReadmePages 用模拟状态生成 README 截图用的授权页 HTML(tools/ui-snapshots/web.sh 调用),平时跳过。
// 通道只放在内存里,不监听端口;访问者 IP 直接写在请求上。
func TestReadmePages(t *testing.T) {
out := os.Getenv("PORTRELAY_README_PAGES")
if out == "" {
t.Skip("PORTRELAY_README_PAGES not set")
}
m := NewManager(33000, 34000, testToken)
m.PublicHost = "192.168.1.10"
cfg := protocol.TunnelConfig{ID: "mysql", Name: "MySQL 数据库", Target: "10.0.1.16:3306", ListenPort: 33660, Enabled: true}
m.mu.Lock()
m.tunnels[cfg.ID] = &Tunnel{Config: cfg, whitelist: acl.NewWhitelist(nil), activeConns: map[string]*ActiveClient{},
meter: traffic.NewMeter(), closed: make(chan struct{})}
m.mu.Unlock()
do := func(method, path, ip string, form url.Values) *httptest.ResponseRecorder {
var body io.Reader
if form != nil {
body = strings.NewReader(form.Encode())
}
r := httptest.NewRequest(method, "http://192.168.1.10:33000"+path, body)
if form != nil {
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
r.RemoteAddr = ip + ":52000"
w := httptest.NewRecorder()
m.handleHTTPRequest(w, r)
return w
}
follow := func(w *httptest.ResponseRecorder, ip string) *httptest.ResponseRecorder {
if loc := w.Header().Get("Location"); loc != "" {
return do(http.MethodGet, loc, ip, nil)
}
return w
}
save := func(name string, w *httptest.ResponseRecorder) {
t.Helper()
if w.Code != http.StatusOK {
t.Fatalf("%s: %d %s", name, w.Code, w.Body)
}
if err := os.WriteFile(filepath.Join(out, name+".html"), w.Body.Bytes(), 0o644); err != nil {
t.Fatal(err)
}
}
// ① 打开邀请链接 → ③ 授权成功
inv := m.state.createInvite(cfg.ID, "张三", 24*time.Hour, 1, "2h", false)
save("auth-form", do(http.MethodGet, "/i/"+inv.Code, "192.168.1.21", nil))
save("auth-ok", do(http.MethodPost, "/auth", "192.168.1.21", url.Values{"code": {inv.Code}, "name": {"张三"}}))
// ② 需要审批
ask := m.state.createInvite(cfg.ID, "测试组", 24*time.Hour, 0, "today", true)
save("auth-pending", follow(do(http.MethodPost, "/auth", "192.168.1.88", url.Values{"code": {ask.Code}, "name": {"李四"}}), "192.168.1.88"))
// ④ ⑤ ⑥ 两小时前开始使用、20 分钟前到期,再打开链接申请延长,管理员批准 2 小时
ip := "192.168.1.47"
rn := m.state.createInvite(cfg.ID, "赵六", 24*time.Hour, 1, "2h", false)
do(http.MethodPost, "/auth", ip, url.Values{"code": {rn.Code}, "name": {"赵六"}})
m.state.startLease(cfg.ID, ip, nil, time.Now().Add(-140*time.Minute))
save("auth-renew", do(http.MethodGet, "/i/"+rn.Code, ip, nil))
status := do(http.MethodPost, "/renew", ip, url.Values{"code": {rn.Code}})
save("auth-renew-pending", follow(status, ip))
if resp := m.extendLease(protocol.ExtendLeaseMessage{TunnelID: cfg.ID, IP: ip, Minutes: 120}); !resp.Success {
t.Fatal(resp.Error)
}
save("auth-renew-ok", follow(status, ip))
}