chore: Add tests for CertificateList and CertificateUploadDialog components
- Implement test to deselect a row checkbox in CertificateList by clicking it a second time. - Add test to close detail dialog via the close button in CertificateList. - Add test to close export dialog via the cancel button in CertificateList. - Add test to show KEY format badge when a .key file is uploaded in CertificateUploadDialog. - Add test to ensure no format badge is shown for unknown file extensions in CertificateUploadDialog.
This commit is contained in:
@@ -315,3 +315,4 @@ validation-evidence/**
|
||||
docs/reports/codecove_patch_report.md
|
||||
vuln-results.json
|
||||
test_output.txt
|
||||
coverage_results.txt
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -339,3 +340,368 @@ func TestGet_DBError(t *testing.T) {
|
||||
// Should be 500 since the table doesn't exist
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
// --- Export handler: re-auth and service error paths ---
|
||||
|
||||
func TestExport_IncludeKey_MissingPassword(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
h.SetDB(db)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem","include_key":true}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_IncludeKey_NoUserContext(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
h.SetDB(db)
|
||||
|
||||
r := gin.New() // no middleware — "user" key absent
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_IncludeKey_InvalidClaimsType(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
h.SetDB(db)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) { c.Set("user", "not-a-map"); c.Next() })
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_IncludeKey_UserIDNotInClaims(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
h.SetDB(db)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) { c.Set("user", map[string]any{}); c.Next() }) // no "id" key
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_IncludeKey_UserNotFoundInDB(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
h.SetDB(db)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) { c.Set("user", map[string]any{"id": float64(9999)}); c.Next() })
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_IncludeKey_WrongPassword(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||
|
||||
u := &models.User{UUID: uuid.New().String(), Email: "export@example.com", Name: "Export User"}
|
||||
require.NoError(t, u.SetPassword("correctpass"))
|
||||
require.NoError(t, db.Create(u).Error)
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
h.SetDB(db)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) { c.Set("user", map[string]any{"id": float64(u.ID)}); c.Next() })
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"wrongpass"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_CertNotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"pem"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestExport_ServiceError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
certUUID := uuid.New().String()
|
||||
cert := models.SSLCertificate{UUID: certUUID, Name: "test", Domains: "test.example.com", Provider: "custom"}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||
|
||||
body := bytes.NewBufferString(`{"format":"unsupported_xyz"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+certUUID+"/export", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
// --- Delete numeric ID paths ---
|
||||
|
||||
func TestDelete_NumericID_UsageCheckError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{})) // no ProxyHost → IsCertificateInUse fails
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/1", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
func TestDelete_NumericID_LowDiskSpace(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
cert := models.SSLCertificate{UUID: uuid.New().String(), Name: "low-space", Domains: "lowspace.example.com", Provider: "custom"}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
backup := &mockBackupService{
|
||||
availableSpaceFunc: func() (int64, error) { return 1024, nil }, // < 100 MB
|
||||
createFunc: func() (string, error) { return "", nil },
|
||||
}
|
||||
h := NewCertificateHandler(svc, backup, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInsufficientStorage, w.Code)
|
||||
}
|
||||
|
||||
func TestDelete_NumericID_BackupError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
cert := models.SSLCertificate{UUID: uuid.New().String(), Name: "backup-err", Domains: "backuperr.example.com", Provider: "custom"}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
backup := &mockBackupService{
|
||||
availableSpaceFunc: func() (int64, error) { return 1 << 30, nil }, // 1 GB — plenty
|
||||
createFunc: func() (string, error) { return "", fmt.Errorf("backup create failed") },
|
||||
}
|
||||
h := NewCertificateHandler(svc, backup, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
func TestDelete_NumericID_DeleteError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{})) // no SSLCertificate → DeleteCertificateByID fails
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/42", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
// --- Delete UUID: internal usage-check error ---
|
||||
|
||||
func TestDelete_UUID_UsageCheckInternalError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{})) // no ProxyHost → IsCertificateInUse fails
|
||||
|
||||
certUUID := uuid.New().String()
|
||||
cert := models.SSLCertificate{UUID: certUUID, Name: "uuid-err", Domains: "uuiderr.example.com", Provider: "custom"}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
// --- sendDeleteNotification: rate limit ---
|
||||
|
||||
func TestSendDeleteNotification_RateLimit(t *testing.T) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
ns := services.NewNotificationService(db, nil)
|
||||
svc := services.NewCertificateService(t.TempDir(), db, nil)
|
||||
h := NewCertificateHandler(svc, nil, ns)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest(http.MethodDelete, "/", http.NoBody)
|
||||
|
||||
certRef := uuid.New().String()
|
||||
h.sendDeleteNotification(ctx, certRef) // first call — sets timestamp
|
||||
h.sendDeleteNotification(ctx, certRef) // second call — hits rate limit branch
|
||||
}
|
||||
|
||||
// --- Update: empty UUID param (lines 207-209) ---
|
||||
|
||||
func TestUpdate_EmptyUUID(t *testing.T) {
|
||||
svc := services.NewCertificateService(t.TempDir(), nil, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPut, "/api/certificates/", bytes.NewBufferString(`{"name":"test"}`))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
// No Params set — c.Param("uuid") returns ""
|
||||
h.Update(ctx)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// --- Update: DB error (non-ErrCertNotFound) → lines 223-225 ---
|
||||
|
||||
func TestUpdate_DBError(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
// Deliberately no AutoMigrate → ssl_certificates table absent → "no such table" error
|
||||
|
||||
svc := services.NewCertificateService(t.TempDir(), db, nil)
|
||||
h := NewCertificateHandler(svc, nil, nil)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(mockAuthMiddleware())
|
||||
r.PUT("/api/certificates/:uuid", h.Update)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"name": "new-name"})
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/certificates/"+uuid.New().String(), bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateForwardHostWarnings_PrivateIP(t *testing.T) {
|
||||
warnings := generateForwardHostWarnings("192.168.1.100")
|
||||
require.Len(t, warnings, 1)
|
||||
assert.Equal(t, "forward_host", warnings[0].Field)
|
||||
}
|
||||
|
||||
func TestBulkUpdateSecurityHeaders_AllFail_Rollback(t *testing.T) {
|
||||
r, _ := setupTestRouterForSecurityHeaders(t)
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"host_uuids": []string{
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
uuid.New().String(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/bulk-update-security-headers", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestBulkUpdateSecurityHeaders_ProfileDB_NonNotFoundError(t *testing.T) {
|
||||
r, db := setupTestRouterForSecurityHeaders(t)
|
||||
|
||||
// Drop the security_header_profiles table so the lookup returns a non-NotFound DB error
|
||||
require.NoError(t, db.Exec("DROP TABLE security_header_profiles").Error)
|
||||
|
||||
profileID := uint(1)
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"host_uuids": []string{uuid.New().String()},
|
||||
"security_header_profile_id": profileID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/bulk-update-security-headers", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
func TestGenerateForwardHostWarnings_DockerBridgeIP(t *testing.T) {
|
||||
warnings := generateForwardHostWarnings("172.17.0.1")
|
||||
require.Len(t, warnings, 1)
|
||||
assert.Equal(t, "forward_host", warnings[0].Field)
|
||||
}
|
||||
|
||||
func TestParseNullableUintField_DefaultType(t *testing.T) {
|
||||
id, exists, err := parseNullableUintField(true, "test_field")
|
||||
assert.Nil(t, id)
|
||||
assert.True(t, exists)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseForwardPortField_StringEmpty(t *testing.T) {
|
||||
_, err := parseForwardPortField("")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseForwardPortField_StringNonNumeric(t *testing.T) {
|
||||
_, err := parseForwardPortField("notaport")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseForwardPortField_StringValid(t *testing.T) {
|
||||
port, err := parseForwardPortField("8080")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 8080, port)
|
||||
}
|
||||
|
||||
func TestParseForwardPortField_DefaultType(t *testing.T) {
|
||||
_, err := parseForwardPortField(true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCreate_InvalidCertificateRef(t *testing.T) {
|
||||
r, _ := setupTestRouterForSecurityHeaders(t)
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"domain_names": "cert-ref.example.com",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"certificate_id": uuid.New().String(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestCreate_InvalidSecurityHeaderProfileRef(t *testing.T) {
|
||||
r, _ := setupTestRouterForSecurityHeaders(t)
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"domain_names": "shp-ref.example.com",
|
||||
"forward_host": "localhost",
|
||||
"forward_port": 8080,
|
||||
"security_header_profile_id": uuid.New().String(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
)
|
||||
|
||||
// TestCheckExpiry_QueryFails covers lines 977-979: CheckExpiringCertificates fails.
|
||||
func TestCheckExpiry_QueryFails(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.Notification{}, &models.NotificationProvider{}))
|
||||
|
||||
// Drop ssl_certificates so CheckExpiringCertificates returns an error
|
||||
require.NoError(t, db.Exec("DROP TABLE ssl_certificates").Error)
|
||||
|
||||
ns := NewNotificationService(db, nil)
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
// Should not panic — logs the error and returns
|
||||
svc.checkExpiry(context.Background(), ns, 30)
|
||||
}
|
||||
|
||||
// TestCheckExpiry_ExpiredCert_Success covers lines 981-998: expired cert notification success path.
|
||||
func TestCheckExpiry_ExpiredCert_Success(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.Notification{}, &models.NotificationProvider{}))
|
||||
|
||||
past := time.Now().Add(-48 * time.Hour)
|
||||
certUUID := uuid.New().String()
|
||||
require.NoError(t, db.Create(&models.SSLCertificate{
|
||||
UUID: certUUID,
|
||||
Name: "expired-cert",
|
||||
Provider: "custom",
|
||||
Domains: "expired.example.com",
|
||||
ExpiresAt: &past,
|
||||
}).Error)
|
||||
|
||||
ns := NewNotificationService(db, nil)
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
svc.checkExpiry(context.Background(), ns, 30)
|
||||
|
||||
var notifications []models.Notification
|
||||
require.NoError(t, db.Find(¬ifications).Error)
|
||||
assert.NotEmpty(t, notifications)
|
||||
}
|
||||
|
||||
// TestCheckExpiry_ExpiringSoonCert_Success covers lines 999-1014: expiring-soon cert notification success path.
|
||||
func TestCheckExpiry_ExpiringSoonCert_Success(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.Notification{}, &models.NotificationProvider{}))
|
||||
|
||||
soon := time.Now().Add(7 * 24 * time.Hour)
|
||||
certUUID := uuid.New().String()
|
||||
require.NoError(t, db.Create(&models.SSLCertificate{
|
||||
UUID: certUUID,
|
||||
Name: "expiring-soon-cert",
|
||||
Provider: "custom",
|
||||
Domains: "soon.example.com",
|
||||
ExpiresAt: &soon,
|
||||
}).Error)
|
||||
|
||||
ns := NewNotificationService(db, nil)
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
svc.checkExpiry(context.Background(), ns, 30)
|
||||
|
||||
var notifications []models.Notification
|
||||
require.NoError(t, db.Find(¬ifications).Error)
|
||||
assert.NotEmpty(t, notifications)
|
||||
}
|
||||
|
||||
// TestCheckExpiry_NotificationFails covers lines 991-992 and 1006-1007:
|
||||
// Create() fails for both expired and expiring-soon certs.
|
||||
func TestCheckExpiry_NotificationFails(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.Notification{}, &models.NotificationProvider{}))
|
||||
|
||||
past := time.Now().Add(-48 * time.Hour)
|
||||
soon := time.Now().Add(7 * 24 * time.Hour)
|
||||
|
||||
require.NoError(t, db.Create(&models.SSLCertificate{
|
||||
UUID: uuid.New().String(),
|
||||
Name: "expired-cert",
|
||||
Provider: "custom",
|
||||
Domains: "expired2.example.com",
|
||||
ExpiresAt: &past,
|
||||
}).Error)
|
||||
require.NoError(t, db.Create(&models.SSLCertificate{
|
||||
UUID: uuid.New().String(),
|
||||
Name: "soon-cert",
|
||||
Provider: "custom",
|
||||
Domains: "soon2.example.com",
|
||||
ExpiresAt: &soon,
|
||||
}).Error)
|
||||
|
||||
// Drop notifications table so Create() fails
|
||||
require.NoError(t, db.Exec("DROP TABLE notifications").Error)
|
||||
|
||||
ns := NewNotificationService(db, nil)
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
// Should not panic — logs errors and continues
|
||||
svc.checkExpiry(context.Background(), ns, 30)
|
||||
}
|
||||
|
||||
func TestUploadCertificate_KeyMismatch(t *testing.T) {
|
||||
cert1PEM, _ := generateTestCertAndKey(t, "cert1.example.com", time.Now().Add(24*time.Hour))
|
||||
_, key2PEM := generateTestCertAndKey(t, "cert2.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}))
|
||||
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
_, err = svc.UploadCertificate("mismatch-test", string(cert1PEM), string(key2PEM), "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "key validation failed")
|
||||
}
|
||||
|
||||
func TestUploadCertificate_DBError(t *testing.T) {
|
||||
certPEM, keyPEM := generateTestCertAndKey(t, "db-err.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
// No AutoMigrate → ssl_certificates table absent → db.Create fails
|
||||
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
_, err = svc.UploadCertificate("db-error-test", string(certPEM), string(keyPEM), "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to save certificate")
|
||||
}
|
||||
|
||||
func TestGetCertificate_DBError(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
// No AutoMigrate → ssl_certificates table absent → First() returns error
|
||||
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
_, err = svc.GetCertificate(uuid.New().String())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to fetch certificate")
|
||||
}
|
||||
|
||||
func TestUpdateCertificate_DBError(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
// No AutoMigrate → ssl_certificates table absent → First() returns non-ErrRecordNotFound error
|
||||
|
||||
svc := NewCertificateService(t.TempDir(), db, nil)
|
||||
|
||||
_, err = svc.UpdateCertificate(uuid.New().String(), "new-name")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to fetch certificate")
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestSyncFromDisk_StagingToProductionUpgrade(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
certRoot := filepath.Join(tmpDir, "certificates")
|
||||
require.NoError(t, os.MkdirAll(certRoot, 0755))
|
||||
|
||||
domain := "staging-upgrade.example.com"
|
||||
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
|
||||
|
||||
certFile := filepath.Join(certRoot, domain+".crt")
|
||||
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
existing := models.SSLCertificate{
|
||||
UUID: uuid.New().String(),
|
||||
Name: domain,
|
||||
Provider: "letsencrypt-staging",
|
||||
Domains: domain,
|
||||
Certificate: "old-content",
|
||||
}
|
||||
require.NoError(t, db.Create(&existing).Error)
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
require.NoError(t, svc.SyncFromDisk())
|
||||
|
||||
var updated models.SSLCertificate
|
||||
require.NoError(t, db.Where("uuid = ?", existing.UUID).First(&updated).Error)
|
||||
assert.Equal(t, "letsencrypt", updated.Provider)
|
||||
}
|
||||
|
||||
func TestSyncFromDisk_ExpiryOnlyUpdate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
certRoot := filepath.Join(tmpDir, "certificates")
|
||||
require.NoError(t, os.MkdirAll(certRoot, 0755))
|
||||
|
||||
domain := "expiry-only.example.com"
|
||||
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
|
||||
|
||||
certFile := filepath.Join(certRoot, domain+".crt")
|
||||
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
existing := models.SSLCertificate{
|
||||
UUID: uuid.New().String(),
|
||||
Name: domain,
|
||||
Provider: "letsencrypt",
|
||||
Domains: domain,
|
||||
Certificate: string(certPEM), // identical content
|
||||
}
|
||||
require.NoError(t, db.Create(&existing).Error)
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
require.NoError(t, svc.SyncFromDisk())
|
||||
|
||||
var updated models.SSLCertificate
|
||||
require.NoError(t, db.Where("uuid = ?", existing.UUID).First(&updated).Error)
|
||||
assert.Equal(t, "letsencrypt", updated.Provider)
|
||||
assert.Equal(t, string(certPEM), updated.Certificate)
|
||||
}
|
||||
|
||||
func TestSyncFromDisk_CertRootStatPermissionError(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("cannot test permission error as root")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
certRoot := filepath.Join(tmpDir, "certificates")
|
||||
require.NoError(t, os.MkdirAll(certRoot, 0755))
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
// Restrict parent dir so os.Stat(certRoot) fails with permission error
|
||||
require.NoError(t, os.Chmod(tmpDir, 0))
|
||||
defer func() { _ = os.Chmod(tmpDir, 0755) }()
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
err = svc.SyncFromDisk()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestListCertificates_StaleCache_TriggersBackgroundSync(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
|
||||
// Simulate stale cache
|
||||
svc.cacheMu.Lock()
|
||||
svc.initialized = true
|
||||
svc.lastScan = time.Now().Add(-10 * time.Minute)
|
||||
before := svc.lastScan
|
||||
svc.cacheMu.Unlock()
|
||||
|
||||
_, err = svc.ListCertificates()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Background goroutine should update lastScan via SyncFromDisk
|
||||
require.Eventually(t, func() bool {
|
||||
svc.cacheMu.RLock()
|
||||
defer svc.cacheMu.RUnlock()
|
||||
return svc.lastScan.After(before)
|
||||
}, 2*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestGetDecryptedPrivateKey_DecryptFails(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
svc := newTestCertServiceWithEnc(t, tmpDir, db)
|
||||
|
||||
cert := models.SSLCertificate{
|
||||
UUID: uuid.New().String(),
|
||||
Name: "enc-fail",
|
||||
Domains: "encfail.example.com",
|
||||
Provider: "custom",
|
||||
PrivateKeyEncrypted: "corrupted-ciphertext",
|
||||
}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
_, err = svc.GetDecryptedPrivateKey(&cert)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeleteCertificate_LetsEncryptProvider_FileCleanup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
certRoot := filepath.Join(tmpDir, "certificates")
|
||||
require.NoError(t, os.MkdirAll(certRoot, 0755))
|
||||
|
||||
domain := "le-cleanup.example.com"
|
||||
certFile := filepath.Join(certRoot, domain+".crt")
|
||||
keyFile := filepath.Join(certRoot, domain+".key")
|
||||
jsonFile := filepath.Join(certRoot, domain+".json")
|
||||
|
||||
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
|
||||
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
|
||||
require.NoError(t, os.WriteFile(keyFile, []byte("key"), 0600))
|
||||
require.NoError(t, os.WriteFile(jsonFile, []byte("{}"), 0600))
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
certUUID := uuid.New().String()
|
||||
cert := models.SSLCertificate{
|
||||
UUID: certUUID,
|
||||
Name: domain,
|
||||
Provider: "letsencrypt",
|
||||
Domains: domain,
|
||||
}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
require.NoError(t, svc.DeleteCertificate(certUUID))
|
||||
|
||||
assert.NoFileExists(t, certFile)
|
||||
assert.NoFileExists(t, keyFile)
|
||||
assert.NoFileExists(t, jsonFile)
|
||||
}
|
||||
|
||||
func TestDeleteCertificate_StagingProvider_FileCleanup(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
certRoot := filepath.Join(tmpDir, "certificates")
|
||||
require.NoError(t, os.MkdirAll(certRoot, 0755))
|
||||
|
||||
domain := "le-staging-cleanup.example.com"
|
||||
certFile := filepath.Join(certRoot, domain+".crt")
|
||||
|
||||
certPEM, _ := generateTestCertAndKey(t, domain, time.Now().Add(24*time.Hour))
|
||||
require.NoError(t, os.WriteFile(certFile, certPEM, 0600))
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||
|
||||
certUUID := uuid.New().String()
|
||||
cert := models.SSLCertificate{
|
||||
UUID: certUUID,
|
||||
Name: domain,
|
||||
Provider: "letsencrypt-staging",
|
||||
Domains: domain,
|
||||
}
|
||||
require.NoError(t, db.Create(&cert).Error)
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
require.NoError(t, svc.DeleteCertificate(certUUID))
|
||||
|
||||
assert.NoFileExists(t, certFile)
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_DBError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
// deliberately do NOT AutoMigrate SSLCertificate
|
||||
|
||||
svc := newTestCertificateService(tmpDir, db)
|
||||
_, err = svc.CheckExpiringCertificates(30)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
func TestDetectFormat_PasswordProtectedPFX(t *testing.T) {
|
||||
cert, key, _, _ := makeRSACertAndKey(t, "pfx-pw.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
pfxData, err := pkcs12.Modern.Encode(key, cert, nil, "custompw")
|
||||
require.NoError(t, err)
|
||||
|
||||
format := DetectFormat(pfxData)
|
||||
assert.Equal(t, FormatPFX, format)
|
||||
}
|
||||
|
||||
func TestParsePEMPrivateKey_PKCS1RSA(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyDER := x509.MarshalPKCS1PrivateKey(key)
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER})
|
||||
|
||||
parsed, err := parsePEMPrivateKey(keyPEM)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, parsed)
|
||||
}
|
||||
|
||||
func TestParsePEMPrivateKey_ECPrivKey(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyDER, err := x509.MarshalECPrivateKey(key)
|
||||
require.NoError(t, err)
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
||||
|
||||
parsed, err := parsePEMPrivateKey(keyPEM)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, parsed)
|
||||
}
|
||||
|
||||
func TestDetectKeyType_ECDSAP384(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "p384.example.com"},
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "ECDSA-P384", detectKeyType(cert))
|
||||
}
|
||||
|
||||
func TestDetectKeyType_ECDSAUnknownCurve(t *testing.T) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "p224.example.com"},
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "ECDSA", detectKeyType(cert))
|
||||
}
|
||||
|
||||
func TestConvertPEMToPFX_EmptyChain(t *testing.T) {
|
||||
_, _, certPEM, keyPEM := makeRSACertAndKey(t, "pfx-chain.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
pfxData, err := ConvertPEMToPFX(string(certPEM), string(keyPEM), "", "testpass")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, pfxData)
|
||||
}
|
||||
|
||||
func TestConvertPEMToDER_NonCertBlock(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
})
|
||||
|
||||
_, err = ConvertPEMToDER(string(keyPEM))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid certificate PEM")
|
||||
}
|
||||
|
||||
func TestFormatSerial_NilInput(t *testing.T) {
|
||||
assert.Equal(t, "", formatSerial(nil))
|
||||
}
|
||||
|
||||
func TestDetectFormat_EmptyPasswordPFX(t *testing.T) {
|
||||
cert, key, _, _ := makeRSACertAndKey(t, "empty-pw.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
pfxData, err := pkcs12.Modern.Encode(key, cert, nil, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
format := DetectFormat(pfxData)
|
||||
assert.Equal(t, FormatPFX, format)
|
||||
}
|
||||
|
||||
func TestParseCertificateInput_BadChainPEM(t *testing.T) {
|
||||
_, _, certPEM, _ := makeRSACertAndKey(t, "bad-chain-test.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
badChain := []byte("-----BEGIN CERTIFICATE-----\naW52YWxpZA==\n-----END CERTIFICATE-----\n")
|
||||
|
||||
_, err := ParseCertificateInput(certPEM, nil, badChain, "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to parse chain PEM")
|
||||
}
|
||||
|
||||
func TestValidateChain_WithIntermediates(t *testing.T) {
|
||||
cert, _, _, _ := makeRSACertAndKey(t, "chain-inter.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
_ = ValidateChain(cert, []*x509.Certificate{cert})
|
||||
}
|
||||
|
||||
func TestConvertPEMToPFX_BadCertPEM(t *testing.T) {
|
||||
badCertPEM := "-----BEGIN CERTIFICATE-----\naW52YWxpZA==\n-----END CERTIFICATE-----\n"
|
||||
|
||||
_, err := ConvertPEMToPFX(badCertPEM, "somekey", "", "pass")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to parse cert PEM")
|
||||
}
|
||||
|
||||
func TestConvertPEMToPFX_BadChainPEM(t *testing.T) {
|
||||
_, _, certPEM, keyPEM := makeRSACertAndKey(t, "pfx-bad-chain.example.com", time.Now().Add(24*time.Hour))
|
||||
|
||||
badChain := "-----BEGIN CERTIFICATE-----\naW52YWxpZA==\n-----END CERTIFICATE-----\n"
|
||||
|
||||
_, err := ConvertPEMToPFX(string(certPEM), string(keyPEM), badChain, "pass")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to parse chain PEM")
|
||||
}
|
||||
|
||||
func TestParsePEMPrivateKey_PKCS8(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
require.NoError(t, err)
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
|
||||
|
||||
parsed, err := parsePEMPrivateKey(keyPEM)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, parsed)
|
||||
}
|
||||
|
||||
func TestEncodeKeyToPEM_UnsupportedKeyType(t *testing.T) {
|
||||
type badKey struct{}
|
||||
|
||||
_, err := encodeKeyToPEM(badKey{})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to marshal private key")
|
||||
}
|
||||
|
||||
func TestDetectKeyType_Unknown(t *testing.T) {
|
||||
cert := &x509.Certificate{
|
||||
PublicKey: "not-a-real-key",
|
||||
}
|
||||
assert.Equal(t, "Unknown", detectKeyType(cert))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+376
-1724
File diff suppressed because it is too large
Load Diff
@@ -440,4 +440,40 @@ describe('CertificateList', () => {
|
||||
await user.click(within(customRow).getByTestId('export-cert-cert-1'))
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('deselects a row checkbox by clicking it a second time', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithClient(<CertificateList />)
|
||||
const rows = await screen.findAllByRole('row')
|
||||
const customRow = rows.find(r => r.textContent?.includes('CustomCert'))!
|
||||
const checkbox = within(customRow).getByRole('checkbox')
|
||||
await user.click(checkbox)
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
await user.click(checkbox)
|
||||
await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('closes detail dialog via the dialog close button', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithClient(<CertificateList />)
|
||||
const rows = await screen.findAllByRole('row')
|
||||
const customRow = rows.find(r => r.textContent?.includes('CustomCert'))!
|
||||
await user.click(within(customRow).getByTestId('view-cert-cert-1'))
|
||||
const dialog = await screen.findByRole('dialog')
|
||||
expect(dialog).toBeInTheDocument()
|
||||
await user.click(within(dialog).getByRole('button', { name: 'Close' }))
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('closes export dialog via the cancel button', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithClient(<CertificateList />)
|
||||
const rows = await screen.findAllByRole('row')
|
||||
const customRow = rows.find(r => r.textContent?.includes('CustomCert'))!
|
||||
await user.click(within(customRow).getByTestId('export-cert-cert-1'))
|
||||
const dialog = await screen.findByRole('dialog')
|
||||
expect(dialog).toBeInTheDocument()
|
||||
await user.click(within(dialog).getByRole('button', { name: 'common.cancel' }))
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -384,4 +384,26 @@ describe('CertificateUploadDialog', () => {
|
||||
expect(screen.queryByTestId('certificate-validation-preview')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows KEY format badge when .key file is uploaded', async () => {
|
||||
const user = userEvent.setup({ applyAccept: false })
|
||||
renderDialog()
|
||||
const certInput = document.getElementById('cert-file') as HTMLInputElement
|
||||
const file = new File(['key-data'], 'server.key', { type: 'application/x-pem-file' })
|
||||
await user.upload(certInput, file)
|
||||
expect(await screen.findByText('KEY')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows no format badge for unknown file extension', async () => {
|
||||
const user = userEvent.setup({ applyAccept: false })
|
||||
renderDialog()
|
||||
const certInput = document.getElementById('cert-file') as HTMLInputElement
|
||||
const file = new File(['data'], 'cert.bin', { type: 'application/octet-stream' })
|
||||
await user.upload(certInput, file)
|
||||
await screen.findByText('cert.bin')
|
||||
expect(screen.queryByText('KEY')).toBeNull()
|
||||
expect(screen.queryByText('DER')).toBeNull()
|
||||
expect(screen.queryByText('PFX/PKCS#12')).toBeNull()
|
||||
expect(screen.queryByText('PEM')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user