Skip to content
This repository was archived by the owner on Sep 21, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 79 additions & 52 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ const dateFormat = "2006-01-02T15:04:05-0700"
const defaultLogLevel = logrus.InfoLevel

// fieldKey is an enum-like type to represent the customfield ID keys
type fieldKey int
type FieldKey int

const (
GitHubID fieldKey = iota
GitHubNumber fieldKey = iota
GitHubLabels fieldKey = iota
GitHubStatus fieldKey = iota
GitHubReporter fieldKey = iota
LastISUpdate fieldKey = iota
GitHubID FieldKey = iota
GitHubNumber FieldKey = iota
GitHubLabels FieldKey = iota
GitHubStatus FieldKey = iota
GitHubReporter FieldKey = iota
LastISUpdate FieldKey = iota
)

// fields represents the custom field IDs of the JIRA custom fields we care about
// Fields represents the custom field IDs of the JIRA custom fields we care about
type fields struct {
githubID string
githubNumber string
Expand All @@ -48,8 +48,26 @@ type fields struct {
lastUpdate string
}

type Config interface {
LoadJIRAConfig(jira.Client) (Config, error)
GetConfigFile() string
GetConfigString(string) string
IsBasicAuth() bool
GetSinceParam() time.Time
GetLogger() logrus.Entry
IsDryRun() bool
GetTimeout() time.Duration
GetFieldID(FieldKey) string
GetFieldKey(FieldKey) string
GetProject() jira.Project
GetProjectKey() string
GetRepo() (string, string)
SetJIRAToken(*oauth1.Token)
SaveConfig() error
}

// Config is the root configuration object the application creates.
type Config struct {
type realConfig struct {
// cmdFile is the file Viper is using for its configuration (default $HOME/.issue-sync.json).
cmdFile string
// cmdConfig is the Viper configuration object created from the command line and config file.
Expand All @@ -76,92 +94,99 @@ type Config struct {
// holds the Viper configuration and the logger, and is validated. The
// JIRA configuration is not yet initialized.
func NewConfig(cmd *cobra.Command) (Config, error) {
config := Config{}

var err error
config.cmdFile, err = cmd.Flags().GetString("config")
cmdFile, err := cmd.Flags().GetString("config")
if err != nil {
config.cmdFile = ""
cmdFile = ""
}

config.cmdConfig = *newViper("issue-sync", config.cmdFile)
config.cmdConfig.BindPFlags(cmd.Flags())
cmdConfig := *newViper("issue-sync", cmdFile)
cmdConfig.BindPFlags(cmd.Flags())

config.cmdFile = config.cmdConfig.ConfigFileUsed()
cmdFile = cmdConfig.ConfigFileUsed()

config.log = *newLogger("issue-sync", config.cmdConfig.GetString("log-level"))
config := realConfig{
cmdFile: cmdFile,
cmdConfig: cmdConfig,
log: *newLogger("issue-sync", cmdConfig.GetString("log-level")),
basicAuth: false,
fieldIDs: fields{},
project: jira.Project{},
since: time.Now(),
}

if err := config.validateConfig(); err != nil {
return Config{}, err
return realConfig{}, err
}

return config, nil
}

// LoadJIRAConfig loads the JIRA configuration (project key,
// custom field IDs) from a remote JIRA server.
func (c *Config) LoadJIRAConfig(client jira.Client) error {
func (c realConfig) LoadJIRAConfig(client jira.Client) (Config, error) {
proj, res, err := client.Project.Get(c.cmdConfig.GetString("jira-project"))
if err != nil {
c.log.Errorf("Error retrieving JIRA project; check key and credentials. Error: %v", err)
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
c.log.Errorf("Error occured trying to read error body: %v", err)
return err
return c, err
}

c.log.Debugf("Error body: %s", body)
return errors.New(string(body))
return c, errors.New(string(body))
}
c.project = *proj

c.fieldIDs, err = c.getFieldIDs(client)
fields, err := c.getFieldIDs(client)
if err != nil {
return err
return c, err
}
c.fieldIDs = fields

return nil
c.project = *proj

return c, nil
}

// GetConfigFile returns the file that Viper loaded the configuration from.
func (c Config) GetConfigFile() string {
func (c realConfig) GetConfigFile() string {
return c.cmdFile
}

// GetConfigString returns a string value from the Viper configuration.
func (c Config) GetConfigString(key string) string {
func (c realConfig) GetConfigString(key string) string {
return c.cmdConfig.GetString(key)
}

// IsBasicAuth is true if we're using HTTP Basic Authentication, and false if
// we're using OAuth.
func (c Config) IsBasicAuth() bool {
func (c realConfig) IsBasicAuth() bool {
return c.basicAuth
}

// GetSinceParam returns the `since` configuration parameter, parsed as a time.Time.
func (c Config) GetSinceParam() time.Time {
func (c realConfig) GetSinceParam() time.Time {
return c.since
}

// GetLogger returns the configured application logger.
func (c Config) GetLogger() logrus.Entry {
func (c realConfig) GetLogger() logrus.Entry {
return c.log
}

// IsDryRun returns whether the application is running in dry-run mode or not.
func (c Config) IsDryRun() bool {
func (c realConfig) IsDryRun() bool {
return c.cmdConfig.GetBool("dry-run")
}

// GetTimeout returns the configured timeout on all API calls, parsed as a time.Duration.
func (c Config) GetTimeout() time.Duration {
func (c realConfig) GetTimeout() time.Duration {
return c.cmdConfig.GetDuration("timeout")
}

// GetFieldID returns the customfield ID of a JIRA custom field.
func (c Config) GetFieldID(key fieldKey) string {
func (c realConfig) GetFieldID(key FieldKey) string {
switch key {
case GitHubID:
return c.fieldIDs.githubID
Expand All @@ -181,22 +206,22 @@ func (c Config) GetFieldID(key fieldKey) string {
}

// GetFieldKey returns customfield_XXXXX, where XXXXX is the custom field ID (see GetFieldID).
func (c Config) GetFieldKey(key fieldKey) string {
func (c realConfig) GetFieldKey(key FieldKey) string {
return fmt.Sprintf("customfield_%s", c.GetFieldID(key))
}

// GetProject returns the JIRA project the user has configured.
func (c Config) GetProject() jira.Project {
func (c realConfig) GetProject() jira.Project {
return c.project
}

// GetProjectKey returns the JIRA key of the configured project.
func (c Config) GetProjectKey() string {
func (c realConfig) GetProjectKey() string {
return c.project.Key
}

// GetRepo returns the user/org name and the repo name of the configured GitHub repository.
func (c Config) GetRepo() (string, string) {
func (c realConfig) GetRepo() (string, string) {
fullName := c.cmdConfig.GetString("repo-name")
parts := strings.Split(fullName, "/")
// We check that repo-name is two parts separated by a slash in NewConfig, so this is safe
Expand All @@ -205,7 +230,7 @@ func (c Config) GetRepo() (string, string) {

// SetJIRAToken adds the JIRA OAuth tokens in the Viper configuration, ensuring that they
// are saved for future runs.
func (c Config) SetJIRAToken(token *oauth1.Token) {
func (c realConfig) SetJIRAToken(token *oauth1.Token) {
c.cmdConfig.Set("jira-token", token.Token)
c.cmdConfig.Set("jira-secret", token.TokenSecret)
}
Expand All @@ -227,7 +252,7 @@ type configFile struct {
}

// SaveConfig updates the `since` parameter to now, then saves the configuration file.
func (c Config) SaveConfig() error {
func (c realConfig) SaveConfig() error {
c.cmdConfig.Set("since", time.Now().Format(dateFormat))

var cf configFile
Expand Down Expand Up @@ -322,7 +347,7 @@ func newLogger(app, level string) *logrus.Entry {
// real URI, etc. This is the first level of checking. It does not confirm
// if a JIRA cli is running at `jira-uri` for example; that is checked
// in getJIRAClient when we actually make a call to the API.
func (c *Config) validateConfig() error {
func (c *realConfig) validateConfig() error {
// Log level and config file location are validated already

c.log.Debug("Checking config variables...")
Expand Down Expand Up @@ -354,16 +379,6 @@ func (c *Config) validateConfig() error {
} else {
c.log.Debug("Using OAuth 1.0a authentication")

token := c.cmdConfig.GetString("jira-token")
if token == "" {
return errors.New("JIRA access token required")
}

secret := c.cmdConfig.GetString("jira-secret")
if secret == "" {
return errors.New("JIRA access token secret required")
}

consumerKey := c.cmdConfig.GetString("jira-consumer-key")
if consumerKey == "" {
return errors.New("JIRA consumer key required for OAuth handshake")
Expand Down Expand Up @@ -439,7 +454,7 @@ type jiraField struct {

// getFieldIDs requests the metadata of every issue field in the JIRA
// project, and saves the IDs of the custom fields used by issue-sync.
func (c Config) getFieldIDs(client jira.Client) (fields, error) {
func (c realConfig) getFieldIDs(client jira.Client) (fields, error) {
c.log.Debug("Collecting field IDs.")
req, err := client.NewRequest("GET", "/rest/api/2/field", nil)
if err != nil {
Expand Down Expand Up @@ -489,3 +504,15 @@ func (c Config) getFieldIDs(client jira.Client) (fields, error) {

return fieldIDs, nil
}

func CreateTestConfig(v viper.Viper) Config {
return realConfig{
cmdFile: "",
cmdConfig: v,
log: *logrus.New().WithField("app-name", "issue-sync-test"),
basicAuth: false,
fieldIDs: fields{},
project: jira.Project{},
since: time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC),
}
}
119 changes: 119 additions & 0 deletions cfg/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package cfg

import (
"testing"
"time"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func TestCreateTestConfig(t *testing.T) {
v := viper.New()
v.Set("abc", 123)
v.Set("foo", "bar")

config := CreateTestConfig(*v)

if config.GetConfigString("foo") != "bar" {
t.Errorf("Config value `foo` wrong: expected \"bar\"; got %q", config.GetConfigString("foo"))
}
if config.GetConfigString("abc") != "123" {
t.Errorf("Config value `abc` wrong: expected \"123\"; got %q", config.GetConfigString("abc"))
}
}

func CreateSampleCommand() cobra.Command {
cm := cobra.Command{}

cm.Flags().String("github-token", "foobar", "")
cm.Flags().String("jira-user", "user", "")
cm.Flags().String("jira-pass", "password", "")
cm.Flags().String("repo-name", "coreos/issue-sync", "")
cm.Flags().String("jira-uri", "https://example.com/", "")
cm.Flags().String("jira-project", "TEST", "")
cm.Flags().String("since", "2017-01-01T00:00:00+0000", "")
cm.Flags().Duration("timeout", 30*time.Second, "")

return cm
}

func TestNewConfig(t *testing.T) {
cm := CreateSampleCommand()

config, err := NewConfig(&cm)
if err != nil {
t.Fatalf("Error returned from config create: %v", err)
}

if config.GetConfigString("github-token") != "foobar" {
t.Errorf("Wrong GitHub token: expected \"foobar\"; got %q", config.GetConfigString("github-token"))
}
if config.GetConfigString("jira-user") != "user" {
t.Errorf("Wrong JIRA user: expected \"user\"; got %q", config.GetConfigString("jira-user"))
}
if config.GetConfigString("jira-pass") != "password" {
t.Errorf("Wrong JIRA password: expected \"pass\"; got %q", config.GetConfigString("jira-pass"))
}
if config.GetConfigString("repo-name") != "coreos/issue-sync" {
t.Errorf("Wrong GitHub repo: expected \"coreos/issue-sync\"; got %q", config.GetConfigString("repo-name"))
}
if config.GetConfigString("jira-uri") != "https://example.com/" {
t.Errorf("Wrong JIRA URI: expected \"https://example.com/\"; got %q", config.GetConfigString("jira-uri"))
}
if config.GetConfigString("jira-project") != "TEST" {
t.Errorf("Wrong JIRA user: expected \"TEST\"; got %q", config.GetConfigString("jira-project"))
}
expectedSince := time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC)
if config.GetSinceParam().Unix() != expectedSince.Unix() {
t.Errorf("Wrong since param: expected %v; got %v", expectedSince, config.GetSinceParam())
}
if config.GetTimeout() != 30*time.Second {
t.Errorf("Wrong timeout: expected 30s; got %v", config.GetTimeout())
}

if config.GetConfigFile() != "" {
t.Errorf("Config file not empty: %v", config.GetConfigFile())
}
if config.IsDryRun() {
t.Errorf("Dry run is true; should be false!")
}
u,r := config.GetRepo()
if u != "coreos" || r != "issue-sync" {
t.Errorf("Repo is wrong! User: expected \"coreos\"; got %s. Repo: expected \"issue-sync\"; got %s", u, r)
}
}

func TestIsBasicAuth_True(t *testing.T) {
cm := CreateSampleCommand()

config, err := NewConfig(&cm)
if err != nil {
t.Fatalf("Error creating config: %v", err)
}

if !config.IsBasicAuth() {
t.Errorf("Basic auth is false; should be true!")
}
}

func TestIsBasicAuth_False(t *testing.T) {
cm := CreateSampleCommand()
cm.Flags().Set("jira-user", "")
cm.Flags().Set("jira-pass", "")

cm.Flags().String("jira-token", "token", "")
cm.Flags().String("jira-secret", "secret", "")
// validateConfig() just checks that the file can be read, not that it's a private key
cm.Flags().String("jira-private-key", "./config_test.go", "")
cm.Flags().String("jira-consumer-key", "Private Key Name", "")

config, err := NewConfig(&cm)
if err != nil {
t.Fatalf("Error creating config: %v", err)
}

if config.IsBasicAuth() {
t.Errorf("Basic auth is true; should be false!")
}
}
Loading