- Removed redundant `gin.SetMode(gin.TestMode)` calls from individual test files. - Introduced a centralized `TestMain` function in `testmain_test.go` to set the Gin mode for all tests. - Ensured consistent test environment setup across various handler test files.
367 lines
9.3 KiB
Go
367 lines
9.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- Sprint 2: Archive Validation Tests ---
|
|
|
|
// createTestArchive creates a test archive with specified files.
|
|
// Returns the archive path.
|
|
func createTestArchive(t *testing.T, format string, files map[string]string, compressed bool) string {
|
|
t.Helper()
|
|
tmpDir := t.TempDir()
|
|
archivePath := filepath.Join(tmpDir, "test."+format)
|
|
|
|
if format == "tar.gz" {
|
|
// #nosec G304 -- archivePath is in test temp directory created by t.TempDir()
|
|
f, err := os.Create(archivePath)
|
|
require.NoError(t, err)
|
|
defer func() { _ = f.Close() }()
|
|
|
|
var w io.Writer = f
|
|
if compressed {
|
|
gw := gzip.NewWriter(f)
|
|
defer func() { _ = gw.Close() }()
|
|
w = gw
|
|
}
|
|
|
|
tw := tar.NewWriter(w)
|
|
defer func() { _ = tw.Close() }()
|
|
|
|
for name, content := range files {
|
|
hdr := &tar.Header{
|
|
Name: name,
|
|
Size: int64(len(content)),
|
|
Mode: 0o644,
|
|
}
|
|
require.NoError(t, tw.WriteHeader(hdr))
|
|
_, err := tw.Write([]byte(content))
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
return archivePath
|
|
}
|
|
|
|
// TestConfigArchiveValidator_ValidFormats tests that valid archive formats are accepted.
|
|
func TestConfigArchiveValidator_ValidFormats(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := &ConfigArchiveValidator{
|
|
MaxSize: 50 * 1024 * 1024,
|
|
MaxUncompressed: 500 * 1024 * 1024,
|
|
MaxCompressionRatio: 100,
|
|
RequiredFiles: []string{"config.yaml"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
format string
|
|
files map[string]string
|
|
}{
|
|
{
|
|
name: "valid tar.gz with config.yaml",
|
|
format: "tar.gz",
|
|
files: map[string]string{
|
|
"config.yaml": "api:\n server:\n listen_uri: 0.0.0.0:8080\n",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
archivePath := createTestArchive(t, tt.format, tt.files, true)
|
|
err := validator.Validate(archivePath)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigArchiveValidator_InvalidFormats tests rejection of invalid formats.
|
|
func TestConfigArchiveValidator_InvalidFormats(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := &ConfigArchiveValidator{
|
|
MaxSize: 50 * 1024 * 1024,
|
|
MaxUncompressed: 500 * 1024 * 1024,
|
|
MaxCompressionRatio: 100,
|
|
RequiredFiles: []string{"config.yaml"},
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
content string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "txt file",
|
|
filename: "test.txt",
|
|
content: "not an archive",
|
|
wantErr: "unsupported format",
|
|
},
|
|
{
|
|
name: "rar file",
|
|
filename: "test.rar",
|
|
content: "Rar!\x1a\x07\x00",
|
|
wantErr: "unsupported format",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := filepath.Join(tmpDir, tt.filename)
|
|
// #nosec G306 -- Test file, 0o600 not required
|
|
err := os.WriteFile(path, []byte(tt.content), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
err = validator.Validate(path)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigArchiveValidator_SizeLimit tests enforcement of size limits.
|
|
func TestConfigArchiveValidator_SizeLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := &ConfigArchiveValidator{
|
|
MaxSize: 1024, // 1KB limit for testing
|
|
MaxUncompressed: 10 * 1024,
|
|
MaxCompressionRatio: 100,
|
|
RequiredFiles: []string{"config.yaml"},
|
|
}
|
|
|
|
// Create multiple large files to exceed compressed size limit
|
|
// Use less compressible content (random-like data)
|
|
largeContent := make([]byte, 2048)
|
|
for i := range largeContent {
|
|
largeContent[i] = byte(i % 256) // Less compressible than repeated chars
|
|
}
|
|
|
|
files := map[string]string{
|
|
"config.yaml": string(largeContent),
|
|
"file2.yaml": string(largeContent),
|
|
"file3.yaml": string(largeContent),
|
|
}
|
|
|
|
archivePath := createTestArchive(t, "tar.gz", files, true)
|
|
|
|
// Verify the archive is actually larger than limit
|
|
info, err := os.Stat(archivePath)
|
|
require.NoError(t, err)
|
|
|
|
// If archive is still under limit, skip this test
|
|
if info.Size() <= validator.MaxSize {
|
|
t.Skipf("Archive size %d is under limit %d, skipping", info.Size(), validator.MaxSize)
|
|
}
|
|
|
|
err = validator.Validate(archivePath)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "exceeds maximum size")
|
|
}
|
|
|
|
// TestConfigArchiveValidator_CompressionRatio tests zip bomb protection.
|
|
func TestConfigArchiveValidator_CompressionRatio(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := &ConfigArchiveValidator{
|
|
MaxSize: 50 * 1024 * 1024,
|
|
MaxUncompressed: 500 * 1024 * 1024,
|
|
MaxCompressionRatio: 10, // Lower ratio for testing
|
|
RequiredFiles: []string{"config.yaml"},
|
|
}
|
|
|
|
// Create highly compressible content (simulating zip bomb)
|
|
highlyCompressible := strings.Repeat("AAAAAAAAAA", 10000)
|
|
files := map[string]string{
|
|
"config.yaml": highlyCompressible,
|
|
}
|
|
|
|
archivePath := createTestArchive(t, "tar.gz", files, true)
|
|
err := validator.Validate(archivePath)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "compression ratio")
|
|
}
|
|
|
|
// TestConfigArchiveValidator_RequiredFiles tests required file validation.
|
|
func TestConfigArchiveValidator_RequiredFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
validator := &ConfigArchiveValidator{
|
|
MaxSize: 50 * 1024 * 1024,
|
|
MaxUncompressed: 500 * 1024 * 1024,
|
|
MaxCompressionRatio: 100,
|
|
RequiredFiles: []string{"config.yaml"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "has required file",
|
|
files: map[string]string{
|
|
"config.yaml": "valid: true",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing required file",
|
|
files: map[string]string{
|
|
"other.yaml": "valid: true",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
archivePath := createTestArchive(t, "tar.gz", tt.files, true)
|
|
err := validator.Validate(archivePath)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "required file")
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestImportConfig_Validation tests the enhanced ImportConfig handler with validation.
|
|
func TestImportConfig_Validation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := OpenTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
|
|
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantStatus int
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "valid archive",
|
|
files: map[string]string{
|
|
"config.yaml": "api:\n server:\n listen_uri: 0.0.0.0:8080\n",
|
|
},
|
|
wantStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "missing config.yaml",
|
|
files: map[string]string{
|
|
"other.yaml": "data: test",
|
|
},
|
|
wantStatus: http.StatusUnprocessableEntity,
|
|
wantErr: "required file",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
archivePath := createTestArchive(t, "tar.gz", tt.files, true)
|
|
|
|
// Create multipart request
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
part, err := writer.CreateFormFile("file", "test.tar.gz")
|
|
require.NoError(t, err)
|
|
|
|
// #nosec G304 -- archivePath is in test temp directory
|
|
archiveData, err := os.ReadFile(archivePath)
|
|
require.NoError(t, err)
|
|
_, err = part.Write(archiveData)
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer.Close())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/crowdsec/import", body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
w := httptest.NewRecorder()
|
|
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = req
|
|
|
|
h.ImportConfig(c)
|
|
|
|
require.Equal(t, tt.wantStatus, w.Code)
|
|
if tt.wantErr != "" {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
require.Contains(t, resp["error"], tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestImportConfig_Rollback tests backup restoration on validation failure.
|
|
func TestImportConfig_Rollback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := OpenTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
|
|
|
|
// Create existing config
|
|
existingConfig := filepath.Join(tmpDir, "existing.yaml")
|
|
// #nosec G306 -- Test file, 0o600 not required
|
|
err := os.WriteFile(existingConfig, []byte("existing: true"), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
// Create invalid archive (missing config.yaml)
|
|
archivePath := createTestArchive(t, "tar.gz", map[string]string{
|
|
"invalid.yaml": "test: data",
|
|
}, true)
|
|
|
|
// Create multipart request
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
part, err := writer.CreateFormFile("file", "test.tar.gz")
|
|
require.NoError(t, err)
|
|
|
|
// #nosec G304 -- archivePath is in test temp directory
|
|
archiveData, err := os.ReadFile(archivePath)
|
|
require.NoError(t, err)
|
|
_, err = part.Write(archiveData)
|
|
require.NoError(t, err)
|
|
require.NoError(t, writer.Close())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/crowdsec/import", body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
w := httptest.NewRecorder()
|
|
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = req
|
|
|
|
h.ImportConfig(c)
|
|
|
|
// Should fail validation
|
|
require.Equal(t, http.StatusUnprocessableEntity, w.Code)
|
|
|
|
// Original config should still exist (rollback)
|
|
_, err = os.Stat(existingConfig)
|
|
require.NoError(t, err)
|
|
}
|