Route credential-less http(s) URLs pasted into the add-link field to subscription import (fixes remaining 'proxy URI missing username'). Extend geoflag with RU country names and city hints; live Remnawave names already carrying emoji flags are kept as-is. Add admin provisioning via configs/remnawave-api.json (GET by-username / POST users, 50 GB MONTH plan) and the «Выдать доступ» UI panel. Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
5.4 KiB
Go
193 lines
5.4 KiB
Go
package remnawave
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func testAdminConfig(baseURL string) *AdminConfig {
|
|
return &AdminConfig{
|
|
PanelURL: baseURL,
|
|
APIToken: "test-token",
|
|
Provision: ProvisionOptions{
|
|
TrafficGB: 50,
|
|
Days: 30,
|
|
Strategy: "MONTH",
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestProvisionUserExisting(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.Method == http.MethodGet && r.URL.Path == "/api/users/by-username/ivan_petrov" {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{
|
|
"uuid": "u-1",
|
|
"shortUuid": "short1",
|
|
"subscriptionUrl": "https://sub.example.com/short1",
|
|
"trafficLimitBytes": int64(50) << 30,
|
|
"expireAt": time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
|
|
}})
|
|
return
|
|
}
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
res, err := ProvisionUser(context.Background(), testAdminConfig(ts.URL), "ivan_petrov")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Created {
|
|
t.Fatal("expected existing user, got created")
|
|
}
|
|
if res.SubscriptionURL != "https://sub.example.com/short1" {
|
|
t.Fatalf("subscription url: %q", res.SubscriptionURL)
|
|
}
|
|
if res.Username != "ivan_petrov" {
|
|
t.Fatalf("username: %q", res.Username)
|
|
}
|
|
}
|
|
|
|
func TestProvisionUserCreate(t *testing.T) {
|
|
var created map[string]any
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/users/by-username/"):
|
|
w.WriteHeader(http.StatusNotFound)
|
|
case r.Method == http.MethodPost && r.URL.Path == "/api/users":
|
|
if err := json.NewDecoder(r.Body).Decode(&created); err != nil {
|
|
t.Errorf("decode create body: %v", err)
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{
|
|
"uuid": "u-2",
|
|
"shortUuid": "short2",
|
|
"subscriptionUrl": "https://sub.example.com/short2",
|
|
"trafficLimitBytes": created["trafficLimitBytes"],
|
|
"expireAt": created["expireAt"],
|
|
}})
|
|
default:
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
res, err := ProvisionUser(context.Background(), testAdminConfig(ts.URL), "@new_client")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.Created {
|
|
t.Fatal("expected created user")
|
|
}
|
|
if res.Username != "new_client" {
|
|
t.Fatalf("username: %q", res.Username)
|
|
}
|
|
if got := created["trafficLimitBytes"].(float64); int64(got) != 50<<30 {
|
|
t.Fatalf("trafficLimitBytes = %v, want %d (50 GB)", got, int64(50)<<30)
|
|
}
|
|
if created["trafficLimitStrategy"] != "MONTH" {
|
|
t.Fatalf("strategy = %v", created["trafficLimitStrategy"])
|
|
}
|
|
if created["status"] != "ACTIVE" {
|
|
t.Fatalf("status = %v", created["status"])
|
|
}
|
|
expireStr, _ := created["expireAt"].(string)
|
|
expire, err := time.Parse(time.RFC3339, expireStr)
|
|
if err != nil {
|
|
t.Fatalf("expireAt %q: %v", expireStr, err)
|
|
}
|
|
days := time.Until(expire).Hours() / 24
|
|
if days < 29 || days > 31 {
|
|
t.Fatalf("expireAt %.1f days from now, want ~30", days)
|
|
}
|
|
}
|
|
|
|
func TestProvisionUserBadToken(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
_, err := ProvisionUser(context.Background(), testAdminConfig(ts.URL), "somebody1")
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "Неверный API ключ") {
|
|
t.Fatalf("error = %v, want «Неверный API ключ»", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeUsername(t *testing.T) {
|
|
cases := []struct {
|
|
in, want string
|
|
wantErr bool
|
|
}{
|
|
{"ivan_petrov", "ivan_petrov", false},
|
|
{"@tg_handle", "tg_handle", false},
|
|
{"user@example.com", "user_vpn", false},
|
|
{"Иван", "", true},
|
|
{"ab", "ab_vpn", false}, // padded to min length
|
|
}
|
|
for _, c := range cases {
|
|
got, err := NormalizeUsername(c.in)
|
|
if c.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("%q: expected error, got %q", c.in, got)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", c.in, err)
|
|
}
|
|
if got != c.want {
|
|
t.Fatalf("%q: got %q want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadAdminConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "remnawave-api.json")
|
|
|
|
if _, err := LoadAdminConfig(path); err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
|
|
body := `{"panel_url":"https://panel.test/","api_token":"tok123","provision":{}}`
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := LoadAdminConfig(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.PanelURL != "https://panel.test" {
|
|
t.Fatalf("panel url: %q", cfg.PanelURL)
|
|
}
|
|
if cfg.Provision.TrafficGB != 50 || cfg.Provision.Days != 30 || cfg.Provision.Strategy != "MONTH" {
|
|
t.Fatalf("defaults not applied: %+v", cfg.Provision)
|
|
}
|
|
|
|
// Placeholder token must be rejected.
|
|
body = `{"panel_url":"https://panel.test","api_token":"PASTE_API_TOKEN_HERE"}`
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadAdminConfig(path); err == nil {
|
|
t.Fatal("expected error for placeholder token")
|
|
}
|
|
}
|