You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/stacks/stackruntime/action_invocation_hooks_val...

216 lines
6.9 KiB

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package stackruntime
import (
"testing"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
)
// TestActionInvocationHooksValidation validates that action invocation status
// hooks work correctly, including enum values, hook data structure, and lifecycle ordering.
func TestActionInvocationHooksValidation(t *testing.T) {
t.Run("hook_capture_mechanism", func(t *testing.T) {
// Verify CapturedHooks mechanism initializes correctly
capturedHooks := NewCapturedHooks(false) // false = apply phase
if capturedHooks == nil {
t.Fatal("CapturedHooks should not be nil")
}
// Verify the hooks slice starts empty (nil or zero length)
if len(capturedHooks.ReportActionInvocationStatus) != 0 {
t.Errorf("expected empty initial hook list, got %d", len(capturedHooks.ReportActionInvocationStatus))
}
// Verify we can append to it
capturedHooks.ReportActionInvocationStatus = append(
capturedHooks.ReportActionInvocationStatus,
&hooks.ActionInvocationStatusHookData{
Addr: mustAbsActionInvocationInstance("component.test.action.example.run"),
ProviderAddr: mustDefaultRootProvider("testing").Provider,
Status: hooks.ActionInvocationRunning,
},
)
if len(capturedHooks.ReportActionInvocationStatus) != 1 {
t.Errorf("after append, expected 1 hook, got %d", len(capturedHooks.ReportActionInvocationStatus))
}
})
t.Run("action_invocation_status_enum", func(t *testing.T) {
// Test that all enum constants are defined and have valid string representations
statuses := []hooks.ActionInvocationStatus{
hooks.ActionInvocationStatusInvalid,
hooks.ActionInvocationPending,
hooks.ActionInvocationRunning,
hooks.ActionInvocationCompleted,
hooks.ActionInvocationErrored,
}
expectedStrings := map[hooks.ActionInvocationStatus]string{
hooks.ActionInvocationStatusInvalid: "ActionInvocationStatusInvalid",
hooks.ActionInvocationPending: "ActionInvocationPending",
hooks.ActionInvocationRunning: "ActionInvocationRunning",
hooks.ActionInvocationCompleted: "ActionInvocationCompleted",
hooks.ActionInvocationErrored: "ActionInvocationErrored",
}
// Verify String() returns expected values
for _, status := range statuses {
str := status.String()
expected, ok := expectedStrings[status]
if !ok {
t.Errorf("unexpected status constant: %v", status)
continue
}
if str != expected {
t.Errorf("status %v: expected String() = %q, got %q", status, expected, str)
}
}
// Verify ForProtobuf() returns valid values (non-negative)
for _, status := range statuses {
proto := status.ForProtobuf()
if proto < 0 {
t.Errorf("status %v has invalid protobuf value: %v", status, proto)
}
}
// Verify we have exactly 5 status values
if len(statuses) != 5 {
t.Errorf("expected 5 status constants, got %d", len(statuses))
}
})
t.Run("hook_data_structure", func(t *testing.T) {
// Validate ActionInvocationStatusHookData structure and methods
hookData := &hooks.ActionInvocationStatusHookData{
Addr: mustAbsActionInvocationInstance("component.test.action.example.run"),
ProviderAddr: mustDefaultRootProvider("testing").Provider,
Status: hooks.ActionInvocationRunning,
}
// Verify fields are set
if hookData.Addr.String() == "" {
t.Error("Addr should not be empty")
}
if hookData.ProviderAddr.String() == "" {
t.Error("ProviderAddr should not be empty")
}
if hookData.Status == hooks.ActionInvocationStatusInvalid {
t.Error("Status should not be Invalid when explicitly set to Running")
}
// Verify String() method
str := hookData.String()
if str == "" || str == "<nil>" {
t.Errorf("String() should return valid representation, got: %q", str)
}
// Verify String() contains address
if !contains(str, "component.test") {
t.Errorf("String() should contain address, got: %q", str)
}
// Verify nil handling
var nilHook *hooks.ActionInvocationStatusHookData
if nilHook.String() != "<nil>" {
t.Errorf("nil hook String() should return <nil>, got: %q", nilHook.String())
}
})
t.Run("hook_status_lifecycle_ordering", func(t *testing.T) {
// Test expected hook status sequences for different scenarios
testCases := []struct {
name string
capturedStatuses []hooks.ActionInvocationStatus
wantValid bool
description string
}{
{
name: "successful_action",
capturedStatuses: []hooks.ActionInvocationStatus{
hooks.ActionInvocationRunning,
hooks.ActionInvocationCompleted,
},
wantValid: true,
description: "Action starts running and completes successfully",
},
{
name: "failed_action",
capturedStatuses: []hooks.ActionInvocationStatus{
hooks.ActionInvocationRunning,
hooks.ActionInvocationErrored,
},
wantValid: true,
description: "Action starts running but encounters an error",
},
{
name: "pending_then_running_then_completed",
capturedStatuses: []hooks.ActionInvocationStatus{
hooks.ActionInvocationPending,
hooks.ActionInvocationRunning,
hooks.ActionInvocationCompleted,
},
wantValid: true,
description: "Action goes through all states including pending",
},
{
name: "invalid_only_completed",
capturedStatuses: []hooks.ActionInvocationStatus{
hooks.ActionInvocationCompleted,
},
wantValid: false,
description: "Invalid: completed without running",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Verify we captured the expected number of statuses
if len(tc.capturedStatuses) == 0 {
t.Error("test case should have at least one status")
return
}
// For valid sequences, verify terminal state is at the end
if tc.wantValid && len(tc.capturedStatuses) > 0 {
lastStatus := tc.capturedStatuses[len(tc.capturedStatuses)-1]
isTerminal := lastStatus == hooks.ActionInvocationCompleted ||
lastStatus == hooks.ActionInvocationErrored
if !isTerminal {
t.Errorf("valid sequence should end in terminal state (Completed/Errored), got %v", lastStatus)
}
}
// For invalid sequences starting with Completed, verify it's actually invalid
if !tc.wantValid && len(tc.capturedStatuses) > 0 {
firstStatus := tc.capturedStatuses[0]
if firstStatus == hooks.ActionInvocationCompleted && len(tc.capturedStatuses) == 1 {
// This is indeed invalid - can't complete without running
t.Logf("correctly identified invalid sequence: %v", tc.capturedStatuses)
}
}
})
}
})
}
// contains checks if a string contains a substring
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}