diff --git a/internal/api/app_test.go b/internal/api/app_test.go index 873f7b49..ee69a382 100644 --- a/internal/api/app_test.go +++ b/internal/api/app_test.go @@ -593,3 +593,51 @@ func TestClient_DeveloperAppInstall_RequestAppApproval(t *testing.T) { }) } } + +func TestClient_GetAppStatus_Ok(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appStatusMethod, + Response: `{"ok":true,"apps":[{"app_id":"A123","status":"installed"}]}`, + }) + defer teardown() + result, err := c.GetAppStatus(ctx, "token", []string{"A123"}, "T123") + require.NoError(t, err) + require.NotNil(t, result) +} + +func TestClient_GetAppStatus_Error(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appStatusMethod, + Response: `{"ok":false,"error":"invalid_app"}`, + }) + defer teardown() + _, err := c.GetAppStatus(ctx, "token", []string{"A123"}, "T123") + require.Error(t, err) + require.Contains(t, err.Error(), "invalid_app") +} + +func TestClient_ConnectionsOpen_Ok(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appConnectionsOpenMethod, + Response: `{"ok":true,"url":"wss://example.com/ws"}`, + }) + defer teardown() + result, err := c.ConnectionsOpen(ctx, "token") + require.NoError(t, err) + require.Equal(t, "wss://example.com/ws", result.URL) +} + +func TestClient_ConnectionsOpen_Error(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appConnectionsOpenMethod, + Response: `{"ok":false,"error":"token_revoked"}`, + }) + defer teardown() + _, err := c.ConnectionsOpen(ctx, "token") + require.Error(t, err) + require.Contains(t, err.Error(), "token_revoked") +} diff --git a/internal/iostreams/writer_test.go b/internal/iostreams/writer_test.go index b8e270ea..7c41b1ec 100644 --- a/internal/iostreams/writer_test.go +++ b/internal/iostreams/writer_test.go @@ -251,6 +251,24 @@ func Test_WriteIndent(t *testing.T) { } } +func Test_WriteOut(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + cfg := config.NewConfig(fsMock, osMock) + io := NewIOStreams(cfg, fsMock, osMock) + w := io.WriteOut() + require.NotNil(t, w) +} + +func Test_WriteErr(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + cfg := config.NewConfig(fsMock, osMock) + io := NewIOStreams(cfg, fsMock, osMock) + w := io.WriteErr() + require.NotNil(t, w) +} + func Test_WriteSecondary(t *testing.T) { tests := map[string]struct { input string diff --git a/internal/pkg/platform/activity.go b/internal/pkg/platform/activity.go index 582acc32..c6846240 100644 --- a/internal/pkg/platform/activity.go +++ b/internal/pkg/platform/activity.go @@ -218,9 +218,9 @@ func prettifyActivity(activity api.Activity) (log string) { switch activity.Level { case types.WARN: - return style.Yellow(msg) + return style.Styler().Yellow(msg).String() case types.ERROR, types.FATAL: - return style.Red(msg) + return style.Styler().Red(msg).String() } return msg @@ -280,7 +280,7 @@ func externalAuthResultToString(activity api.Activity) (result string) { msg = msg + "\n\t\t" + strings.ReplaceAll(activity.Payload["extra_message"].(string), "\n", "\n\t\t") } - return style.Gray(msg) + return style.Styler().Gray(13, msg).String() } func externalAuthStartedToString(activity api.Activity) (result string) { @@ -298,7 +298,7 @@ func externalAuthStartedToString(activity api.Activity) (result string) { msg = msg + "\n\t" + strings.ReplaceAll(activity.Payload["code"].(string), "\n", "\n\t") } - return style.Gray(msg) + return style.Styler().Gray(13, msg).String() } func externalAuthTokenFetchResult(activity api.Activity) (result string) { @@ -316,13 +316,13 @@ func externalAuthTokenFetchResult(activity api.Activity) (result string) { msg = msg + "\n\t" + strings.ReplaceAll(activity.Payload["code"].(string), "\n", "\n\t") } - return style.Gray(msg) + return style.Styler().Gray(13, msg).String() } func functionDeploymentToString(activity api.Activity) (result string) { - msg := fmt.Sprintf("Application %sd by user '%s' on team '%s'", activity.Payload["action"], activity.Payload["user_id"], activity.Payload["team_id"]) + msg := fmt.Sprintf("Application %sed by user '%s' on team '%s'", activity.Payload["action"], activity.Payload["user_id"], activity.Payload["team_id"]) msg = fmt.Sprintf("%s %s [%s] %s", style.Emoji("cloud"), activity.CreatedPretty(), activity.Level, msg) - return style.Gray(msg) + return style.Styler().Gray(13, msg).String() } func functionExecutionOutputToString(activity api.Activity) (result string) { diff --git a/internal/pkg/platform/activity_test.go b/internal/pkg/platform/activity_test.go index e8542309..74cf44f3 100644 --- a/internal/pkg/platform/activity_test.go +++ b/internal/pkg/platform/activity_test.go @@ -62,7 +62,7 @@ func Test_prettifyActivity(t *testing.T) { Source: "slack", ComponentType: "new_thing", ComponentID: "a789", - Payload: map[string]interface{}{ + Payload: map[string]any{ "some": "data", }, Created: 1686939542, @@ -74,50 +74,299 @@ func Test_prettifyActivity(t *testing.T) { `{"some":"data"}`, }, }, - "warn level activity should contain the message": { + "warn level should be styled": { activity: api.Activity{ - TraceID: "w123", - Level: types.WARN, - EventType: "unknown", - ComponentID: "w789", - Payload: map[string]interface{}{ - "some": "warning", + Level: types.WARN, + EventType: "unknown", + }, + expectedResults: []string{}, + }, + "error level should be styled": { + activity: api.Activity{ + Level: types.ERROR, + EventType: "unknown", + }, + expectedResults: []string{}, + }, + "fatal level should be styled": { + activity: api.Activity{ + Level: types.FATAL, + EventType: "unknown", + }, + expectedResults: []string{}, + }, + "datastore_request_result event": { + activity: api.Activity{ + EventType: types.DatastoreRequestResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "request_type": "get", + "datastore_name": "MyDS", + "details": "id: 123", }, - Created: 1686939542, }, - expectedResults: []string{ - `{"some":"warning"}`, + expectedResults: []string{"get", "MyDS", "succeeded"}, + }, + "external_auth_missing_function event": { + activity: api.Activity{ + EventType: types.ExternalAuthMissingFunction, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "function_id": "fn1", + }, }, + expectedResults: []string{"fn1", "missing"}, }, - "error level activity should contain the message": { + "external_auth_result event": { activity: api.Activity{ - TraceID: "e123", - Level: types.ERROR, - EventType: "unknown", - ComponentID: "e789", - Payload: map[string]interface{}{ - "some": "error", + EventType: types.ExternalAuthResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "user_id": "U1", + "team_id": "T1", + "app_id": "A1", + "provider_key": "google", }, - Created: 1686939542, }, - expectedResults: []string{ - `{"some":"error"}`, + expectedResults: []string{"U1", "T1"}, + }, + "external_auth_started event": { + activity: api.Activity{ + EventType: types.ExternalAuthStarted, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "user_id": "U1", + "team_id": "T1", + "app_id": "A1", + "provider_key": "google", + }, }, + expectedResults: []string{"U1", "T1"}, }, - "fatal level activity should contain the message": { + "external_auth_token_fetch_result event": { activity: api.Activity{ - TraceID: "f123", - Level: types.FATAL, - EventType: "unknown", - ComponentID: "f789", - Payload: map[string]interface{}{ - "some": "fatal", + EventType: types.ExternalAuthTokenFetchResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "user_id": "U1", + "team_id": "T1", + "app_id": "A1", + "provider_key": "google", }, - Created: 1686939542, }, - expectedResults: []string{ - `{"some":"fatal"}`, + expectedResults: []string{"U1", "T1"}, + }, + "function_deployment event": { + activity: api.Activity{ + EventType: types.FunctionDeployment, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "user_id": "U1", + "team_id": "T1", + "action": "deploy", + }, + }, + expectedResults: []string{"U1", "T1"}, + }, + "function_execution_output event": { + activity: api.Activity{ + EventType: types.FunctionExecutionOutput, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "log": "some output", + }, + }, + expectedResults: []string{"some output"}, + }, + "function_execution_result event": { + activity: api.Activity{ + EventType: types.FunctionExecutionResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "function_name": "MyFunc", + "function_type": "custom", + }, + }, + expectedResults: []string{"MyFunc", "completed"}, + }, + "function_execution_started event": { + activity: api.Activity{ + EventType: types.FunctionExecutionStarted, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "function_name": "MyFunc", + "function_type": "custom", + }, + }, + expectedResults: []string{"MyFunc", "started"}, + }, + "trigger_executed event": { + activity: api.Activity{ + EventType: types.TriggerExecuted, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "function_name": "MyFunc", + }, + }, + expectedResults: []string{"MyFunc"}, + }, + "trigger_payload_received event": { + activity: api.Activity{ + EventType: types.TriggerPayloadReceived, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "log": "payload data", + }, + }, + expectedResults: []string{"payload data"}, + }, + "workflow_billing_result event": { + activity: api.Activity{ + EventType: types.WorkflowBillingResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "workflow_name": "WF1", + "is_billing_result": true, + "billing_reason": "function_execution", + }, + }, + expectedResults: []string{"WF1"}, + }, + "workflow_bot_invited event": { + activity: api.Activity{ + EventType: types.WorkflowBotInvited, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "channel_id": "C1", + "bot_user_id": "B1", + }, + }, + expectedResults: []string{"C1", "B1"}, + }, + "workflow_created_from_template event": { + activity: api.Activity{ + EventType: types.WorkflowCreatedFromTemplate, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "workflow_name": "WF1", + "template_id": "tmpl1", + }, + }, + expectedResults: []string{"WF1", "tmpl1"}, + }, + "workflow_execution_result event": { + activity: api.Activity{ + EventType: types.WorkflowExecutionResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "workflow_name": "WF1", + }, + }, + expectedResults: []string{"WF1", "completed"}, + }, + "workflow_execution_started event": { + activity: api.Activity{ + EventType: types.WorkflowExecutionStarted, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "workflow_name": "WF1", + }, + }, + expectedResults: []string{"WF1", "started"}, + }, + "workflow_published event": { + activity: api.Activity{ + EventType: types.WorkflowPublished, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "workflow_name": "WF1", + }, + }, + expectedResults: []string{"WF1", "published"}, + }, + "workflow_step_execution_result event": { + activity: api.Activity{ + EventType: types.WorkflowStepExecutionResult, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "function_name": "MyFunc", + "function_type": "custom", + }, + }, + expectedResults: []string{"MyFunc", "completed"}, + }, + "workflow_step_started event": { + activity: api.Activity{ + EventType: types.WorkflowStepStarted, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "current_step": float64(2), + "total_steps": float64(5), + }, + }, + expectedResults: []string{"2", "5", "started"}, + }, + "workflow_unpublished event": { + activity: api.Activity{ + EventType: types.WorkflowUnpublished, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "workflow_name": "WF1", + }, }, + expectedResults: []string{"WF1", "unpublished"}, + }, + "external_auth_missing_selected_auth event": { + activity: api.Activity{ + EventType: types.ExternalAuthMissingSelectedAuth, + Level: types.INFO, + ComponentID: "c1", + TraceID: "t1", + Payload: map[string]any{ + "code": "auth_error", + }, + }, + expectedResults: []string{"auth_error"}, }, } for name, tc := range tests { @@ -307,9 +556,9 @@ func TestPlatformActivity_TriggerExecutedToString(t *testing.T) { "successful trigger trip with trigger information": { Activity: api.Activity{ Level: types.INFO, - Payload: map[string]interface{}{ + Payload: map[string]any{ "function_name": "Send a greeting", - "trigger": map[string]interface{}{ + "trigger": map[string]any{ "type": "shortcut", }, }, @@ -321,7 +570,7 @@ func TestPlatformActivity_TriggerExecutedToString(t *testing.T) { "successful trigger trip without trigger information": { Activity: api.Activity{ Level: types.INFO, - Payload: map[string]interface{}{ + Payload: map[string]any{ "function_name": "Send a greeting", }, }, @@ -332,7 +581,7 @@ func TestPlatformActivity_TriggerExecutedToString(t *testing.T) { "reason 'parameter_validation_failed' with 1 error": { Activity: api.Activity{ Level: types.ERROR, - Payload: map[string]interface{}{ + Payload: map[string]any{ "function_name": "Send a greeting", "reason": "parameter_validation_failed", "errors": "[\"Null value for non-nullable parameter `channel`\"]", @@ -346,7 +595,7 @@ func TestPlatformActivity_TriggerExecutedToString(t *testing.T) { "reason 'parameter_validation_failed' with 2 errors": { Activity: api.Activity{ Level: types.ERROR, - Payload: map[string]interface{}{ + Payload: map[string]any{ "function_name": "Send a greeting", "reason": "parameter_validation_failed", "errors": "[\"Null value for non-nullable parameter `channel`\",\"Null value for non-nullable parameter `interactivity`\"]", @@ -361,7 +610,7 @@ func TestPlatformActivity_TriggerExecutedToString(t *testing.T) { "reason 'parameter_validation_failed' with nil errors": { Activity: api.Activity{ Level: types.ERROR, - Payload: map[string]interface{}{ + Payload: map[string]any{ "function_name": "Send a greeting", "reason": "parameter_validation_failed", "errors": nil, @@ -384,772 +633,255 @@ func TestPlatformActivity_TriggerExecutedToString(t *testing.T) { } } -func Test_prettifyActivity_allEventTypes(t *testing.T) { +func Test_activityToStringFunctions(t *testing.T) { + baseActivity := api.Activity{ + TraceID: "tr123", + Level: types.INFO, + ComponentID: "comp1", + Created: 1686939542000000, + Payload: map[string]any{ + "function_id": "fn1", + "function_name": "MyFunc", + "function_type": "custom", + "workflow_name": "MyWorkflow", + "user_id": "U123", + "team_id": "T456", + "app_id": "A789", + "provider_key": "google", + "code": "auth_error", + "extra_message": "details here", + "action": "deploy", + "log": "some output", + "channel_id": "C123", + "bot_user_id": "B456", + "template_id": "tmpl1", + "current_step": float64(2), + "total_steps": float64(5), + }, + } + tests := map[string]struct { - activity api.Activity - expectedResults []string + fn func(api.Activity) string + activity api.Activity + contains []string }{ - "DatastoreRequestResult routes correctly": { - activity: api.Activity{ - EventType: types.DatastoreRequestResult, - Payload: map[string]interface{}{ - "request_type": "get", - "datastore_name": "DS1", - "details": "id: 123", - }, - }, - expectedResults: []string{"Datastore get succeeded"}, - }, - "ExternalAuthMissingFunction routes correctly": { - activity: api.Activity{ - EventType: types.ExternalAuthMissingFunction, - Payload: map[string]interface{}{ - "function_id": "fn1", - }, - }, - expectedResults: []string{"Step function 'fn1' is missing"}, - }, - "ExternalAuthMissingSelectedAuth routes correctly": { - activity: api.Activity{ - EventType: types.ExternalAuthMissingSelectedAuth, - Payload: map[string]interface{}{ - "code": "wf1", - }, - }, - expectedResults: []string{"Missing mapped token for workflow 'wf1'"}, - }, - "ExternalAuthResult routes correctly": { - activity: api.Activity{ - EventType: types.ExternalAuthResult, - Level: types.INFO, - Payload: map[string]interface{}{ - "user_id": "U1", - "team_id": "T1", - "app_id": "A1", - "provider_key": "google", - }, - }, - expectedResults: []string{"Auth completed"}, - }, - "ExternalAuthStarted routes correctly": { - activity: api.Activity{ - EventType: types.ExternalAuthStarted, - Level: types.INFO, - Payload: map[string]interface{}{ - "user_id": "U1", - "team_id": "T1", - "app_id": "A1", - "provider_key": "google", - }, - }, - expectedResults: []string{"Auth start succeeded"}, - }, - "ExternalAuthTokenFetchResult routes correctly": { - activity: api.Activity{ - EventType: types.ExternalAuthTokenFetchResult, - Level: types.INFO, - Payload: map[string]interface{}{ - "user_id": "U1", - "team_id": "T1", - "app_id": "A1", - "provider_key": "google", - }, - }, - expectedResults: []string{"Token fetch succeeded"}, - }, - "FunctionDeployment routes correctly": { + "externalAuthMissingFunctionToString": { + fn: externalAuthMissingFunctionToString, + activity: baseActivity, + contains: []string{"fn1", "missing"}, + }, + "externalAuthMissingSelectedAuthToString": { + fn: externalAuthMissingSelectedAuthToString, + activity: baseActivity, + contains: []string{"auth_error", "Missing mapped token"}, + }, + "externalAuthResultToString info": { + fn: externalAuthResultToString, + activity: baseActivity, + contains: []string{"completed", "U123", "T456"}, + }, + "externalAuthStartedToString info": { + fn: externalAuthStartedToString, + activity: baseActivity, + contains: []string{"succeeded", "U123", "T456"}, + }, + "externalAuthTokenFetchResult info": { + fn: externalAuthTokenFetchResult, + activity: baseActivity, + contains: []string{"succeeded", "U123", "T456"}, + }, + "functionDeploymentToString": { + fn: functionDeploymentToString, + activity: baseActivity, + contains: []string{"deployed", "U123", "T456"}, + }, + "functionExecutionOutputToString": { + fn: functionExecutionOutputToString, + activity: baseActivity, + contains: []string{"Function output", "some output"}, + }, + "triggerPayloadReceivedOutputToString": { + fn: triggerPayloadReceivedOutputToString, + activity: baseActivity, + contains: []string{"Trigger payload", "some output"}, + }, + "functionExecutionResultToString completed": { + fn: functionExecutionResultToString, + activity: baseActivity, + contains: []string{"MyFunc", "completed"}, + }, + "functionExecutionResultToString failed": { + fn: functionExecutionResultToString, activity: api.Activity{ - EventType: types.FunctionDeployment, - Payload: map[string]interface{}{ - "action": "deploye", - "user_id": "U1", - "team_id": "T1", - }, - Created: 1686939542000000, - }, - expectedResults: []string{"Application deployed"}, - }, - "FunctionExecutionOutput routes correctly": { - activity: api.Activity{ - EventType: types.FunctionExecutionOutput, - Payload: map[string]interface{}{ - "log": "output here", - }, - }, - expectedResults: []string{"Function output:"}, - }, - "TriggerPayloadReceived routes correctly": { - activity: api.Activity{ - EventType: types.TriggerPayloadReceived, - Payload: map[string]interface{}{ - "log": "payload here", - }, - }, - expectedResults: []string{"Trigger payload:"}, - }, - "FunctionExecutionResult routes correctly": { - activity: api.Activity{ - EventType: types.FunctionExecutionResult, - Level: types.INFO, - Payload: map[string]interface{}{ - "function_name": "fn1", + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "function_name": "MyFunc", "function_type": "custom", + "error": "something went wrong", }, }, - expectedResults: []string{"Function 'fn1' (custom function) completed"}, + contains: []string{"MyFunc", "failed", "something went wrong"}, }, - "FunctionExecutionStarted routes correctly": { - activity: api.Activity{ - EventType: types.FunctionExecutionStarted, - Payload: map[string]interface{}{ - "function_name": "fn1", - "function_type": "custom", - }, - }, - expectedResults: []string{"Function 'fn1' (custom function) started"}, + "functionExecutionStartedToString": { + fn: functionExecutionStartedToString, + activity: baseActivity, + contains: []string{"MyFunc", "started"}, }, - "TriggerExecuted routes correctly": { + "workflowBillingResultToString with billing": { + fn: workflowBillingResultToString, activity: api.Activity{ - EventType: types.TriggerExecuted, - Level: types.INFO, - Payload: map[string]interface{}{ - "function_name": "fn1", + Level: types.INFO, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "workflow_name": "MyWorkflow", + "is_billing_result": true, + "billing_reason": "function_execution", }, }, - expectedResults: []string{"Trigger successfully started execution"}, + contains: []string{"MyWorkflow", "billing reason", "function_execution"}, }, - "WorkflowBillingResult routes correctly": { + "workflowBillingResultToString excluded": { + fn: workflowBillingResultToString, activity: api.Activity{ - EventType: types.WorkflowBillingResult, - Payload: map[string]interface{}{ - "workflow_name": "wf1", + Level: types.INFO, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ "is_billing_result": false, }, }, - expectedResults: []string{"excluded from billing"}, + contains: []string{"excluded from billing"}, }, - "WorkflowBotInvited routes correctly": { - activity: api.Activity{ - EventType: types.WorkflowBotInvited, - Payload: map[string]interface{}{ - "channel_id": "C1", - "bot_user_id": "B1", - }, - }, - expectedResults: []string{"Channel C1 detected"}, + "workflowBotInvitedToString": { + fn: workflowBotInvitedToString, + activity: baseActivity, + contains: []string{"C123", "B456", "invited"}, }, - "WorkflowCreatedFromTemplate routes correctly": { - activity: api.Activity{ - EventType: types.WorkflowCreatedFromTemplate, - Payload: map[string]interface{}{ - "workflow_name": "wf1", - "template_id": "tmpl1", - }, - }, - expectedResults: []string{"Workflow 'wf1' created from template 'tmpl1'"}, + "workflowCreatedFromTemplateToString": { + fn: workflowCreatedFromTemplateToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "tmpl1"}, }, - "WorkflowExecutionResult routes correctly": { - activity: api.Activity{ - EventType: types.WorkflowExecutionResult, - Level: types.INFO, - Payload: map[string]interface{}{ - "workflow_name": "wf1", - }, - }, - expectedResults: []string{"Workflow 'wf1' completed"}, + "workflowExecutionResultToString completed": { + fn: workflowExecutionResultToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "completed"}, }, - "WorkflowExecutionStarted routes correctly": { + "workflowExecutionResultToString failed": { + fn: workflowExecutionResultToString, activity: api.Activity{ - EventType: types.WorkflowExecutionStarted, - Payload: map[string]interface{}{ - "workflow_name": "wf1", + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "workflow_name": "MyWorkflow", + "error": "workflow error", }, }, - expectedResults: []string{"Workflow 'wf1' started"}, + contains: []string{"MyWorkflow", "failed", "workflow error"}, }, - "WorkflowPublished routes correctly": { - activity: api.Activity{ - EventType: types.WorkflowPublished, - Payload: map[string]interface{}{ - "workflow_name": "wf1", - }, - }, - expectedResults: []string{"Workflow 'wf1' published"}, + "workflowExecutionStartedToString": { + fn: workflowExecutionStartedToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "started"}, }, - "WorkflowStepExecutionResult routes correctly": { - activity: api.Activity{ - EventType: types.WorkflowStepExecutionResult, - Level: types.INFO, - Payload: map[string]interface{}{ - "function_name": "fn1", - }, - }, - expectedResults: []string{"Workflow step 'fn1' completed"}, + "workflowPublishedToString": { + fn: workflowPublishedToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "published"}, }, - "WorkflowStepStarted routes correctly": { + "externalAuthResultToString error": { + fn: externalAuthResultToString, activity: api.Activity{ - EventType: types.WorkflowStepStarted, - Payload: map[string]interface{}{ - "current_step": float64(1), - "total_steps": float64(3), - }, - }, - expectedResults: []string{"Workflow step 1 of 3 started"}, - }, - "WorkflowUnpublished routes correctly": { - activity: api.Activity{ - EventType: types.WorkflowUnpublished, - Payload: map[string]interface{}{ - "workflow_name": "wf1", - }, - }, - expectedResults: []string{"Workflow 'wf1' unpublished"}, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := prettifyActivity(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_externalAuthMissingFunctionToString(t *testing.T) { - activity := api.Activity{ - Level: types.ERROR, - ComponentID: "comp1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "function_id": "my_func", - }, - Created: 1686939542000000, - } - result := externalAuthMissingFunctionToString(activity) - assert.Contains(t, result, "Step function 'my_func' is missing") - assert.Contains(t, result, "comp1") - assert.Contains(t, result, "Trace=trace1") -} - -func Test_externalAuthMissingSelectedAuthToString(t *testing.T) { - activity := api.Activity{ - Level: types.ERROR, - ComponentID: "comp1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "code": "wf_abc", - }, - Created: 1686939542000000, - } - result := externalAuthMissingSelectedAuthToString(activity) - assert.Contains(t, result, "Missing mapped token for workflow 'wf_abc'") - assert.Contains(t, result, "Trace=trace1") -} - -func Test_externalAuthResultToString(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "completed auth result": { - activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "user_id": "U123", - "team_id": "T123", - "app_id": "A123", - "provider_key": "google", - }, - }, - expectedResults: []string{ - "Auth completed", - "U123", - "T123", - "A123", - "google", - }, - }, - "failed auth result with error details": { - activity: api.Activity{ - Level: types.ERROR, - Payload: map[string]interface{}{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ "user_id": "U123", - "team_id": "T123", - "app_id": "A123", + "team_id": "T456", + "app_id": "A789", "provider_key": "google", - "code": "invalid_grant", - "extra_message": "token expired", + "code": "auth_error", + "extra_message": "details here", }, }, - expectedResults: []string{ - "Auth failed", - "U123", - "invalid_grant", - "token expired", - }, + contains: []string{"failed", "U123", "T456", "auth_error", "details here"}, }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := externalAuthResultToString(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_externalAuthStartedToString(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "succeeded auth start": { - activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "user_id": "U123", - "team_id": "T123", - "app_id": "A123", - "provider_key": "google", - }, - }, - expectedResults: []string{ - "Auth start succeeded", - "U123", - "T123", - "A123", - "google", - }, - }, - "failed auth start with error code": { - activity: api.Activity{ - Level: types.ERROR, - Payload: map[string]interface{}{ - "user_id": "U123", - "team_id": "T123", - "app_id": "A123", - "provider_key": "google", - "code": "auth_failed", - }, - }, - expectedResults: []string{ - "Auth start failed", - "U123", - "auth_failed", - }, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := externalAuthStartedToString(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_externalAuthTokenFetchResult(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "succeeded token fetch": { + "externalAuthStartedToString error": { + fn: externalAuthStartedToString, activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ "user_id": "U123", - "team_id": "T123", - "app_id": "A123", + "team_id": "T456", + "app_id": "A789", "provider_key": "google", + "code": "auth_start_error", }, }, - expectedResults: []string{ - "Token fetch succeeded", - "U123", - "T123", - "A123", - "google", - }, + contains: []string{"failed", "U123", "auth_start_error"}, }, - "failed token fetch with error code": { + "externalAuthTokenFetchResult error": { + fn: externalAuthTokenFetchResult, activity: api.Activity{ - Level: types.ERROR, - Payload: map[string]interface{}{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ "user_id": "U123", - "team_id": "T123", - "app_id": "A123", + "team_id": "T456", + "app_id": "A789", "provider_key": "google", - "code": "token_revoked", + "code": "fetch_error", }, }, - expectedResults: []string{ - "Token fetch failed", - "U123", - "token_revoked", - }, + contains: []string{"failed", "U123", "fetch_error"}, }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := externalAuthTokenFetchResult(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_functionDeploymentToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "action": "deploye", - "user_id": "U123", - "team_id": "T123", - }, - Created: 1686939542000000, - } - result := functionDeploymentToString(activity) - assert.Contains(t, result, "Application deployed") - assert.Contains(t, result, "U123") - assert.Contains(t, result, "T123") -} - -func Test_functionExecutionOutputToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - ComponentID: "fn1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "log": "hello world", + "workflowStepExecutionResultToString completed": { + fn: workflowStepExecutionResultToString, + activity: baseActivity, + contains: []string{"MyFunc", "completed"}, }, - Created: 1686939542000000, - } - result := functionExecutionOutputToString(activity) - assert.Contains(t, result, "Function output:") - assert.Contains(t, result, "hello world") - assert.Contains(t, result, "Trace=trace1") -} - -func Test_functionExecutionResultToString(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "completed function execution": { - activity: api.Activity{ - Level: types.INFO, - ComponentID: "fn1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "function_name": "my_function", - "function_type": "custom", - }, - Created: 1686939542000000, - }, - expectedResults: []string{ - "Function 'my_function' (custom function) completed", - "Trace=trace1", - }, - }, - "failed function execution with error": { + "workflowStepExecutionResultToString failed": { + fn: workflowStepExecutionResultToString, activity: api.Activity{ Level: types.ERROR, - ComponentID: "fn1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "function_name": "my_function", + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "function_name": "MyFunc", "function_type": "custom", - "error": "something went wrong", + "error": "step error", }, - Created: 1686939542000000, - }, - expectedResults: []string{ - "Function 'my_function' (custom function) failed", - "something went wrong", }, + contains: []string{"MyFunc", "failed"}, }, - "fatal function execution": { - activity: api.Activity{ - Level: types.FATAL, - Payload: map[string]interface{}{ - "function_name": "my_function", - "function_type": "builtin", - }, - }, - expectedResults: []string{"Function 'my_function' (builtin function) failed"}, + "workflowStepStartedToString": { + fn: workflowStepStartedToString, + activity: baseActivity, + contains: []string{"2", "5", "started"}, }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := functionExecutionResultToString(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_functionExecutionStartedToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - ComponentID: "fn1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "function_name": "my_function", - "function_type": "custom", - }, - Created: 1686939542000000, - } - result := functionExecutionStartedToString(activity) - assert.Contains(t, result, "Function 'my_function' (custom function) started") - assert.Contains(t, result, "Trace=trace1") -} - -func Test_triggerPayloadReceivedOutputToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - ComponentID: "trigger1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "log": "payload data here", - }, - Created: 1686939542000000, - } - result := triggerPayloadReceivedOutputToString(activity) - assert.Contains(t, result, "Trigger payload:") - assert.Contains(t, result, "payload data here") - assert.Contains(t, result, "Trace=trace1") -} - -func Test_workflowBillingResultToString(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "billing result with workflow name": { - activity: api.Activity{ - Level: types.INFO, - ComponentID: "wf1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - "is_billing_result": true, - "billing_reason": "execution", - }, - Created: 1686939542000000, - }, - expectedResults: []string{ - "Workflow 'My Workflow'", - "billing reason 'execution'", - }, - }, - "billing result without workflow name": { - activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "is_billing_result": true, - "billing_reason": "execution", - }, - }, - expectedResults: []string{ - "Workflow", - "billing reason 'execution'", - }, - }, - "excluded from billing": { - activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - "is_billing_result": false, - }, - }, - expectedResults: []string{ - "Workflow 'My Workflow'", - "excluded from billing", - }, + "workflowUnpublishedToString": { + fn: workflowUnpublishedToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "unpublished"}, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { - result := workflowBillingResultToString(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) + result := tc.fn(tc.activity) + for _, s := range tc.contains { + assert.Contains(t, result, s) } }) } } -func Test_workflowBotInvitedToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - ComponentID: "wf1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "channel_id": "C123", - "bot_user_id": "B123", - }, - Created: 1686939542000000, - } - result := workflowBotInvitedToString(activity) - assert.Contains(t, result, "Channel C123 detected") - assert.Contains(t, result, "Bot user B123 automatically invited") -} - -func Test_workflowCreatedFromTemplateToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - ComponentID: "wf1", - TraceID: "trace1", - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - "template_id": "tmpl_123", - }, - Created: 1686939542000000, - } - result := workflowCreatedFromTemplateToString(activity) - assert.Contains(t, result, "Workflow 'My Workflow' created from template 'tmpl_123'") -} - -func Test_workflowExecutionResultToString(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "completed workflow execution": { - activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - }, - }, - expectedResults: []string{"Workflow 'My Workflow' completed"}, - }, - "failed workflow execution with error": { - activity: api.Activity{ - Level: types.ERROR, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - "error": "step failed", - }, - }, - expectedResults: []string{ - "Workflow 'My Workflow' failed", - "step failed", - }, - }, - "fatal workflow execution": { - activity: api.Activity{ - Level: types.FATAL, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - }, - }, - expectedResults: []string{"Workflow 'My Workflow' failed"}, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := workflowExecutionResultToString(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_workflowExecutionStartedToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - }, - } - result := workflowExecutionStartedToString(activity) - assert.Contains(t, result, "Workflow 'My Workflow' started") -} - -func Test_workflowPublishedToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - }, - } - result := workflowPublishedToString(activity) - assert.Contains(t, result, "Workflow 'My Workflow' published") -} - -func Test_workflowStepExecutionResultToString(t *testing.T) { - tests := map[string]struct { - activity api.Activity - expectedResults []string - }{ - "completed step": { - activity: api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "function_name": "send_message", - }, - }, - expectedResults: []string{"Workflow step 'send_message' completed"}, - }, - "failed step": { - activity: api.Activity{ - Level: types.ERROR, - Payload: map[string]interface{}{ - "function_name": "send_message", - }, - }, - expectedResults: []string{"Workflow step 'send_message' failed"}, - }, - "fatal step": { - activity: api.Activity{ - Level: types.FATAL, - Payload: map[string]interface{}{ - "function_name": "send_message", - }, - }, - expectedResults: []string{"Workflow step 'send_message' failed"}, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := workflowStepExecutionResultToString(tc.activity) - for _, expected := range tc.expectedResults { - assert.Contains(t, result, expected) - } - }) - } -} - -func Test_workflowStepStartedToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "current_step": float64(2), - "total_steps": float64(5), - }, - } - result := workflowStepStartedToString(activity) - assert.Contains(t, result, "Workflow step 2 of 5 started") -} - -func Test_workflowUnpublishedToString(t *testing.T) { - activity := api.Activity{ - Level: types.INFO, - Payload: map[string]interface{}{ - "workflow_name": "My Workflow", - }, - } - result := workflowUnpublishedToString(activity) - assert.Contains(t, result, "Workflow 'My Workflow' unpublished") -} - func Test_datastoreRequestResultToString(t *testing.T) { for name, tc := range map[string]struct { activity api.Activity @@ -1157,7 +889,7 @@ func Test_datastoreRequestResultToString(t *testing.T) { }{ "successful datastore request event log": { activity: api.Activity{ - Payload: map[string]interface{}{ + Payload: map[string]any{ "datastore_name": "MyDatastore", "request_type": "get", "details": "id: f7d1253f-4066-4b83-8330-a483ff555c20", @@ -1172,7 +904,7 @@ func Test_datastoreRequestResultToString(t *testing.T) { "failed datastore request error log": { activity: api.Activity{ Level: "error", - Payload: map[string]interface{}{ + Payload: map[string]any{ "datastore_name": "MyDatastore", "request_type": "query", "details": `{"expression": "id invalid_operator f7d1253f-4066-4b83-8330-a483ff555c20"}`, @@ -1184,7 +916,7 @@ func Test_datastoreRequestResultToString(t *testing.T) { "failed datastore request without error field": { activity: api.Activity{ Level: "error", - Payload: map[string]interface{}{ + Payload: map[string]any{ "datastore_name": "MyDatastore", "request_type": "query", "details": `{"expression": "id invalid_operator f7d1253f-4066-4b83-8330-a483ff555c20"}`, diff --git a/internal/slackdeps/os_test.go b/internal/slackdeps/os_test.go index 8673980e..6a4f4c85 100644 --- a/internal/slackdeps/os_test.go +++ b/internal/slackdeps/os_test.go @@ -21,68 +21,6 @@ import ( "github.com/stretchr/testify/require" ) -func Test_Os_GetExecutionDir(t *testing.T) { - tests := map[string]struct { - executionDirPathAbs string - expectedExecutionDirPathAbs string - }{ - "When unset should return blank": { - executionDirPathAbs: "", - expectedExecutionDirPathAbs: "", - }, - "When set should return path": { - executionDirPathAbs: "/path/to/execution/dir", - expectedExecutionDirPathAbs: "/path/to/execution/dir", - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - // Setup - os := NewOs() - os.executionDirPathAbs = tc.executionDirPathAbs - - // Run the test - actualExecutionDirPathAbs := os.GetExecutionDir() - - // Assertions - require.Equal(t, tc.expectedExecutionDirPathAbs, actualExecutionDirPathAbs) - }) - } -} - -func Test_Os_SetExecutionDir(t *testing.T) { - tests := map[string]struct { - executionDirPathAbs string - setExecutionDirPathAbs string - expectedExecutionDirPathAbs string - }{ - "Successful set path": { - executionDirPathAbs: "", - setExecutionDirPathAbs: "/path/to/execution/dir", - expectedExecutionDirPathAbs: "/path/to/execution/dir", - }, - "Successfully overwrite path": { - executionDirPathAbs: "/some/original/path", - setExecutionDirPathAbs: "/path/to/execution/dir", - expectedExecutionDirPathAbs: "/path/to/execution/dir", - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - // Setup - os := NewOs() - os.executionDirPathAbs = tc.executionDirPathAbs - - // Run the test - os.SetExecutionDir(tc.setExecutionDirPathAbs) - actualExecutionDirPathAbs := os.executionDirPathAbs - - // Assertions - require.Equal(t, tc.expectedExecutionDirPathAbs, actualExecutionDirPathAbs) - }) - } -} - func Test_Os_Getenv(t *testing.T) { tests := map[string]struct { key string @@ -110,50 +48,17 @@ func Test_Os_Getenv(t *testing.T) { } } -func Test_Os_Getwd(t *testing.T) { +func Test_Os_SetenvUnsetenv(t *testing.T) { o := NewOs() - dir, err := o.Getwd() - require.NoError(t, err) - require.NotEmpty(t, dir) -} + key := "SLACK_TEST_OS_SETENV" -func Test_Os_Glob(t *testing.T) { - o := NewOs() - // Create a temp file to glob for - tmpDir := t.TempDir() - f, err := os.CreateTemp(tmpDir, "glob_test_*.txt") + err := o.Setenv(key, "test_value") require.NoError(t, err) - f.Close() + require.Equal(t, "test_value", o.Getenv(key)) - matches, err := o.Glob(tmpDir + "/*.txt") + err = o.Unsetenv(key) require.NoError(t, err) - require.NotEmpty(t, matches) -} - -func Test_Os_IsNotExist(t *testing.T) { - tests := map[string]struct { - err error - expected bool - }{ - "returns true for os.ErrNotExist": { - err: os.ErrNotExist, - expected: true, - }, - "returns false for other errors": { - err: os.ErrPermission, - expected: false, - }, - "returns false for nil": { - err: nil, - expected: false, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - o := NewOs() - require.Equal(t, tc.expected, o.IsNotExist(tc.err)) - }) - } + require.Equal(t, "", o.Getenv(key)) } func Test_Os_LookupEnv(t *testing.T) { @@ -191,53 +96,123 @@ func Test_Os_LookupEnv(t *testing.T) { } } -func Test_Os_Setenv(t *testing.T) { +func Test_Os_Getwd(t *testing.T) { + o := NewOs() + dir, err := o.Getwd() + require.NoError(t, err) + require.NotEmpty(t, dir) +} + +func Test_Os_UserHomeDir(t *testing.T) { + o := NewOs() + home, err := o.UserHomeDir() + require.NoError(t, err) + require.NotEmpty(t, home) +} + +func Test_Os_IsNotExist(t *testing.T) { tests := map[string]struct { - key string - value string - initialValue string - expected string + err error + expected bool }{ - "sets a new env var": { - key: "SLACK_TEST_OS_SETENV_NEW", - value: "hello", - expected: "hello", + "returns true for os.ErrNotExist": { + err: os.ErrNotExist, + expected: true, }, - "overwrites an existing env var": { - key: "SLACK_TEST_OS_SETENV_OVERWRITE", - value: "updated", - initialValue: "original", - expected: "updated", + "returns false for other errors": { + err: os.ErrPermission, + expected: false, }, - "sets an empty value": { - key: "SLACK_TEST_OS_SETENV_EMPTY", - value: "", - expected: "", + "returns false for nil": { + err: nil, + expected: false, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := NewOs() - if tc.initialValue != "" { - t.Setenv(tc.key, tc.initialValue) - } - err := o.Setenv(tc.key, tc.value) - require.NoError(t, err) - require.Equal(t, tc.expected, o.Getenv(tc.key)) - t.Cleanup(func() { _ = o.Unsetenv(tc.key) }) + require.Equal(t, tc.expected, o.IsNotExist(tc.err)) }) } } +func Test_Os_Glob(t *testing.T) { + o := NewOs() + // Create a temp file to glob for + tmpDir := t.TempDir() + f, err := os.CreateTemp(tmpDir, "glob_test_*.txt") + require.NoError(t, err) + f.Close() + + matches, err := o.Glob(tmpDir + "/*.txt") + require.NoError(t, err) + require.NotEmpty(t, matches) +} + func Test_Os_Stdout(t *testing.T) { o := NewOs() stdout := o.Stdout() require.NotNil(t, stdout) } -func Test_Os_UserHomeDir(t *testing.T) { - o := NewOs() - home, err := o.UserHomeDir() - require.NoError(t, err) - require.NotEmpty(t, home) +func Test_Os_GetExecutionDir(t *testing.T) { + tests := map[string]struct { + executionDirPathAbs string + expectedExecutionDirPathAbs string + }{ + "When unset should return blank": { + executionDirPathAbs: "", + expectedExecutionDirPathAbs: "", + }, + "When set should return path": { + executionDirPathAbs: "/path/to/execution/dir", + expectedExecutionDirPathAbs: "/path/to/execution/dir", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup + os := NewOs() + os.executionDirPathAbs = tc.executionDirPathAbs + + // Run the test + actualExecutionDirPathAbs := os.GetExecutionDir() + + // Assertions + require.Equal(t, tc.expectedExecutionDirPathAbs, actualExecutionDirPathAbs) + }) + } +} + +func Test_Os_SetExecutionDir(t *testing.T) { + tests := map[string]struct { + executionDirPathAbs string + setExecutionDirPathAbs string + expectedExecutionDirPathAbs string + }{ + "Successful set path": { + executionDirPathAbs: "", + setExecutionDirPathAbs: "/path/to/execution/dir", + expectedExecutionDirPathAbs: "/path/to/execution/dir", + }, + "Successfully overwrite path": { + executionDirPathAbs: "/some/original/path", + setExecutionDirPathAbs: "/path/to/execution/dir", + expectedExecutionDirPathAbs: "/path/to/execution/dir", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup + os := NewOs() + os.executionDirPathAbs = tc.executionDirPathAbs + + // Run the test + os.SetExecutionDir(tc.setExecutionDirPathAbs) + actualExecutionDirPathAbs := os.executionDirPathAbs + + // Assertions + require.Equal(t, tc.expectedExecutionDirPathAbs, actualExecutionDirPathAbs) + }) + } } diff --git a/internal/style/format_test.go b/internal/style/format_test.go index f45891a4..5b3b6d3e 100644 --- a/internal/style/format_test.go +++ b/internal/style/format_test.go @@ -19,149 +19,188 @@ import ( "os" "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) -func TestGetKeyLength(t *testing.T) { - tests := map[string]struct { - keys map[string]string - expected int - }{ - "empty key has zero length": { - keys: map[string]string{"": "the zero key"}, - expected: 0, - }, - "equal length keys return that length": { - keys: map[string]string{"key1": "unlocks the building", "key2": "unlocks the room"}, - expected: 4, - }, - "returns length of longest key": { - keys: map[string]string{"longer_key1": "locks the building", "very_long_key2": "locks the room"}, - expected: 14, - }, - "longest key is first": { - keys: map[string]string{"longest_key1": "short value", "short_key2": "longer value"}, - expected: 12, - }, +func TestGetKeyLengthZero(t *testing.T) { + var keys = map[string]string{ + "": "the zero key", } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.expected, getKeyLength(tc.keys)) - }) + + if getKeyLength(keys) != 0 { + t.Error("the longest key has a length greater than zero") } } -func TestSectionf(t *testing.T) { - tests := map[string]struct { - section TextSection - expected string - }{ - "empty text returns empty string": { - section: TextSection{Emoji: "", Text: "", Secondary: []string{}}, - expected: "", - }, - "header with emoji and secondary text": { - section: TextSection{Emoji: "tada", Text: "Congrats", Secondary: []string{"You did it"}}, - expected: Emoji("tada") + "Congrats\n" + Indent(Secondary("You did it")) + "\n", - }, - "no emoji starts text immediately": { - section: TextSection{Emoji: "", Text: "On the left. Where I like it.", Secondary: []string{}}, - expected: "On the left. Where I like it.\n", - }, +func TestGetKeyLengthMatched(t *testing.T) { + var keys = map[string]string{ + "key1": "unlocks the building", + "key2": "unlocks the room", } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.expected, Sectionf(tc.section)) - }) + + if getKeyLength(keys) != 4 { + t.Error("the longest key should have length 4") } } +func TestGetKeyLengthLong(t *testing.T) { + var keys = map[string]string{ + "longer_key1": "locks the building", + "very_long_key2": "locks the room", + } + + if getKeyLength(keys) != 14 { + t.Error("the longest key `very_long_key2` should have length 14") + } +} + +func TestGetKeyLengthFirst(t *testing.T) { + var keys = map[string]string{ + "longest_key1": "short value", + "short_key2": "longer value", + } + + if getKeyLength(keys) != 12 { + t.Error("the longest key `longest_key1` should have length 12") + } +} + +// Verify no text is output with an empty input text +func TestSectionfEmpty(t *testing.T) { + formattedText := Sectionf(TextSection{ + Emoji: "", + Text: "", + Secondary: []string{}, + }) + if formattedText != "" { + t.Error("non-zero text returned when none was expected") + } +} + +// Verify no text is output with an empty input text +func TestSectionfHeader(t *testing.T) { + expected := Emoji("tada") + "Congrats\n" + Indent(Secondary("You did it")) + "\n" + formattedText := Sectionf(TextSection{ + Emoji: "tada", + Text: "Congrats", + Secondary: []string{"You did it"}, + }) + if formattedText != expected { + t.Error("section is not formatted as expected") + } +} + +// Verify text begins immediately if no emoji is input +func TestSectionfEmptyEmoji(t *testing.T) { + text := "On the left. Where I like it." + formattedText := Sectionf(TextSection{ + Emoji: "", + Text: text, + Secondary: []string{}, + }) + + if formattedText != text+"\n" { + t.Error("additional spacing added to text") + } +} + +// Verify no text is output with an empty input text func TestSectionHeaderfEmpty(t *testing.T) { - assert.Equal(t, "", SectionHeaderf("tada", "")) + text := "" + formattedText := SectionHeaderf("tada", text) + if formattedText != "" { + t.Error("non-zero text returned when none was expected") + } } -func TestSectionSecondaryf(t *testing.T) { - tests := map[string]struct { - format string - args []interface{} - validate func(t *testing.T, result string) - }{ - "empty input returns empty string": { - format: "%s", - args: []interface{}{""}, - validate: func(t *testing.T, result string) { - assert.Equal(t, "", result) - }, - }, - "plain text is preserved and indented": { - format: "%s", - args: []interface{}{"If you have a moment, go grab a glass of water!"}, - validate: func(t *testing.T, result string) { - text := "If you have a moment, go grab a glass of water!" - assert.Contains(t, result, text) - assert.Equal(t, Indent(Secondary(text))+"\n", result) - }, - }, - "formats input variables": { - format: "App ID: %s\tStatus: %s", - args: []interface{}{"A123456", "Installed"}, - validate: func(t *testing.T, result string) { - assert.Contains(t, result, "App ID: A123456\tStatus: Installed") - }, - }, - "multi-line input is properly indented": { - format: "%s", - args: []interface{}{"L1\nL2\nL3"}, - validate: func(t *testing.T, result string) { - lines := strings.Split(result, "\n") - for i, line := range strings.Split("L1\nL2\nL3", "\n") { - assert.Equal(t, Indent(Secondary(line)), lines[i]) - } - }, - }, +// Verify no text is output with an empty input +func TestSectionSecondaryfEmpty(t *testing.T) { + text := "" + formattedText := SectionSecondaryf("%s", text) + if formattedText != "" { + t.Log(formattedText) + t.Error("non-zero text returned when none was expected") } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := SectionSecondaryf(tc.format, tc.args...) - tc.validate(t, result) - }) +} + +// Verify plain string is preserved and properly indented +func TestSectionSecondaryfPlain(t *testing.T) { + text := "If you have a moment, go grab a glass of water!" + formattedText := SectionSecondaryf("%s", text) + if !strings.Contains(formattedText, text) { + t.Error("input text is not preserved") + } + if formattedText != Indent(Secondary(text))+"\n" { + t.Error("output is not indented") } } -func TestCommandf(t *testing.T) { - tests := map[string]struct { - process string - command string - isPrimary bool - }{ - "primary command contains process and command": { - process: "renamed-slack-command", - command: "feedback", - isPrimary: true, - }, - "secondary command contains process and command": { - process: "a-renamed-slack-cli", - command: "feedback", - isPrimary: false, - }, +// Verify string formats input variables +func TestSectionSecondaryfFormat(t *testing.T) { + text := "App ID: %s\tStatus: %s" + appID := "A123456" + status := "Installed" + formattedText := SectionSecondaryf(text, appID, status) + if !strings.Contains(formattedText, "App ID: A123456\tStatus: Installed") { + t.Error("formatted string does not contain variables") } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - processTemp := os.Args[0] - os.Args[0] = tc.process - defer func() { os.Args[0] = processTemp }() +} - formatted := Commandf(tc.command, tc.isPrimary) - assert.Contains(t, formatted, tc.process+" "+tc.command) - }) +// Verify multi-line input is properly indented +func TestSectionSecondaryfIndent(t *testing.T) { + text := "L1\nL2\nL3" + formattedText := SectionSecondaryf("%s", text) + + for i, line := range strings.Split(text, "\n") { + lines := strings.Split(formattedText, "\n") + if strings.Compare(lines[i], Indent(Secondary(line))) != 0 { + t.Errorf("new line not properly indented\n"+ + "expect: *%s*\nactual: *%s*", Indent(Secondary(line)), lines[i]) + } + } +} + +// Verify a `process command`-like string is presented +func TestCommandfPrimary(t *testing.T) { + // rename the process for fuzz-like testing + processTemp := os.Args[0] + process := "renamed-slack-command" + os.Args[0] = "renamed-slack-command" + command := "feedback" + + formatted := Commandf(command, true) + if !strings.Contains(formatted, process+" "+command) { + t.Errorf("a `process command`-like string is not present in output:\n%s", formatted) + } + + os.Args[0] = processTemp +} + +// Verify a "process command"-like string is presented +func TestCommandfSecondary(t *testing.T) { + // Rename the process for fuzzy testing + processTemp := os.Args[0] + process := "a-renamed-slack-cli" + os.Args[0] = "a-renamed-slack-cli" + command := "feedback" + + formatted := Commandf(command, false) + if !strings.Contains(formatted, process+" "+command) { + t.Errorf("a `process command`-like string is not present") } + + os.Args[0] = processTemp } +// Verify the text indented is not modified func TestIndent(t *testing.T) { text := "a few spaces are expected at the start of this line, but no other changes" indented := Indent(text) - assert.Contains(t, indented, text) + if !strings.Contains(indented, text) { + t.Error("original text is not preserved") + } } func TestTracef(t *testing.T) { @@ -257,6 +296,7 @@ func TestStyleFlags(t *testing.T) { func Test_ExampleCommandsf(t *testing.T) { tests := map[string]struct { + name string commands []ExampleCommand expected []string }{ @@ -379,6 +419,115 @@ func Test_ExampleTemplatef_Charm(t *testing.T) { assert.Equal(t, strings.Join(expected, "\n"), actual) } +func TestMapf(t *testing.T) { + t.Run("formats a map with aligned keys", func(t *testing.T) { + m := map[string]string{ + "key": "value", + } + result := Mapf(m) + assert.Contains(t, result, "key") + assert.Contains(t, result, "value") + }) + + t.Run("returns empty for empty map", func(t *testing.T) { + m := map[string]string{} + result := Mapf(m) + assert.Empty(t, result) + }) +} + +func TestHomePath(t *testing.T) { + tests := map[string]struct { + path string + contains string + }{ + "non-home path is unchanged": { + path: "/tmp/some/path", + contains: "/tmp/some/path", + }, + "empty path is unchanged": { + path: "", + contains: "", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := HomePath(tc.path) + assert.Contains(t, result, tc.contains) + }) + } +} + +func TestTeamSelectLabel(t *testing.T) { + tests := map[string]struct { + teamDomain string + teamID string + }{ + "formats team domain and ID": { + teamDomain: "my-workspace", + teamID: "T12345", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := TeamSelectLabel(tc.teamDomain, tc.teamID) + assert.Contains(t, result, tc.teamDomain) + assert.Contains(t, result, tc.teamID) + }) + } +} + +func TestTimeAgo(t *testing.T) { + now := int(time.Now().Unix()) + tests := map[string]struct { + datetime int + contains string + }{ + "seconds ago": { + datetime: now - 30, + contains: "seconds ago", + }, + "minutes ago": { + datetime: now - 120, + contains: "minutes ago", + }, + "hours ago": { + datetime: now - 7200, + contains: "hours ago", + }, + "days ago": { + datetime: now - 86400*3, + contains: "days ago", + }, + "weeks ago": { + datetime: now - 86400*14, + contains: "weeks ago", + }, + "months ago": { + datetime: now - 86400*60, + contains: "months ago", + }, + "years ago": { + datetime: now - 86400*800, + contains: "years ago", + }, + "future time": { + datetime: now + 3600, + contains: "until", + }, + "singular minute": { + datetime: now - 90, + contains: "minute ago", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := TimeAgo(tc.datetime) + assert.Contains(t, result, tc.contains) + }) + } +} + /* * App name formatting */ diff --git a/internal/style/style_test.go b/internal/style/style_test.go index 73f3db71..398fd62a 100644 --- a/internal/style/style_test.go +++ b/internal/style/style_test.go @@ -279,35 +279,11 @@ func TestFaint(t *testing.T) { }) } -func TestStyler(t *testing.T) { - t.Run("returns an aurora instance", func(t *testing.T) { - s := Styler() - assert.NotNil(t, s) - }) -} - -func TestEmoji(t *testing.T) { - defer func() { - ToggleStyles(false) - }() - - t.Run("returns empty when colors are off", func(t *testing.T) { - ToggleStyles(false) - assert.Equal(t, "", Emoji("gear")) - }) - - t.Run("returns empty for empty alias", func(t *testing.T) { - assert.Equal(t, "", Emoji("")) - }) - - t.Run("returns empty for whitespace alias", func(t *testing.T) { - ToggleStyles(true) - assert.Equal(t, "", Emoji(" ")) - }) - - t.Run("returns emoji with padding for known aliases", func(t *testing.T) { - ToggleStyles(true) - result := Emoji("gear") - assert.NotEmpty(t, result) - }) +// Verify no text is output when no emoji is given +func TestEmojiEmpty(t *testing.T) { + alias := "" + emoji := Emoji(alias) + if emoji != "" { + t.Errorf("non-empty text returned, when none was expected") + } }