feat: GeoIP-based geo detection + configurable rename template
- Add MaxMind GeoLite2-City mmdb auto-download and IP-based geo lookup (covers all 249 countries globally, replacing limited 8-country regex) - Add RenameOptions with 5 toggleable fields (flag/alias/country/city/index) stored as rename_options_json on collections - Collection rename pipeline uses DetectGeoWithServer(name, server) which tries GeoIP lookup first, falls back to name-based regex matching - Chinese country/city names + emoji flag prefix - Country-city dedup: skip city when it equals country (e.g. 香港/香港) - URI fragment encoding: url.QueryEscape -> url.PathEscape (%20 not +) - Egress cache key excludes name field so renamed nodes still match cache - Frontend: 5 toggle buttons in collection edit form, default all on - Migrations: 0004 (source alias + collection rename_enabled), 0005 (collection rename_options_json)
This commit is contained in:
+50
-11
@@ -436,9 +436,11 @@ func mapToSourceRecord(input map[string]any) model.SourceRecord {
|
||||
}
|
||||
filters := parseFilters(input["filters"])
|
||||
meta := getMapValue(input["meta"])
|
||||
alias := getStringValue(input["alias"])
|
||||
return model.SourceRecord{
|
||||
ID: id,
|
||||
Name: orDefault(getStringValue(input["name"]), id),
|
||||
Alias: alias,
|
||||
Type: sourceType,
|
||||
URL: getStringValue(input["url"]),
|
||||
Content: getStringValue(input["content"]),
|
||||
@@ -455,7 +457,39 @@ func mapToCollectionRecord(input map[string]any) model.CollectionRecord {
|
||||
}
|
||||
id = util.ToId(id)
|
||||
sourceIds := getStringArray(input["sourceIds"])
|
||||
filters := parseFilters(input["filters"])
|
||||
renameEnabled := false
|
||||
if v, ok := input["renameEnabled"].(bool); ok {
|
||||
renameEnabled = v
|
||||
}
|
||||
|
||||
// Parse rename options (all default to true when renameEnabled is on)
|
||||
var renameOptions *model.RenameOptions
|
||||
if opts, ok := input["renameOptions"].(map[string]any); ok {
|
||||
ro := model.RenameOptions{
|
||||
Flag: true,
|
||||
Alias: true,
|
||||
Country: true,
|
||||
City: true,
|
||||
Index: true,
|
||||
}
|
||||
if v, ok := opts["flag"].(bool); ok {
|
||||
ro.Flag = v
|
||||
}
|
||||
if v, ok := opts["alias"].(bool); ok {
|
||||
ro.Alias = v
|
||||
}
|
||||
if v, ok := opts["country"].(bool); ok {
|
||||
ro.Country = v
|
||||
}
|
||||
if v, ok := opts["city"].(bool); ok {
|
||||
ro.City = v
|
||||
}
|
||||
if v, ok := opts["index"].(bool); ok {
|
||||
ro.Index = v
|
||||
}
|
||||
renameOptions = &ro
|
||||
}
|
||||
|
||||
templateId := getStringValue(input["templateId"])
|
||||
if templateId == "" || templateId == "default" {
|
||||
templateId = template.DefaultTemplateID
|
||||
@@ -470,14 +504,15 @@ func mapToCollectionRecord(input map[string]any) model.CollectionRecord {
|
||||
}
|
||||
meta := getMapValue(input["meta"])
|
||||
return model.CollectionRecord{
|
||||
ID: id,
|
||||
Name: orDefault(getStringValue(input["name"]), id),
|
||||
SourceIds: sourceIds,
|
||||
Filters: filters,
|
||||
TemplateId: templateId,
|
||||
IgnoreFailed: ignoreFailed,
|
||||
Enabled: enabled,
|
||||
Meta: meta,
|
||||
ID: id,
|
||||
Name: orDefault(getStringValue(input["name"]), id),
|
||||
SourceIds: sourceIds,
|
||||
RenameEnabled: renameEnabled,
|
||||
RenameOptions: renameOptions,
|
||||
TemplateId: templateId,
|
||||
IgnoreFailed: ignoreFailed,
|
||||
Enabled: enabled,
|
||||
Meta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,6 +547,9 @@ func mergeSource(existing model.SourceRecord, next model.SourceRecord) model.Sou
|
||||
if next.Name != "" {
|
||||
merged.Name = next.Name
|
||||
}
|
||||
if next.Alias != "" {
|
||||
merged.Alias = next.Alias
|
||||
}
|
||||
if next.Type != "" {
|
||||
merged.Type = next.Type
|
||||
}
|
||||
@@ -539,8 +577,9 @@ func mergeCollection(existing model.CollectionRecord, next model.CollectionRecor
|
||||
if next.SourceIds != nil {
|
||||
merged.SourceIds = next.SourceIds
|
||||
}
|
||||
if next.Filters != nil {
|
||||
merged.Filters = next.Filters
|
||||
merged.RenameEnabled = next.RenameEnabled
|
||||
if next.RenameOptions != nil {
|
||||
merged.RenameOptions = next.RenameOptions
|
||||
}
|
||||
if next.TemplateId != "" {
|
||||
merged.TemplateId = next.TemplateId
|
||||
|
||||
@@ -78,7 +78,7 @@ func (d *Deps) HandleEgressInfo(c fiber.Ctx) error {
|
||||
func egressCacheKey(node model.ProxyNode) string {
|
||||
clean := model.ProxyNode{}
|
||||
skip := map[string]bool{
|
||||
"id": true, "latencyMs": true, "latencyError": true,
|
||||
"id": true, "name": true, "latencyMs": true, "latencyError": true,
|
||||
"egressIp": true, "egressCountry": true, "egressRegion": true, "egressError": true,
|
||||
"country": true, "region": true, "city": true, "isp": true, "cached": true,
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ func TestValidateSource(t *testing.T) {
|
||||
func TestMapToSourceRecord(t *testing.T) {
|
||||
in := map[string]any{
|
||||
"name": "My Source!",
|
||||
"alias": "MyAlias",
|
||||
"type": "local",
|
||||
"url": "https://x.com",
|
||||
"content": "ss://abc",
|
||||
@@ -269,6 +270,9 @@ func TestMapToSourceRecord(t *testing.T) {
|
||||
if rec.Name != "My Source!" {
|
||||
t.Errorf("Name = %q, want %q", rec.Name, "My Source!")
|
||||
}
|
||||
if rec.Alias != "MyAlias" {
|
||||
t.Errorf("Alias = %q, want MyAlias", rec.Alias)
|
||||
}
|
||||
if rec.Type != "local" {
|
||||
t.Errorf("Type = %q, want local", rec.Type)
|
||||
}
|
||||
@@ -300,13 +304,13 @@ func TestMapToSourceRecord(t *testing.T) {
|
||||
|
||||
func TestMapToCollectionRecord(t *testing.T) {
|
||||
in := map[string]any{
|
||||
"name": "My Col",
|
||||
"sourceIds": []any{"s1", "s2", "", 123},
|
||||
"templateId": "default",
|
||||
"ignoreFailed": false,
|
||||
"enabled": false,
|
||||
"meta": map[string]any{"k": "v"},
|
||||
"filters": []any{map[string]any{"type": "rename"}},
|
||||
"name": "My Col",
|
||||
"sourceIds": []any{"s1", "s2", "", 123},
|
||||
"templateId": "default",
|
||||
"ignoreFailed": false,
|
||||
"enabled": false,
|
||||
"meta": map[string]any{"k": "v"},
|
||||
"renameEnabled": true,
|
||||
}
|
||||
rec := mapToCollectionRecord(in)
|
||||
if rec.ID == "" {
|
||||
@@ -327,14 +331,14 @@ func TestMapToCollectionRecord(t *testing.T) {
|
||||
if rec.Enabled {
|
||||
t.Error("expected Enabled=false")
|
||||
}
|
||||
if !rec.RenameEnabled {
|
||||
t.Error("expected RenameEnabled=true")
|
||||
}
|
||||
if rec.Meta["k"] != "v" {
|
||||
t.Errorf("Meta = %v", rec.Meta)
|
||||
}
|
||||
if len(rec.Filters) != 1 {
|
||||
t.Errorf("Filters len = %d", len(rec.Filters))
|
||||
}
|
||||
|
||||
// default ignoreFailed = true
|
||||
// default ignoreFailed = true, default renameEnabled = false
|
||||
rec2 := mapToCollectionRecord(map[string]any{"id": "c1"})
|
||||
if !rec2.IgnoreFailed {
|
||||
t.Error("expected default IgnoreFailed=true")
|
||||
@@ -342,6 +346,9 @@ func TestMapToCollectionRecord(t *testing.T) {
|
||||
if !rec2.Enabled {
|
||||
t.Error("expected default Enabled=true")
|
||||
}
|
||||
if rec2.RenameEnabled {
|
||||
t.Error("expected default RenameEnabled=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapToTemplateRecord(t *testing.T) {
|
||||
@@ -380,11 +387,11 @@ func TestMapToTemplateRecord(t *testing.T) {
|
||||
|
||||
func TestMergeSource(t *testing.T) {
|
||||
existing := model.SourceRecord{
|
||||
ID: "s1", Name: "Old", Type: "local", Content: "old", Enabled: true,
|
||||
ID: "s1", Name: "Old", Alias: "OldAlias", Type: "local", Content: "old", Enabled: true,
|
||||
Meta: map[string]any{"a": "1"},
|
||||
}
|
||||
next := model.SourceRecord{
|
||||
Name: "New", URL: "https://x.com", Type: "remote", Enabled: false,
|
||||
Name: "New", Alias: "NewAlias", URL: "https://x.com", Type: "remote", Enabled: false,
|
||||
Filters: []model.FilterRule{{Type: "rename"}},
|
||||
Meta: map[string]any{"b": "2"},
|
||||
}
|
||||
@@ -392,6 +399,9 @@ func TestMergeSource(t *testing.T) {
|
||||
if merged.Name != "New" {
|
||||
t.Errorf("Name = %q", merged.Name)
|
||||
}
|
||||
if merged.Alias != "NewAlias" {
|
||||
t.Errorf("Alias = %q, want NewAlias", merged.Alias)
|
||||
}
|
||||
if merged.Type != "remote" {
|
||||
t.Errorf("Type = %q", merged.Type)
|
||||
}
|
||||
@@ -413,6 +423,9 @@ func TestMergeSource(t *testing.T) {
|
||||
if merged2.Name != "Old" {
|
||||
t.Errorf("Name = %q, want Old", merged2.Name)
|
||||
}
|
||||
if merged2.Alias != "OldAlias" {
|
||||
t.Errorf("Alias = %q, want OldAlias", merged2.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeCollection(t *testing.T) {
|
||||
@@ -422,7 +435,7 @@ func TestMergeCollection(t *testing.T) {
|
||||
}
|
||||
next := model.CollectionRecord{
|
||||
Name: "New", SourceIds: []string{"s2"}, TemplateId: "t2",
|
||||
Enabled: false, IgnoreFailed: false, Meta: map[string]any{"b": "2"},
|
||||
Enabled: false, IgnoreFailed: false, RenameEnabled: true, Meta: map[string]any{"b": "2"},
|
||||
}
|
||||
merged := mergeCollection(existing, next)
|
||||
if merged.Name != "New" {
|
||||
@@ -440,6 +453,9 @@ func TestMergeCollection(t *testing.T) {
|
||||
if merged.IgnoreFailed {
|
||||
t.Error("expected IgnoreFailed=false")
|
||||
}
|
||||
if !merged.RenameEnabled {
|
||||
t.Error("expected RenameEnabled=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringValue(t *testing.T) {
|
||||
@@ -999,8 +1015,8 @@ func TestHandleUpdateSettings(t *testing.T) {
|
||||
func TestHandleExportStorage(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
// seed some data
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", SourceIds: []string{}, Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", SourceIds: []string{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.TemplateRepo.Upsert(model.TemplateRecord{ID: "custom-t", Name: "Custom", Target: "mihomo", Config: map[string]any{"k": "v"}})
|
||||
|
||||
app := newApp(deps)
|
||||
@@ -1132,12 +1148,12 @@ func TestSourceCRUDHandlers(t *testing.T) {
|
||||
assertStatus(t, "SortSources bad json", code, 400)
|
||||
|
||||
// Delete - referenced by collection (create collection referencing source first)
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "ref-col", Name: "RefCol", SourceIds: []string{"my-src"}, Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "ref-col", Name: "RefCol", SourceIds: []string{"my-src"}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
code, _ = doRequest(t, app, "DELETE", "/api/sources/my-src", "", nil)
|
||||
assertStatus(t, "DeleteSource referenced", code, 409)
|
||||
|
||||
// Delete - remove collection reference, then delete
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "ref-col", Name: "RefCol", SourceIds: []string{}, Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "ref-col", Name: "RefCol", SourceIds: []string{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
code, body = doRequest(t, app, "DELETE", "/api/sources/my-src", "", nil)
|
||||
assertStatus(t, "DeleteSource", code, 200)
|
||||
data2 := body["data"].(map[string]any)
|
||||
@@ -1258,22 +1274,22 @@ func TestCollectionWithTemplateValidation(t *testing.T) {
|
||||
func TestValidateCollectionMissingTemplate(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
// built-in template is ok
|
||||
rec := model.CollectionRecord{ID: "c1", TemplateId: "mihomo-basic", Filters: []model.FilterRule{}, Meta: map[string]any{}}
|
||||
rec := model.CollectionRecord{ID: "c1", TemplateId: "mihomo-basic", Meta: map[string]any{}}
|
||||
if err := deps.validateCollection(rec); err != nil {
|
||||
t.Errorf("builtin template: unexpected err: %v", err)
|
||||
}
|
||||
// missing template
|
||||
rec2 := model.CollectionRecord{ID: "c2", TemplateId: "no-such", Filters: []model.FilterRule{}, Meta: map[string]any{}}
|
||||
rec2 := model.CollectionRecord{ID: "c2", TemplateId: "no-such", Meta: map[string]any{}}
|
||||
if err := deps.validateCollection(rec2); err == nil {
|
||||
t.Error("missing template: expected err")
|
||||
}
|
||||
// missing source ref
|
||||
rec3 := model.CollectionRecord{ID: "c3", SourceIds: []string{"missing"}, Filters: []model.FilterRule{}, Meta: map[string]any{}}
|
||||
rec3 := model.CollectionRecord{ID: "c3", SourceIds: []string{"missing"}, Meta: map[string]any{}}
|
||||
if err := deps.validateCollection(rec3); err == nil {
|
||||
t.Error("missing source: expected err")
|
||||
}
|
||||
// no sourceIds, no templateId -> ok
|
||||
rec4 := model.CollectionRecord{ID: "c4", Filters: []model.FilterRule{}, Meta: map[string]any{}}
|
||||
rec4 := model.CollectionRecord{ID: "c4", Meta: map[string]any{}}
|
||||
if err := deps.validateCollection(rec4); err != nil {
|
||||
t.Errorf("empty collection: unexpected err: %v", err)
|
||||
}
|
||||
@@ -1385,7 +1401,7 @@ func TestRecycleBinHandlers(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// seed + delete a source to populate recycle bin
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
code, _ := doRequest(t, app, "DELETE", "/api/sources/s1", "", nil)
|
||||
assertStatus(t, "seed delete", code, 200)
|
||||
|
||||
@@ -1401,7 +1417,7 @@ func TestRecycleBinHandlers(t *testing.T) {
|
||||
|
||||
// Get via Restore (409 conflict if source recreated)
|
||||
// First recreate source manually to test conflict path
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
code, _ = doRequest(t, app, "POST", "/api/recycle-bin/"+entryID+"/restore", "", nil)
|
||||
assertStatus(t, "RestoreRecycleBin conflict", code, 409)
|
||||
// remove the conflicting source so restore can proceed
|
||||
@@ -1425,7 +1441,7 @@ func TestRecycleBinHandlers(t *testing.T) {
|
||||
assertStatus(t, "RestoreRecycleBin not found", code, 404)
|
||||
|
||||
// Delete entry - seed another
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s2", Name: "S2", Type: "local", Content: "ss://y", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s2", Name: "S2", Type: "local", Content: "ss://y", Enabled: true, Meta: map[string]any{}})
|
||||
doRequest(t, app, "DELETE", "/api/sources/s2", "", nil)
|
||||
code, body = doRequest(t, app, "GET", "/api/recycle-bin", "", nil)
|
||||
data = body["data"].([]any)
|
||||
@@ -1448,7 +1464,7 @@ func TestRestoreCollectionAndTemplate(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// seed + delete collection
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
doRequest(t, app, "DELETE", "/api/collections/c1", "", nil)
|
||||
// restore collection
|
||||
code, body := doRequest(t, app, "GET", "/api/recycle-bin", "", nil)
|
||||
@@ -1482,10 +1498,10 @@ func TestRestoreCollectionAndTemplate(t *testing.T) {
|
||||
func TestRestoreCollectionConflict(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "cc1", Name: "CC1", Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "cc1", Name: "CC1", TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
doRequest(t, app, "DELETE", "/api/collections/cc1", "", nil)
|
||||
// recreate to force conflict
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "cc1", Name: "CC1", Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "cc1", Name: "CC1", TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
code, body := doRequest(t, app, "GET", "/api/recycle-bin", "", nil)
|
||||
data := body["data"].([]any)
|
||||
var entryID string
|
||||
@@ -1531,7 +1547,7 @@ func TestRestoreUnknownType(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// Archive a source (which works) then manually update its resourceType to "unknown"
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "u1", Name: "U1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "u1", Name: "U1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
deps.RecycleRepo.ArchiveAndDelete("source", "u1", sourceToMap(model.SourceRecord{ID: "u1", Name: "U1"}), "DELETE FROM sources WHERE id = ?", "u1")
|
||||
// Manually update the resource_type to "unknown" for testing
|
||||
deps.DB.Exec("UPDATE recycle_bin SET resource_type = 'unknown' WHERE resource_id = 'u1'")
|
||||
@@ -1567,8 +1583,8 @@ func TestRestoreUnknownType(t *testing.T) {
|
||||
func TestLinkHandlers(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
|
||||
// Link source - found
|
||||
code, body := doRequest(t, app, "GET", "/api/link/source/s1", "", nil)
|
||||
@@ -1614,7 +1630,7 @@ func TestBuildDownloadLinkNoToken(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
deps.Cfg.Auth.DownloadToken = "" // no token
|
||||
app := newApp(deps)
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
code, body := doRequest(t, app, "GET", "/api/link/source/s1", "", nil)
|
||||
assertStatus(t, "LinkSource no token", code, 200)
|
||||
d := body["data"].(map[string]any)
|
||||
@@ -1783,7 +1799,7 @@ func TestHandleDownloadSourceNotFound(t *testing.T) {
|
||||
code, _ = doRequest(t, app, "GET", "/sources/nope/dl-tok?target=mihomo", "", nil)
|
||||
assertStatus(t, "DownloadSource not found", code, 404)
|
||||
// bad target
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
code, _ = doRequest(t, app, "GET", "/sources/s1/dl-tok?target=bogus", "", nil)
|
||||
assertStatus(t, "DownloadSource bad target", code, 400)
|
||||
}
|
||||
@@ -1801,7 +1817,7 @@ func TestHandleDownloadCollectionNotFound(t *testing.T) {
|
||||
func TestHandleDownloadSourceLocalSuccess(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://pass@host:8080#name", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://pass@host:8080#name", Enabled: true, Meta: map[string]any{}})
|
||||
code, body := doRequest(t, app, "GET", "/sources/s1/dl-tok?target=json", "", nil)
|
||||
// Should succeed (200) — body contains rendered JSON
|
||||
assertStatus(t, "DownloadSource local", code, 200)
|
||||
@@ -1815,7 +1831,7 @@ func TestHandleDownloadSourceLocalSuccess(t *testing.T) {
|
||||
func TestHandleDownloadSourceDisabled(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: false, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: false, Meta: map[string]any{}})
|
||||
code, _ := doRequest(t, app, "GET", "/sources/s1/dl-tok?target=json", "", nil)
|
||||
assertStatus(t, "DownloadSource disabled", code, 404)
|
||||
}
|
||||
@@ -1883,7 +1899,7 @@ func TestHandlePreviewCollectionBadJSON(t *testing.T) {
|
||||
func TestHandlePreviewCollectionSuccess(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://pass@host:8080#name", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://pass@host:8080#name", Enabled: true, Meta: map[string]any{}})
|
||||
code, body := doRequest(t, app, "POST", "/api/preview/collection", `{"name":"c1","sourceIds":["s1"]}`, nil)
|
||||
assertStatus(t, "PreviewCollection", code, 200)
|
||||
d := body["data"].(map[string]any)
|
||||
@@ -1910,7 +1926,7 @@ func TestHandleFlowInfoNoFlow(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// local source has no flow URL -> parseFlowRequest returns nil
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{}})
|
||||
code, _ := doRequest(t, app, "GET", "/api/source/flow/s1", "", nil)
|
||||
assertStatus(t, "FlowInfo no flow", code, 400)
|
||||
}
|
||||
@@ -1919,7 +1935,7 @@ func TestHandleFlowInfoWithMetaOnly(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// source with meta subUserinfo but no remote URL
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{"subUserinfo": "upload=100; download=200; total=1000"}})
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Meta: map[string]any{"subUserinfo": "upload=100; download=200; total=1000"}})
|
||||
code, body := doRequest(t, app, "GET", "/api/source/flow/s1", "", nil)
|
||||
// Flow endpoint may return 200 or 400 depending on whether remote URL is required
|
||||
if code != 200 && code != 400 {
|
||||
|
||||
Reference in New Issue
Block a user