Skip to content

Commit

Permalink
chore: add tests on configuration.go (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
rancoud authored Feb 11, 2024
1 parent e01baea commit de215ff
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 19 deletions.
8 changes: 7 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ linters-settings:
tagliatelle:
case:
rules:
json: snake
json: snake
varnamelen:
check-return: true
check-type-param: true
ignore-names:
- err
- tt
49 changes: 35 additions & 14 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"reflect"
"runtime"
Expand All @@ -17,28 +18,35 @@ var (
ErrDiscordName = errors.New("invalid json value: discord.name is empty")
ErrDiscordToken = errors.New("invalid json value: discord.token is empty")
ErrLogFilename = errors.New("invalid json value: log.filename is empty")
ErrLogLevel = errors.New("invalid json value: log.level is invalid")
)

// Support only field's type string, int, bool, []string

// Configuration contains Discord, Log and Modules parameters.
type Configuration struct {
Discord struct {
Name string `env:"DBOT_DISCORD_NAME" json:"name"`
Token string `env:"DBOT_DISCORD_TOKEN" json:"token"`
} `json:"discord"`
Log struct {
Filename string `env:"DBOT_LOG_FILENAME" json:"filename"`
Level string `env:"DBOT_LOG_LEVEL" json:"level"`
} `json:"log"`
Modules struct {
WelcomeConfiguration welcome.Configuration `json:"welcome"`
} `json:"modules"`
Discord `json:"discord"`
Log `json:"log"`
Modules `json:"modules"`
}

// ReadConfiguration read config.json file and update some values with env if found.
func ReadConfiguration(filename string) (*Configuration, error) {
filedata, err := os.ReadFile(filename)
type Discord struct {
Name string `env:"DBOT_DISCORD_NAME" json:"name"`
Token string `env:"DBOT_DISCORD_TOKEN" json:"token"`
}

type Log struct {
Filename string `env:"DBOT_LOG_FILENAME" json:"filename"`
Level string `env:"DBOT_LOG_LEVEL" json:"level"`
}

type Modules struct {
WelcomeConfiguration welcome.Configuration `json:"welcome"`
}

// ReadConfiguration read `config.json` file and update values with env if found.
func ReadConfiguration(fsys fs.FS, filename string) (*Configuration, error) {
filedata, err := fs.ReadFile(fsys, filename)
if err != nil {
return nil, fmt.Errorf("%w", err)
}
Expand Down Expand Up @@ -127,5 +135,18 @@ func checkBasicConfiguration(config Configuration) error {
return ErrLogFilename
}

isValidLogLevel := false

validLogLevelValue := []string{"", "trace", "debug", "info", "warn", "error", "fatal", "panic"}
for _, levelValue := range validLogLevelValue {
if config.Log.Level == levelValue {
isValidLogLevel = true
}
}

if !isValidLogLevel {
return ErrLogLevel
}

return nil
}
172 changes: 169 additions & 3 deletions configuration/configuration_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package configuration_test

import (
"io/fs"
"testing"
"testing/fstest"

"github.com/blueprintue/discord-bot/configuration"
"github.com/stretchr/testify/require"
Expand All @@ -10,7 +12,171 @@ import (
func TestReadConfiguration(t *testing.T) {
t.Parallel()

conf, err := configuration.ReadConfiguration("")
require.Error(t, err)
require.Nil(t, conf)
fsys := fstest.MapFS{
"config.json": {
Data: []byte(`{"discord": {"name": "foo","token": "bar"},"log": {"filename": "oof"}}`),
},
}

expected := &configuration.Configuration{
Discord: configuration.Discord{
Name: "foo",
Token: "bar",
},
Log: configuration.Log{
Filename: "oof",
},
}

actualConfiguration, actualErr := configuration.ReadConfiguration(fsys, "config.json")

require.NoError(t, actualErr)
require.Equal(t, expected, actualConfiguration)
}

// because using t.SetEnv, no `t.Parallel()` allowed here.
func TestReadConfigurationWithEnvValues(t *testing.T) {
fsys := fstest.MapFS{
"config.json": {
Data: []byte(`{"discord": {"name": "foo","token": "bar"},"log": {"filename": "oof"}}`),
},
}

t.Setenv("DBOT_DISCORD_NAME", "env_foo")
t.Setenv("DBOT_DISCORD_TOKEN", "env_bar")
t.Setenv("DBOT_LOG_FILENAME", "env_oof")
t.Setenv("DBOT_LOG_LEVEL", "panic")

expected := &configuration.Configuration{
Discord: configuration.Discord{
Name: "env_foo",
Token: "env_bar",
},
Log: configuration.Log{
Filename: "env_oof",
Level: "panic",
},
}

actualConfiguration, actualErr := configuration.ReadConfiguration(fsys, "config.json")

require.NoError(t, actualErr)
require.Equal(t, expected, actualConfiguration)
}

//nolint:funlen
func TestReadConfigurationErrors(t *testing.T) {
t.Parallel()

fsys := fstest.MapFS{
"my_directory": {
Mode: fs.ModeDir,
},
"config_invalid.json": {
Data: []byte("foobar"),
},
"config_missing_discord_name.json": {
Data: []byte(`{"discord": {"name": "","token": ""},"log": {"filename": ""}}`),
},
"config_missing_discord_token.json": {
Data: []byte(`{"discord": {"name": "foo","token": ""},"log": {"filename": ""}}`),
},
"config_missing_log_filename.json": {
Data: []byte(`{"discord": {"name": "foo","token": "bar"},"log": {"filename": ""}}`),
},
"config_invalid_log_level.json": {
Data: []byte(`{"discord": {"name": "foo","token": "bar"},"log": {"filename": "rab", "level": "none"}}`),
},
}

type args struct {
filename string
}

type want struct {
errorMessage string
}

testCases := map[string]struct {
args args
want want
}{
"should return error when no file provided": {
args: args{
filename: "",
},
want: want{
errorMessage: "open : file does not exist",
},
},
"should return error when file provided is invalid": {
args: args{
filename: "foobar",
},
want: want{
errorMessage: "open foobar: file does not exist",
},
},
"should return error when file provided is directory": {
args: args{
filename: "my_directory",
},
want: want{
errorMessage: "read my_directory: invalid argument",
},
},
"should return error when file provided is invalid json": {
args: args{
filename: "config_invalid.json",
},
want: want{
errorMessage: "invalid character 'o' in literal false (expecting 'a')",
},
},
"should return error when config missing Discord.Name": {
args: args{
filename: "config_missing_discord_name.json",
},
want: want{
errorMessage: "invalid json value: discord.name is empty",
},
},
"should return error when config missing Discord.Token": {
args: args{
filename: "config_missing_discord_token.json",
},
want: want{
errorMessage: "invalid json value: discord.token is empty",
},
},
"should return error when config missing Log.Filename": {
args: args{
filename: "config_missing_log_filename.json",
},
want: want{
errorMessage: "invalid json value: log.filename is empty",
},
},
"should return error when config has invalid Log.Level": {
args: args{
filename: "config_invalid_log_level.json",
},
want: want{
errorMessage: "invalid json value: log.level is invalid",
},
},
}

for testCaseName, testCase := range testCases {
testCaseName, testCase := testCaseName, testCase

t.Run(testCaseName, func(tt *testing.T) {
tt.Parallel()

actualConfiguration, actualErr := configuration.ReadConfiguration(fsys, testCase.args.filename)

require.ErrorContains(tt, actualErr, testCase.want.errorMessage)
require.Nil(tt, actualConfiguration)
})
}
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func main() {

log.Info().Msgf("Read configuration from file: %s", configurationFilename)

config, err := configuration.ReadConfiguration(configurationFilename)
config, err := configuration.ReadConfiguration(os.DirFS("."), configurationFilename)
if err != nil {
log.Fatal().Err(err).Msg("Error on configuration")
}
Expand Down

0 comments on commit de215ff

Please sign in to comment.