Skip to content

Commit 81bf7a6

Browse files
fix: return real defaults when codeowners.toml fails to parse
ReadConfig handed the same default Config to the TOML parser and to its own error paths. go-toml dereferences a non-nil pointer in place rather than allocating, so a file which failed halfway left its partially parsed values in the instance the error path then returned. The caller logs "using default config" and carries on, with whatever the parser managed to read before it failed. A malformed file could therefore turn enforcement off, or widen admin bypass, while the logs said defaults were in force. Build a fresh instance per call instead: the parser gets its own, and every error path builds another. The nil-section fixups go with it, since defaults can no longer be clobbered and TOML cannot express a null table. The regression test asserts the whole struct against pristine defaults rather than sampling a couple of sections. On the unfixed code fourteen fields survive the failed parse, including enforcement.approval and admin_bypass.enabled. Coverage badge regenerated.
1 parent 86a9bc8 commit 81bf7a6

4 files changed

Lines changed: 112 additions & 20 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Code Ownership & Review Assignment Tool - GitHub CODEOWNERS but better
44

55
[![Go Report Card](https://goreportcard.com/badge/github.com/multimediallc/codeowners-plus)](https://goreportcard.com/report/github.com/multimediallc/codeowners-plus?kill_cache=1)
66
[![Tests](https://github.com/multimediallc/codeowners-plus/actions/workflows/go.yml/badge.svg)](https://github.com/multimediallc/codeowners-plus/actions/workflows/go.yml)
7-
![Coverage](https://img.shields.io/badge/Coverage-82.6%25-brightgreen)
7+
![Coverage](https://img.shields.io/badge/Coverage-82.7%25-brightgreen)
88
[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
99
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
1010

internal/config/config.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,8 @@ type AdminBypass struct {
3434
AllowedUsers []string `toml:"allowed_users"`
3535
}
3636

37-
func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error) {
38-
if !strings.HasSuffix(path, "/") {
39-
path += "/"
40-
}
41-
42-
defaultConfig := &Config{
37+
func newDefaultConfig() *Config {
38+
return &Config{
4339
MaxReviews: nil,
4440
MinReviews: nil,
4541
UnskippableReviewers: []string{},
@@ -53,6 +49,12 @@ func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error)
5349
RequireBothBranchReviewers: false,
5450
DisableReviewStatusComments: false,
5551
}
52+
}
53+
54+
func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error) {
55+
if !strings.HasSuffix(path, "/") {
56+
path += "/"
57+
}
5658

5759
// Use filesystem reader if none provided
5860
if fileReader == nil {
@@ -62,22 +64,15 @@ func ReadConfig(path string, fileReader codeowners.FileReader) (*Config, error)
6264
fileName := path + "codeowners.toml"
6365

6466
if !fileReader.PathExists(fileName) {
65-
return defaultConfig, nil
67+
return newDefaultConfig(), nil
6668
}
6769
file, err := fileReader.ReadFile(fileName)
6870
if err != nil {
69-
return defaultConfig, err
70-
}
71-
config := defaultConfig
72-
err = toml.Unmarshal(file, &config)
73-
if err != nil {
74-
return defaultConfig, err
75-
}
76-
if config.Enforcement == nil {
77-
config.Enforcement = defaultConfig.Enforcement
71+
return newDefaultConfig(), err
7872
}
79-
if config.AdminBypass == nil {
80-
config.AdminBypass = defaultConfig.AdminBypass
73+
config := newDefaultConfig()
74+
if err := toml.Unmarshal(file, config); err != nil {
75+
return newDefaultConfig(), err
8176
}
8277
return config, nil
8378
}

internal/config/config_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package owners
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
7+
"strings"
68
"testing"
79
)
810

@@ -250,6 +252,101 @@ func TestReadConfigFileError(t *testing.T) {
250252
}
251253
}
252254

255+
func TestReadConfigInvalidTomlReturnsDefaults(t *testing.T) {
256+
testDir := t.TempDir()
257+
content := `
258+
max_reviews = 9
259+
min_reviews = 9
260+
unskippable_reviewers = ["@someone"]
261+
ignore = ["vendor"]
262+
high_priority_labels = ["urgent"]
263+
detailed_reviewers = true
264+
disable_smart_dismissal = true
265+
require_both_branch_reviewers = true
266+
suppress_unowned_warning = true
267+
allow_self_approval = true
268+
self_approval_via_teams = true
269+
disable_review_status_comments = true
270+
[enforcement]
271+
approval = true
272+
fail_check = false
273+
[admin_bypass]
274+
enabled = true
275+
allowed_users = ["someone"]
276+
trailing = invalid
277+
`
278+
if err := os.WriteFile(filepath.Join(testDir, "codeowners.toml"), []byte(content), 0644); err != nil {
279+
t.Fatalf("failed to write test config: %v", err)
280+
}
281+
282+
config, err := ReadConfig(testDir, nil)
283+
if err == nil {
284+
t.Fatal("expected a parse error")
285+
}
286+
if config == nil {
287+
t.Fatal("expected a config alongside the error")
288+
}
289+
290+
if diff := configDiff(config, newDefaultConfig()); diff != "" {
291+
t.Errorf("expected pristine defaults after a failed parse, got %s", diff)
292+
}
293+
}
294+
295+
func configDiff(got, want *Config) string {
296+
problems := make([]string, 0, 8)
297+
add := func(format string, args ...any) {
298+
problems = append(problems, fmt.Sprintf(format, args...))
299+
}
300+
if got.MaxReviews != nil {
301+
add("MaxReviews=%d (want nil)", *got.MaxReviews)
302+
}
303+
if got.MinReviews != nil {
304+
add("MinReviews=%d (want nil)", *got.MinReviews)
305+
}
306+
if !sliceEqual(got.UnskippableReviewers, want.UnskippableReviewers) {
307+
add("UnskippableReviewers=%v", got.UnskippableReviewers)
308+
}
309+
if !sliceEqual(got.Ignore, want.Ignore) {
310+
add("Ignore=%v", got.Ignore)
311+
}
312+
if !sliceEqual(got.HighPriorityLabels, want.HighPriorityLabels) {
313+
add("HighPriorityLabels=%v", got.HighPriorityLabels)
314+
}
315+
if got.Enforcement == nil {
316+
add("Enforcement=nil")
317+
} else if *got.Enforcement != *want.Enforcement {
318+
add("Enforcement=%+v (want %+v)", *got.Enforcement, *want.Enforcement)
319+
}
320+
if got.AdminBypass == nil {
321+
add("AdminBypass=nil")
322+
} else {
323+
if got.AdminBypass.Enabled != want.AdminBypass.Enabled {
324+
add("AdminBypass.Enabled=%v", got.AdminBypass.Enabled)
325+
}
326+
if !sliceEqual(got.AdminBypass.AllowedUsers, want.AdminBypass.AllowedUsers) {
327+
add("AdminBypass.AllowedUsers=%v", got.AdminBypass.AllowedUsers)
328+
}
329+
}
330+
for _, f := range []struct {
331+
name string
332+
got bool
333+
want bool
334+
}{
335+
{"DetailedReviewers", got.DetailedReviewers, want.DetailedReviewers},
336+
{"DisableSmartDismissal", got.DisableSmartDismissal, want.DisableSmartDismissal},
337+
{"RequireBothBranchReviewers", got.RequireBothBranchReviewers, want.RequireBothBranchReviewers},
338+
{"SuppressUnownedWarning", got.SuppressUnownedWarning, want.SuppressUnownedWarning},
339+
{"AllowSelfApproval", got.AllowSelfApproval, want.AllowSelfApproval},
340+
{"SelfApprovalViaTeams", got.SelfApprovalViaTeams, want.SelfApprovalViaTeams},
341+
{"DisableReviewStatusComments", got.DisableReviewStatusComments, want.DisableReviewStatusComments},
342+
} {
343+
if f.got != f.want {
344+
add("%s=%v (want %v)", f.name, f.got, f.want)
345+
}
346+
}
347+
return strings.Join(problems, "; ")
348+
}
349+
253350
// Helper functions
254351
func intPtr(i int) *int {
255352
return &i

internal/git/diff_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ Binary files a/assets/img/offline.png and b/assets/img/offline.png differ`,
129129
expectedErr: false,
130130
expectedFiles: 2,
131131
expectedHunks: map[string]int{
132-
"file1.go": 1,
132+
"file1.go": 1,
133133
"assets/img/offline.png": 0,
134134
},
135135
},

0 commit comments

Comments
 (0)