package automation import ( "context" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCalculateNextRun(t *testing.T) { e := &Engine{} tests := []struct { name string cron string tz string wantHour int // approximate check }{ {"daily midnight", "0 0 * * *", "UTC", 0}, {"every hour", "0 * * * *", "UTC", -1}, // any hour {"every 5 min", "*/5 * * * *", "UTC", -1}, {"invalid fallback", "0 0 * * *", "Bad/Timezone", -1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := e.calculateNextRun(tt.cron, tt.tz) require.NoError(t, err) assert.True(t, got.After(time.Now()), "next run should be in the future") if tt.wantHour >= 0 { assert.Equal(t, tt.wantHour, got.Hour()) } }) } } func TestCalculateNextRun_Invalid(t *testing.T) { e := &Engine{} _, err := e.calculateNextRun("not-a-cron", "UTC") assert.Error(t, err) } func TestExecuteAction_SendEmail(t *testing.T) { db, mock, err := sqlmock.New() require.NoError(t, err) defer db.Close() e := NewEngine(db, zerolog.New(nil)) ctx := context.Background() // Valid email action err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "send_email", "to": "test@example.com", "subject": "Hello", "body": "World", }) assert.NoError(t, err) // Missing required fields err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "send_email", "to": "test@example.com", }) assert.Error(t, err) assert.Contains(t, err.Error(), "requires") // Unknown action err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "unknown_action", }) assert.Error(t, err) assert.NoError(t, mock.ExpectationsWereMet()) } func TestExecuteAction_CreateTask(t *testing.T) { db, mock, err := sqlmock.New() require.NoError(t, err) defer db.Close() e := NewEngine(db, zerolog.New(nil)) ctx := context.Background() mock.ExpectExec("INSERT INTO boc_tickets"). WithArgs("tenant-1", "Fix bug", "user-1"). WillReturnResult(sqlmock.NewResult(1, 1)) err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "create_task", "title": "Fix bug", "assignee": "user-1", }) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) } func TestExecuteAction_UpdateRecord(t *testing.T) { db, mock, err := sqlmock.New() require.NoError(t, err) defer db.Close() e := NewEngine(db, zerolog.New(nil)) ctx := context.Background() // Valid update mock.ExpectExec("UPDATE boc_customers SET status = \\$(.+) WHERE id = \\$(.+) AND tenant_id = \\$(.+)"). WithArgs("active", "cust-1", "tenant-1"). WillReturnResult(sqlmock.NewResult(1, 1)) err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "update_record", "table": "boc_customers", "record_id": "cust-1", "field": "status", "value": "active", }) assert.NoError(t, err) // Invalid table (SQL injection attempt) err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "update_record", "table": "users; DROP TABLE boc_customers;--", "record_id": "1", "field": "name", "value": "hacked", }) assert.Error(t, err) assert.Contains(t, err.Error(), "not allowed") assert.NoError(t, mock.ExpectationsWereMet()) } func TestExecuteAction_Webhook(t *testing.T) { db, mock, err := sqlmock.New() require.NoError(t, err) defer db.Close() e := NewEngine(db, zerolog.New(nil)) ctx := context.Background() // Valid webhook (currently just logs) err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "webhook", "url": "https://example.com/webhook", }) assert.NoError(t, err) // Missing URL err = e.executeAction(ctx, "tenant-1", map[string]interface{}{ "type": "webhook", }) assert.Error(t, err) assert.NoError(t, mock.ExpectationsWereMet()) } func TestEngine_StartStop(t *testing.T) { db, _, err := sqlmock.New() require.NoError(t, err) defer db.Close() e := NewEngine(db, zerolog.New(nil)) ctx, cancel := context.WithCancel(context.Background()) e.Start(ctx) time.Sleep(50 * time.Millisecond) // Let it start // Stop should not panic cancel() e.Stop() } func TestRunReportJob(t *testing.T) { db, _, err := sqlmock.New() require.NoError(t, err) defer db.Close() e := NewEngine(db, zerolog.New(nil)) ctx := context.Background() job := ScheduledJob{ JobConfig: map[string]interface{}{"report_type": "monthly_sales"}, } output, err := e.runReportJob(ctx, job) assert.NoError(t, err) assert.Equal(t, "monthly_sales", output["report_type"]) assert.Equal(t, "generated", output["status"]) }