167 lines
5.5 KiB
Go
167 lines
5.5 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSingBoxProfilesCRUDSecretsAndRevision(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
oldStatePath := singBoxProfilesStatePath
|
|
oldSecretsDir := singBoxSecretsRootDir
|
|
singBoxProfilesStatePath = filepath.Join(tmp, "transport", "singbox-profiles.json")
|
|
singBoxSecretsRootDir = filepath.Join(tmp, "transport", "secrets", "singbox")
|
|
t.Cleanup(func() {
|
|
singBoxProfilesStatePath = oldStatePath
|
|
singBoxSecretsRootDir = oldSecretsDir
|
|
})
|
|
|
|
createBody := `{
|
|
"name":"Main DE",
|
|
"mode":"typed",
|
|
"protocol":"vless",
|
|
"typed":{"server":"example.org","port":443},
|
|
"secrets":{"uuid":"abc-123-token","password":"secret-pass"}
|
|
}`
|
|
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/transport/singbox/profiles", strings.NewReader(createBody))
|
|
createRec := httptest.NewRecorder()
|
|
handleTransportSingBoxProfiles(createRec, createReq)
|
|
if createRec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected create status: %d body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
|
|
var createResp SingBoxProfilesResponse
|
|
if err := json.Unmarshal(createRec.Body.Bytes(), &createResp); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
if !createResp.OK || createResp.Item == nil {
|
|
t.Fatalf("create failed: %#v", createResp)
|
|
}
|
|
item := *createResp.Item
|
|
if item.ID == "" {
|
|
t.Fatalf("empty profile id")
|
|
}
|
|
if !item.HasSecrets {
|
|
t.Fatalf("profile must report has_secrets=true")
|
|
}
|
|
if got := item.SecretsMasked["uuid"]; got == "" || strings.Contains(got, "abc-123-token") {
|
|
t.Fatalf("secrets must be masked, got=%q", got)
|
|
}
|
|
|
|
stateData, err := os.ReadFile(singBoxProfilesStatePath)
|
|
if err != nil {
|
|
t.Fatalf("read state: %v", err)
|
|
}
|
|
if bytes.Contains(stateData, []byte("abc-123-token")) || bytes.Contains(stateData, []byte("secret-pass")) {
|
|
t.Fatalf("state file must not contain plain secrets")
|
|
}
|
|
|
|
secretPath := filepath.Join(singBoxSecretsRootDir, item.ID+".json")
|
|
secretData, err := os.ReadFile(secretPath)
|
|
if err != nil {
|
|
t.Fatalf("read secrets file: %v", err)
|
|
}
|
|
if !bytes.Contains(secretData, []byte("abc-123-token")) {
|
|
t.Fatalf("secrets file must contain original value")
|
|
}
|
|
|
|
patchBadReq := httptest.NewRequest(
|
|
http.MethodPatch,
|
|
"/api/v1/transport/singbox/profiles/"+item.ID,
|
|
strings.NewReader(`{"base_revision":9999,"name":"Main FR"}`),
|
|
)
|
|
patchBadRec := httptest.NewRecorder()
|
|
handleTransportSingBoxProfileByID(patchBadRec, patchBadReq)
|
|
if patchBadRec.Code != http.StatusConflict {
|
|
t.Fatalf("unexpected patch conflict status: %d body=%s", patchBadRec.Code, patchBadRec.Body.String())
|
|
}
|
|
|
|
patchGoodBody := fmt.Sprintf(`{
|
|
"base_revision":%d,
|
|
"name":"Main FR",
|
|
"secrets":{"uuid":"new-token","password":""}
|
|
}`, item.ProfileRevision)
|
|
patchGoodReq := httptest.NewRequest(
|
|
http.MethodPatch,
|
|
"/api/v1/transport/singbox/profiles/"+item.ID,
|
|
strings.NewReader(patchGoodBody),
|
|
)
|
|
patchGoodRec := httptest.NewRecorder()
|
|
handleTransportSingBoxProfileByID(patchGoodRec, patchGoodReq)
|
|
if patchGoodRec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected patch status: %d body=%s", patchGoodRec.Code, patchGoodRec.Body.String())
|
|
}
|
|
|
|
var patchResp SingBoxProfilesResponse
|
|
if err := json.Unmarshal(patchGoodRec.Body.Bytes(), &patchResp); err != nil {
|
|
t.Fatalf("decode patch response: %v", err)
|
|
}
|
|
if !patchResp.OK || patchResp.Item == nil {
|
|
t.Fatalf("patch failed: %#v", patchResp)
|
|
}
|
|
if patchResp.Item.ProfileRevision != item.ProfileRevision+1 {
|
|
t.Fatalf("revision must be incremented: got=%d want=%d", patchResp.Item.ProfileRevision, item.ProfileRevision+1)
|
|
}
|
|
secretDataAfterPatch, err := os.ReadFile(secretPath)
|
|
if err != nil {
|
|
t.Fatalf("read secrets after patch: %v", err)
|
|
}
|
|
if !bytes.Contains(secretDataAfterPatch, []byte("new-token")) {
|
|
t.Fatalf("secrets update not persisted")
|
|
}
|
|
if bytes.Contains(secretDataAfterPatch, []byte("secret-pass")) {
|
|
t.Fatalf("secret key with empty value must be removed")
|
|
}
|
|
|
|
deleteBadReq := httptest.NewRequest(
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v1/transport/singbox/profiles/%s?base_revision=%d", item.ID, item.ProfileRevision),
|
|
nil,
|
|
)
|
|
deleteBadRec := httptest.NewRecorder()
|
|
handleTransportSingBoxProfileByID(deleteBadRec, deleteBadReq)
|
|
if deleteBadRec.Code != http.StatusConflict {
|
|
t.Fatalf("unexpected delete conflict status: %d body=%s", deleteBadRec.Code, deleteBadRec.Body.String())
|
|
}
|
|
|
|
deleteReq := httptest.NewRequest(
|
|
http.MethodDelete,
|
|
fmt.Sprintf("/api/v1/transport/singbox/profiles/%s?base_revision=%d", item.ID, patchResp.Item.ProfileRevision),
|
|
nil,
|
|
)
|
|
deleteRec := httptest.NewRecorder()
|
|
handleTransportSingBoxProfileByID(deleteRec, deleteReq)
|
|
if deleteRec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected delete status: %d body=%s", deleteRec.Code, deleteRec.Body.String())
|
|
}
|
|
if _, err := os.Stat(secretPath); !os.IsNotExist(err) {
|
|
t.Fatalf("secrets file must be deleted, err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSingBoxFeaturesEndpoint(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/transport/singbox/features", nil)
|
|
rec := httptest.NewRecorder()
|
|
handleTransportSingBoxFeatures(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
var resp SingBoxFeaturesResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if !resp.OK {
|
|
t.Fatalf("response must be ok: %#v", resp)
|
|
}
|
|
if !resp.ProfileModes[string(SingBoxProfileModeTyped)] || !resp.ProfileModes[string(SingBoxProfileModeRaw)] {
|
|
t.Fatalf("profile modes not advertised: %#v", resp.ProfileModes)
|
|
}
|
|
}
|