Files
opsx-vpn/backend/internal/service/openvpn/openvpn_test.go
T
Logan.Li 1e2f8ef24f feat: 全面优化 — UI/UX重构 + 流量审计与会话管理 + 协议部署
UI/UX 优化:
- 登录页分屏布局 + 可访问性修复 + 内联错误提示
- 侧栏菜单分组(专线运维/资源管理/系统) + 图标修正 + 子路由高亮
- Dashboard 统计卡渐变图标 + 空状态引导 + 统计卡可点击
- 创建向导: 依赖检测门控 + 高级折叠 + 必填校验 + WireGuard推荐标
- 专线列表: 空状态CTA + 操作列「更多」下拉 + 状态筛选 + SSE日志流
- 协议品牌SVG图标(ProtocolIcon组件)替换emoji
- 设计令牌系统(tokens.css)统一颜色/间距/圆角/阴影

功能增强:
- 在线会话管理(sessionmgr): 跨协议会话同步 + 踢下线 + 连接审计日志
- 流量配额引擎(trafficmgr): 配额检查 + 超额动作(block/warn/throttle) + 周期重置
- 异常流量告警: 突发流量检测(>90MB/15s) + 审计记录
- 节点SSH凭证管理: 密码/私钥认证 + 即时测试连接 + AES-GCM加密存储
- 协议二进制管理(binmgr): FRP/OpenVPN GitHub下载 + 系统包安装引导(apt/yum/dnf)
- 右上角修改密码弹窗 + MFA独立验证页 + 密码强度提示
- 用户管理: 批量导入 + LDAP浏览导入 + 平台用户导入VPN拨号用户
- 404路由 + IPSec对接页可达性修复
- MFA备用码持久化弹窗(替代Toast)
- 轮询可见性守卫 + ECharts resize修复

后端:
- 3张新表: VPNSession/ConnectionLog/TrafficPolicy
- monitor增强: 会话同步+配额检查+异常告警+到期审计
- changePassword统一8位+新旧不同校验
- openvpn-otp文案专业化(移除抄袭感)
- SSE日志流(token降级query参数)
2026-07-23 01:42:36 +08:00

137 lines
4.0 KiB
Go

package openvpn
import (
"encoding/base64"
"strings"
"testing"
"time"
"github.com/pquerna/otp/totp"
"vpn-platform/backend/internal/model"
)
func TestValidateOpenVPNModes(t *testing.T) {
cfg, err := New().Validate(map[string]interface{}{"auth_mode": "password", "subnet": "10.8.0.0 255.255.255.0"})
if err != nil {
t.Fatal(err)
}
if cfg["proto"] != "udp" || cfg["port"] != 1194 {
t.Fatalf("defaults not applied: %#v", cfg)
}
if _, err := New().Validate(map[string]interface{}{"auth_mode": "unknown"}); err == nil {
t.Fatal("expected invalid auth mode error")
}
// 前端历史值 tcp 应归一为 tcp-server
cfg2, err := New().Validate(map[string]interface{}{"proto": "tcp", "subnet": "10.8.0.0 255.255.255.0"})
if err != nil {
t.Fatal(err)
}
if cfg2["proto"] != "tcp-server" {
t.Fatalf("tcp not normalized: %#v", cfg2)
}
cfg3, err := New().Validate(map[string]interface{}{
"subnet": "10.8.0.0 255.255.255.0", "push_routes": "192.168.1.0/24",
})
if err != nil {
t.Fatal(err)
}
if cfg3["redirect_gateway"] != false {
t.Fatalf("push_routes should default redirect_gateway off: %#v", cfg3)
}
// MFA 仅允许密码相关模式
if _, err := New().Validate(map[string]interface{}{
"auth_mode": "certificate", "mfa_mode": "required", "subnet": "10.8.0.0 255.255.255.0",
}); err == nil {
t.Fatal("expected MFA + certificate-only to fail")
}
cfg4, err := New().Validate(map[string]interface{}{
"auth_mode": "password", "mfa_mode": "required", "subnet": "10.8.0.0 255.255.255.0", "proto": "tcp",
})
if err != nil {
t.Fatal(err)
}
if cfg4["proto"] != "tcp-server" || cfg4["mfa_mode"] != "required" {
t.Fatalf("mfa/tcp normalize: %#v", cfg4)
}
}
func TestSplitPasswordAndOTP(t *testing.T) {
pw, otp := splitPasswordAndOTP("secretpass123456", "")
if pw != "secretpass" || otp != "123456" {
t.Fatalf("suffix: %q %q", pw, otp)
}
scrv := "SCRV1:" + base64.StdEncoding.EncodeToString([]byte("mypass")) + ":" + base64.StdEncoding.EncodeToString([]byte("654321"))
pw, otp = splitPasswordAndOTP(scrv, "")
if pw != "mypass" || otp != "654321" {
t.Fatalf("scrv1: %q %q", pw, otp)
}
pw, otp = splitPasswordAndOTP("onlypass", "111222")
if pw != "onlypass" || otp != "111222" {
t.Fatalf("challenge line: %q %q", pw, otp)
}
}
func TestClientProto(t *testing.T) {
if clientProto("tcp-server") != "tcp-client" || clientProto("udp") != "udp" || clientProto("tcp") != "tcp-client" {
t.Fatal("clientProto mapping failed")
}
}
func TestServerConfigIncludesProductionTunings(t *testing.T) {
cfg, err := New().Validate(map[string]interface{}{
"auth_mode": "password", "proto": "tcp", "subnet": "10.8.0.0 255.255.240.0",
"max_clients": 1000, "mfa_mode": "off",
})
if err != nil {
t.Fatal(err)
}
if cfg["proto"] != "tcp-server" {
t.Fatalf("proto: %#v", cfg["proto"])
}
tunn := &model.Tunnel{ID: 1, Protocol: "openvpn"}
raw := New().serverConfig(tunn, cfg)
for _, want := range []string{
"proto tcp-server",
"tun-mtu 1500",
"mssfix 1450",
"keepalive 5 30",
"reneg-sec 0",
"verify-client-cert none",
"username-as-common-name",
"max-clients 1000",
"duplicate-cn",
"via-file",
} {
if !strings.Contains(raw, want) {
t.Fatalf("server.conf missing %q\n%s", want, raw)
}
}
if strings.Contains(raw, "via-env") {
t.Fatal("should use via-file not via-env")
}
}
func TestValidateTOTPSkew(t *testing.T) {
// 使用固定 secret 生成当前码并校验
secret := "JBSWY3DPEHPK3PXP" // "Hello!" base32, common test secret
code, err := totp.GenerateCode(secret, time.Now())
if err != nil {
t.Fatal(err)
}
ok, counter := validateTOTPSkew(code, secret, 1)
if !ok || counter == 0 {
t.Fatalf("expected valid totp, ok=%v counter=%d", ok, counter)
}
if ok2, _ := validateTOTPSkew("000000", secret, 1); ok2 {
t.Fatal("expected invalid code")
}
}
func TestCertificateCNIsSafeAndStable(t *testing.T) {
cn := certificateCN("张三 / alice@example.com")
if !certificateName.MatchString(cn) || !strings.HasPrefix(cn, "u-") {
t.Fatalf("unsafe CN: %q", cn)
}
}