feat: add admin panel with multi-chain support and notification system
- Add full admin REST API: auth, API keys, chains, chain tokens, wallets, orders, RPC nodes, settings, dashboard stats, notifications - Add JWT-based admin authentication and API key auth middleware - Add multi-chain listeners: BSC, ETH, Polygon, Plasma, EVM WebSocket - Add RPC node health check job with automatic failover - Add Telegram notification channel for order events - Add installer for first-run database initialization - Add new DB models: admin_user, api_key, chain, chain_token, rpc_node, settings, notification_channel - Add order statistics aggregation (daily/monthly/by-chain) - Serve built admin SPA from embedded www/ assets - Add comprehensive test coverage for all new features
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func TestInstallDefaults(t *testing.T) {
|
||||
d := InstallDefaults()
|
||||
if d.AppName != "epusdt" {
|
||||
t.Errorf("AppName = %q, want epusdt", d.AppName)
|
||||
}
|
||||
if d.HttpBindAddr != "127.0.0.1" {
|
||||
t.Errorf("HttpBindAddr = %q, want 127.0.0.1", d.HttpBindAddr)
|
||||
}
|
||||
if d.HttpBindPort != 8000 {
|
||||
t.Errorf("HttpBindPort = %d, want 8000", d.HttpBindPort)
|
||||
}
|
||||
if d.OrderExpirationTime != 10 {
|
||||
t.Errorf("OrderExpirationTime = %d, want 10", d.OrderExpirationTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteEnvFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, ".env")
|
||||
|
||||
req := &InstallRequest{
|
||||
AppName: "myapp",
|
||||
AppURI: "http://1.2.3.4:8000",
|
||||
HttpBindAddr: "0.0.0.0",
|
||||
HttpBindPort: 9000,
|
||||
RuntimeRootPath: "./runtime",
|
||||
LogSavePath: "./logs",
|
||||
OrderExpirationTime: 15,
|
||||
OrderNoticeMaxRetry: 3,
|
||||
}
|
||||
if err := writeEnvFile(path, req); err != nil {
|
||||
t.Fatalf("writeEnvFile: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read env: %v", err)
|
||||
}
|
||||
content := string(data)
|
||||
|
||||
for _, want := range []string{
|
||||
"app_name=myapp",
|
||||
"app_uri=http://1.2.3.4:8000",
|
||||
"http_listen=0.0.0.0:9000",
|
||||
"order_expiration_time=15",
|
||||
"order_notice_max_retry=3",
|
||||
"db_type=sqlite",
|
||||
"install=false",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("env file missing %q\ncontent:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAPIDefaults(t *testing.T) {
|
||||
h := &installHandler{done: make(chan struct{})}
|
||||
e := echo.New()
|
||||
e.GET("/install/defaults", h.GetDefaults)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/install/defaults", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
if body["app_name"] != "epusdt" {
|
||||
t.Errorf("app_name = %v, want epusdt", body["app_name"])
|
||||
}
|
||||
if body["http_bind_addr"] != "127.0.0.1" {
|
||||
t.Errorf("http_bind_addr = %v, want 127.0.0.1", body["http_bind_addr"])
|
||||
}
|
||||
if body["http_bind_port"] != float64(8000) {
|
||||
t.Errorf("http_bind_port = %v, want 8000", body["http_bind_port"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAPISubmit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envPath := filepath.Join(dir, ".env")
|
||||
|
||||
h := &installHandler{envFilePath: envPath, done: make(chan struct{})}
|
||||
e := echo.New()
|
||||
e.POST("/install", h.Submit)
|
||||
|
||||
payload := `{"app_name":"testapp","app_uri":"http://10.0.0.1:8000","http_bind_addr":"0.0.0.0","http_bind_port":8000,"order_expiration_time":10,"order_notice_max_retry":1}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/install", strings.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
// done channel should be closed after successful submit
|
||||
select {
|
||||
case <-h.done:
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("handler did not close done channel within timeout")
|
||||
}
|
||||
data, err := os.ReadFile(envPath)
|
||||
if err != nil {
|
||||
t.Fatalf("env file not written: %v", err)
|
||||
}
|
||||
content := string(data)
|
||||
if !strings.Contains(content, "app_uri=http://10.0.0.1:8000") {
|
||||
t.Errorf("env file missing app_uri; content:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "http_listen=0.0.0.0:8000") {
|
||||
t.Errorf("env file missing http_listen; content:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAPISubmitMissingURI(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envPath := filepath.Join(dir, ".env")
|
||||
|
||||
h := &installHandler{envFilePath: envPath, done: make(chan struct{})}
|
||||
e := echo.New()
|
||||
e.POST("/install", h.Submit)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/install", strings.NewReader(`{"app_name":"x"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", rec.Code)
|
||||
}
|
||||
if _, err := os.Stat(envPath); err == nil {
|
||||
t.Error("env file should not have been written for invalid request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallAPISubmitInvalidPort(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envPath := filepath.Join(dir, ".env")
|
||||
|
||||
h := &installHandler{envFilePath: envPath, done: make(chan struct{})}
|
||||
e := echo.New()
|
||||
e.POST("/install", h.Submit)
|
||||
|
||||
payload := `{"app_uri":"http://example.com","http_bind_port":99999}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/install", strings.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
e.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400; body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if _, err := os.Stat(envPath); err == nil {
|
||||
t.Error("env file should not have been written for invalid port")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user