1140 lines
36 KiB
Go
1140 lines
36 KiB
Go
package crowdsec
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
)
|
|
|
|
type stubEnvExecutor struct {
|
|
responses []struct {
|
|
out []byte
|
|
err error
|
|
}
|
|
defaultResponse struct {
|
|
out []byte
|
|
err error
|
|
}
|
|
calls []struct {
|
|
name string
|
|
args []string
|
|
env map[string]string
|
|
}
|
|
}
|
|
|
|
func (s *stubEnvExecutor) ExecuteWithEnv(ctx context.Context, name string, args []string, env map[string]string) ([]byte, error) {
|
|
s.calls = append(s.calls, struct {
|
|
name string
|
|
args []string
|
|
env map[string]string
|
|
}{name, args, env})
|
|
|
|
if len(s.calls) <= len(s.responses) {
|
|
resp := s.responses[len(s.calls)-1]
|
|
return resp.out, resp.err
|
|
}
|
|
return s.defaultResponse.out, s.defaultResponse.err
|
|
}
|
|
|
|
func (s *stubEnvExecutor) callCount() int {
|
|
return len(s.calls)
|
|
}
|
|
|
|
func (s *stubEnvExecutor) lastArgs() []string {
|
|
if len(s.calls) == 0 {
|
|
return nil
|
|
}
|
|
return s.calls[len(s.calls)-1].args
|
|
}
|
|
|
|
func openConsoleTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
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.CrowdsecConsoleEnrollment{}))
|
|
return db
|
|
}
|
|
|
|
func TestConsoleEnrollSuccess(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{} // Default success
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "super-secret")
|
|
svc.nowFn = func() time.Time { return time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC) }
|
|
|
|
status, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "abc123def4g", Tenant: "tenant-a", AgentName: "agent-one"})
|
|
require.NoError(t, err)
|
|
// Status is pending_acceptance because user must accept enrollment on crowdsec.net
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
require.True(t, status.KeyPresent)
|
|
require.NotEmpty(t, status.CorrelationID)
|
|
|
|
// Expect 3 calls: lapi status, capi register, then console enroll
|
|
require.Equal(t, 3, exec.callCount())
|
|
require.Contains(t, exec.calls[0].args, "lapi")
|
|
require.Equal(t, []string{"capi", "register"}, exec.calls[1].args)
|
|
require.Equal(t, "abc123def4g", exec.lastArgs()[len(exec.lastArgs())-1])
|
|
|
|
var rec models.CrowdsecConsoleEnrollment
|
|
require.NoError(t, db.First(&rec).Error)
|
|
require.NotEqual(t, "abc123def4g", rec.EncryptedEnrollKey)
|
|
plain, decErr := svc.decrypt(rec.EncryptedEnrollKey)
|
|
require.NoError(t, decErr)
|
|
require.Equal(t, "abc123def4g", plain)
|
|
}
|
|
|
|
func TestConsoleEnrollFailureRedactsSecret(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: nil, err: nil}, // lapi status success
|
|
{out: nil, err: nil}, // capi register success
|
|
{out: []byte("invalid secretKEY123"), err: fmt.Errorf("bad key secretKEY123")}, // enroll failure
|
|
},
|
|
}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "redactme")
|
|
|
|
status, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "secretKEY123", Tenant: "tenant", AgentName: "agent"})
|
|
require.Error(t, err)
|
|
require.Equal(t, consoleStatusFailed, status.Status)
|
|
require.NotContains(t, status.LastError, "secretKEY123")
|
|
require.NotContains(t, err.Error(), "secretKEY123")
|
|
}
|
|
|
|
func TestConsoleEnrollIdempotentWhenAlreadyEnrolled(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
_, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "abc123def4g", Tenant: "tenant", AgentName: "agent"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
|
|
status, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "ignoredignored", Tenant: "tenant", AgentName: "agent"})
|
|
require.NoError(t, err)
|
|
// Status is pending_acceptance because user must accept enrollment on crowdsec.net
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
// Should call lapi status and capi register again, but then stop because already pending
|
|
require.Equal(t, 5, exec.callCount(), "second call should check lapi, then capi, then stop")
|
|
require.Equal(t, []string{"capi", "register"}, exec.lastArgs())
|
|
}
|
|
|
|
func TestConsoleEnrollBlockedWhenInProgress(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
rec := models.CrowdsecConsoleEnrollment{UUID: "u1", Status: consoleStatusEnrolling}
|
|
require.NoError(t, db.Create(&rec).Error)
|
|
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
status, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "abc123def4g", Tenant: "tenant", AgentName: "agent"})
|
|
require.Error(t, err)
|
|
require.Equal(t, consoleStatusEnrolling, status.Status)
|
|
// lapi status and capi register are called before status check blocks enrollment
|
|
require.Equal(t, 2, exec.callCount())
|
|
require.Contains(t, exec.calls[0].args, "lapi")
|
|
require.Contains(t, exec.calls[0].args, "status")
|
|
require.Equal(t, []string{"capi", "register"}, exec.calls[1].args)
|
|
}
|
|
|
|
func TestConsoleEnrollNormalizesFullCommand(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
status, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "sudo cscli console enroll cmj0r0uer000202lebd5luvxh", Tenant: "tenant", AgentName: "agent"})
|
|
require.NoError(t, err)
|
|
// Status is pending_acceptance because user must accept enrollment on crowdsec.net
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
require.Equal(t, "cmj0r0uer000202lebd5luvxh", exec.lastArgs()[len(exec.lastArgs())-1])
|
|
}
|
|
|
|
func TestConsoleEnrollRejectsUnsafeInput(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
_, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{EnrollmentKey: "cscli console enroll cmj0r0uer000202lebd5luvxh; rm -rf /", Tenant: "tenant", AgentName: "agent"})
|
|
require.Error(t, err)
|
|
require.Contains(t, strings.ToLower(err.Error()), "invalid enrollment key")
|
|
require.Equal(t, 0, exec.callCount())
|
|
}
|
|
|
|
func TestConsoleEnrollPassesTenantAsTags(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
req := ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
Tenant: "some-tenant-id",
|
|
AgentName: "agent-one",
|
|
}
|
|
|
|
status, err := svc.Enroll(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
|
|
// Verify that --tags tenant:X is passed to the command arguments
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
args := exec.lastArgs()
|
|
require.Contains(t, args, "--tags")
|
|
require.Contains(t, args, "tenant:some-tenant-id")
|
|
}
|
|
|
|
func TestConsoleEnrollNoTenantOmitsTags(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
// Request without tenant
|
|
req := ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "agent-one",
|
|
}
|
|
|
|
status, err := svc.Enroll(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
|
|
// Verify that --tags is NOT in the command arguments when tenant is empty
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
require.NotContains(t, exec.lastArgs(), "--tags")
|
|
}
|
|
|
|
func TestConsoleEnrollPassesForceAsOverwrite(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
req := ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "agent-one",
|
|
Force: true,
|
|
}
|
|
|
|
status, err := svc.Enroll(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
|
|
// Verify that --overwrite is passed when Force is true
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
require.Contains(t, exec.lastArgs(), "--overwrite")
|
|
}
|
|
|
|
func TestConsoleEnrollNoForceOmitsOverwrite(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
req := ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "agent-one",
|
|
Force: false,
|
|
}
|
|
|
|
status, err := svc.Enroll(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
|
|
// Verify that --overwrite is NOT in the command arguments when Force is false
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
require.NotContains(t, exec.lastArgs(), "--overwrite")
|
|
}
|
|
|
|
func TestConsoleEnrollWithTenantAndForce(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
req := ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
Tenant: "my-tenant",
|
|
AgentName: "agent-one",
|
|
Force: true,
|
|
}
|
|
|
|
status, err := svc.Enroll(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
|
|
// Verify both --tags and --overwrite are passed
|
|
require.Equal(t, 3, exec.callCount()) // lapi status + capi register + enroll
|
|
args := exec.lastArgs()
|
|
require.Contains(t, args, "--tags")
|
|
require.Contains(t, args, "tenant:my-tenant")
|
|
require.Contains(t, args, "--overwrite")
|
|
// Token should be the last argument
|
|
require.Equal(t, "abc123def4g", args[len(args)-1])
|
|
}
|
|
|
|
// ============================================
|
|
// SecureCommandExecutor Tests
|
|
// ============================================
|
|
|
|
func TestSecureCommandExecutorExecuteWithEnv(t *testing.T) {
|
|
// Skip this test if not on a system with echo command
|
|
exec := &SecureCommandExecutor{}
|
|
ctx := context.Background()
|
|
|
|
t.Run("executes command successfully", func(t *testing.T) {
|
|
output, err := exec.ExecuteWithEnv(ctx, "echo", []string{"hello"}, nil)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "hello")
|
|
})
|
|
|
|
t.Run("passes environment variables", func(t *testing.T) {
|
|
output, err := exec.ExecuteWithEnv(ctx, "sh", []string{"-c", "echo $TEST_VAR"}, map[string]string{"TEST_VAR": "test_value"})
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "test_value")
|
|
})
|
|
|
|
t.Run("handles empty env map", func(t *testing.T) {
|
|
output, err := exec.ExecuteWithEnv(ctx, "echo", []string{"no-env"}, map[string]string{})
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(output), "no-env")
|
|
})
|
|
|
|
t.Run("handles command failure", func(t *testing.T) {
|
|
_, err := exec.ExecuteWithEnv(ctx, "false", nil, nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("handles context timeout", func(t *testing.T) {
|
|
ctxTimeout, cancel := context.WithTimeout(ctx, time.Nanosecond)
|
|
defer cancel()
|
|
time.Sleep(time.Millisecond) // ensure timeout
|
|
|
|
_, err := exec.ExecuteWithEnv(ctxTimeout, "sleep", []string{"10"}, nil)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// formatEnv Tests
|
|
// ============================================
|
|
|
|
func TestFormatEnv(t *testing.T) {
|
|
t.Run("formats single env var", func(t *testing.T) {
|
|
result := formatEnv(map[string]string{"KEY": "value"})
|
|
require.Len(t, result, 1)
|
|
require.Equal(t, "KEY=value", result[0])
|
|
})
|
|
|
|
t.Run("formats multiple env vars", func(t *testing.T) {
|
|
result := formatEnv(map[string]string{
|
|
"KEY1": "value1",
|
|
"KEY2": "value2",
|
|
})
|
|
require.Len(t, result, 2)
|
|
require.Contains(t, result, "KEY1=value1")
|
|
require.Contains(t, result, "KEY2=value2")
|
|
})
|
|
|
|
t.Run("handles empty map", func(t *testing.T) {
|
|
result := formatEnv(map[string]string{})
|
|
require.Nil(t, result)
|
|
})
|
|
|
|
t.Run("handles nil map", func(t *testing.T) {
|
|
result := formatEnv(nil)
|
|
require.Nil(t, result)
|
|
})
|
|
|
|
t.Run("handles special characters", func(t *testing.T) {
|
|
result := formatEnv(map[string]string{"KEY": "value with spaces"})
|
|
require.Len(t, result, 1)
|
|
require.Equal(t, "KEY=value with spaces", result[0])
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// ConsoleEnrollmentService.Status Tests
|
|
// ============================================
|
|
|
|
func TestConsoleEnrollmentStatus(t *testing.T) {
|
|
t.Run("returns not_enrolled for new service", func(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
status, err := svc.Status(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusNotEnrolled, status.Status)
|
|
})
|
|
|
|
t.Run("returns pending_acceptance status after enrollment", func(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
// First enroll
|
|
_, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "test-agent",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Then check status - should be pending_acceptance until user accepts on crowdsec.net
|
|
status, err := svc.Status(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
require.Equal(t, "test-agent", status.AgentName)
|
|
require.True(t, status.KeyPresent)
|
|
// EnrolledAt is nil because user hasn't accepted on crowdsec.net yet
|
|
require.Nil(t, status.EnrolledAt)
|
|
// LastAttemptAt should be set to when the enrollment request was sent
|
|
require.NotNil(t, status.LastAttemptAt)
|
|
})
|
|
|
|
t.Run("returns failed status after failed enrollment", func(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: nil, err: nil}, // lapi status success
|
|
{out: nil, err: nil}, // capi register success
|
|
{out: []byte("error"), err: fmt.Errorf("enroll failed")}, // enroll failure
|
|
},
|
|
}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
// Attempt enrollment (will fail)
|
|
_, _ = svc.Enroll(context.Background(), ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "test-agent",
|
|
})
|
|
|
|
// Check status
|
|
status, err := svc.Status(context.Background())
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusFailed, status.Status)
|
|
require.NotEmpty(t, status.LastError)
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// deriveKey Tests
|
|
// ============================================
|
|
|
|
func TestDeriveKey(t *testing.T) {
|
|
t.Run("derives consistent key", func(t *testing.T) {
|
|
key1 := deriveKey("secret")
|
|
key2 := deriveKey("secret")
|
|
require.Equal(t, key1, key2)
|
|
})
|
|
|
|
t.Run("derives different keys for different secrets", func(t *testing.T) {
|
|
key1 := deriveKey("secret1")
|
|
key2 := deriveKey("secret2")
|
|
require.NotEqual(t, key1, key2)
|
|
})
|
|
|
|
t.Run("uses default for empty secret", func(t *testing.T) {
|
|
key := deriveKey("")
|
|
require.Len(t, key, 32) // SHA-256 output
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// normalizeEnrollmentKey Tests
|
|
// ============================================
|
|
|
|
func TestNormalizeEnrollmentKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "valid raw key",
|
|
input: "abc123def4g",
|
|
expected: "abc123def4g",
|
|
},
|
|
{
|
|
name: "full command with sudo",
|
|
input: "sudo cscli console enroll abc123def4g",
|
|
expected: "abc123def4g",
|
|
},
|
|
{
|
|
name: "full command without sudo",
|
|
input: "cscli console enroll abc123def4g",
|
|
expected: "abc123def4g",
|
|
},
|
|
{
|
|
name: "key with whitespace",
|
|
input: " abc123def4g ",
|
|
expected: "abc123def4g",
|
|
},
|
|
{
|
|
name: "empty key",
|
|
input: "",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "only whitespace",
|
|
input: " ",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "invalid format",
|
|
input: "invalid key format here",
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "injection attempt",
|
|
input: "abc123; rm -rf /",
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, err := normalizeEnrollmentKey(tc.input)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// redactSecret Tests
|
|
// ============================================
|
|
|
|
func TestRedactSecret(t *testing.T) {
|
|
t.Run("redacts secret from message", func(t *testing.T) {
|
|
result := redactSecret("Error: invalid token secretKEY123", "secretKEY123")
|
|
require.Equal(t, "Error: invalid token <redacted>", result)
|
|
})
|
|
|
|
t.Run("handles empty secret", func(t *testing.T) {
|
|
result := redactSecret("Error message", "")
|
|
require.Equal(t, "Error message", result)
|
|
})
|
|
|
|
t.Run("handles secret not in message", func(t *testing.T) {
|
|
result := redactSecret("Error message", "secret")
|
|
require.Equal(t, "Error message", result)
|
|
})
|
|
|
|
t.Run("redacts multiple occurrences", func(t *testing.T) {
|
|
result := redactSecret("Token ABC123 failed, please retry with ABC123", "ABC123")
|
|
require.Equal(t, "Token <redacted> failed, please retry with <redacted>", result)
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// extractCscliErrorMessage Tests
|
|
// ============================================
|
|
|
|
func TestExtractCscliErrorMessage(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "msg format with quotes",
|
|
input: `level=error msg="the attachment key provided is not valid (hint: get your enrollement key from console...)"`,
|
|
expected: "the attachment key provided is not valid (hint: get your enrollement key from console...)",
|
|
},
|
|
{
|
|
name: "ERRO format with timestamp",
|
|
input: `ERRO[2024-01-15T10:30:00Z] unable to enroll: API returned error code 401`,
|
|
expected: "unable to enroll: API returned error code 401",
|
|
},
|
|
{
|
|
name: "plain error message",
|
|
input: "error: invalid enrollment token",
|
|
expected: "error: invalid enrollment token",
|
|
},
|
|
{
|
|
name: "multiline with error in middle",
|
|
input: "INFO[2024-01-15] Starting enrollment...\nERRO[2024-01-15] enrollment failed: bad token\nINFO[2024-01-15] Cleanup complete",
|
|
expected: "enrollment failed: bad token",
|
|
},
|
|
{
|
|
name: "empty output",
|
|
input: "",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "whitespace only",
|
|
input: " \n\t ",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "no recognizable pattern - returns first line",
|
|
input: "Something went wrong\nMore details here",
|
|
expected: "Something went wrong",
|
|
},
|
|
{
|
|
name: "failed keyword detection",
|
|
input: "Operation failed due to network timeout",
|
|
expected: "Operation failed due to network timeout",
|
|
},
|
|
{
|
|
name: "invalid keyword detection",
|
|
input: "The token is invalid",
|
|
expected: "The token is invalid",
|
|
},
|
|
{
|
|
name: "complex cscli output with msg",
|
|
input: `time="2024-01-15T10:30:00Z" level=fatal msg="unable to configure hub: while syncing hub: creating hub index: failed to read index file: open /etc/crowdsec/hub/.index.json: no such file or directory"`,
|
|
expected: "unable to configure hub: while syncing hub: creating hub index: failed to read index file: open /etc/crowdsec/hub/.index.json: no such file or directory",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := extractCscliErrorMessage(tc.input)
|
|
require.Equal(t, tc.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Encryption Tests
|
|
// ============================================
|
|
|
|
func TestEncryptDecrypt(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
svc := NewConsoleEnrollmentService(db, &stubEnvExecutor{}, t.TempDir(), "test-secret")
|
|
|
|
t.Run("encrypts and decrypts successfully", func(t *testing.T) {
|
|
original := "sensitive-enrollment-key"
|
|
encrypted, err := svc.encrypt(original)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, original, encrypted)
|
|
|
|
decrypted, err := svc.decrypt(encrypted)
|
|
require.NoError(t, err)
|
|
require.Equal(t, original, decrypted)
|
|
})
|
|
|
|
t.Run("handles empty string", func(t *testing.T) {
|
|
encrypted, err := svc.encrypt("")
|
|
require.NoError(t, err)
|
|
require.Empty(t, encrypted)
|
|
|
|
decrypted, err := svc.decrypt("")
|
|
require.NoError(t, err)
|
|
require.Empty(t, decrypted)
|
|
})
|
|
|
|
t.Run("different encryptions produce different ciphertext", func(t *testing.T) {
|
|
original := "test-key"
|
|
encrypted1, _ := svc.encrypt(original)
|
|
encrypted2, _ := svc.encrypt(original)
|
|
require.NotEqual(t, encrypted1, encrypted2, "encryptions should use different nonces")
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// LAPI Availability Check Retry Tests
|
|
// ============================================
|
|
|
|
// TestCheckLAPIAvailable_Retries verifies that checkLAPIAvailable retries 3 times with delays.
|
|
func TestCheckLAPIAvailable_Retries(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: nil, err: fmt.Errorf("connection refused")}, // Attempt 1: fail
|
|
{out: nil, err: fmt.Errorf("connection refused")}, // Attempt 2: fail
|
|
{out: []byte("ok"), err: nil}, // Attempt 3: success
|
|
},
|
|
}
|
|
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
// Track start time to verify delays
|
|
start := time.Now()
|
|
err := svc.checkLAPIAvailable(context.Background())
|
|
elapsed := time.Since(start)
|
|
|
|
require.NoError(t, err, "should succeed on 3rd attempt")
|
|
require.Equal(t, 3, exec.callCount(), "should make 3 attempts")
|
|
|
|
// Verify delays were applied (should be at least 4 seconds: 2s + 2s delays)
|
|
require.GreaterOrEqual(t, elapsed, 4*time.Second, "should wait at least 4 seconds with 2 retries")
|
|
|
|
// Verify all calls were lapi status checks
|
|
for _, call := range exec.calls {
|
|
require.Contains(t, call.args, "lapi")
|
|
require.Contains(t, call.args, "status")
|
|
}
|
|
}
|
|
|
|
// TestCheckLAPIAvailable_RetriesExhausted verifies proper error message when all retries fail.
|
|
func TestCheckLAPIAvailable_RetriesExhausted(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: nil, err: fmt.Errorf("connection refused")}, // Attempt 1: fail
|
|
{out: nil, err: fmt.Errorf("connection refused")}, // Attempt 2: fail
|
|
{out: nil, err: fmt.Errorf("connection refused")}, // Attempt 3: fail
|
|
},
|
|
}
|
|
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
err := svc.checkLAPIAvailable(context.Background())
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "after 3 attempts")
|
|
require.Contains(t, err.Error(), "5-10 seconds")
|
|
require.Equal(t, 3, exec.callCount(), "should make exactly 3 attempts")
|
|
}
|
|
|
|
// TestCheckLAPIAvailable_FirstAttemptSuccess verifies no retries when LAPI is immediately available.
|
|
func TestCheckLAPIAvailable_FirstAttemptSuccess(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: []byte("ok"), err: nil}, // Attempt 1: success
|
|
},
|
|
}
|
|
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
start := time.Now()
|
|
err := svc.checkLAPIAvailable(context.Background())
|
|
elapsed := time.Since(start)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, exec.callCount(), "should make only 1 attempt")
|
|
|
|
// Should complete quickly without delays
|
|
require.Less(t, elapsed, 1*time.Second, "should complete immediately")
|
|
}
|
|
|
|
// ============================================
|
|
// LAPI Availability Check Tests
|
|
// ============================================
|
|
|
|
// TestEnroll_RequiresLAPI verifies that enrollment fails with proper error when LAPI is not running.
|
|
// This ensures users get clear feedback to enable CrowdSec via GUI before attempting enrollment.
|
|
func TestEnroll_RequiresLAPI(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: nil, err: fmt.Errorf("dial tcp 127.0.0.1:8085: connection refused")}, // lapi status fails - attempt 1
|
|
{out: nil, err: fmt.Errorf("dial tcp 127.0.0.1:8085: connection refused")}, // lapi status fails - attempt 2
|
|
{out: nil, err: fmt.Errorf("dial tcp 127.0.0.1:8085: connection refused")}, // lapi status fails - attempt 3
|
|
},
|
|
}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
_, err := svc.Enroll(context.Background(), ConsoleEnrollRequest{
|
|
EnrollmentKey: "test123token",
|
|
AgentName: "agent",
|
|
})
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Local API is not running")
|
|
require.Contains(t, err.Error(), "after 3 attempts")
|
|
|
|
// Verify that we retried lapi status check 3 times
|
|
require.Equal(t, 3, exec.callCount())
|
|
require.Contains(t, exec.calls[0].args, "lapi")
|
|
require.Contains(t, exec.calls[0].args, "status")
|
|
}
|
|
|
|
// ============================================
|
|
// ClearEnrollment Tests
|
|
// ============================================
|
|
|
|
func TestConsoleEnrollService_ClearEnrollment(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// Create an enrollment record
|
|
rec := &models.CrowdsecConsoleEnrollment{
|
|
UUID: "test-uuid",
|
|
Status: "enrolled",
|
|
AgentName: "test-agent",
|
|
Tenant: "test-tenant",
|
|
}
|
|
require.NoError(t, db.Create(rec).Error)
|
|
|
|
// Verify record exists
|
|
var countBefore int64
|
|
db.Model(&models.CrowdsecConsoleEnrollment{}).Count(&countBefore)
|
|
require.Equal(t, int64(1), countBefore)
|
|
|
|
// Clear it
|
|
err := svc.ClearEnrollment(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify it's gone
|
|
var countAfter int64
|
|
db.Model(&models.CrowdsecConsoleEnrollment{}).Count(&countAfter)
|
|
assert.Equal(t, int64(0), countAfter)
|
|
}
|
|
|
|
func TestConsoleEnrollService_ClearEnrollment_NoRecord(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// Should not error when no record exists
|
|
err := svc.ClearEnrollment(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestConsoleEnrollService_ClearEnrollment_NilDB(t *testing.T) {
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(nil, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// Should error when DB is nil
|
|
err := svc.ClearEnrollment(ctx)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "database not initialized")
|
|
}
|
|
|
|
func TestConsoleEnrollService_ClearEnrollment_ThenReenroll(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// First enrollment
|
|
_, err := svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "agent-one",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify enrolled
|
|
status, err := svc.Status(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
|
|
// Clear enrollment
|
|
err = svc.ClearEnrollment(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify status is now not_enrolled (new record will be created on next Status call)
|
|
status, err = svc.Status(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusNotEnrolled, status.Status)
|
|
|
|
// Re-enroll with new key should work without force
|
|
_, err = svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "newkey12345",
|
|
AgentName: "agent-two",
|
|
Force: false, // Force NOT required after clear
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Verify new enrollment
|
|
status, err = svc.Status(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
require.Equal(t, "agent-two", status.AgentName)
|
|
}
|
|
|
|
// ============================================
|
|
// Logging When Skipped Tests
|
|
// ============================================
|
|
|
|
func TestConsoleEnrollService_LogsWhenSkipped(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
|
|
// Use a test logger that captures output
|
|
logger := logrus.New()
|
|
var logBuf bytes.Buffer
|
|
logger.SetOutput(&logBuf)
|
|
logger.SetLevel(logrus.InfoLevel)
|
|
logger.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true})
|
|
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// Create an existing enrollment
|
|
rec := &models.CrowdsecConsoleEnrollment{
|
|
UUID: "test-uuid",
|
|
Status: "enrolled",
|
|
AgentName: "test-agent",
|
|
Tenant: "test-tenant",
|
|
}
|
|
require.NoError(t, db.Create(rec).Error)
|
|
|
|
// Try to enroll without force - this should be skipped
|
|
status, err := svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "newkey12345",
|
|
AgentName: "new-agent",
|
|
Force: false,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Enrollment should be skipped - status remains enrolled
|
|
require.Equal(t, "enrolled", status.Status)
|
|
|
|
// The actual logging is done via the logger package, which uses a global logger.
|
|
// We can't easily capture that here without modifying the package.
|
|
// Instead, we verify the behavior is correct by checking exec.callCount()
|
|
// - if skipped properly, we should see lapi + capi calls but NO enroll call
|
|
require.Equal(t, 2, exec.callCount(), "should only call lapi status and capi register, not enroll")
|
|
}
|
|
|
|
func TestConsoleEnrollService_LogsWhenSkipped_PendingAcceptance(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// Create an existing enrollment with pending_acceptance status
|
|
rec := &models.CrowdsecConsoleEnrollment{
|
|
UUID: "test-uuid",
|
|
Status: consoleStatusPendingAcceptance,
|
|
AgentName: "test-agent",
|
|
Tenant: "test-tenant",
|
|
}
|
|
require.NoError(t, db.Create(rec).Error)
|
|
|
|
// Try to enroll without force - this should also be skipped
|
|
status, err := svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "newkey12345",
|
|
AgentName: "new-agent",
|
|
Force: false,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Enrollment should be skipped - status remains pending_acceptance
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
require.Equal(t, 2, exec.callCount(), "should only call lapi status and capi register, not enroll")
|
|
}
|
|
|
|
func TestConsoleEnrollService_ForceOverridesSkip(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "test-secret")
|
|
ctx := context.Background()
|
|
|
|
// Create an existing enrollment
|
|
rec := &models.CrowdsecConsoleEnrollment{
|
|
UUID: "test-uuid",
|
|
Status: "enrolled",
|
|
AgentName: "test-agent",
|
|
Tenant: "test-tenant",
|
|
}
|
|
require.NoError(t, db.Create(rec).Error)
|
|
|
|
// Try to enroll WITH force - this should NOT be skipped
|
|
status, err := svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "newkey12345",
|
|
AgentName: "new-agent",
|
|
Force: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Force enrollment should proceed - status becomes pending_acceptance
|
|
require.Equal(t, consoleStatusPendingAcceptance, status.Status)
|
|
require.Equal(t, "new-agent", status.AgentName)
|
|
require.Equal(t, 3, exec.callCount(), "should call lapi status, capi register, AND enroll")
|
|
}
|
|
|
|
// ============================================
|
|
// Phase 2: Missing Coverage Tests
|
|
// ============================================
|
|
|
|
// TestEnroll_InvalidAgentNameCharacters tests Lines 117-119
|
|
func TestEnroll_InvalidAgentNameCharacters(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
ctx := context.Background()
|
|
|
|
_, err := svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "agent@name!",
|
|
})
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "may only include letters, numbers, dot, dash, underscore")
|
|
require.Equal(t, 0, exec.callCount(), "should not call any commands when validation fails")
|
|
}
|
|
|
|
// TestEnroll_InvalidTenantNameCharacters tests Lines 121-123
|
|
func TestEnroll_InvalidTenantNameCharacters(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
ctx := context.Background()
|
|
|
|
_, err := svc.Enroll(ctx, ConsoleEnrollRequest{
|
|
EnrollmentKey: "abc123def4g",
|
|
AgentName: "valid-agent",
|
|
Tenant: "tenant$invalid",
|
|
})
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "may only include letters, numbers, dot, dash, underscore")
|
|
require.Equal(t, 0, exec.callCount(), "should not call any commands when validation fails")
|
|
}
|
|
|
|
// TestEnsureCAPIRegistered_StandardLayoutExists tests Lines 198-201
|
|
func TestEnsureCAPIRegistered_StandardLayoutExists(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create config directory with credentials file (standard layout)
|
|
configDir := filepath.Join(tmpDir, "config")
|
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
|
credsPath := filepath.Join(configDir, "online_api_credentials.yaml")
|
|
require.NoError(t, os.WriteFile(credsPath, []byte("url: https://api.crowdsec.net\nlogin: test"), 0o644))
|
|
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, tmpDir, "secret")
|
|
ctx := context.Background()
|
|
|
|
err := svc.ensureCAPIRegistered(ctx)
|
|
require.NoError(t, err)
|
|
// Should not call capi register because credentials file exists
|
|
require.Equal(t, 0, exec.callCount())
|
|
}
|
|
|
|
// TestEnsureCAPIRegistered_RegisterError tests Lines 212-214
|
|
func TestEnsureCAPIRegistered_RegisterError(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
exec := &stubEnvExecutor{
|
|
responses: []struct {
|
|
out []byte
|
|
err error
|
|
}{
|
|
{out: []byte("registration failed: network error"), err: fmt.Errorf("exit status 1")},
|
|
},
|
|
}
|
|
svc := NewConsoleEnrollmentService(db, exec, tmpDir, "secret")
|
|
ctx := context.Background()
|
|
|
|
err := svc.ensureCAPIRegistered(ctx)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "capi register")
|
|
require.Contains(t, err.Error(), "registration failed")
|
|
require.Equal(t, 1, exec.callCount())
|
|
}
|
|
|
|
// TestFindConfigPath_StandardLayout tests Lines 218-222 (standard path)
|
|
func TestFindConfigPath_StandardLayout(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create config directory with config.yaml (standard layout)
|
|
configDir := filepath.Join(tmpDir, "config")
|
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
|
configPath := filepath.Join(configDir, "config.yaml")
|
|
require.NoError(t, os.WriteFile(configPath, []byte("common:\n daemonize: false"), 0o644))
|
|
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, tmpDir, "secret")
|
|
|
|
result := svc.findConfigPath()
|
|
require.Equal(t, configPath, result)
|
|
}
|
|
|
|
// TestFindConfigPath_RootLayout tests Lines 218-222 (fallback path)
|
|
func TestFindConfigPath_RootLayout(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create config.yaml in root (not in config/ subdirectory)
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
require.NoError(t, os.WriteFile(configPath, []byte("common:\n daemonize: false"), 0644))
|
|
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, tmpDir, "secret")
|
|
|
|
result := svc.findConfigPath()
|
|
require.Equal(t, configPath, result)
|
|
}
|
|
|
|
// TestFindConfigPath_NeitherExists tests Lines 218-222 (empty string return)
|
|
func TestFindConfigPath_NeitherExists(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
tmpDir := t.TempDir()
|
|
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, tmpDir, "secret")
|
|
|
|
result := svc.findConfigPath()
|
|
require.Equal(t, "", result, "should return empty string when no config file exists")
|
|
}
|
|
|
|
// TestStatusFromModel_NilModel tests Lines 268-270
|
|
func TestStatusFromModel_NilModel(t *testing.T) {
|
|
db := openConsoleTestDB(t)
|
|
exec := &stubEnvExecutor{}
|
|
svc := NewConsoleEnrollmentService(db, exec, t.TempDir(), "secret")
|
|
|
|
status := svc.statusFromModel(nil)
|
|
require.Equal(t, consoleStatusNotEnrolled, status.Status)
|
|
require.False(t, status.KeyPresent)
|
|
require.Empty(t, status.AgentName)
|
|
}
|
|
|
|
// TestNormalizeEnrollmentKey_InvalidFormat tests Lines 374-376
|
|
func TestNormalizeEnrollmentKey_InvalidCharacters(t *testing.T) {
|
|
_, err := normalizeEnrollmentKey("abc@123#def")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid enrollment key")
|
|
}
|
|
|
|
func TestNormalizeEnrollmentKey_TooShort(t *testing.T) {
|
|
_, err := normalizeEnrollmentKey("ab123")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid enrollment key")
|
|
}
|
|
|
|
func TestNormalizeEnrollmentKey_NonMatchingFormat(t *testing.T) {
|
|
_, err := normalizeEnrollmentKey("this is not a valid key format")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid enrollment key")
|
|
}
|