Skip to content

Commit 2cad49d

Browse files
committed
chore: Add tests for backup service, crowdsec startup, log service, and security headers
- Implement tests for BackupService to handle database extraction from backup archives with SHM and WAL entries. - Add tests for BackupService to validate behavior when creating backups for non-SQLite databases and handling oversized database entries. - Introduce tests for CrowdSec startup to ensure proper error handling during configuration creation. - Enhance LogService tests to cover scenarios for skipping dot and empty directories and handling read directory errors. - Add tests for SecurityHeadersService to ensure proper error handling during preset creation and updates. - Update ProxyHostForm tests to include HSTS subdomains toggle and validation for port input handling. - Enhance DNSProviders tests to validate manual challenge completion and error handling when no providers are available. - Extend UsersPage tests to ensure fallback mechanisms for clipboard operations when the clipboard API fails.
1 parent 9713908 commit 2cad49d

41 files changed

Lines changed: 4071 additions & 4 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/internal/api/handlers/certificate_handler_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,45 @@ func TestCertificateHandler_Upload_MissingKeyFile(t *testing.T) {
399399
}
400400
}
401401

402+
func TestCertificateHandler_Upload_MissingKeyFile_MultipartWithCert(t *testing.T) {
403+
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
404+
if err != nil {
405+
t.Fatalf("failed to open db: %v", err)
406+
}
407+
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
408+
t.Fatalf("failed to migrate: %v", err)
409+
}
410+
411+
gin.SetMode(gin.TestMode)
412+
r := gin.New()
413+
r.Use(mockAuthMiddleware())
414+
svc := services.NewCertificateService("/tmp", db)
415+
h := NewCertificateHandler(svc, nil, nil)
416+
r.POST("/api/certificates", h.Upload)
417+
418+
var body bytes.Buffer
419+
writer := multipart.NewWriter(&body)
420+
_ = writer.WriteField("name", "testcert")
421+
part, createErr := writer.CreateFormFile("certificate_file", "cert.pem")
422+
if createErr != nil {
423+
t.Fatalf("failed to create form file: %v", createErr)
424+
}
425+
_, _ = part.Write([]byte("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----"))
426+
_ = writer.Close()
427+
428+
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
429+
req.Header.Set("Content-Type", writer.FormDataContentType())
430+
w := httptest.NewRecorder()
431+
r.ServeHTTP(w, req)
432+
433+
if w.Code != http.StatusBadRequest {
434+
t.Fatalf("expected 400 Bad Request, got %d, body=%s", w.Code, w.Body.String())
435+
}
436+
if !strings.Contains(w.Body.String(), "key_file") {
437+
t.Fatalf("expected error message about key_file, got: %s", w.Body.String())
438+
}
439+
}
440+
402441
// Test Upload handler success path using a mock CertificateService
403442
func TestCertificateHandler_Upload_Success(t *testing.T) {
404443
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package handlers
2+
3+
import (
4+
"bytes"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestResolveAcquisitionConfigPath_Validation(t *testing.T) {
17+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "")
18+
resolved, err := resolveAcquisitionConfigPath()
19+
require.NoError(t, err)
20+
require.Equal(t, "/etc/crowdsec/acquis.yaml", resolved)
21+
22+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "relative/acquis.yaml")
23+
_, err = resolveAcquisitionConfigPath()
24+
require.Error(t, err)
25+
26+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "/tmp/../etc/acquis.yaml")
27+
_, err = resolveAcquisitionConfigPath()
28+
require.Error(t, err)
29+
30+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "/tmp/acquis.yaml")
31+
resolved, err = resolveAcquisitionConfigPath()
32+
require.NoError(t, err)
33+
require.Equal(t, "/tmp/acquis.yaml", resolved)
34+
}
35+
36+
func TestReadAcquisitionConfig_ErrorsAndSuccess(t *testing.T) {
37+
tmp := t.TempDir()
38+
path := filepath.Join(tmp, "acquis.yaml")
39+
require.NoError(t, os.WriteFile(path, []byte("source: file\n"), 0o600))
40+
41+
content, err := readAcquisitionConfig(path)
42+
require.NoError(t, err)
43+
assert.Contains(t, string(content), "source: file")
44+
45+
_, err = readAcquisitionConfig(filepath.Join(tmp, "missing.yaml"))
46+
require.Error(t, err)
47+
}
48+
49+
func TestCrowdsec_AcquisitionEndpoints_InvalidConfiguredPath(t *testing.T) {
50+
gin.SetMode(gin.TestMode)
51+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "relative/path.yaml")
52+
53+
h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir())
54+
r := gin.New()
55+
g := r.Group("/api/v1")
56+
h.RegisterRoutes(g)
57+
58+
wGet := httptest.NewRecorder()
59+
reqGet := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/acquisition", http.NoBody)
60+
r.ServeHTTP(wGet, reqGet)
61+
require.Equal(t, http.StatusInternalServerError, wGet.Code)
62+
63+
wPut := httptest.NewRecorder()
64+
reqPut := httptest.NewRequest(http.MethodPut, "/api/v1/admin/crowdsec/acquisition", bytes.NewBufferString(`{"content":"source: file"}`))
65+
reqPut.Header.Set("Content-Type", "application/json")
66+
r.ServeHTTP(wPut, reqPut)
67+
require.Equal(t, http.StatusInternalServerError, wPut.Code)
68+
}
69+
70+
func TestCrowdsec_GetBouncerKey_NotConfigured(t *testing.T) {
71+
gin.SetMode(gin.TestMode)
72+
t.Setenv("CROWDSEC_API_KEY", "")
73+
t.Setenv("CROWDSEC_BOUNCER_API_KEY", "")
74+
t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "")
75+
t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "")
76+
t.Setenv("CPM_SECURITY_CROWDSEC_API_KEY", "")
77+
78+
h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir())
79+
r := gin.New()
80+
g := r.Group("/api/v1")
81+
h.RegisterRoutes(g)
82+
83+
w := httptest.NewRecorder()
84+
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/bouncer/key", http.NoBody)
85+
r.ServeHTTP(w, req)
86+
require.Equal(t, http.StatusNotFound, w.Code)
87+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package handlers
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/url"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/Wikid82/charon/backend/internal/models"
12+
"github.com/gin-gonic/gin"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestCrowdsecWave5_ResolveAcquisitionConfigPath_RelativeRejected(t *testing.T) {
17+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", "relative/acquis.yaml")
18+
_, err := resolveAcquisitionConfigPath()
19+
require.Error(t, err)
20+
require.Contains(t, err.Error(), "must be absolute")
21+
}
22+
23+
func TestCrowdsecWave5_ReadAcquisitionConfig_InvalidFilenameBranch(t *testing.T) {
24+
_, err := readAcquisitionConfig("/")
25+
require.Error(t, err)
26+
require.Contains(t, err.Error(), "filename is invalid")
27+
}
28+
29+
func TestCrowdsecWave5_GetLAPIDecisions_Unauthorized(t *testing.T) {
30+
gin.SetMode(gin.TestMode)
31+
db := setupCrowdDB(t)
32+
tmpDir := t.TempDir()
33+
34+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35+
w.WriteHeader(http.StatusUnauthorized)
36+
}))
37+
t.Cleanup(server.Close)
38+
39+
original := validateCrowdsecLAPIBaseURLFunc
40+
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
41+
return url.Parse(raw)
42+
}
43+
t.Cleanup(func() {
44+
validateCrowdsecLAPIBaseURLFunc = original
45+
})
46+
47+
require.NoError(t, db.Create(&models.SecurityConfig{UUID: "default", CrowdSecAPIURL: server.URL}).Error)
48+
49+
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
50+
r := gin.New()
51+
g := r.Group("/api/v1")
52+
h.RegisterRoutes(g)
53+
54+
w := httptest.NewRecorder()
55+
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/lapi", http.NoBody)
56+
r.ServeHTTP(w, req)
57+
58+
require.Equal(t, http.StatusUnauthorized, w.Code)
59+
require.Contains(t, w.Body.String(), "authentication failed")
60+
}
61+
62+
func TestCrowdsecWave5_GetLAPIDecisions_NonJSONContentTypeFallsBack(t *testing.T) {
63+
gin.SetMode(gin.TestMode)
64+
db := setupCrowdDB(t)
65+
tmpDir := t.TempDir()
66+
67+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68+
w.Header().Set("Content-Type", "text/html")
69+
w.WriteHeader(http.StatusOK)
70+
_, _ = w.Write([]byte("<html>not-json</html>"))
71+
}))
72+
t.Cleanup(server.Close)
73+
74+
original := validateCrowdsecLAPIBaseURLFunc
75+
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
76+
return url.Parse(raw)
77+
}
78+
t.Cleanup(func() {
79+
validateCrowdsecLAPIBaseURLFunc = original
80+
})
81+
82+
require.NoError(t, db.Create(&models.SecurityConfig{UUID: "default", CrowdSecAPIURL: server.URL}).Error)
83+
84+
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
85+
h.CmdExec = &mockCmdExecutor{output: []byte("[]"), err: nil}
86+
r := gin.New()
87+
g := r.Group("/api/v1")
88+
h.RegisterRoutes(g)
89+
90+
w := httptest.NewRecorder()
91+
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/lapi", http.NoBody)
92+
r.ServeHTTP(w, req)
93+
94+
require.Equal(t, http.StatusOK, w.Code)
95+
require.Contains(t, w.Body.String(), "decisions")
96+
}
97+
98+
func TestCrowdsecWave5_GetBouncerInfo_And_GetBouncerKey_FileSource(t *testing.T) {
99+
gin.SetMode(gin.TestMode)
100+
t.Setenv("CROWDSEC_BOUNCER_API_KEY", "")
101+
t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "")
102+
t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "")
103+
t.Setenv("CPM_SECURITY_CROWDSEC_API_KEY", "")
104+
db := setupCrowdDB(t)
105+
tmpDir := t.TempDir()
106+
107+
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
108+
keyPath := h.bouncerKeyPath()
109+
require.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o750))
110+
require.NoError(t, os.WriteFile(keyPath, []byte("abcdefghijklmnop1234567890"), 0o600))
111+
112+
r := gin.New()
113+
g := r.Group("/api/v1")
114+
h.RegisterRoutes(g)
115+
116+
wInfo := httptest.NewRecorder()
117+
reqInfo := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/bouncer", http.NoBody)
118+
r.ServeHTTP(wInfo, reqInfo)
119+
require.Equal(t, http.StatusOK, wInfo.Code)
120+
require.Contains(t, wInfo.Body.String(), "file")
121+
122+
wKey := httptest.NewRecorder()
123+
reqKey := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/bouncer/key", http.NoBody)
124+
r.ServeHTTP(wKey, reqKey)
125+
require.Equal(t, http.StatusOK, wKey.Code)
126+
require.Contains(t, wKey.Body.String(), "\"source\":\"file\"")
127+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/gin-gonic/gin"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestCrowdsecWave6_BouncerKeyPath_UsesEnvFallback(t *testing.T) {
14+
t.Setenv("CHARON_CROWDSEC_BOUNCER_KEY_PATH", "/tmp/test-bouncer-key")
15+
h := &CrowdsecHandler{}
16+
require.Equal(t, "/tmp/test-bouncer-key", h.bouncerKeyPath())
17+
}
18+
19+
func TestCrowdsecWave6_GetBouncerInfo_NoneSource(t *testing.T) {
20+
gin.SetMode(gin.TestMode)
21+
t.Setenv("CROWDSEC_API_KEY", "")
22+
t.Setenv("CROWDSEC_BOUNCER_API_KEY", "")
23+
t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "")
24+
t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "")
25+
t.Setenv("CPM_SECURITY_CROWDSEC_API_KEY", "")
26+
t.Setenv("CHARON_CROWDSEC_BOUNCER_KEY_PATH", "/tmp/non-existent-wave6-key")
27+
28+
h := &CrowdsecHandler{CmdExec: &mockCmdExecutor{output: []byte(`[]`)}}
29+
30+
w := httptest.NewRecorder()
31+
c, _ := gin.CreateTestContext(w)
32+
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/bouncer", nil)
33+
34+
h.GetBouncerInfo(c)
35+
36+
require.Equal(t, http.StatusOK, w.Code)
37+
var payload map[string]any
38+
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
39+
require.Equal(t, "none", payload["key_source"])
40+
}
41+
42+
func TestCrowdsecWave6_GetKeyStatus_NoKeyConfiguredMessage(t *testing.T) {
43+
gin.SetMode(gin.TestMode)
44+
t.Setenv("CROWDSEC_API_KEY", "")
45+
t.Setenv("CROWDSEC_BOUNCER_API_KEY", "")
46+
t.Setenv("CERBERUS_SECURITY_CROWDSEC_API_KEY", "")
47+
t.Setenv("CHARON_SECURITY_CROWDSEC_API_KEY", "")
48+
t.Setenv("CPM_SECURITY_CROWDSEC_API_KEY", "")
49+
t.Setenv("CHARON_CROWDSEC_BOUNCER_KEY_PATH", "/tmp/non-existent-wave6-key")
50+
51+
h := &CrowdsecHandler{}
52+
53+
w := httptest.NewRecorder()
54+
c, _ := gin.CreateTestContext(w)
55+
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/key-status", nil)
56+
57+
h.GetKeyStatus(c)
58+
59+
require.Equal(t, http.StatusOK, w.Code)
60+
var payload map[string]any
61+
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
62+
require.Equal(t, "none", payload["key_source"])
63+
require.Equal(t, false, payload["valid"])
64+
require.Contains(t, payload["message"], "No CrowdSec API key configured")
65+
}

0 commit comments

Comments
 (0)