package api import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "math/big" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/tlsservice" ) func pemPairForAPI(t *testing.T, host string) (certPEM, keyPEM string) { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) } serial, err := rand.Int(rand.Reader, big.NewInt(1<<62)) if err != nil { t.Fatal(err) } tmpl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: host}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), DNSNames: []string{host}, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) if err != nil { t.Fatal(err) } keyDER, err := x509.MarshalECPrivateKey(key) if err != nil { t.Fatal(err) } return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})) } func TestUploadTLSCertificateAPI(t *testing.T) { gin.SetMode(gin.TestMode) dir := t.TempDir() t.Setenv("VPN_DATA_DIR", dir) t.Setenv("VPN_TLS_DOMAIN", "") t.Setenv("VPN_TLS_SCHEME", "") t.Setenv("VPN_TLS_PORT", "") t.Setenv("VPN_TLS_MODE", "") t.Setenv("VPN_TLS_HTTP_ADDR", "127.0.0.1:0") t.Setenv("VPN_TLS_HTTPS_ADDR", "127.0.0.1:0") if err := tlsservice.ConfigureMode(dir, "nginx"); err != nil { t.Fatal(err) } if err := tlsservice.ConfigurePublicEntry(dir, "https", "vpn.example.com", 0); err != nil { t.Fatal(err) } if err := tlsservice.Start(http.NotFoundHandler(), dir); err != nil { t.Fatal(err) } _ = service.BaseDir() // ensure env seen router := gin.New() s := &Server{} router.POST("/api/system/tls/certificate", func(c *gin.Context) { c.Set("role", "admin") c.Set("username", "admin") c.Next() }, s.uploadTLSCertificate) certPEM, keyPEM := pemPairForAPI(t, "vpn.example.com") body, _ := json.Marshal(map[string]string{ "certificate_pem": certPEM, "private_key_pem": keyPEM, "host": "vpn.example.com", }) req := httptest.NewRequest(http.MethodPost, "/api/system/tls/certificate", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp := httptest.NewRecorder() router.ServeHTTP(resp, req) if resp.Code != http.StatusOK { t.Fatalf("upload HTTP %d: %s", resp.Code, resp.Body.String()) } var out map[string]any if err := json.Unmarshal(resp.Body.Bytes(), &out); err != nil { t.Fatal(err) } status, _ := out["status"].(map[string]any) if status["cert_source"] != "upload" || status["certificate_valid"] != true { t.Fatalf("status=%v", status) } // mismatch _, otherKey := pemPairForAPI(t, "vpn.example.com") bad, _ := json.Marshal(map[string]string{ "certificate_pem": certPEM, "private_key_pem": otherKey, "host": "vpn.example.com", }) req = httptest.NewRequest(http.MethodPost, "/api/system/tls/certificate", bytes.NewReader(bad)) req.Header.Set("Content-Type", "application/json") resp = httptest.NewRecorder() router.ServeHTTP(resp, req) if resp.Code != http.StatusBadRequest { t.Fatalf("mismatch want 400 got %d", resp.Code) } }