Files
-Fonu/internal/proxy/custom_endpoints_test.go
mrlee fd1661622e feat: 上游 1.0.3-1.0.5 同步第 1-8 项(v1.2.2)
剪贴板:新增 web/src/utils/clipboard.ts,5 处调用点支持非 HTTPS 环境(对齐 1.0.4)

日志中心:新增「FRP 日志」Tab,解析 frpc 日志为 时间/级别/来源/消息(对齐 1.0.3)

通知:单通道密文解密失败不再拖垮整份配置(对齐 1.0.5);容器补 TZ + tzdata(对齐 1.0.4);Telegram 代理自动补 http:// 并校验 scheme(对齐 1.0.3)

反向代理:复制不再追加「-复制」(对齐 1.0.5);监听占用校验合并表单规则与手动 Nginx 配置两张表(迁移 024 + endpoints 解析器),冲突提示写明占用规则;改端口的并发冲突改为可读提示

FRP:frps 弹窗补「VPS 防火墙放行 TCP 远程端口」提示(对齐 1.0.3);反代配置弹窗补齐内容区内边距与来源说明

文档:新增 docs/upstream-sync-plan.md(逐版本对照 + 第 5 项详细整理);VERSION 与 CHANGELOG 记 v1.2.2
2026-09-23 17:46:21 +08:00

110 lines
3.5 KiB
Go

package proxy
import (
"context"
"database/sql"
"fmt"
"path/filepath"
"strings"
"testing"
_ "modernc.org/sqlite"
"github.com/fonu/fonu/internal/db"
)
func newTestStore(t *testing.T) (*Store, *sql.DB) {
t.Helper()
ctx := context.Background()
name := strings.ReplaceAll(t.Name(), "/", "_")
conn, err := sql.Open("sqlite", fmt.Sprintf("file:%s?mode=memory&cache=shared&_pragma=foreign_keys(1)", name))
if err != nil {
t.Fatalf("open test db: %v", err)
}
conn.SetMaxOpenConns(1)
if err := db.Migrate(ctx, conn, filepath.Join("..", "..", "migrations")); err != nil {
t.Fatalf("migrate test db: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
return NewStore(conn), conn
}
func createTestRule(t *testing.T, store *Store, name, host string, port int) Rule {
t.Helper()
rule, err := store.Create(context.Background(), CreateInput{
Upstream: "http://192.168.1.10:8080",
ListenPort: port,
ListenIPv4: true,
Hosts: []string{host},
Enabled: true,
Name: name,
})
if err != nil {
t.Fatalf("create rule %s: %v", name, err)
}
return rule
}
func TestEnsureEndpointsAvailableNamesOwningRule(t *testing.T) {
store, _ := newTestStore(t)
ctx := context.Background()
owner := createTestRule(t, store, "官网", "a.com", 8011)
err := store.EnsureEndpointsAvailable(ctx, []Endpoint{{Hostname: "a.com", Port: 8011}}, 0)
if err == nil {
t.Fatal("expected conflict error")
}
if !strings.Contains(err.Error(), "规则「官网」") {
t.Fatalf("expected owning rule name in error, got %q", err.Error())
}
// 编辑自身不应被判为冲突
if err := store.EnsureEndpointsAvailable(ctx, []Endpoint{{Hostname: "a.com", Port: 8011}}, owner.ID); err != nil {
t.Fatalf("exclude self should pass, got %v", err)
}
// 同域名不同端口是合法虚拟主机
if err := store.EnsureEndpointsAvailable(ctx, []Endpoint{{Hostname: "a.com", Port: 8012}}, 0); err != nil {
t.Fatalf("different port should pass, got %v", err)
}
}
func TestEnsureEndpointsAvailableSeesCustomEndpoints(t *testing.T) {
store, conn := newTestStore(t)
ctx := context.Background()
custom := createTestRule(t, store, "手动规则", "b.com", 8011)
if err := store.ReplaceCustomEndpoints(ctx, custom.ID, []Endpoint{{Hostname: "c.com", Port: 8013}}); err != nil {
t.Fatalf("replace custom endpoints: %v", err)
}
err := store.EnsureEndpointsAvailable(ctx, []Endpoint{{Hostname: "c.com", Port: 8013}}, 0)
if err == nil {
t.Fatal("expected conflict with custom endpoints")
}
if !strings.Contains(err.Error(), "手动 Nginx 配置") {
t.Fatalf("expected custom-rule hint in error, got %q", err.Error())
}
// 删除规则后占用记录随外键级联清除
if _, err := conn.ExecContext(ctx, `DELETE FROM proxy_rules WHERE id = ?`, custom.ID); err != nil {
t.Fatalf("delete rule: %v", err)
}
if err := store.EnsureEndpointsAvailable(ctx, []Endpoint{{Hostname: "c.com", Port: 8013}}, 0); err != nil {
t.Fatalf("cascade should free the binding, got %v", err)
}
}
func TestClearCustomEndpoints(t *testing.T) {
store, _ := newTestStore(t)
ctx := context.Background()
rule := createTestRule(t, store, "手动规则", "b.com", 8011)
if err := store.ReplaceCustomEndpoints(ctx, rule.ID, []Endpoint{{Hostname: "c.com", Port: 8013}}); err != nil {
t.Fatalf("replace custom endpoints: %v", err)
}
if err := store.ClearCustomEndpoints(ctx, rule.ID); err != nil {
t.Fatalf("clear custom endpoints: %v", err)
}
if err := store.EnsureEndpointsAvailable(ctx, []Endpoint{{Hostname: "c.com", Port: 8013}}, 0); err != nil {
t.Fatalf("cleared binding should be free, got %v", err)
}
}