diff --git a/internal/api/activity_test.go b/internal/api/activity_test.go index 5eb5d69a..f000a933 100644 --- a/internal/api/activity_test.go +++ b/internal/api/activity_test.go @@ -16,10 +16,12 @@ package api import ( "testing" + "time" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/slackapi/slack-cli/internal/slackcontext" "github.com/slackapi/slack-cli/internal/slackerror" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -211,3 +213,11 @@ func Test_APIClient_ActivityInvalidJSON(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), slackerror.ErrUnableToParseJSON) } + +func Test_Activity_CreatedPretty(t *testing.T) { + // Created is in microseconds + createdMicroseconds := int64(1700000000000000) + activity := Activity{Created: createdMicroseconds} + expected := time.Unix(createdMicroseconds/1000000, 0).Format("2006-01-02 15:04:05") + assert.Equal(t, expected, activity.CreatedPretty()) +} diff --git a/internal/api/workflows_test.go b/internal/api/workflows_test.go index 5608cd28..fcc8a3c9 100644 --- a/internal/api/workflows_test.go +++ b/internal/api/workflows_test.go @@ -15,13 +15,22 @@ package api import ( + "fmt" "testing" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/slackapi/slack-cli/internal/slackcontext" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func Test_TriggerCreateOrUpdateError_Error(t *testing.T) { + err := &TriggerCreateOrUpdateError{ + Err: fmt.Errorf("something went wrong"), + } + assert.Equal(t, "something went wrong", err.Error()) +} + func TestClient_WorkflowsTriggerCreate(t *testing.T) { var inputs = make(map[string]*Input) inputs["test"] = &Input{Value: "val"} diff --git a/internal/pkg/create/template_test.go b/internal/pkg/create/template_test.go index b7cbf889..e419d693 100644 --- a/internal/pkg/create/template_test.go +++ b/internal/pkg/create/template_test.go @@ -23,6 +23,24 @@ import ( "github.com/stretchr/testify/require" ) +func Test_Template_GetSubdir(t *testing.T) { + t.Run("default is empty string", func(t *testing.T) { + tmpl := Template{} + assert.Equal(t, "", tmpl.GetSubdir()) + }) + t.Run("returns value set by SetSubdir", func(t *testing.T) { + tmpl := Template{} + tmpl.SetSubdir("subpath") + assert.Equal(t, "subpath", tmpl.GetSubdir()) + }) +} + +func Test_Template_SetSubdir(t *testing.T) { + tmpl := Template{} + tmpl.SetSubdir("custom/dir") + assert.Equal(t, "custom/dir", tmpl.GetSubdir()) +} + func TestTemplate_ResolveTemplateURL(t *testing.T) { tests := map[string]struct { url string diff --git a/internal/shared/types/app_manifest_test.go b/internal/shared/types/app_manifest_test.go index 4fe0b8c5..02a64354 100644 --- a/internal/shared/types/app_manifest_test.go +++ b/internal/shared/types/app_manifest_test.go @@ -48,6 +48,34 @@ func Test_RawJSON_UnmarshalJSON(t *testing.T) { } } +func Test_RawJSON_MarshalJSON(t *testing.T) { + tests := map[string]struct { + rawJSON RawJSON + expected string + }{ + "marshals JSONData when present": { + rawJSON: func() RawJSON { + raw := json.RawMessage(`{"name":"foo"}`) + return RawJSON{JSONData: &raw} + }(), + expected: `{"name":"foo"}`, + }, + "marshals from Data when JSONData is nil": { + rawJSON: RawJSON{Data: &yaml.MapSlice{ + {Key: "name", Value: "bar"}, + }}, + expected: `{"name":"bar"}`, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result, err := tc.rawJSON.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, tc.expected, string(result)) + }) + } +} + func Test_RawJSON_UnmarshalYAML(t *testing.T) { rawJSON := RawJSON{Data: &yaml.MapSlice{ {Key: "name", Value: "foo"}, diff --git a/internal/shared/types/slack_yaml_test.go b/internal/shared/types/slack_yaml_test.go new file mode 100644 index 00000000..f9264288 --- /dev/null +++ b/internal/shared/types/slack_yaml_test.go @@ -0,0 +1,112 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_SlackYaml_hasValidIconPath(t *testing.T) { + tests := map[string]struct { + icon string + setup func(t *testing.T, dir string) + expected bool + }{ + "valid custom icon path returns true": { + icon: "custom/icon.png", + setup: func(t *testing.T, dir string) { + require.NoError(t, os.MkdirAll(filepath.Join(dir, "custom"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "custom", "icon.png"), []byte("img"), 0o644)) + }, + expected: true, + }, + "invalid custom icon path returns false": { + icon: "missing/icon.png", + setup: func(t *testing.T, dir string) {}, + expected: false, + }, + "no icon with default assets/icon.png present returns true": { + icon: "", + setup: func(t *testing.T, dir string) { + require.NoError(t, os.MkdirAll(filepath.Join(dir, "assets"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "assets", "icon.png"), []byte("img"), 0o644)) + }, + expected: true, + }, + "no icon and no default returns true": { + icon: "", + setup: func(t *testing.T, dir string) {}, + expected: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + tc.setup(t, dir) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) + defer func() { require.NoError(t, os.Chdir(origDir)) }() + + sy := &SlackYaml{Icon: tc.icon} + assert.Equal(t, tc.expected, sy.hasValidIconPath()) + }) + } +} + +func Test_SlackYaml_Verify(t *testing.T) { + tests := map[string]struct { + icon string + setup func(t *testing.T, dir string) + expectErr bool + }{ + "valid icon returns nil error": { + icon: "icon.png", + setup: func(t *testing.T, dir string) { + require.NoError(t, os.WriteFile(filepath.Join(dir, "icon.png"), []byte("img"), 0o644)) + }, + expectErr: false, + }, + "invalid icon returns error": { + icon: "missing.png", + setup: func(t *testing.T, dir string) {}, + expectErr: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + tc.setup(t, dir) + + origDir, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) + defer func() { require.NoError(t, os.Chdir(origDir)) }() + + sy := &SlackYaml{Icon: tc.icon} + if tc.expectErr { + assert.Error(t, sy.Verify()) + } else { + assert.NoError(t, sy.Verify()) + } + }) + } +} diff --git a/internal/style/format_test.go b/internal/style/format_test.go index f45891a4..8f8edcbf 100644 --- a/internal/style/format_test.go +++ b/internal/style/format_test.go @@ -17,8 +17,10 @@ package style import ( "fmt" "os" + "runtime" "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -52,6 +54,43 @@ func TestGetKeyLength(t *testing.T) { } } +func Test_Mapf(t *testing.T) { + ToggleStyles(false) + defer ToggleStyles(false) + tests := map[string]struct { + input map[string]string + validate func(t *testing.T, result string) + }{ + "empty map returns empty string": { + input: map[string]string{}, + validate: func(t *testing.T, result string) { + assert.Equal(t, "", result) + }, + }, + "single entry formats correctly": { + input: map[string]string{"Name": "my-app"}, + validate: func(t *testing.T, result string) { + assert.Contains(t, result, "Name") + assert.Contains(t, result, "my-app") + }, + }, + "multiple entries contain all key-value pairs": { + input: map[string]string{"ID": "A123", "Name": "my-app"}, + validate: func(t *testing.T, result string) { + assert.Contains(t, result, "ID") + assert.Contains(t, result, "A123") + assert.Contains(t, result, "Name") + assert.Contains(t, result, "my-app") + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tc.validate(t, Mapf(tc.input)) + }) + } +} + func TestSectionf(t *testing.T) { tests := map[string]struct { section TextSection @@ -158,6 +197,38 @@ func TestCommandf(t *testing.T) { } } +func TestCommandf_WithColor(t *testing.T) { + tests := map[string]struct { + command string + isPrimary bool + }{ + "primary command with color does not use backticks": { + command: "deploy", + isPrimary: true, + }, + "secondary command with color does not use backticks": { + command: "deploy", + isPrimary: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + processTemp := os.Args[0] + os.Args[0] = "slack" + globalColorShown := isColorShown + isColorShown = true + defer func() { + os.Args[0] = processTemp + isColorShown = globalColorShown + }() + + formatted := Commandf(tc.command, tc.isPrimary) + assert.NotContains(t, formatted, "`") + assert.Contains(t, formatted, tc.command) + }) + } +} + func TestIndent(t *testing.T) { text := "a few spaces are expected at the start of this line, but no other changes" indented := Indent(text) @@ -255,6 +326,50 @@ func TestStyleFlags(t *testing.T) { } } +func Test_HomePath(t *testing.T) { + homeDir, err := os.UserHomeDir() + if err != nil { + t.Skip("unable to determine home directory") + } + isWindows := runtime.GOOS == "windows" + tests := map[string]struct { + input string + expected string + }{ + "path under home directory gets tilde": { + input: homeDir + "/Documents/project", + expected: func() string { + if isWindows { + return homeDir + "/Documents/project" + } + return "~/Documents/project" + }(), + }, + "path not under home directory is unchanged": { + input: "/tmp/something", + expected: "/tmp/something", + }, + "empty string returns empty string": { + input: "", + expected: "", + }, + "home directory itself gets tilde": { + input: homeDir, + expected: func() string { + if isWindows { + return homeDir + } + return "~" + }(), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.expected, HomePath(tc.input)) + }) + } +} + func Test_ExampleCommandsf(t *testing.T) { tests := map[string]struct { commands []ExampleCommand @@ -408,3 +523,73 @@ func TestLocalRunDisplayNamePlain(t *testing.T) { }) } } + +func Test_TeamSelectLabel(t *testing.T) { + ToggleStyles(false) + defer ToggleStyles(false) + result := TeamSelectLabel("workspace", "T12345") + assert.Contains(t, result, "workspace") + assert.Contains(t, result, "T12345") +} + +func Test_TimeAgo(t *testing.T) { + now := int(time.Now().Unix()) + tests := map[string]struct { + datetime int + containsUnit string + containsDir string + }{ + "seconds ago": { + datetime: now - 30, + containsUnit: "second", + containsDir: "ago", + }, + "1 minute ago": { + datetime: now - 90, + containsUnit: "minute", + containsDir: "ago", + }, + "minutes ago": { + datetime: now - 300, + containsUnit: "minute", + containsDir: "ago", + }, + "hours ago": { + datetime: now - 7200, + containsUnit: "hour", + containsDir: "ago", + }, + "days ago": { + datetime: now - 172800, + containsUnit: "day", + containsDir: "ago", + }, + "weeks ago": { + datetime: now - 1209600, + containsUnit: "week", + containsDir: "ago", + }, + "months ago": { + datetime: now - 5184000, + containsUnit: "month", + containsDir: "ago", + }, + "years ago": { + datetime: now - 63072000, + containsUnit: "year", + containsDir: "ago", + }, + "future timestamp shows until": { + datetime: now + 172800, + containsUnit: "day", + containsDir: "until", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := TimeAgo(tc.datetime) + assert.Contains(t, result, tc.containsUnit) + assert.Contains(t, result, tc.containsDir) + }) + } +}