|
@@ -37,7 +37,6 @@ func TestExpandRemarkVars(t *testing.T) {
|
|
|
|
|
|
|
|
cases := []struct{ tmpl, want string }{
|
|
cases := []struct{ tmpl, want string }{
|
|
|
{"{{EMAIL}}", "[email protected]"},
|
|
{"{{EMAIL}}", "[email protected]"},
|
|
|
- {"{{USERNAME}}", "[email protected]"},
|
|
|
|
|
{"{{INBOUND}}", "Germany"}, // no host remark in ctx → inbound remark
|
|
{"{{INBOUND}}", "Germany"}, // no host remark in ctx → inbound remark
|
|
|
{"{{HOST}}", ""}, // no host remark in ctx → empty
|
|
{"{{HOST}}", ""}, // no host remark in ctx → empty
|
|
|
{"{{ID}}", client.ID},
|
|
{"{{ID}}", client.ID},
|
|
@@ -169,10 +168,10 @@ func hostRemarkService(template string) (*SubService, *model.Inbound, model.Clie
|
|
|
// never substitutes it (it is reachable only through {{HOST}}).
|
|
// never substitutes it (it is reachable only through {{HOST}}).
|
|
|
func TestGenHostRemark_ConfigNameUsesInbound(t *testing.T) {
|
|
func TestGenHostRemark_ConfigNameUsesInbound(t *testing.T) {
|
|
|
s, inbound, client := hostRemarkService("") // no template → config name only
|
|
s, inbound, client := hostRemarkService("") // no template → config name only
|
|
|
- if got := s.genHostRemark(inbound, client, "Relay"); got != "DE" {
|
|
|
|
|
|
|
+ if got := s.genHostRemark(inbound, client, "Relay", ""); got != "DE" {
|
|
|
t.Fatalf("genHostRemark = %q, want %q (inbound remark, host ignored)", got, "DE")
|
|
t.Fatalf("genHostRemark = %q, want %q (inbound remark, host ignored)", got, "DE")
|
|
|
}
|
|
}
|
|
|
- if got := s.genHostRemark(inbound, client, ""); got != "DE" {
|
|
|
|
|
|
|
+ if got := s.genHostRemark(inbound, client, "", ""); got != "DE" {
|
|
|
t.Fatalf("genHostRemark (no host remark) = %q, want %q", got, "DE")
|
|
t.Fatalf("genHostRemark (no host remark) = %q, want %q", got, "DE")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -182,17 +181,17 @@ func TestGenHostRemark_ConfigNameUsesInbound(t *testing.T) {
|
|
|
func TestGenHostRemark_GlobalTemplate(t *testing.T) {
|
|
func TestGenHostRemark_GlobalTemplate(t *testing.T) {
|
|
|
// {{INBOUND}} resolves to the inbound remark regardless of the host remark.
|
|
// {{INBOUND}} resolves to the inbound remark regardless of the host remark.
|
|
|
s, inbound, client := hostRemarkService("{{INBOUND}} | {{TRAFFIC_LEFT}} | {{DAYS_LEFT}}d")
|
|
s, inbound, client := hostRemarkService("{{INBOUND}} | {{TRAFFIC_LEFT}} | {{DAYS_LEFT}}d")
|
|
|
- if got := s.genHostRemark(inbound, client, "CDN"); got != "DE | 80.00GB | 10d" {
|
|
|
|
|
|
|
+ if got := s.genHostRemark(inbound, client, "CDN", ""); got != "DE | 80.00GB | 10d" {
|
|
|
t.Fatalf("global template ({{INBOUND}} = inbound) = %q", got)
|
|
t.Fatalf("global template ({{INBOUND}} = inbound) = %q", got)
|
|
|
}
|
|
}
|
|
|
// {{INBOUND}} and {{HOST}} side by side show both, distinctly (#5443).
|
|
// {{INBOUND}} and {{HOST}} side by side show both, distinctly (#5443).
|
|
|
s2, inbound2, client2 := hostRemarkService("{{INBOUND}}|{{HOST}}|{{TRAFFIC_LEFT}}")
|
|
s2, inbound2, client2 := hostRemarkService("{{INBOUND}}|{{HOST}}|{{TRAFFIC_LEFT}}")
|
|
|
- if got := s2.genHostRemark(inbound2, client2, "CDN"); got != "DE|CDN|80.00GB" {
|
|
|
|
|
|
|
+ if got := s2.genHostRemark(inbound2, client2, "CDN", ""); got != "DE|CDN|80.00GB" {
|
|
|
t.Fatalf("global template (inbound + host) = %q, want %q", got, "DE|CDN|80.00GB")
|
|
t.Fatalf("global template (inbound + host) = %q, want %q", got, "DE|CDN|80.00GB")
|
|
|
}
|
|
}
|
|
|
// {{HOST}} is the host's own remark even when the inbound has one of its own.
|
|
// {{HOST}} is the host's own remark even when the inbound has one of its own.
|
|
|
s3, inbound3, client3 := hostRemarkService("{{HOST}}")
|
|
s3, inbound3, client3 := hostRemarkService("{{HOST}}")
|
|
|
- if got := s3.genHostRemark(inbound3, client3, "CDN"); got != "CDN" {
|
|
|
|
|
|
|
+ if got := s3.genHostRemark(inbound3, client3, "CDN", ""); got != "CDN" {
|
|
|
t.Fatalf("{{HOST}} token = %q, want CDN", got)
|
|
t.Fatalf("{{HOST}} token = %q, want CDN", got)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -201,7 +200,7 @@ func TestGenHostRemark_GlobalTemplate(t *testing.T) {
|
|
|
// legacy externalProxy remark passed as extra.
|
|
// legacy externalProxy remark passed as extra.
|
|
|
func TestGenRemark_GlobalTemplate(t *testing.T) {
|
|
func TestGenRemark_GlobalTemplate(t *testing.T) {
|
|
|
s, inbound, _ := hostRemarkService("{{EMAIL}} | {{TRAFFIC_LEFT}}")
|
|
s, inbound, _ := hostRemarkService("{{EMAIL}} | {{TRAFFIC_LEFT}}")
|
|
|
- got := s.genRemark(inbound, "[email protected]", "")
|
|
|
|
|
|
|
+ got := s.genRemark(inbound, "[email protected]", "", "")
|
|
|
if got != "[email protected] | 80.00GB" {
|
|
if got != "[email protected] | 80.00GB" {
|
|
|
t.Fatalf("global template (non-host) = %q", got)
|
|
t.Fatalf("global template (non-host) = %q", got)
|
|
|
}
|
|
}
|
|
@@ -210,7 +209,7 @@ func TestGenRemark_GlobalTemplate(t *testing.T) {
|
|
|
// With no template, genRemark composes the fallback model and adds no suffix.
|
|
// With no template, genRemark composes the fallback model and adds no suffix.
|
|
|
func TestGenRemark_NoTemplate_NoSuffix(t *testing.T) {
|
|
func TestGenRemark_NoTemplate_NoSuffix(t *testing.T) {
|
|
|
s, inbound, _ := hostRemarkService("")
|
|
s, inbound, _ := hostRemarkService("")
|
|
|
- got := s.genRemark(inbound, "[email protected]", "Relay")
|
|
|
|
|
|
|
+ got := s.genRemark(inbound, "[email protected]", "Relay", "")
|
|
|
if got != "DE-Relay" {
|
|
if got != "DE-Relay" {
|
|
|
t.Fatalf("genRemark = %q, want %q (no suffix)", got, "DE-Relay")
|
|
t.Fatalf("genRemark = %q, want %q (no suffix)", got, "DE-Relay")
|
|
|
}
|
|
}
|
|
@@ -220,8 +219,8 @@ func TestGenRemark_NoTemplate_NoSuffix(t *testing.T) {
|
|
|
// link of the request; later links show the name-only template.
|
|
// link of the request; later links show the name-only template.
|
|
|
func TestUsageOnFirstLinkOnly(t *testing.T) {
|
|
func TestUsageOnFirstLinkOnly(t *testing.T) {
|
|
|
s, inbound, client := hostRemarkService("{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D")
|
|
s, inbound, client := hostRemarkService("{{INBOUND}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D")
|
|
|
- first := s.genHostRemark(inbound, client, "")
|
|
|
|
|
- second := s.genHostRemark(inbound, client, "")
|
|
|
|
|
|
|
+ first := s.genHostRemark(inbound, client, "", "")
|
|
|
|
|
+ second := s.genHostRemark(inbound, client, "", "")
|
|
|
if !strings.Contains(first, "📊") || !strings.Contains(first, "80.00GB") {
|
|
if !strings.Contains(first, "📊") || !strings.Contains(first, "80.00GB") {
|
|
|
t.Fatalf("first link should carry usage: %q", first)
|
|
t.Fatalf("first link should carry usage: %q", first)
|
|
|
}
|
|
}
|
|
@@ -241,15 +240,15 @@ func TestRemarkInDisplayContext(t *testing.T) {
|
|
|
s.subscriptionBody = false
|
|
s.subscriptionBody = false
|
|
|
// A host link in a display shows only the config name — the inbound's remark,
|
|
// A host link in a display shows only the config name — the inbound's remark,
|
|
|
// with no per-client email or usage info and the host remark ignored.
|
|
// with no per-client email or usage info and the host remark ignored.
|
|
|
- if got := s.genHostRemark(inbound, client, "CDN"); got != "DE" {
|
|
|
|
|
|
|
+ if got := s.genHostRemark(inbound, client, "CDN", ""); got != "DE" {
|
|
|
t.Fatalf("display host link = %q, want config name %q", got, "DE")
|
|
t.Fatalf("display host link = %q, want config name %q", got, "DE")
|
|
|
}
|
|
}
|
|
|
// With no host remark, the config name is likewise the inbound's own remark.
|
|
// With no host remark, the config name is likewise the inbound's own remark.
|
|
|
- if got := s.genHostRemark(inbound, client, ""); got != "DE" {
|
|
|
|
|
|
|
+ if got := s.genHostRemark(inbound, client, "", ""); got != "DE" {
|
|
|
t.Fatalf("display host link (no host) = %q, want %q", got, "DE")
|
|
t.Fatalf("display host link (no host) = %q, want %q", got, "DE")
|
|
|
}
|
|
}
|
|
|
// genRemark (non-host) likewise drops the template in display context.
|
|
// genRemark (non-host) likewise drops the template in display context.
|
|
|
- if got := s.genRemark(inbound, client.Email, ""); got != "DE" {
|
|
|
|
|
|
|
+ if got := s.genRemark(inbound, client.Email, "", ""); got != "DE" {
|
|
|
t.Fatalf("display genRemark = %q, want %q", got, "DE")
|
|
t.Fatalf("display genRemark = %q, want %q", got, "DE")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -294,9 +293,176 @@ func TestStatsForClient_CrossInboundFallback(t *testing.T) {
|
|
|
func TestGenHostRemark_PerClient(t *testing.T) {
|
|
func TestGenHostRemark_PerClient(t *testing.T) {
|
|
|
s := &SubService{remarkTemplate: "{{EMAIL}}", subscriptionBody: true}
|
|
s := &SubService{remarkTemplate: "{{EMAIL}}", subscriptionBody: true}
|
|
|
inbound := &model.Inbound{}
|
|
inbound := &model.Inbound{}
|
|
|
- a := s.genHostRemark(inbound, model.Client{Email: "alice@x"}, "")
|
|
|
|
|
- b := s.genHostRemark(inbound, model.Client{Email: "bob@x"}, "")
|
|
|
|
|
|
|
+ a := s.genHostRemark(inbound, model.Client{Email: "alice@x"}, "", "")
|
|
|
|
|
+ b := s.genHostRemark(inbound, model.Client{Email: "bob@x"}, "", "")
|
|
|
if a != "alice@x" || b != "bob@x" {
|
|
if a != "alice@x" || b != "bob@x" {
|
|
|
t.Fatalf("per-client expansion failed: a=%q b=%q", a, b)
|
|
t.Fatalf("per-client expansion failed: a=%q b=%q", a, b)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+func TestStatusEmoji(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ stats xray.ClientTraffic
|
|
|
|
|
+ want string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {xray.ClientTraffic{Enable: true, Total: 10 * gb, Up: gb}, "✅"},
|
|
|
|
|
+ {xray.ClientTraffic{Enable: true, Total: 10 * gb, Up: 10 * gb, Down: 1}, "🚫"},
|
|
|
|
|
+ {xray.ClientTraffic{Enable: false}, "🚫"},
|
|
|
|
|
+ {xray.ClientTraffic{Enable: true, ExpiryTime: 1000}, "⏳"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := statusEmoji(c.stats); got != c.want {
|
|
|
|
|
+ t.Errorf("statusEmoji(%+v) = %q, want %q", c.stats, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUsagePercentage(t *testing.T) {
|
|
|
|
|
+ if got := usagePercentage(xray.ClientTraffic{Total: 100 * gb, Up: 25 * gb, Down: 25 * gb}); got != "50.0%" {
|
|
|
|
|
+ t.Errorf("usagePercentage 50%% = %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := usagePercentage(xray.ClientTraffic{Total: 0}); got != "" {
|
|
|
|
|
+ t.Errorf("usagePercentage unlimited = %q, want empty", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 10 * gb}); got != "100.0%" {
|
|
|
|
|
+ t.Errorf("usagePercentage 100%% = %q", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Over-quota usage clamps to 100%, consistent with TRAFFIC_LEFT.
|
|
|
|
|
+ if got := usagePercentage(xray.ClientTraffic{Total: 10 * gb, Up: 25 * gb}); got != "100.0%" {
|
|
|
|
|
+ t.Errorf("usagePercentage over-quota = %q, want 100.0%%", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestTimeLeftLabel(t *testing.T) {
|
|
|
|
|
+ if got := timeLeftLabel(0); got != "∞" {
|
|
|
|
|
+ t.Errorf("timeLeftLabel(0) = %q, want ∞", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Delayed-start: negative expiry = duration in ms. 1000ms = 1 second = "0m".
|
|
|
|
|
+ if got := timeLeftLabel(-1000); got != "0m" {
|
|
|
|
|
+ t.Errorf("timeLeftLabel(-1000) = %q, want 0m", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestGregorianToJalali(t *testing.T) {
|
|
|
|
|
+ cases := []struct {
|
|
|
|
|
+ gy, gm, gd int
|
|
|
|
|
+ jy, jm, jd int
|
|
|
|
|
+ }{
|
|
|
|
|
+ {2024, 1, 1, 1402, 10, 11},
|
|
|
|
|
+ {2000, 3, 20, 1379, 1, 1},
|
|
|
|
|
+ {1979, 2, 11, 1357, 11, 22},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ jy, jm, jd := gregorianToJalali(c.gy, c.gm, c.gd)
|
|
|
|
|
+ if jy != c.jy || jm != c.jm || jd != c.jd {
|
|
|
|
|
+ t.Errorf("gregorianToJalali(%d,%d,%d) = (%d,%d,%d), want (%d,%d,%d)",
|
|
|
|
|
+ c.gy, c.gm, c.gd, jy, jm, jd, c.jy, c.jm, c.jd)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestJalaliExpireDateLabel(t *testing.T) {
|
|
|
|
|
+ if got := jalaliExpireDateLabel(0); got != "" {
|
|
|
|
|
+ t.Errorf("jalaliExpireDateLabel(0) = %q, want empty", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := jalaliExpireDateLabel(-1000); got != "" {
|
|
|
|
|
+ t.Errorf("jalaliExpireDateLabel(-1000) = %q, want empty", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestExpandNewTokensInTemplate(t *testing.T) {
|
|
|
|
|
+ inbound := &model.Inbound{Remark: "DE", Protocol: "vless"}
|
|
|
|
|
+ client := model.Client{Email: "[email protected]", ID: "abc-123"}
|
|
|
|
|
+ stats := xray.ClientTraffic{Enable: true, Total: 100 * gb, Up: 50 * gb, Down: 0}
|
|
|
|
|
+ ctx := remarkContext{
|
|
|
|
|
+ client: client,
|
|
|
|
|
+ stats: stats,
|
|
|
|
|
+ inbound: inbound,
|
|
|
|
|
+ transport: "ws",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cases := []struct{ tmpl, want string }{
|
|
|
|
|
+ {"{{STATUS_EMOJI}}", "✅"},
|
|
|
|
|
+ {"{{USAGE_PERCENTAGE}}", "50.0%"},
|
|
|
|
|
+ {"{{PROTOCOL}}", "VLESS"},
|
|
|
|
|
+ {"{{TRANSPORT}}", "ws"},
|
|
|
|
|
+ {"{{STATUS_EMOJI}} {{INBOUND}}", "✅ DE"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := expandRemarkVars(c.tmpl, ctx); got != c.want {
|
|
|
|
|
+ t.Errorf("expandRemarkVars(%q) = %q, want %q", c.tmpl, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestTranslateUISingleBrackets(t *testing.T) {
|
|
|
|
|
+ cases := []struct{ in, want string }{
|
|
|
|
|
+ {"{EMAIL}", "{{EMAIL}}"},
|
|
|
|
|
+ {"{DATA_LEFT}", "{{TRAFFIC_LEFT}}"},
|
|
|
|
|
+ {"{DATA_LEFT} of {DATA_LIMIT}", "{{TRAFFIC_LEFT}} of {{TRAFFIC_TOTAL}}"},
|
|
|
|
|
+ {"{STATUS_EMOJI} {INBOUND}", "{{STATUS_EMOJI}} {INBOUND}"},
|
|
|
|
|
+ {"{UNKNOWN_TOKEN}", "{UNKNOWN_TOKEN}"},
|
|
|
|
|
+ {"no braces", "no braces"},
|
|
|
|
|
+ {"{{TRAFFIC_LEFT}}", "{{TRAFFIC_LEFT}}"},
|
|
|
|
|
+ {"{username}", "{username}"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := translateUISingleBrackets(c.in); got != c.want {
|
|
|
|
|
+ t.Errorf("translateUISingleBrackets(%q) = %q, want %q", c.in, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestExpandRemarkVars_SingleBracketUI(t *testing.T) {
|
|
|
|
|
+ inbound := &model.Inbound{Remark: "DE", Protocol: "vless"}
|
|
|
|
|
+ stats := xray.ClientTraffic{Enable: true, Total: 100 * gb, Up: 50 * gb, Down: 0}
|
|
|
|
|
+ ctx := remarkContext{
|
|
|
|
|
+ client: model.Client{Email: "[email protected]"},
|
|
|
|
|
+ stats: stats,
|
|
|
|
|
+ inbound: inbound,
|
|
|
|
|
+ transport: "ws",
|
|
|
|
|
+ }
|
|
|
|
|
+ cases := []struct{ tmpl, want string }{
|
|
|
|
|
+ {"{EMAIL}", "[email protected]"},
|
|
|
|
|
+ {"{DATA_LEFT}", "50.00GB"},
|
|
|
|
|
+ {"{DATA_USAGE}", "50.00GB"},
|
|
|
|
|
+ {"{DATA_LIMIT}", "100.00GB"},
|
|
|
|
|
+ {"{STATUS_EMOJI}", "✅"},
|
|
|
|
|
+ {"{USAGE_PERCENTAGE}", "50.0%"},
|
|
|
|
|
+ {"{PROTOCOL}", "VLESS"},
|
|
|
|
|
+ {"{TRANSPORT}", "ws"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, c := range cases {
|
|
|
|
|
+ if got := expandRemarkVars(c.tmpl, ctx); got != c.want {
|
|
|
|
|
+ t.Errorf("expandRemarkVars(%q) = %q, want %q", c.tmpl, got, c.want)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestUsageOnFirstLinkOnly_SingleBracket(t *testing.T) {
|
|
|
|
|
+ s := &SubService{
|
|
|
|
|
+ remarkTemplate: "{STATUS_EMOJI} {{INBOUND}}|📊{{TRAFFIC_LEFT}}",
|
|
|
|
|
+ subscriptionBody: true,
|
|
|
|
|
+ usageShown: map[string]bool{},
|
|
|
|
|
+ }
|
|
|
|
|
+ inbound := &model.Inbound{
|
|
|
|
|
+ Remark: "DE",
|
|
|
|
|
+ ClientStats: []xray.ClientTraffic{{
|
|
|
|
|
+ Email: "alice@x",
|
|
|
|
|
+ Enable: true,
|
|
|
|
|
+ Total: 100 * gb,
|
|
|
|
|
+ Up: 20 * gb,
|
|
|
|
|
+ Down: 10 * gb,
|
|
|
|
|
+ }},
|
|
|
|
|
+ }
|
|
|
|
|
+ client := model.Client{Email: "alice@x"}
|
|
|
|
|
+ first := s.genTemplatedRemark(inbound, client, "", "ws")
|
|
|
|
|
+ s.usageShown["alice@x"] = true
|
|
|
|
|
+ second := s.genTemplatedRemark(inbound, client, "", "ws")
|
|
|
|
|
+ if !strings.Contains(first, "📊") {
|
|
|
|
|
+ t.Fatalf("first link should carry usage: %q", first)
|
|
|
|
|
+ }
|
|
|
|
|
+ if strings.Contains(second, "📊") {
|
|
|
|
|
+ t.Fatalf("second link must not carry usage: %q", second)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|