diff --git a/cmd/datastore/query_test.go b/cmd/datastore/query_test.go index 0f704327..37b3d2b1 100644 --- a/cmd/datastore/query_test.go +++ b/cmd/datastore/query_test.go @@ -465,3 +465,55 @@ func prepareExportMockData(cm *shared.ClientsMock, numberOfItems int, maxItemsTo } return data, nil } + +func Test_getExpressionPatterns(t *testing.T) { + tests := map[string]struct { + expression string + wantAttrs int + wantVals int + }{ + "expression with attributes and values": { + expression: "#name = :name AND #status = :status", + wantAttrs: 2, + wantVals: 2, + }, + "empty expression": { + expression: "", + wantAttrs: 0, + wantVals: 0, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + attrs, vals := getExpressionPatterns(tc.expression) + assert.Len(t, attrs, tc.wantAttrs) + assert.Len(t, vals, tc.wantVals) + }) + } +} + +func Test_mapAttributeFlag(t *testing.T) { + tests := map[string]struct { + flag string + wantErr bool + }{ + "valid JSON": { + flag: `{"#name":"name"}`, + }, + "invalid JSON": { + flag: `not json`, + wantErr: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result, err := mapAttributeFlag(tc.flag) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + } + }) + } +} diff --git a/internal/api/app_test.go b/internal/api/app_test.go index f1e84015..7e5a752c 100644 --- a/internal/api/app_test.go +++ b/internal/api/app_test.go @@ -519,3 +519,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/api/datastore_test.go b/internal/api/datastore_test.go index c1126118..77f305bc 100644 --- a/internal/api/datastore_test.go +++ b/internal/api/datastore_test.go @@ -490,3 +490,192 @@ func TestClient_AppsDatastoreGet(t *testing.T) { }) } } + +func TestClient_AppsDatastoreBulkPut(t *testing.T) { + tests := map[string]struct { + request types.AppDatastoreBulkPut + httpResponseJSON string + statusCode int + wantErr bool + errMessage string + }{ + "success": { + request: types.AppDatastoreBulkPut{ + Datastore: "my_ds", + App: "A1", + Items: []map[string]interface{}{{"id": "1", "name": "test"}}, + }, + httpResponseJSON: `{"ok":true,"datastore":"my_ds"}`, + }, + "api_error": { + request: types.AppDatastoreBulkPut{ + Datastore: "my_ds", + App: "A1", + Items: []map[string]interface{}{{"id": "1"}}, + }, + httpResponseJSON: `{"ok":false,"error":"datastore_error"}`, + wantErr: true, + errMessage: "datastore_error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appDatastoreBulkPutMethod, + Response: tc.httpResponseJSON, + StatusCode: tc.statusCode, + }) + defer teardown() + _, err := c.AppsDatastoreBulkPut(ctx, "token", tc.request) + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMessage) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestClient_AppsDatastoreCount(t *testing.T) { + tests := map[string]struct { + request types.AppDatastoreCount + httpResponseJSON string + statusCode int + wantCount int + wantErr bool + errMessage string + }{ + "success": { + request: types.AppDatastoreCount{ + Datastore: "my_ds", + App: "A1", + }, + httpResponseJSON: `{"ok":true,"datastore":"my_ds","count":42}`, + wantCount: 42, + }, + "api_error": { + request: types.AppDatastoreCount{ + Datastore: "my_ds", + App: "A1", + }, + httpResponseJSON: `{"ok":false,"error":"datastore_error"}`, + wantErr: true, + errMessage: "datastore_error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appDatastoreCountMethod, + Response: tc.httpResponseJSON, + StatusCode: tc.statusCode, + }) + defer teardown() + got, err := c.AppsDatastoreCount(ctx, "token", tc.request) + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMessage) + } else { + require.NoError(t, err) + require.Equal(t, tc.wantCount, got.Count) + } + }) + } +} + +func TestClient_AppsDatastoreBulkDelete(t *testing.T) { + tests := map[string]struct { + request types.AppDatastoreBulkDelete + httpResponseJSON string + statusCode int + wantErr bool + errMessage string + }{ + "success": { + request: types.AppDatastoreBulkDelete{ + Datastore: "my_ds", + App: "A1", + IDs: []string{"id1", "id2"}, + }, + httpResponseJSON: `{"ok":true}`, + }, + "api_error": { + request: types.AppDatastoreBulkDelete{ + Datastore: "my_ds", + App: "A1", + IDs: []string{"id1"}, + }, + httpResponseJSON: `{"ok":false,"error":"not_found"}`, + wantErr: true, + errMessage: "not_found", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appDatastoreBulkDeleteMethod, + Response: tc.httpResponseJSON, + StatusCode: tc.statusCode, + }) + defer teardown() + _, err := c.AppsDatastoreBulkDelete(ctx, "token", tc.request) + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMessage) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestClient_AppsDatastoreBulkGet(t *testing.T) { + tests := map[string]struct { + request types.AppDatastoreBulkGet + httpResponseJSON string + statusCode int + wantErr bool + errMessage string + }{ + "success": { + request: types.AppDatastoreBulkGet{ + Datastore: "my_ds", + App: "A1", + IDs: []string{"id1", "id2"}, + }, + httpResponseJSON: `{"ok":true,"datastore":"my_ds","items":[{"id":"id1","name":"test"}]}`, + }, + "api_error": { + request: types.AppDatastoreBulkGet{ + Datastore: "my_ds", + App: "A1", + IDs: []string{"id1"}, + }, + httpResponseJSON: `{"ok":false,"error":"not_found"}`, + wantErr: true, + errMessage: "not_found", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: appDatastoreBulkGetMethod, + Response: tc.httpResponseJSON, + StatusCode: tc.statusCode, + }) + defer teardown() + _, err := c.AppsDatastoreBulkGet(ctx, "token", tc.request) + if tc.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMessage) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/config/flags_test.go b/internal/config/flags_test.go index 3db25406..6c5d3565 100644 --- a/internal/config/flags_test.go +++ b/internal/config/flags_test.go @@ -24,6 +24,44 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSetFlags(t *testing.T) { + fs := slackdeps.NewFsMock() + os := slackdeps.NewOsMock() + config := NewConfig(fs, os) + cmd := &cobra.Command{} + cmd.Flags().String("test-flag", "default", "a test flag") + + config.SetFlags(cmd) + assert.NotNil(t, config.Flags) + f := config.Flags.Lookup("test-flag") + assert.NotNil(t, f) + assert.Equal(t, "default", f.DefValue) +} + +func TestInitializeGlobalFlags(t *testing.T) { + fs := slackdeps.NewFsMock() + os := slackdeps.NewOsMock() + config := NewConfig(fs, os) + cmd := &cobra.Command{} + + config.InitializeGlobalFlags(cmd) + + // Verify that key persistent flags were registered + flagNames := []string{ + "apihost", "app", "config-dir", "experiment", + "force", "no-color", "skip-update", "slackdev", + "runtime", "team", "token", "verbose", + } + for _, name := range flagNames { + f := cmd.PersistentFlags().Lookup(name) + assert.NotNil(t, f, "flag %s should be registered", name) + } + + // Verify hidden flags + assert.True(t, cmd.PersistentFlags().Lookup("apihost").Hidden) + assert.True(t, cmd.PersistentFlags().Lookup("slackdev").Hidden) +} + func TestDeprecatedFlagSubstitutions(t *testing.T) { tests := map[string]struct { expectedWarnings []string diff --git a/internal/deputil/url_test.go b/internal/deputil/url_test.go index e26fee9f..f69b55e4 100644 --- a/internal/deputil/url_test.go +++ b/internal/deputil/url_test.go @@ -53,6 +53,30 @@ func Test_URLChecker(t *testing.T) { httpClientMock.On("Head", mock.Anything).Return(nil, fmt.Errorf("HTTPClient error")) }, }, + "Returns an empty string for HTTP 500 Internal Server Error": { + url: "https://example.com/server-error", + expectedURL: "", + setupHTTPClientMock: func(httpClientMock *slackhttp.HTTPClientMock) { + res := slackhttp.MockHTTPResponse(http.StatusInternalServerError, "Internal Server Error") + httpClientMock.On("Head", mock.Anything).Return(res, nil) + }, + }, + "Returns an empty string for HTTP 301 redirect": { + url: "https://example.com/redirect", + expectedURL: "", + setupHTTPClientMock: func(httpClientMock *slackhttp.HTTPClientMock) { + res := slackhttp.MockHTTPResponse(http.StatusMovedPermanently, "Moved") + httpClientMock.On("Head", mock.Anything).Return(res, nil) + }, + }, + "Returns an empty string for HTTP 403 Forbidden": { + url: "https://example.com/forbidden", + expectedURL: "", + setupHTTPClientMock: func(httpClientMock *slackhttp.HTTPClientMock) { + res := slackhttp.MockHTTPResponse(http.StatusForbidden, "Forbidden") + httpClientMock.On("Head", mock.Anything).Return(res, nil) + }, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { diff --git a/internal/iostreams/iostreams_test.go b/internal/iostreams/iostreams_test.go index 55c752d3..9bb52c3d 100644 --- a/internal/iostreams/iostreams_test.go +++ b/internal/iostreams/iostreams_test.go @@ -20,6 +20,7 @@ import ( "github.com/slackapi/slack-cli/internal/config" "github.com/slackapi/slack-cli/internal/slackdeps" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,6 +35,36 @@ func Test_IOSteams_NewIOStreams(t *testing.T) { require.True(t, io.config.DebugEnabled, "iostreams references config") } +func Test_IOStreams_ExitCode(t *testing.T) { + tests := map[string]struct { + setCode ExitCode + expected ExitCode + }{ + "default is ExitOK": { + setCode: ExitOK, + expected: ExitOK, + }, + "set to ExitError": { + setCode: ExitError, + expected: ExitError, + }, + "set to ExitCancel": { + setCode: ExitCancel, + expected: ExitCancel, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + cfg := config.NewConfig(fsMock, osMock) + io := NewIOStreams(cfg, fsMock, osMock) + io.SetExitCode(tc.setCode) + assert.Equal(t, tc.expected, io.GetExitCode()) + }) + } +} + func Test_IOStreams_IsTTY(t *testing.T) { tests := map[string]struct { fileInfo os.FileInfo @@ -65,3 +96,15 @@ func Test_IOStreams_IsTTY(t *testing.T) { }) } } + +func Test_SetCmdIO(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + cfg := config.NewConfig(fsMock, osMock) + io := NewIOStreams(cfg, fsMock, osMock) + cmd := &cobra.Command{Use: "test"} + io.SetCmdIO(cmd) + assert.NotNil(t, cmd.InOrStdin()) + assert.NotNil(t, cmd.OutOrStdout()) + assert.NotNil(t, cmd.ErrOrStderr()) +} diff --git a/internal/iostreams/survey_test.go b/internal/iostreams/survey_test.go index c874b598..a33f8441 100644 --- a/internal/iostreams/survey_test.go +++ b/internal/iostreams/survey_test.go @@ -26,6 +26,214 @@ import ( "github.com/stretchr/testify/assert" ) +func TestConfirmPromptConfig(t *testing.T) { + tests := map[string]struct { + cfg ConfirmPromptConfig + required bool + }{ + "required true": { + cfg: ConfirmPromptConfig{Required: true}, + required: true, + }, + "required false": { + cfg: ConfirmPromptConfig{Required: false}, + required: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.required, tc.cfg.IsRequired()) + assert.Empty(t, tc.cfg.GetFlags()) + }) + } +} + +func TestInputPromptConfig(t *testing.T) { + tests := map[string]struct { + cfg InputPromptConfig + required bool + }{ + "required true": { + cfg: InputPromptConfig{Required: true, Placeholder: "hint"}, + required: true, + }, + "required false": { + cfg: InputPromptConfig{Required: false}, + required: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.required, tc.cfg.IsRequired()) + assert.Empty(t, tc.cfg.GetFlags()) + }) + } +} + +func TestMultiSelectPromptConfig(t *testing.T) { + tests := map[string]struct { + cfg MultiSelectPromptConfig + required bool + }{ + "required true": { + cfg: MultiSelectPromptConfig{Required: true}, + required: true, + }, + "required false": { + cfg: MultiSelectPromptConfig{Required: false}, + required: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.required, tc.cfg.IsRequired()) + assert.Empty(t, tc.cfg.GetFlags()) + }) + } +} + +func TestPasswordPromptConfig(t *testing.T) { + t.Run("without flag", func(t *testing.T) { + cfg := PasswordPromptConfig{Required: true} + assert.True(t, cfg.IsRequired()) + assert.Empty(t, cfg.GetFlags()) + }) + t.Run("with flag", func(t *testing.T) { + var val string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.StringVar(&val, "token", "", "token flag") + flag := fs.Lookup("token") + cfg := PasswordPromptConfig{Required: false, Flag: flag} + assert.False(t, cfg.IsRequired()) + assert.Len(t, cfg.GetFlags(), 1) + assert.Equal(t, "token", cfg.GetFlags()[0].Name) + }) +} + +func TestSelectPromptConfig(t *testing.T) { + t.Run("no flags", func(t *testing.T) { + cfg := SelectPromptConfig{Required: true} + assert.True(t, cfg.IsRequired()) + assert.Empty(t, cfg.GetFlags()) + }) + t.Run("single flag", func(t *testing.T) { + var val string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.StringVar(&val, "app", "", "app flag") + flag := fs.Lookup("app") + cfg := SelectPromptConfig{Flag: flag} + assert.Len(t, cfg.GetFlags(), 1) + }) + t.Run("multiple flags", func(t *testing.T) { + var v1, v2 string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.StringVar(&v1, "a", "", "") + fs.StringVar(&v2, "b", "", "") + cfg := SelectPromptConfig{Flags: []*pflag.Flag{fs.Lookup("a"), fs.Lookup("b")}} + assert.Len(t, cfg.GetFlags(), 2) + }) +} + +func TestSurveyOptions(t *testing.T) { + tests := map[string]struct { + cfg PromptConfig + expectedLen int + }{ + "required config returns 5 options": { + cfg: ConfirmPromptConfig{Required: true}, + expectedLen: 5, + }, + "optional config returns 5 options": { + cfg: ConfirmPromptConfig{Required: false}, + expectedLen: 5, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + opts := SurveyOptions(tc.cfg) + assert.Len(t, opts, tc.expectedLen) + }) + } +} + +func TestDefaultSelectPromptConfig(t *testing.T) { + cfg := DefaultSelectPromptConfig() + assert.True(t, cfg.IsRequired()) + assert.Empty(t, cfg.GetFlags()) +} + +func TestRetrieveFlagValue(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + cfg := config.NewConfig(fsMock, osMock) + io := NewIOStreams(cfg, fsMock, osMock) + + tests := map[string]struct { + flagset []*pflag.Flag + expectedFlag bool + expectedError string + }{ + "nil flagset returns nil": { + flagset: nil, + expectedFlag: false, + }, + "no changed flags returns nil": { + flagset: func() []*pflag.Flag { + var v string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.StringVar(&v, "x", "", "") + return []*pflag.Flag{fs.Lookup("x")} + }(), + expectedFlag: false, + }, + "one changed flag returns it": { + flagset: func() []*pflag.Flag { + var v string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.StringVar(&v, "y", "", "") + f := fs.Lookup("y") + f.Changed = true + return []*pflag.Flag{f} + }(), + expectedFlag: true, + }, + "two changed flags returns error": { + flagset: func() []*pflag.Flag { + var v1, v2 string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.StringVar(&v1, "a", "", "") + fs.StringVar(&v2, "b", "", "") + fa := fs.Lookup("a") + fa.Changed = true + fb := fs.Lookup("b") + fb.Changed = true + return []*pflag.Flag{fa, fb} + }(), + expectedError: slackerror.ErrMismatchedFlags, + }, + "nil flag in set is skipped": { + flagset: []*pflag.Flag{nil}, + expectedFlag: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + flag, err := io.retrieveFlagValue(tc.flagset) + if tc.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedError) + } else { + assert.NoError(t, err) + if tc.expectedFlag { + assert.NotNil(t, flag) + } else { + assert.Nil(t, flag) + } + } + }) + } +} + func TestPasswordPrompt(t *testing.T) { tests := map[string]struct { FlagChanged bool diff --git a/internal/iostreams/writer_test.go b/internal/iostreams/writer_test.go index d9411b30..9921bec0 100644 --- a/internal/iostreams/writer_test.go +++ b/internal/iostreams/writer_test.go @@ -233,6 +233,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 b1a58695..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,6 +633,255 @@ func TestPlatformActivity_TriggerExecutedToString(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 { + fn func(api.Activity) string + activity api.Activity + contains []string + }{ + "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{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "function_name": "MyFunc", + "function_type": "custom", + "error": "something went wrong", + }, + }, + contains: []string{"MyFunc", "failed", "something went wrong"}, + }, + "functionExecutionStartedToString": { + fn: functionExecutionStartedToString, + activity: baseActivity, + contains: []string{"MyFunc", "started"}, + }, + "workflowBillingResultToString with billing": { + fn: workflowBillingResultToString, + activity: api.Activity{ + Level: types.INFO, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "workflow_name": "MyWorkflow", + "is_billing_result": true, + "billing_reason": "function_execution", + }, + }, + contains: []string{"MyWorkflow", "billing reason", "function_execution"}, + }, + "workflowBillingResultToString excluded": { + fn: workflowBillingResultToString, + activity: api.Activity{ + Level: types.INFO, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "is_billing_result": false, + }, + }, + contains: []string{"excluded from billing"}, + }, + "workflowBotInvitedToString": { + fn: workflowBotInvitedToString, + activity: baseActivity, + contains: []string{"C123", "B456", "invited"}, + }, + "workflowCreatedFromTemplateToString": { + fn: workflowCreatedFromTemplateToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "tmpl1"}, + }, + "workflowExecutionResultToString completed": { + fn: workflowExecutionResultToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "completed"}, + }, + "workflowExecutionResultToString failed": { + fn: workflowExecutionResultToString, + activity: api.Activity{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "workflow_name": "MyWorkflow", + "error": "workflow error", + }, + }, + contains: []string{"MyWorkflow", "failed", "workflow error"}, + }, + "workflowExecutionStartedToString": { + fn: workflowExecutionStartedToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "started"}, + }, + "workflowPublishedToString": { + fn: workflowPublishedToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "published"}, + }, + "externalAuthResultToString error": { + fn: externalAuthResultToString, + activity: api.Activity{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "user_id": "U123", + "team_id": "T456", + "app_id": "A789", + "provider_key": "google", + "code": "auth_error", + "extra_message": "details here", + }, + }, + contains: []string{"failed", "U123", "T456", "auth_error", "details here"}, + }, + "externalAuthStartedToString error": { + fn: externalAuthStartedToString, + activity: api.Activity{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "user_id": "U123", + "team_id": "T456", + "app_id": "A789", + "provider_key": "google", + "code": "auth_start_error", + }, + }, + contains: []string{"failed", "U123", "auth_start_error"}, + }, + "externalAuthTokenFetchResult error": { + fn: externalAuthTokenFetchResult, + activity: api.Activity{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "user_id": "U123", + "team_id": "T456", + "app_id": "A789", + "provider_key": "google", + "code": "fetch_error", + }, + }, + contains: []string{"failed", "U123", "fetch_error"}, + }, + "workflowStepExecutionResultToString completed": { + fn: workflowStepExecutionResultToString, + activity: baseActivity, + contains: []string{"MyFunc", "completed"}, + }, + "workflowStepExecutionResultToString failed": { + fn: workflowStepExecutionResultToString, + activity: api.Activity{ + Level: types.ERROR, + ComponentID: "comp1", + TraceID: "tr1", + Payload: map[string]any{ + "function_name": "MyFunc", + "function_type": "custom", + "error": "step error", + }, + }, + contains: []string{"MyFunc", "failed"}, + }, + "workflowStepStartedToString": { + fn: workflowStepStartedToString, + activity: baseActivity, + contains: []string{"2", "5", "started"}, + }, + "workflowUnpublishedToString": { + fn: workflowUnpublishedToString, + activity: baseActivity, + contains: []string{"MyWorkflow", "unpublished"}, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := tc.fn(tc.activity) + for _, s := range tc.contains { + assert.Contains(t, result, s) + } + }) + } +} + func Test_datastoreRequestResultToString(t *testing.T) { for name, tc := range map[string]struct { activity api.Activity @@ -391,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", @@ -406,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"}`, @@ -418,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/pkg/version/version_test.go b/internal/pkg/version/version_test.go index 3c93c2cc..c4cbdc27 100644 --- a/internal/pkg/version/version_test.go +++ b/internal/pkg/version/version_test.go @@ -28,6 +28,62 @@ func TestVersion(t *testing.T) { } // Test overriding the Version with an environment variable +func TestGet(t *testing.T) { + tests := map[string]struct { + version string + expected string + }{ + "adds v prefix when missing": { + version: "1.2.3", + expected: "v1.2.3", + }, + "keeps v prefix when present": { + version: "v1.2.3", + expected: "v1.2.3", + }, + "handles empty string": { + version: "", + expected: "", + }, + "handles version with pre-release": { + version: "1.0.0-beta", + expected: "v1.0.0-beta", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + original := Version + defer func() { Version = original }() + Version = tc.version + assert.Equal(t, tc.expected, Get()) + }) + } +} + +func TestRaw(t *testing.T) { + tests := map[string]struct { + version string + expected string + }{ + "returns version unchanged with v prefix": { + version: "v1.2.3", + expected: "v1.2.3", + }, + "returns version unchanged without v prefix": { + version: "1.2.3", + expected: "1.2.3", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + original := Version + defer func() { Version = original }() + Version = tc.version + assert.Equal(t, tc.expected, Raw()) + }) + } +} + func Test_EnvTestVersion(t *testing.T) { // Setup var _EnvTestVersion = os.Getenv(EnvTestVersion) diff --git a/internal/slackdeps/browser_test.go b/internal/slackdeps/browser_test.go index 9d6574ad..e10fa819 100644 --- a/internal/slackdeps/browser_test.go +++ b/internal/slackdeps/browser_test.go @@ -24,6 +24,15 @@ import ( "github.com/stretchr/testify/require" ) +func TestNewBrowser(t *testing.T) { + t.Run("returns a GoBrowser with the provided writer", func(t *testing.T) { + buff := bytes.Buffer{} + b := NewBrowser(&buff) + assert.NotNil(t, b) + assert.Equal(t, &buff, b.out) + }) +} + func TestBrowserOpenURL(t *testing.T) { t.Run("print the url if opening the browser fails", func(t *testing.T) { path := os.Getenv("PATH") diff --git a/internal/slackdeps/os_test.go b/internal/slackdeps/os_test.go index 4fca3042..6a4f4c85 100644 --- a/internal/slackdeps/os_test.go +++ b/internal/slackdeps/os_test.go @@ -15,11 +15,146 @@ package slackdeps import ( + "os" "testing" "github.com/stretchr/testify/require" ) +func Test_Os_Getenv(t *testing.T) { + tests := map[string]struct { + key string + value string + expected string + }{ + "returns set env var": { + key: "SLACK_TEST_OS_GETENV", + value: "hello", + expected: "hello", + }, + "returns empty for unset env var": { + key: "SLACK_TEST_OS_GETENV_UNSET", + expected: "", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + o := NewOs() + if tc.value != "" { + t.Setenv(tc.key, tc.value) + } + require.Equal(t, tc.expected, o.Getenv(tc.key)) + }) + } +} + +func Test_Os_SetenvUnsetenv(t *testing.T) { + o := NewOs() + key := "SLACK_TEST_OS_SETENV" + + err := o.Setenv(key, "test_value") + require.NoError(t, err) + require.Equal(t, "test_value", o.Getenv(key)) + + err = o.Unsetenv(key) + require.NoError(t, err) + require.Equal(t, "", o.Getenv(key)) +} + +func Test_Os_LookupEnv(t *testing.T) { + tests := map[string]struct { + key string + value string + setValue bool + expectedValue string + expectedPresent bool + }{ + "returns value and true for set env var": { + key: "SLACK_TEST_OS_LOOKUPENV", + value: "present", + setValue: true, + expectedValue: "present", + expectedPresent: true, + }, + "returns empty and false for unset env var": { + key: "SLACK_TEST_OS_LOOKUPENV_UNSET", + setValue: false, + expectedValue: "", + expectedPresent: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + o := NewOs() + if tc.setValue { + t.Setenv(tc.key, tc.value) + } + val, present := o.LookupEnv(tc.key) + require.Equal(t, tc.expectedValue, val) + require.Equal(t, tc.expectedPresent, present) + }) + } +} + +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 { + 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)) + }) + } +} + +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_GetExecutionDir(t *testing.T) { tests := map[string]struct { executionDirPathAbs string diff --git a/internal/slackerror/error_test.go b/internal/slackerror/error_test.go index 06bfbd68..ba3e8600 100644 --- a/internal/slackerror/error_test.go +++ b/internal/slackerror/error_test.go @@ -495,6 +495,146 @@ func Test_WithRootCause(t *testing.T) { } } +func Test_AppendMessage(t *testing.T) { + tests := map[string]struct { + initialMsg string + msgToAppend string + expected string + }{ + "previously empty error message": { + initialMsg: "", + msgToAppend: "hello world", + expected: "hello world", + }, + "previously non-empty messages": { + initialMsg: "hello", + msgToAppend: "world", + expected: "hello\nworld", + }, + "appending an empty string": { + initialMsg: "hello world", + msgToAppend: "", + expected: "hello world", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := New("").WithMessage("%s", tc.initialMsg).AppendMessage(tc.msgToAppend) + require.Equal(t, tc.expected, err.Message) + }) + } +} + +func Test_Wrap(t *testing.T) { + tests := map[string]struct { + cause error + message string + }{ + "wraps a known error": { + cause: New("inner error"), + message: ErrAccessDenied, + }, + "wraps an unknown error": { + cause: errors.New("some standard error"), + message: "outer message", + }, + "wraps with nil cause": { + cause: nil, + message: "no cause", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := Wrap(tc.cause, tc.message) + require.NotNil(t, err) + require.Equal(t, tc.cause, err.Cause) + }) + } +} + +func Test_Wrapf(t *testing.T) { + tests := map[string]struct { + err error + format string + args []any + expected bool + }{ + "wraps non-nil error with formatted message": { + err: errors.New("root cause"), + format: "failed to %s: %d", + args: []any{"deploy", 42}, + expected: true, + }, + "returns nil for nil error": { + err: nil, + format: "should not appear", + expected: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := Wrapf(tc.err, tc.format, tc.args...) + if tc.expected { + require.NotNil(t, result) + slackErr := result.(*Error) + require.Equal(t, tc.err, slackErr.Cause) + } else { + require.Nil(t, result) + } + }) + } +} + +func Test_Is(t *testing.T) { + tests := map[string]struct { + err error + errorCode string + expected bool + }{ + "returns true when error contains the code": { + err: New(ErrAccessDenied), + errorCode: ErrAccessDenied, + expected: true, + }, + "returns false when error does not contain the code": { + err: New("some other error"), + errorCode: ErrAccessDenied, + expected: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := Is(tc.err, tc.errorCode) + require.Equal(t, tc.expected, result) + }) + } +} + +func Test_IsErrorType(t *testing.T) { + tests := map[string]struct { + err error + code string + expected bool + }{ + "matching error type returns true": { + err: New(ErrAuthToken), + code: ErrAuthToken, + expected: true, + }, + "non-matching error type returns false": { + err: errors.New("plain error"), + code: ErrAuthToken, + expected: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := IsErrorType(tc.err, tc.code) + require.Equal(t, tc.expected, result) + }) + } +} + func Test_nil_error(t *testing.T) { var testErr *Error = nil require.Nil(t, testErr.WithCode("test")) diff --git a/internal/slackhttp/http_test.go b/internal/slackhttp/http_test.go index 3d15a806..b0eb9765 100644 --- a/internal/slackhttp/http_test.go +++ b/internal/slackhttp/http_test.go @@ -53,6 +53,27 @@ func Test_NewHTTPClient(t *testing.T) { timeout: 120 * time.Second, expectedTimeout: 120 * time.Second, }, + "Zero timeout uses default timeout": { + timeout: 0, + expectedTimeout: defaultTotalTimeout, + expectedSkipTLSVerify: false, + }, + "Custom timeout is used when non-zero": { + timeout: 60 * time.Second, + expectedTimeout: 60 * time.Second, + expectedSkipTLSVerify: false, + }, + "SkipTLSVerify false keeps verification enabled": { + skipTLSVerify: false, + expectedSkipTLSVerify: false, + expectedTimeout: defaultTotalTimeout, + }, + "Retries zero returns non-retrying transport": { + retries: 0, + expectedRetries: 0, + expectedTimeout: defaultTotalTimeout, + expectedSkipTLSVerify: false, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { diff --git a/internal/style/format_test.go b/internal/style/format_test.go index 41504e9e..55571ead 100644 --- a/internal/style/format_test.go +++ b/internal/style/format_test.go @@ -19,150 +19,189 @@ import ( "os" "strings" "testing" + "time" "github.com/AlecAivazis/survey/v2/core" "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) { @@ -226,61 +265,9 @@ func TestSurveyIcons(t *testing.T) { * Example commands */ -func TestStyleFlags(t *testing.T) { - tests := map[string]struct { - charmEnabled bool - input string - expectedFunc func() string - }{ - "short and long flag with type and description": { - charmEnabled: true, - input: " -s, --long string Description text", - expectedFunc: func() string { return Yellow(" -s, --long string ") + Secondary("Description text") }, - }, - "long-only flag with description": { - charmEnabled: true, - input: " --verbose Enable verbose output", - expectedFunc: func() string { return Yellow(" --verbose ") + Secondary("Enable verbose output") }, - }, - "plain text without flag pattern returned unchanged": { - charmEnabled: true, - input: "some plain text", - expectedFunc: func() string { return "some plain text" }, - }, - "empty string returned unchanged": { - charmEnabled: true, - input: "", - expectedFunc: func() string { return "" }, - }, - "multiline flag output": { - charmEnabled: true, - input: " -a, --all Show all\n --verbose Enable verbose", - expectedFunc: func() string { - return Yellow(" -a, --all ") + Secondary("Show all") + "\n" + Yellow(" --verbose ") + Secondary("Enable verbose") - }, - }, - "charm disabled returns input unchanged": { - charmEnabled: false, - input: " -s, --long string Description text", - expectedFunc: func() string { return " -s, --long string Description text" }, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - ToggleStyles(tc.charmEnabled) - ToggleCharm(tc.charmEnabled) - defer func() { - ToggleStyles(false) - ToggleCharm(false) - }() - actual := StyleFlags(tc.input) - assert.Equal(t, tc.expectedFunc(), actual) - }) - } -} - func Test_ExampleCommandsf(t *testing.T) { tests := map[string]struct { + name string commands []ExampleCommand expected []string }{ @@ -379,28 +366,113 @@ func Test_ExampleTemplatef(t *testing.T) { } } -func Test_ExampleTemplatef_Charm(t *testing.T) { - defer func() { - ToggleStyles(false) - ToggleCharm(false) - }() - ToggleStyles(true) - ToggleCharm(true) - - template := []string{ - "# Create a new project from a selected template", - "$ slack create", - "", - "$ slack create my-project -t sample/repo-url # Create a named project", - } - expected := []string{ - fmt.Sprintf(" %s", Secondary("# Create a new project from a selected template")), - fmt.Sprintf(" %s%s", Yellow("$ "), CommandText("slack create")), - "", - fmt.Sprintf(" %s%s%s", Yellow("$ "), CommandText("slack create my-project -t sample/repo-url"), Secondary(" # Create a named project")), - } - actual := ExampleTemplatef(strings.Join(template, "\n")) - 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) + }) + } } /* diff --git a/internal/style/style_test.go b/internal/style/style_test.go index 564c2a7c..8133320d 100644 --- a/internal/style/style_test.go +++ b/internal/style/style_test.go @@ -118,166 +118,93 @@ func TestPluralize(t *testing.T) { } } -func TestToggleCharm(t *testing.T) { +func TestStyleFunctions(t *testing.T) { tests := map[string]struct { - initial bool - toggle bool - expected bool + fn func(string) string + input string + expected string }{ - "enables charm styling": { - initial: false, - toggle: true, - expected: true, + "CommandText returns non-empty": { + fn: CommandText, + input: "deploy", + }, + "Error returns non-empty": { + fn: Error, + input: "something failed", + }, + "Warning returns non-empty": { + fn: Warning, + input: "be careful", + }, + "Input returns non-empty": { + fn: Input, + input: "user input", }, - "disables charm styling": { - initial: true, - toggle: false, - expected: false, + "Bright returns non-empty": { + fn: Bright, + input: "bright text", + }, + "Bold returns non-empty": { + fn: Bold, + input: "bold text", + }, + "Darken returns non-empty": { + fn: Darken, + input: "dark text", + }, + "Highlight returns non-empty": { + fn: Highlight, + input: "important", + }, + "Underline returns non-empty": { + fn: Underline, + input: "underlined", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { - isCharmEnabled = tc.initial - defer func() { isCharmEnabled = false }() - ToggleCharm(tc.toggle) - assert.Equal(t, tc.expected, isCharmEnabled) + result := tc.fn(tc.input) + assert.Contains(t, result, tc.input) }) } } -// testStyleFunc verifies a style function returns the original text (stripped of ANSI) -// and behaves correctly across all three modes: colors off, legacy aurora, and charm lipgloss. -func testStyleFunc(t *testing.T, name string, fn func(string) string) { - t.Helper() - defer func() { - ToggleStyles(false) - ToggleCharm(false) - }() - - input := "hello" - - t.Run(name+" returns plain text when colors are off", func(t *testing.T) { - ToggleStyles(false) - ToggleCharm(false) - result := fn(input) - assert.Equal(t, input, RemoveANSI(result)) - }) - - t.Run(name+" returns styled text with legacy aurora", func(t *testing.T) { - ToggleStyles(true) - ToggleCharm(false) - result := fn(input) - assert.Contains(t, RemoveANSI(result), input) - }) - - t.Run(name+" returns styled text with charm lipgloss", func(t *testing.T) { - ToggleStyles(true) - ToggleCharm(true) - result := fn(input) - assert.Contains(t, RemoveANSI(result), input) - }) -} - -func TestColorStyleFunctions(t *testing.T) { - testStyleFunc(t, "Secondary", Secondary) - testStyleFunc(t, "CommandText", CommandText) - testStyleFunc(t, "LinkText", LinkText) - testStyleFunc(t, "Selector", Selector) - testStyleFunc(t, "Error", Error) - testStyleFunc(t, "Warning", Warning) - testStyleFunc(t, "Input", Input) - testStyleFunc(t, "Green", Green) - testStyleFunc(t, "Red", Red) - testStyleFunc(t, "Yellow", Yellow) - testStyleFunc(t, "Gray", Gray) -} - -func TestTextStyleFunctions(t *testing.T) { - testStyleFunc(t, "Bright", Bright) - testStyleFunc(t, "Bold", Bold) - testStyleFunc(t, "Darken", Darken) - testStyleFunc(t, "Highlight", Highlight) - testStyleFunc(t, "Underline", Underline) -} - -func TestHeader(t *testing.T) { - defer func() { - ToggleStyles(false) - ToggleCharm(false) - }() - - t.Run("uppercases text", func(t *testing.T) { - ToggleStyles(true) - ToggleCharm(true) - result := Header("commands") - assert.Contains(t, RemoveANSI(result), "COMMANDS") - }) - - t.Run("uppercases text with legacy", func(t *testing.T) { - ToggleStyles(true) - ToggleCharm(false) - result := Header("commands") - assert.Contains(t, RemoveANSI(result), "COMMANDS") - }) -} - func TestFaint(t *testing.T) { - defer func() { - ToggleStyles(false) - ToggleCharm(false) - }() - - t.Run("returns plain text when colors are off", func(t *testing.T) { - ToggleStyles(false) - result := Faint("hello") - assert.Equal(t, "hello", result) - }) - - t.Run("returns styled text with legacy", func(t *testing.T) { - ToggleStyles(true) - ToggleCharm(false) - result := Faint("hello") - assert.Contains(t, result, "hello") - assert.NotEqual(t, "hello", result) - }) - - t.Run("returns styled text with charm", func(t *testing.T) { - ToggleStyles(true) - ToggleCharm(true) - result := Faint("hello") - assert.Contains(t, RemoveANSI(result), "hello") - }) -} - -func TestStyler(t *testing.T) { - t.Run("returns an aurora instance", func(t *testing.T) { - s := Styler() - assert.NotNil(t, s) - }) + tests := map[string]struct { + colorShown bool + input string + }{ + "with color disabled returns input unchanged": { + colorShown: false, + input: "faint text", + }, + "with color enabled wraps with ANSI codes": { + colorShown: true, + input: "faint text", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prev := isColorShown + defer func() { isColorShown = prev }() + isColorShown = tc.colorShown + + result := Faint(tc.input) + assert.Contains(t, result, tc.input) + if tc.colorShown { + assert.Contains(t, result, "\x1b[0;2m") + } else { + assert.Equal(t, tc.input, result) + } + }) + } } -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") + } } diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 906e3295..6c69bdf0 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -111,6 +111,39 @@ func Test_Update_HasUpdate(t *testing.T) { } } +func Test_Update_SetEnabled(t *testing.T) { + u := &UpdateNotification{} + u.SetEnabled(true) + require.True(t, u.Enabled()) + u.SetEnabled(false) + require.False(t, u.Enabled()) +} + +func Test_Update_SetEnv(t *testing.T) { + u := &UpdateNotification{} + u.SetEnv("SLACK_SKIP_UPDATE") + require.Equal(t, "SLACK_SKIP_UPDATE", u.Env()) + u.SetEnv("") + require.Equal(t, "", u.Env()) +} + +func Test_Update_SetHours(t *testing.T) { + u := &UpdateNotification{} + u.SetHours(48.0) + require.Equal(t, 48.0, u.Hours()) + u.SetHours(0) + require.Equal(t, 0.0, u.Hours()) +} + +func Test_Update_Dependencies(t *testing.T) { + dep := &mockDependency{} + u := &UpdateNotification{ + dependencies: []Dependency{dep}, + } + require.Len(t, u.Dependencies(), 1) + require.Equal(t, dep, u.Dependencies()[0]) +} + func Test_Update_isIgnoredCommand(t *testing.T) { for name, tc := range map[string]struct { command string