Pulse/internal/api/license_handlers_test.go

522 lines
16 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/license"
)
type licenseFeaturesResponse struct {
LicenseStatus string `json:"license_status"`
Features map[string]bool `json:"features"`
UpgradeURL string `json:"upgrade_url"`
}
func TestHandleLicenseFeatures_MethodNotAllowed(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodPost, "/api/license/features", nil)
rec := httptest.NewRecorder()
handler.HandleLicenseFeatures(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleLicenseFeatures_NoLicense(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodGet, "/api/license/features", nil)
rec := httptest.NewRecorder()
handler.HandleLicenseFeatures(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp licenseFeaturesResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.LicenseStatus != string(license.LicenseStateNone) {
t.Fatalf("expected license_status %q, got %q", license.LicenseStateNone, resp.LicenseStatus)
}
if resp.UpgradeURL == "" {
t.Fatalf("expected upgrade_url to be set")
}
expectedFeatures := []string{
license.FeatureAIPatrol,
license.FeatureAIAlerts,
license.FeatureAIAutoFix,
license.FeatureKubernetesAI,
}
for _, feature := range expectedFeatures {
if value, ok := resp.Features[feature]; !ok {
t.Fatalf("expected feature %q in response", feature)
} else if value {
t.Fatalf("expected feature %q to be false without a license", feature)
}
}
}
func TestHandleLicenseFeatures_WithActiveLicense(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handler := NewLicenseHandlers(t.TempDir())
licenseKey, err := license.GenerateLicenseForTesting("test@example.com", license.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("failed to generate test license: %v", err)
}
if _, err := handler.Service().Activate(licenseKey); err != nil {
t.Fatalf("failed to activate test license: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/license/features", nil)
rec := httptest.NewRecorder()
handler.HandleLicenseFeatures(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp licenseFeaturesResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.LicenseStatus != string(license.LicenseStateActive) {
t.Fatalf("expected license_status %q, got %q", license.LicenseStateActive, resp.LicenseStatus)
}
expectedFeatures := []string{
license.FeatureAIPatrol,
license.FeatureAIAlerts,
license.FeatureAIAutoFix,
license.FeatureKubernetesAI,
}
for _, feature := range expectedFeatures {
if value, ok := resp.Features[feature]; !ok {
t.Fatalf("expected feature %q in response", feature)
} else if !value {
t.Fatalf("expected feature %q to be true with active license", feature)
}
}
}
// ========================================
// HandleLicenseStatus tests
// ========================================
func TestHandleLicenseStatus_MethodNotAllowed(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodPost, "/api/license/status", nil)
rec := httptest.NewRecorder()
handler.HandleLicenseStatus(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleLicenseStatus_NoLicense(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodGet, "/api/license/status", nil)
rec := httptest.NewRecorder()
handler.HandleLicenseStatus(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp license.LicenseStatus
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// LicenseStatus uses Valid field and Tier field (no State field)
// For no license, Valid should be false and Tier should be TierFree
if resp.Valid {
t.Fatalf("expected Valid=false for no license")
}
if resp.Tier != license.TierFree {
t.Fatalf("expected tier %q, got %q", license.TierFree, resp.Tier)
}
}
func TestHandleLicenseStatus_WithActiveLicense(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handler := NewLicenseHandlers(t.TempDir())
licenseKey, err := license.GenerateLicenseForTesting("test@example.com", license.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("failed to generate test license: %v", err)
}
if _, err := handler.Service().Activate(licenseKey); err != nil {
t.Fatalf("failed to activate test license: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/license/status", nil)
rec := httptest.NewRecorder()
handler.HandleLicenseStatus(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp license.LicenseStatus
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// LicenseStatus uses Valid field and Tier field
// For active license, Valid should be true and Tier should be TierPro
if !resp.Valid {
t.Fatalf("expected Valid=true for active license")
}
if resp.Email != "test@example.com" {
t.Fatalf("expected email %q, got %q", "test@example.com", resp.Email)
}
if resp.Tier != license.TierPro {
t.Fatalf("expected tier %q, got %q", license.TierPro, resp.Tier)
}
}
// ========================================
// HandleActivateLicense tests
// ========================================
func TestHandleActivateLicense_MethodNotAllowed(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodGet, "/api/license/activate", nil)
rec := httptest.NewRecorder()
handler.HandleActivateLicense(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleActivateLicense_EmptyKey(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
body := []byte(`{"license_key":""}`)
req := httptest.NewRequest(http.MethodPost, "/api/license/activate", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleActivateLicense(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
var resp ActivateLicenseResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Success {
t.Fatalf("expected Success=false for empty key")
}
if resp.Message != "License key is required" {
t.Fatalf("expected message %q, got %q", "License key is required", resp.Message)
}
}
func TestHandleActivateLicense_InvalidKey(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
body := []byte(`{"license_key":"invalid-license-key"}`)
req := httptest.NewRequest(http.MethodPost, "/api/license/activate", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleActivateLicense(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
var resp ActivateLicenseResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Success {
t.Fatalf("expected Success=false for invalid key")
}
}
func TestHandleActivateLicense_InvalidBody(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
body := []byte(`{invalid json}`)
req := httptest.NewRequest(http.MethodPost, "/api/license/activate", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleActivateLicense(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
var resp ActivateLicenseResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Success {
t.Fatalf("expected Success=false for invalid body")
}
if resp.Message != "Invalid request body" {
t.Fatalf("expected message %q, got %q", "Invalid request body", resp.Message)
}
}
func TestHandleActivateLicense_ValidKey(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handler := NewLicenseHandlers(t.TempDir())
licenseKey, err := license.GenerateLicenseForTesting("pro@example.com", license.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("failed to generate test license: %v", err)
}
body, _ := json.Marshal(map[string]string{"license_key": licenseKey})
req := httptest.NewRequest(http.MethodPost, "/api/license/activate", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.HandleActivateLicense(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
}
var resp ActivateLicenseResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if !resp.Success {
t.Fatalf("expected Success=true, got message: %s", resp.Message)
}
if resp.Status == nil {
t.Fatalf("expected Status to be non-nil")
}
if resp.Status.Email != "pro@example.com" {
t.Fatalf("expected email %q, got %q", "pro@example.com", resp.Status.Email)
}
}
// ========================================
// HandleClearLicense tests
// ========================================
func TestHandleClearLicense_MethodNotAllowed(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodGet, "/api/license/clear", nil)
rec := httptest.NewRecorder()
handler.HandleClearLicense(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestHandleClearLicense_NoLicense(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
req := httptest.NewRequest(http.MethodPost, "/api/license/clear", nil)
rec := httptest.NewRecorder()
handler.HandleClearLicense(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var resp map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if success, ok := resp["success"].(bool); !ok || !success {
t.Fatalf("expected success=true")
}
}
func TestHandleClearLicense_WithActiveLicense(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handler := NewLicenseHandlers(t.TempDir())
licenseKey, err := license.GenerateLicenseForTesting("test@example.com", license.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("failed to generate test license: %v", err)
}
if _, err := handler.Service().Activate(licenseKey); err != nil {
t.Fatalf("failed to activate test license: %v", err)
}
// Verify license is active
if !handler.Service().IsValid() {
t.Fatalf("expected license to be valid before clearing")
}
req := httptest.NewRequest(http.MethodPost, "/api/license/clear", nil)
rec := httptest.NewRecorder()
handler.HandleClearLicense(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
// Verify license is cleared
if handler.Service().IsValid() {
t.Fatalf("expected license to be invalid after clearing")
}
var resp map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if success, ok := resp["success"].(bool); !ok || !success {
t.Fatalf("expected success=true")
}
}
// ========================================
// RequireLicenseFeature middleware tests
// ========================================
func TestRequireLicenseFeature_NoLicense(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
handlerCalled := false
wrappedHandler := RequireLicenseFeature(handler.Service(), license.FeatureAIPatrol, func(w http.ResponseWriter, r *http.Request) {
handlerCalled = true
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusPaymentRequired {
t.Fatalf("expected status %d, got %d", http.StatusPaymentRequired, rec.Code)
}
if handlerCalled {
t.Fatalf("expected handler not to be called when license is missing")
}
}
func TestRequireLicenseFeature_WithLicense(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handler := NewLicenseHandlers(t.TempDir())
licenseKey, err := license.GenerateLicenseForTesting("test@example.com", license.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("failed to generate test license: %v", err)
}
if _, err := handler.Service().Activate(licenseKey); err != nil {
t.Fatalf("failed to activate test license: %v", err)
}
handlerCalled := false
wrappedHandler := RequireLicenseFeature(handler.Service(), license.FeatureAIPatrol, func(w http.ResponseWriter, r *http.Request) {
handlerCalled = true
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
if !handlerCalled {
t.Fatalf("expected handler to be called when license is valid")
}
}
// ========================================
// LicenseGatedEmptyResponse middleware tests
// ========================================
func TestLicenseGatedEmptyResponse_NoLicense(t *testing.T) {
handler := NewLicenseHandlers(t.TempDir())
handlerCalled := false
wrappedHandler := LicenseGatedEmptyResponse(handler.Service(), license.FeatureAIPatrol, func(w http.ResponseWriter, r *http.Request) {
handlerCalled = true
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"data":"real"}`))
})
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
if handlerCalled {
t.Fatalf("expected handler not to be called when license is missing")
}
// LicenseGatedEmptyResponse returns empty array, not empty object
if rec.Body.String() != "[]" {
t.Fatalf("expected empty array [], got %s", rec.Body.String())
}
}
func TestLicenseGatedEmptyResponse_WithLicense(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "true")
handler := NewLicenseHandlers(t.TempDir())
licenseKey, err := license.GenerateLicenseForTesting("test@example.com", license.TierPro, 24*time.Hour)
if err != nil {
t.Fatalf("failed to generate test license: %v", err)
}
if _, err := handler.Service().Activate(licenseKey); err != nil {
t.Fatalf("failed to activate test license: %v", err)
}
handlerCalled := false
wrappedHandler := LicenseGatedEmptyResponse(handler.Service(), license.FeatureAIPatrol, func(w http.ResponseWriter, r *http.Request) {
handlerCalled = true
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"data":"real"}`))
})
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
rec := httptest.NewRecorder()
wrappedHandler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
if !handlerCalled {
t.Fatalf("expected handler to be called when license is valid")
}
if rec.Body.String() != `{"data":"real"}` {
t.Fatalf("expected real data, got %s", rec.Body.String())
}
}