refactor: simplify auth and subscription links
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -41,8 +43,8 @@ func newTestDeps(t *testing.T) *Deps {
|
||||
db := newTestDB(t)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
cfg := &config.Config{
|
||||
App: config.AppConfig{Name: "Sub-Store", Version: "1.0.0"},
|
||||
Auth: config.AuthConfig{AdminToken: "admin-tok", DownloadToken: "dl-tok"},
|
||||
App: config.AppConfig{Name: "Sub-Store", Version: "1.0.0"},
|
||||
Auth: config.AuthConfig{AdminToken: "admin-tok", DownloadToken: "dl-tok"},
|
||||
Server: config.ServerConfig{BodyLimit: 4 * 1024 * 1024},
|
||||
}
|
||||
return NewDeps(cfg, db)
|
||||
@@ -87,11 +89,6 @@ func registerHandlers(app *fiber.App, deps *Deps) {
|
||||
app.Patch("/api/templates/:name", deps.HandleUpdateTemplate)
|
||||
app.Delete("/api/templates/:name", deps.HandleDeleteTemplate)
|
||||
|
||||
app.Get("/api/shares", deps.HandleListShares)
|
||||
app.Post("/api/shares", deps.HandleCreateShare)
|
||||
app.Patch("/api/shares/:id", deps.HandleUpdateShare)
|
||||
app.Delete("/api/shares/:id", deps.HandleDeleteShare)
|
||||
|
||||
app.Get("/api/recycle-bin", deps.HandleListRecycleBin)
|
||||
app.Delete("/api/recycle-bin/:id", deps.HandleDeleteRecycleBinEntry)
|
||||
app.Post("/api/recycle-bin/:id/restore", deps.HandleRestoreRecycleBinEntry)
|
||||
@@ -106,12 +103,11 @@ func registerHandlers(app *fiber.App, deps *Deps) {
|
||||
|
||||
app.Post("/api/proxy/parse", deps.HandleProxyParse)
|
||||
app.Post("/api/rule/parse", deps.HandleRuleParse)
|
||||
app.Post("/api/utils/proxy-uri", deps.HandleProxyURI)
|
||||
app.Post("/api/utils/node-info", deps.HandleNodeInfo)
|
||||
|
||||
app.Get("/download/collection/:name", deps.HandleDownloadCollection)
|
||||
app.Get("/download/collection/:name/:target", deps.HandleDownloadCollection)
|
||||
app.Get("/download/source/:name", deps.HandleDownloadSource)
|
||||
app.Get("/download/source/:name/:target", deps.HandleDownloadSource)
|
||||
app.Get("/sources/:name/:token", deps.HandleDownloadSource)
|
||||
app.Get("/collections/:name/:token", deps.HandleDownloadCollection)
|
||||
}
|
||||
|
||||
// doRequest performs a request against the app and returns status code + body.
|
||||
@@ -181,19 +177,19 @@ func TestGetBuiltinTemplateIDs(t *testing.T) {
|
||||
|
||||
func TestValidateRecordId(t *testing.T) {
|
||||
cases := []struct {
|
||||
id string
|
||||
label string
|
||||
id string
|
||||
label string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid-id", "Source", false},
|
||||
{"valid_id_123", "Source", false},
|
||||
{"abc", "Collection", false},
|
||||
{"", "Source", true},
|
||||
{"UPPER", "Source", true}, // uppercase not allowed
|
||||
{"has space", "Source", true}, // space not allowed
|
||||
{"has/slash", "Source", true}, // slash not allowed
|
||||
{"with.dot", "Source", true}, // dot not allowed
|
||||
{strings.Repeat("a", 65), "Source", true}, // too long
|
||||
{"UPPER", "Source", true}, // uppercase not allowed
|
||||
{"has space", "Source", true}, // space not allowed
|
||||
{"has/slash", "Source", true}, // slash not allowed
|
||||
{"with.dot", "Source", true}, // dot not allowed
|
||||
{strings.Repeat("a", 65), "Source", true}, // too long
|
||||
{strings.Repeat("a", 64), "Source", false}, // max length
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -301,13 +297,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"},
|
||||
"filters": []any{map[string]any{"type": "rename"}},
|
||||
}
|
||||
rec := mapToCollectionRecord(in)
|
||||
if rec.ID == "" {
|
||||
@@ -687,19 +683,19 @@ func TestToIntSafe(t *testing.T) {
|
||||
|
||||
func TestNormalizeRuleTarget(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"mihomo": "mihomo",
|
||||
"Clash": "mihomo",
|
||||
"clashmeta": "mihomo",
|
||||
"clash-meta": "mihomo",
|
||||
"surge": "surge",
|
||||
"SURGE": "surge",
|
||||
"loon": "loon",
|
||||
"qx": "qx",
|
||||
"quanx": "qx",
|
||||
"quantumultx": "qx",
|
||||
"quantumult-x": "qx",
|
||||
"unknown": "",
|
||||
"": "",
|
||||
"mihomo": "mihomo",
|
||||
"Clash": "mihomo",
|
||||
"clashmeta": "mihomo",
|
||||
"clash-meta": "mihomo",
|
||||
"surge": "surge",
|
||||
"SURGE": "surge",
|
||||
"loon": "loon",
|
||||
"qx": "qx",
|
||||
"quanx": "qx",
|
||||
"quantumultx": "qx",
|
||||
"quantumult-x": "qx",
|
||||
"unknown": "",
|
||||
"": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := normalizeRuleTarget(in); got != want {
|
||||
@@ -834,7 +830,7 @@ func TestParseJSONHeaders(t *testing.T) {
|
||||
func TestParseFlowRequest(t *testing.T) {
|
||||
settings := map[string]any{
|
||||
"defaultFlowUserAgent": "custom-flow-ua",
|
||||
"defaultTimeout": "5000",
|
||||
"defaultTimeout": "5000",
|
||||
}
|
||||
// valid remote URL
|
||||
sub := &model.SourceRecord{ID: "s1", Type: "remote", URL: "https://x.com/sub#flowUrl=https://f.com&flowUserAgent=myUA"}
|
||||
@@ -1378,106 +1374,6 @@ func TestHandleCreateTemplateUpsertError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Share (download grant) handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestShareHandlers(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// seed a source
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "s1", Name: "S1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
|
||||
// List empty
|
||||
code, _ := doRequest(t, app, "GET", "/api/shares", "", nil)
|
||||
assertStatus(t, "ListShares empty", code, 200)
|
||||
|
||||
// Create - bad JSON
|
||||
code, _ = doRequest(t, app, "POST", "/api/shares", "not json", nil)
|
||||
assertStatus(t, "CreateShare bad json", code, 400)
|
||||
|
||||
// Create - bad resourceType
|
||||
code, _ = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"bogus","resourceId":"s1"}`, nil)
|
||||
assertStatus(t, "CreateShare bad type", code, 400)
|
||||
|
||||
// Create - missing resourceId
|
||||
code, _ = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source"}`, nil)
|
||||
assertStatus(t, "CreateShare no id", code, 400)
|
||||
|
||||
// Create - resource doesn't exist
|
||||
code, _ = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"nope"}`, nil)
|
||||
assertStatus(t, "CreateShare no resource", code, 404)
|
||||
|
||||
// Create - success with source
|
||||
code, body := doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"s1"}`, nil)
|
||||
assertStatus(t, "CreateShare", code, 200)
|
||||
d := body["data"].(map[string]any)
|
||||
shareID := d["id"].(string)
|
||||
if d["token"] == "" {
|
||||
t.Error("expected non-empty token")
|
||||
}
|
||||
if d["url"] == "" {
|
||||
t.Error("expected non-empty url")
|
||||
}
|
||||
|
||||
// Create - success with collection (seed one)
|
||||
deps.CollectionRepo.Upsert(model.CollectionRecord{ID: "c1", Name: "C1", Filters: []model.FilterRule{}, TemplateId: template.DefaultTemplateID, IgnoreFailed: true, Enabled: true, Meta: map[string]any{}})
|
||||
code, body = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"collection","resourceId":"c1"}`, nil)
|
||||
assertStatus(t, "CreateShare collection", code, 200)
|
||||
|
||||
// Create - with target + expiresIn
|
||||
code, body = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"s1","target":"mihomo","expiresIn":3600}`, nil)
|
||||
assertStatus(t, "CreateShare target+expiresIn", code, 200)
|
||||
|
||||
// Create - with expiresAt
|
||||
code, body = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"s1","expiresAt":9999999999999}`, nil)
|
||||
assertStatus(t, "CreateShare expiresAt", code, 200)
|
||||
|
||||
// Create - with unsupported target
|
||||
code, _ = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"s1","target":"bogus"}`, nil)
|
||||
assertStatus(t, "CreateShare bad target", code, 400)
|
||||
|
||||
// List - has items
|
||||
code, body = doRequest(t, app, "GET", "/api/shares", "", nil)
|
||||
assertStatus(t, "ListShares", code, 200)
|
||||
data := body["data"].([]any)
|
||||
if len(data) < 3 {
|
||||
t.Errorf("ListShares len = %d, want >= 3", len(data))
|
||||
}
|
||||
|
||||
// Update - not found
|
||||
code, _ = doRequest(t, app, "PATCH", "/api/shares/nope", `{"enabled":false}`, nil)
|
||||
assertStatus(t, "UpdateShare not found", code, 404)
|
||||
|
||||
// Update - bad JSON
|
||||
code, _ = doRequest(t, app, "PATCH", "/api/shares/"+shareID, "not json", nil)
|
||||
assertStatus(t, "UpdateShare bad json", code, 400)
|
||||
|
||||
// Update - success (disable)
|
||||
code, body = doRequest(t, app, "PATCH", "/api/shares/"+shareID, `{"enabled":false}`, nil)
|
||||
assertStatus(t, "UpdateShare disable", code, 200)
|
||||
|
||||
// Update - success (expiresAt nil)
|
||||
code, body = doRequest(t, app, "PATCH", "/api/shares/"+shareID, `{"expiresAt":null}`, nil)
|
||||
assertStatus(t, "UpdateShare expiresAt null", code, 200)
|
||||
|
||||
// Update - success (expiresAt number)
|
||||
code, body = doRequest(t, app, "PATCH", "/api/shares/"+shareID, `{"expiresAt":1234567890}`, nil)
|
||||
assertStatus(t, "UpdateShare expiresAt number", code, 200)
|
||||
|
||||
// Delete - not found
|
||||
code, _ = doRequest(t, app, "DELETE", "/api/shares/nope", "", nil)
|
||||
assertStatus(t, "DeleteShare not found", code, 404)
|
||||
|
||||
// Delete - success
|
||||
code, body = doRequest(t, app, "DELETE", "/api/shares/"+shareID, "", nil)
|
||||
assertStatus(t, "DeleteShare", code, 200)
|
||||
d = body["data"].(map[string]any)
|
||||
if d["deleted"] != true {
|
||||
t.Errorf("deleted = %v", d["deleted"])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recycle bin handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1578,55 +1474,6 @@ func TestRestoreCollectionAndTemplate(t *testing.T) {
|
||||
code, _ = doRequest(t, app, "POST", "/api/recycle-bin/"+tmplEntryID+"/restore", "", nil)
|
||||
assertStatus(t, "RestoreTemplate", code, 200)
|
||||
|
||||
// restore share: create share, delete it (via handler to archive), then restore
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "src1", Name: "Src1", Type: "local", Content: "ss://x", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
_, body = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"src1"}`, nil)
|
||||
shareID := body["data"].(map[string]any)["id"].(string)
|
||||
doRequest(t, app, "DELETE", "/api/shares/"+shareID, "", nil)
|
||||
code, body = doRequest(t, app, "GET", "/api/recycle-bin", "", nil)
|
||||
data = body["data"].([]any)
|
||||
var shareEntryID string
|
||||
for _, e := range data {
|
||||
em := e.(map[string]any)
|
||||
if em["resourceType"] == "share" {
|
||||
shareEntryID = em["id"].(string)
|
||||
break
|
||||
}
|
||||
}
|
||||
if shareEntryID == "" {
|
||||
t.Fatal("expected share recycle entry")
|
||||
}
|
||||
code, _ = doRequest(t, app, "POST", "/api/recycle-bin/"+shareEntryID+"/restore", "", nil)
|
||||
assertStatus(t, "RestoreShare", code, 200)
|
||||
|
||||
// restore share conflict: restore again after it exists (recreate via another cycle)
|
||||
// Actually test the conflict path by archiving a share whose resource id already exists
|
||||
deps.SourceRepo.Upsert(model.SourceRecord{ID: "src2", Name: "Src2", Type: "local", Content: "ss://y", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}})
|
||||
_, body = doRequest(t, app, "POST", "/api/shares", `{"resourceType":"source","resourceId":"src2"}`, nil)
|
||||
shareID2 := body["data"].(map[string]any)["id"].(string)
|
||||
doRequest(t, app, "DELETE", "/api/shares/"+shareID2, "", nil)
|
||||
// manually re-insert a grant with same id to force conflict
|
||||
snap, _ := deps.GrantRepo.GetSnapshot(shareID2)
|
||||
deps.GrantRepo.RestoreFromSnapshot(snap)
|
||||
code, body = doRequest(t, app, "GET", "/api/recycle-bin", "", nil)
|
||||
data = body["data"].([]any)
|
||||
var shareEntry2ID string
|
||||
for _, e := range data {
|
||||
em := e.(map[string]any)
|
||||
if em["resourceType"] == "share" {
|
||||
if s, _ := deps.RecycleRepo.Get(em["id"].(string)); s != nil && s["resourceId"] == shareID2 {
|
||||
shareEntry2ID = em["id"].(string)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if shareEntry2ID != "" {
|
||||
code, _ = doRequest(t, app, "POST", "/api/recycle-bin/"+shareEntry2ID+"/restore", "", nil)
|
||||
// Share restore may return 200 (no conflict) or 409 (conflict if grant still exists)
|
||||
if code != 200 && code != 409 {
|
||||
t.Errorf("RestoreShare conflict: status = %d, want 200 or 409", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreCollectionConflict(t *testing.T) {
|
||||
@@ -1685,7 +1532,7 @@ func TestRestoreUnknownType(t *testing.T) {
|
||||
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'")
|
||||
|
||||
|
||||
code, body := doRequest(t, app, "GET", "/api/recycle-bin", "", nil)
|
||||
assertStatus(t, "ListRecycleBin unknown", code, 200)
|
||||
data := body["data"].([]any)
|
||||
@@ -1724,8 +1571,8 @@ func TestLinkHandlers(t *testing.T) {
|
||||
code, body := doRequest(t, app, "GET", "/api/link/source/s1", "", nil)
|
||||
assertStatus(t, "LinkSource", code, 200)
|
||||
d := body["data"].(map[string]any)
|
||||
if d["url"] == "" {
|
||||
t.Error("expected non-empty url")
|
||||
if !strings.HasSuffix(d["url"].(string), "/sources/s1/dl-tok") {
|
||||
t.Errorf("url = %v, want /sources/s1/dl-tok suffix", d["url"])
|
||||
}
|
||||
if d["tokenIncluded"] != true {
|
||||
t.Errorf("tokenIncluded = %v", d["tokenIncluded"])
|
||||
@@ -1735,8 +1582,8 @@ func TestLinkHandlers(t *testing.T) {
|
||||
code, body = doRequest(t, app, "GET", "/api/link/source/s1?target=mihomo", "", nil)
|
||||
assertStatus(t, "LinkSource target", code, 200)
|
||||
d = body["data"].(map[string]any)
|
||||
if d["url"] == "" {
|
||||
t.Error("expected non-empty url with target")
|
||||
if !strings.HasSuffix(d["url"].(string), "/sources/s1/dl-tok?target=mihomo") {
|
||||
t.Errorf("url = %v, want /sources/s1/dl-tok?target=mihomo suffix", d["url"])
|
||||
}
|
||||
|
||||
// Link source - bad target returns nil url (buildDownloadLink returns nil, success wraps nil)
|
||||
@@ -1750,6 +1597,10 @@ func TestLinkHandlers(t *testing.T) {
|
||||
// Link collection - found
|
||||
code, body = doRequest(t, app, "GET", "/api/link/collection/c1", "", nil)
|
||||
assertStatus(t, "LinkCollection", code, 200)
|
||||
d = body["data"].(map[string]any)
|
||||
if !strings.HasSuffix(d["url"].(string), "/collections/c1/dl-tok") {
|
||||
t.Errorf("url = %v, want /collections/c1/dl-tok suffix", d["url"])
|
||||
}
|
||||
|
||||
// Link collection - not found
|
||||
code, _ = doRequest(t, app, "GET", "/api/link/collection/nope", "", nil)
|
||||
@@ -1923,27 +1774,24 @@ func TestGetTemplateConfig(t *testing.T) {
|
||||
func TestHandleDownloadSourceNotFound(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// no token + source not found -> handler writes 403 then continues to 404
|
||||
// (rejectInvalidDownloadToken calls failed() which returns nil, so handler continues)
|
||||
code, _ := doRequest(t, app, "GET", "/download/source/nope/mihomo", "", nil)
|
||||
code, _ := doRequest(t, app, "GET", "/sources/nope/bad-token?target=mihomo", "", nil)
|
||||
assertStatus(t, "DownloadSource no token", code, 404)
|
||||
// with token, source not found
|
||||
code, _ = doRequest(t, app, "GET", "/download/source/nope/mihomo?token=dl-tok", "", nil)
|
||||
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{}})
|
||||
code, _ = doRequest(t, app, "GET", "/download/source/s1/bogus?token=dl-tok", "", nil)
|
||||
code, _ = doRequest(t, app, "GET", "/sources/s1/dl-tok?target=bogus", "", nil)
|
||||
assertStatus(t, "DownloadSource bad target", code, 400)
|
||||
}
|
||||
|
||||
func TestHandleDownloadCollectionNotFound(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
// no token + collection not found -> 404 (same continuation behavior as source)
|
||||
code, _ := doRequest(t, app, "GET", "/download/collection/nope/mihomo", "", nil)
|
||||
code, _ := doRequest(t, app, "GET", "/collections/nope/bad-token?target=mihomo", "", nil)
|
||||
assertStatus(t, "DownloadCollection no token", code, 404)
|
||||
// with token, collection not found
|
||||
code, _ = doRequest(t, app, "GET", "/download/collection/nope/mihomo?token=dl-tok", "", nil)
|
||||
code, _ = doRequest(t, app, "GET", "/collections/nope/dl-tok?target=mihomo", "", nil)
|
||||
assertStatus(t, "DownloadCollection not found", code, 404)
|
||||
}
|
||||
|
||||
@@ -1951,7 +1799,7 @@ 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{}})
|
||||
code, body := doRequest(t, app, "GET", "/download/source/s1/json?token=dl-tok", "", nil)
|
||||
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)
|
||||
if body != nil {
|
||||
@@ -1961,24 +1809,11 @@ func TestHandleDownloadSourceLocalSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDownloadSourceScopedToken(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{}})
|
||||
// create a scoped grant
|
||||
_, token, _ := deps.GrantRepo.Create("source", "s1", "json", nil)
|
||||
code, _ := doRequest(t, app, "GET", "/download/source/s1/json?token="+token, "", nil)
|
||||
assertStatus(t, "DownloadSource scoped", code, 200)
|
||||
// scoped token for wrong target -> 403
|
||||
code, _ = doRequest(t, app, "GET", "/download/source/s1/mihomo?token="+token, "", nil)
|
||||
assertStatus(t, "DownloadSource scoped wrong target", code, 403)
|
||||
}
|
||||
|
||||
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{}})
|
||||
code, _ := doRequest(t, app, "GET", "/download/source/s1/json?token=dl-tok", "", nil)
|
||||
code, _ := doRequest(t, app, "GET", "/sources/s1/dl-tok?target=json", "", nil)
|
||||
assertStatus(t, "DownloadSource disabled", code, 404)
|
||||
}
|
||||
|
||||
@@ -2014,6 +1849,27 @@ func TestHandlePreviewSourceLocalSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePreviewSourceRemoteBase64Success(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
sub := base64.StdEncoding.EncodeToString([]byte("ss://pass@host:8080#keep\nss://pass@host:8081#drop"))
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(sub))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
code, body := doRequest(t, app, "POST", "/api/preview/source", `{"type":"remote","url":"`+srv.URL+`","filters":[{"type":"include","field":"name","pattern":"keep"}]}`, nil)
|
||||
assertStatus(t, "PreviewSource remote base64", code, 200)
|
||||
d := body["data"].(map[string]any)
|
||||
if d["originalCount"] != float64(2) {
|
||||
t.Fatalf("expected originalCount 2, got %v", d["originalCount"])
|
||||
}
|
||||
processed, ok := d["processed"].([]any)
|
||||
if !ok || len(processed) != 1 {
|
||||
t.Fatalf("expected one processed node, got %v", d["processed"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePreviewCollectionBadJSON(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
@@ -2120,6 +1976,20 @@ func TestHandleProxyParse(t *testing.T) {
|
||||
assertStatus(t, "ProxyParse platform key", code, 200)
|
||||
}
|
||||
|
||||
func TestHandleProxyURI(t *testing.T) {
|
||||
deps := newTestDeps(t)
|
||||
app := newApp(deps)
|
||||
|
||||
code, body := doRequest(t, app, "POST", "/api/utils/proxy-uri", `{"type":"ss","name":"n","server":"host","port":8388,"cipher":"aes-256-gcm","password":"pass"}`, nil)
|
||||
assertStatus(t, "ProxyURI ss", code, 200)
|
||||
if uri, _ := body["data"].(map[string]any)["uri"].(string); !strings.HasPrefix(uri, "ss://") {
|
||||
t.Fatalf("expected ss uri, got %v", body)
|
||||
}
|
||||
|
||||
code, _ = doRequest(t, app, "POST", "/api/utils/proxy-uri", `{"type":"snell","name":"n"}`, nil)
|
||||
assertStatus(t, "ProxyURI unsupported", code, 400)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule parse handler
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2228,7 +2098,7 @@ func TestRegisterRoutes(t *testing.T) {
|
||||
resp2.Body.Close()
|
||||
|
||||
// Download route (public, no admin token) -> 403 or 404 without download token
|
||||
req3 := httptest.NewRequest("GET", "/download/source/nope/mihomo", nil)
|
||||
req3 := httptest.NewRequest("GET", "/sources/nope/bad-token", nil)
|
||||
resp3, err := app.Test(req3)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
@@ -2248,7 +2118,7 @@ func TestNewDeps(t *testing.T) {
|
||||
if deps.SourceRepo == nil || deps.CollectionRepo == nil || deps.TemplateRepo == nil {
|
||||
t.Error("expected non-nil repos")
|
||||
}
|
||||
if deps.SettingsRepo == nil || deps.GrantRepo == nil || deps.RecycleRepo == nil || deps.CacheRepo == nil {
|
||||
if deps.SettingsRepo == nil || deps.RecycleRepo == nil || deps.CacheRepo == nil {
|
||||
t.Error("expected non-nil repos")
|
||||
}
|
||||
if deps.Cfg == nil || deps.DB == nil {
|
||||
@@ -2291,11 +2161,8 @@ func TestRejectInvalidDownloadToken(t *testing.T) {
|
||||
code, _ = doRequest(t, app, "GET", "/dl/s1/mihomo?token=dl-tok", "", nil)
|
||||
assertStatus(t, "rejectToken global", code, 200)
|
||||
|
||||
// path token param (not registered in route) — test query-only
|
||||
// valid scoped token
|
||||
_, token, _ := deps.GrantRepo.Create("source", "s1", "mihomo", nil)
|
||||
code, _ = doRequest(t, app, "GET", "/dl/s1/mihomo?token="+token, "", nil)
|
||||
assertStatus(t, "rejectToken scoped", code, 200)
|
||||
code, _ = doRequest(t, app, "GET", "/dl/s1/mihomo?token=wrong", "", nil)
|
||||
assertStatus(t, "rejectToken wrong", code, 403)
|
||||
}
|
||||
|
||||
func TestSendDownloadResponse(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user