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