Skip to content
Draft
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
36 changes: 15 additions & 21 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,10 @@ func (r *ApprovalRetention) defaultOff(flag *bool) bool {
return r.Enabled && flag != nil && *flag
}

func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error) {
if !strings.HasSuffix(path, "/") {
path += "/"
}

defaultConfig := &Config{
// Rebuilt per call: toml.Unmarshal merges into the pointer fields already there,
// so a shared instance would hand partially parsed values back to error paths.
func newDefaultConfig() *Config {
return &Config{
MaxReviews: nil,
MinReviews: nil,
UnskippableReviewers: []string{},
Expand All @@ -123,6 +121,12 @@ func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error)
RequireBothBranchReviewers: false,
DisableReviewStatusComments: false,
}
}

func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error) {
if !strings.HasSuffix(path, "/") {
path += "/"
}

// Use filesystem reader if none provided
if fileReader == nil {
Expand All @@ -132,25 +136,15 @@ func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error)
fileName := path + "codeowners.toml"

if !fileReader.PathExists(fileName) {
return defaultConfig, nil
return newDefaultConfig(), nil
}
file, err := fileReader.ReadFile(fileName)
if err != nil {
return defaultConfig, err
}
config := defaultConfig
err = toml.Unmarshal(file, &config)
if err != nil {
return defaultConfig, err
}
if config.Enforcement == nil {
config.Enforcement = defaultConfig.Enforcement
}
if config.AdminBypass == nil {
config.AdminBypass = defaultConfig.AdminBypass
return newDefaultConfig(), err
}
if config.ApprovalRetention == nil {
config.ApprovalRetention = defaultConfig.ApprovalRetention
config := newDefaultConfig()
if err := toml.Unmarshal(file, config); err != nil {
return newDefaultConfig(), err
}
return config, nil
}
39 changes: 39 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,45 @@ func TestReadConfigFileError(t *testing.T) {
}
}

// Callers read the config as a security boundary and only warn on error, so a
// half-parsed file leaking through would let a malformed config relax enforcement.
func TestReadConfigInvalidTomlReturnsDefaults(t *testing.T) {
testDir := t.TempDir()
content := `
[enforcement]
approval = true
fail_check = false
[approval_retention]
enabled = true
max_reviews = invalid
`
err := os.WriteFile(filepath.Join(testDir, "codeowners.toml"), []byte(content), 0644)
if err != nil {
t.Fatalf("failed to write test config: %v", err)
}

config, err := ReadConfig(testDir, nil)
if err == nil {
t.Fatal("expected a parse error")
}
if config == nil {
t.Fatal("expected a config alongside the error")
}

if config.Enforcement == nil {
t.Fatal("expected Enforcement to be set")
}
if config.Enforcement.Approval || !config.Enforcement.FailCheck {
t.Errorf("Enforcement: expected default {Approval:false FailCheck:true}, got %+v", *config.Enforcement)
}
if config.ApprovalRetention == nil {
t.Fatal("expected ApprovalRetention to be set")
}
if config.ApprovalRetention.Enabled {
t.Error("ApprovalRetention.Enabled: expected default false")
}
Comment on lines +278 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Incomplete default regression coverage

The malformed-config test checks only Enforcement and ApprovalRetention.Enabled, leaving fields such as AdminBypass, review limits, and slice-valued defaults outside the stated all-fields invariant. Comparing the complete result with a pristine default would prevent an untested field from leaking through a future regression while this test still passes.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}

func TestApprovalRetention(t *testing.T) {
type resolved struct {
whitespace bool
Expand Down
Loading