Files
navi/internal/remnawave/provision_test.go
T
NavisandCursor 77bd7da861 Release 3.9.0+3: subscription-URL routing, emoji flags, Remnawave provisioning.
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>
2026-08-01 17:36:54 +03:00

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")
}
}