Files
3x-ui/internal/sub/external_only_sub_test.go
T
Tomi lla 129f50d92a feat(sub): auto-detect subscription format by User-Agent (Updated) (#5826)
* feat(settings): add subscription format controls

* feat(sub): auto-detect subscription formats

* fix(xray): validate balancer regexes before save

* Revert "fix(xray): validate balancer regexes before save"

This reverts commit 8a208ce71b.

* doc(endpoints): align indent spaces

* doc(settings): improve error message formatting in validateSubUserAgentRegex

- Use NewErrorf with proper formatting instead of NewError with string concatenation
- Add comment explaining the rationale for returning original pattern value
- This preserves the intentional design where empty input is stored as empty
  in the DB and inherited as the runtime default at read time

---------

Co-authored-by: Tomilla <5007859+Tomilla@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-07-14 13:01:40 +02:00

70 lines
2.5 KiB
Go

package sub
import (
"encoding/json"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// A subscription whose only entries are external links — no enabled standard
// inbound — must still render in the JSON and Clash formats, not just the raw
// one. Regression guard for the premature len(inbounds)==0 early return that
// short-circuited GetJson/GetClash before external links were ever fetched.
func TestJsonAndClashServeExternalLinkOnlySub(t *testing.T) {
initSubDB(t)
db := database.GetDB()
rec := &model.ClientRecord{Email: "ext@x", SubID: "ext-only", UUID: "ext-uuid", Enable: true}
if err := db.Create(rec).Error; err != nil {
t.Fatalf("seed client: %v", err)
}
link := "vless://11111111-1111-1111-1111-111111111111@example.com:443?type=tcp&security=reality&pbk=abc&sid=12&fp=chrome#orig"
if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: link, Remark: "DE-Provider", SortIndex: 1}).Error; err != nil {
t.Fatalf("seed external link: %v", err)
}
base := NewSubService("")
jsonService := NewSubJsonService("", "", "", base)
jsonOut, _, err := jsonService.GetJson("ext-only", "sub.example.com", false)
if err != nil {
t.Fatalf("GetJson err = %v", err)
}
if jsonOut == "" {
t.Fatal("GetJson returned empty for an external-link-only sub")
}
if !strings.Contains(jsonOut, "DE-Provider") {
t.Fatalf("GetJson missing external remark: %s", jsonOut)
}
var config map[string]any
if err := json.Unmarshal([]byte(jsonOut), &config); err != nil {
t.Fatalf("legacy GetJson must return an object for a single profile: %v; body=%s", err, jsonOut)
}
standardOut, _, err := jsonService.GetJson("ext-only", "sub.example.com", true)
if err != nil {
t.Fatalf("standards-compliant GetJson err = %v", err)
}
var configs []map[string]any
if err := json.Unmarshal([]byte(standardOut), &configs); err != nil {
t.Fatalf("standards-compliant GetJson must return an array for a single profile: %v; body=%s", err, standardOut)
}
if len(configs) != 1 {
t.Fatalf("standards-compliant GetJson profile count = %d, want 1", len(configs))
}
clashOut, _, err := NewSubClashService(false, "", base).GetClash("ext-only", "sub.example.com")
if err != nil {
t.Fatalf("GetClash err = %v", err)
}
if clashOut == "" {
t.Fatal("GetClash returned empty for an external-link-only sub")
}
if !strings.Contains(clashOut, "DE-Provider") {
t.Fatalf("GetClash missing external proxy: %s", clashOut)
}
}