Skip to content

Commit f63d0ff

Browse files
committed
refactor: use a slackdotenv package for reading variables from .env file
1 parent 72828ec commit f63d0ff

8 files changed

Lines changed: 145 additions & 149 deletions

File tree

internal/config/dotenv.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,10 @@ package config
1717
import (
1818
"strings"
1919

20-
"github.com/joho/godotenv"
2120
"github.com/slackapi/slack-cli/internal/version"
22-
"github.com/spf13/afero"
2321
)
2422

25-
// GetDotEnvFileVariables collects only the variables in the .env file
26-
func (c *Config) GetDotEnvFileVariables() (map[string]string, error) {
27-
variables := map[string]string{}
28-
file, err := afero.ReadFile(c.fs, ".env")
29-
if err != nil && !c.os.IsNotExist(err) {
30-
return variables, err
31-
}
32-
return godotenv.UnmarshalBytes(file)
33-
}
34-
3523
// LoadEnvironmentVariables sets flags based on their environment variable value
36-
//
37-
// Note: Values are not loaded from the .env file. Use: `GetDotEnvFileVariables`
3824
func (c *Config) LoadEnvironmentVariables() error {
3925
// Skip when dependencies are not configured
4026
if c.os == nil {

internal/config/dotenv_test.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,9 @@ import (
1818
"testing"
1919

2020
"github.com/slackapi/slack-cli/internal/slackdeps"
21-
"github.com/spf13/afero"
2221
"github.com/stretchr/testify/assert"
2322
)
2423

25-
func Test_DotEnv_GetDotEnvFileVariables(t *testing.T) {
26-
tests := map[string]struct {
27-
globalVariableName string
28-
globalVariableValue string
29-
localEnvFile string
30-
expectedValues map[string]string
31-
}{
32-
"environment file variables are read": {
33-
localEnvFile: "SLACK_VARIABLE=12\n",
34-
expectedValues: map[string]string{"SLACK_VARIABLE": "12"},
35-
},
36-
"variable casing is preserved on load": {
37-
localEnvFile: "secret_Token=Key123!\n",
38-
expectedValues: map[string]string{"secret_Token": "Key123!"},
39-
},
40-
"global environment variables are ignored": {
41-
globalVariableName: "SLACK_VARIABLE",
42-
globalVariableValue: "12",
43-
expectedValues: map[string]string{},
44-
},
45-
}
46-
for name, tc := range tests {
47-
t.Run(name, func(t *testing.T) {
48-
fs := slackdeps.NewFsMock()
49-
os := slackdeps.NewOsMock()
50-
os.AddDefaultMocks()
51-
os.Setenv(tc.globalVariableName, tc.globalVariableValue)
52-
err := afero.WriteFile(fs, ".env", []byte(tc.localEnvFile), 0600)
53-
assert.NoError(t, err)
54-
config := NewConfig(fs, os)
55-
variables, err := config.GetDotEnvFileVariables()
56-
assert.NoError(t, err)
57-
assert.Equal(t, tc.expectedValues, variables)
58-
})
59-
}
60-
}
61-
6224
func Test_DotEnv_LoadEnvironmentVariables(t *testing.T) {
6325
tableTests := map[string]struct {
6426
envName string

internal/hooks/hooks.go

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,16 @@ import (
1919
"os"
2020
"strings"
2121

22-
"github.com/joho/godotenv"
2322
"github.com/slackapi/slack-cli/internal/goutils"
2423
"github.com/slackapi/slack-cli/internal/iostreams"
24+
"github.com/slackapi/slack-cli/internal/slackdotenv"
2525
"github.com/spf13/afero"
2626
)
2727

2828
type HookExecutor interface {
2929
Execute(ctx context.Context, opts HookExecOpts) (response string, err error)
3030
}
3131

32-
// LoadDotEnv reads and parses a .env file from the working directory using the
33-
// provided filesystem. It returns nil if the file does not exist.
34-
func LoadDotEnv(fs afero.Fs) (map[string]string, error) {
35-
if fs == nil {
36-
return nil, nil
37-
}
38-
file, err := afero.ReadFile(fs, ".env")
39-
if err != nil {
40-
if os.IsNotExist(err) {
41-
return nil, nil
42-
}
43-
return nil, err
44-
}
45-
return godotenv.UnmarshalBytes(file)
46-
}
47-
4832
func GetHookExecutor(ios iostreams.IOStreamer, fs afero.Fs, cfg SDKCLIConfig) HookExecutor {
4933
protocol := cfg.Config.SupportedProtocols.Preferred()
5034
switch protocol {
@@ -74,7 +58,7 @@ func processExecOpts(ctx context.Context, opts HookExecOpts, fs afero.Fs, io ios
7458
cmdArgVars = append(cmdArgVars, goutils.MapToStringSlice(opts.Args, "--")...)
7559

7660
// Load .env file variables
77-
dotEnv, err := LoadDotEnv(fs)
61+
dotEnv, err := slackdotenv.Read(fs)
7862
if err != nil {
7963
io.PrintDebug(ctx, "Warning: failed to parse .env file: %s", err)
8064
}

internal/hooks/hooks_test.go

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -20,86 +20,9 @@ import (
2020
"github.com/slackapi/slack-cli/internal/config"
2121
"github.com/slackapi/slack-cli/internal/iostreams"
2222
"github.com/slackapi/slack-cli/internal/slackdeps"
23-
"github.com/spf13/afero"
24-
"github.com/stretchr/testify/assert"
2523
"github.com/stretchr/testify/require"
2624
)
2725

28-
func Test_Hooks_LoadDotEnv(t *testing.T) {
29-
tests := map[string]struct {
30-
fs afero.Fs
31-
dotenv string
32-
writeDotenv bool
33-
expected map[string]string
34-
expectErr bool
35-
}{
36-
"returns nil when fs is nil": {
37-
fs: nil,
38-
expected: nil,
39-
},
40-
"returns nil when .env file does not exist": {
41-
fs: afero.NewMemMapFs(),
42-
expected: nil,
43-
},
44-
"returns empty map for empty .env file": {
45-
fs: afero.NewMemMapFs(),
46-
dotenv: "",
47-
writeDotenv: true,
48-
expected: map[string]string{},
49-
},
50-
"parses single variable": {
51-
fs: afero.NewMemMapFs(),
52-
dotenv: "FOO=bar\n",
53-
writeDotenv: true,
54-
expected: map[string]string{"FOO": "bar"},
55-
},
56-
"parses multiple variables": {
57-
fs: afero.NewMemMapFs(),
58-
dotenv: "FOO=bar\nBAZ=qux\n",
59-
writeDotenv: true,
60-
expected: map[string]string{"FOO": "bar", "BAZ": "qux"},
61-
},
62-
"parses quoted values": {
63-
fs: afero.NewMemMapFs(),
64-
dotenv: `TOKEN="my secret token"` + "\n",
65-
writeDotenv: true,
66-
expected: map[string]string{"TOKEN": "my secret token"},
67-
},
68-
"skips comment lines": {
69-
fs: afero.NewMemMapFs(),
70-
dotenv: "# this is a comment\nFOO=bar\n",
71-
writeDotenv: true,
72-
expected: map[string]string{"FOO": "bar"},
73-
},
74-
"handles values with equals signs": {
75-
fs: afero.NewMemMapFs(),
76-
dotenv: "URL=https://example.com?foo=bar&baz=qux\n",
77-
writeDotenv: true,
78-
expected: map[string]string{"URL": "https://example.com?foo=bar&baz=qux"},
79-
},
80-
"handles empty values": {
81-
fs: afero.NewMemMapFs(),
82-
dotenv: "EMPTY=\n",
83-
writeDotenv: true,
84-
expected: map[string]string{"EMPTY": ""},
85-
},
86-
}
87-
for name, tc := range tests {
88-
t.Run(name, func(t *testing.T) {
89-
if tc.writeDotenv && tc.fs != nil {
90-
_ = afero.WriteFile(tc.fs, ".env", []byte(tc.dotenv), 0644)
91-
}
92-
result, err := LoadDotEnv(tc.fs)
93-
if tc.expectErr {
94-
assert.Error(t, err)
95-
} else {
96-
assert.NoError(t, err)
97-
}
98-
assert.Equal(t, tc.expected, result)
99-
})
100-
}
101-
}
102-
10326
func Test_Hooks_GetHookExecutor(t *testing.T) {
10427
tests := map[string]struct {
10528
protocolVersions ProtocolVersions

internal/pkg/platform/localserver.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/slackapi/slack-cli/internal/pkg/apps"
3636
"github.com/slackapi/slack-cli/internal/shared"
3737
"github.com/slackapi/slack-cli/internal/shared/types"
38+
"github.com/slackapi/slack-cli/internal/slackdotenv"
3839
"github.com/slackapi/slack-cli/internal/slackerror"
3940
"github.com/slackapi/slack-cli/internal/slacktrace"
4041
"github.com/slackapi/slack-cli/internal/style"
@@ -309,7 +310,7 @@ func (r *LocalServer) StartDelegate(ctx context.Context) error {
309310
var cmdArgVars = cmdArgs[1:] // omit the first item because that is the command name
310311

311312
// Load .env file variables
312-
dotEnv, err := hooks.LoadDotEnv(r.clients.Fs)
313+
dotEnv, err := slackdotenv.Read(r.clients.Fs)
313314
if err != nil {
314315
r.clients.IO.PrintDebug(ctx, "Warning: failed to parse .env file: %s", err)
315316
}

internal/pkg/platform/run.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/slackapi/slack-cli/internal/pkg/apps"
2727
"github.com/slackapi/slack-cli/internal/shared"
2828
"github.com/slackapi/slack-cli/internal/shared/types"
29+
"github.com/slackapi/slack-cli/internal/slackdotenv"
2930
"github.com/slackapi/slack-cli/internal/slackerror"
3031
"github.com/slackapi/slack-cli/internal/slacktrace"
3132
"github.com/slackapi/slack-cli/internal/style"
@@ -102,11 +103,14 @@ func Run(ctx context.Context, clients *shared.ClientFactory, runArgs RunArgs) (t
102103
}
103104

104105
// Gather environment variables from an environment file
105-
variables, err := clients.Config.GetDotEnvFileVariables()
106+
variables, err := slackdotenv.Read(clients.Fs)
106107
if err != nil {
107108
return "", slackerror.Wrap(err, slackerror.ErrLocalAppRun).
108109
WithMessage("Failed to read the local .env file")
109110
}
111+
if variables == nil {
112+
variables = map[string]string{}
113+
}
110114

111115
// Set SLACK_API_URL to the resolved host value found in the environment
112116
if value, ok := variables["SLACK_API_URL"]; ok {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package slackdotenv
16+
17+
import (
18+
"os"
19+
20+
"github.com/joho/godotenv"
21+
"github.com/spf13/afero"
22+
)
23+
24+
// Read parses a .env file from the working directory using the provided
25+
// filesystem. It returns nil if the filesystem is nil or the file does not
26+
// exist.
27+
func Read(fs afero.Fs) (map[string]string, error) {
28+
if fs == nil {
29+
return nil, nil
30+
}
31+
file, err := afero.ReadFile(fs, ".env")
32+
if err != nil {
33+
if os.IsNotExist(err) {
34+
return nil, nil
35+
}
36+
return nil, err
37+
}
38+
return godotenv.UnmarshalBytes(file)
39+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package slackdotenv
16+
17+
import (
18+
"testing"
19+
20+
"github.com/spf13/afero"
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
func Test_Read(t *testing.T) {
25+
tests := map[string]struct {
26+
fs afero.Fs
27+
dotenv string
28+
writeDotenv bool
29+
expected map[string]string
30+
expectErr bool
31+
}{
32+
"returns nil when fs is nil": {
33+
fs: nil,
34+
expected: nil,
35+
},
36+
"returns nil when .env file does not exist": {
37+
fs: afero.NewMemMapFs(),
38+
expected: nil,
39+
},
40+
"returns empty map for empty .env file": {
41+
fs: afero.NewMemMapFs(),
42+
dotenv: "",
43+
writeDotenv: true,
44+
expected: map[string]string{},
45+
},
46+
"parses single variable": {
47+
fs: afero.NewMemMapFs(),
48+
dotenv: "FOO=bar\n",
49+
writeDotenv: true,
50+
expected: map[string]string{"FOO": "bar"},
51+
},
52+
"parses multiple variables": {
53+
fs: afero.NewMemMapFs(),
54+
dotenv: "FOO=bar\nBAZ=qux\n",
55+
writeDotenv: true,
56+
expected: map[string]string{"FOO": "bar", "BAZ": "qux"},
57+
},
58+
"parses quoted values": {
59+
fs: afero.NewMemMapFs(),
60+
dotenv: `TOKEN="my secret token"` + "\n",
61+
writeDotenv: true,
62+
expected: map[string]string{"TOKEN": "my secret token"},
63+
},
64+
"skips comment lines": {
65+
fs: afero.NewMemMapFs(),
66+
dotenv: "# this is a comment\nFOO=bar\n",
67+
writeDotenv: true,
68+
expected: map[string]string{"FOO": "bar"},
69+
},
70+
"handles values with equals signs": {
71+
fs: afero.NewMemMapFs(),
72+
dotenv: "URL=https://example.com?foo=bar&baz=qux\n",
73+
writeDotenv: true,
74+
expected: map[string]string{"URL": "https://example.com?foo=bar&baz=qux"},
75+
},
76+
"handles empty values": {
77+
fs: afero.NewMemMapFs(),
78+
dotenv: "EMPTY=\n",
79+
writeDotenv: true,
80+
expected: map[string]string{"EMPTY": ""},
81+
},
82+
}
83+
for name, tc := range tests {
84+
t.Run(name, func(t *testing.T) {
85+
if tc.writeDotenv && tc.fs != nil {
86+
_ = afero.WriteFile(tc.fs, ".env", []byte(tc.dotenv), 0644)
87+
}
88+
result, err := Read(tc.fs)
89+
if tc.expectErr {
90+
assert.Error(t, err)
91+
} else {
92+
assert.NoError(t, err)
93+
}
94+
assert.Equal(t, tc.expected, result)
95+
})
96+
}
97+
}

0 commit comments

Comments
 (0)