diff --git a/internal/brokers/broker_test.go b/internal/brokers/broker_test.go index ea2c04024..3d846b928 100644 --- a/internal/brokers/broker_test.go +++ b/internal/brokers/broker_test.go @@ -39,6 +39,8 @@ var supportedLayouts = map[string]map[string]string{ func TestNewBroker(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) + tests := map[string]struct { configFile string @@ -83,7 +85,8 @@ func TestNewBroker(t *testing.T) { gotString := fmt.Sprintf("ID: %s\nName: %s\nBrand Icon: %s\n", got.ID, got.Name, got.BrandIconPath) - wantString := testutils.LoadWithUpdateFromGolden(t, gotString) + wantString := testutils.LoadWithUpdateFromGolden(t, gotString, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, wantString, gotString, "NewBroker should return the expected broker, but did not") }) } @@ -93,6 +96,7 @@ func TestGetAuthenticationModes(t *testing.T) { t.Parallel() b := newBrokerForTests(t, "", "") + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sessionID string @@ -135,7 +139,8 @@ func TestGetAuthenticationModes(t *testing.T) { require.NoError(t, err, "Post: error when marshaling result") got := "MODES:\n" + string(modesStr) + "\n\nVALIDATORS:\n" + b.LayoutValidatorsString(prefixID(t, tc.sessionID)) - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "GetAuthenticationModes should return the expected modes, but did not") }) } @@ -145,6 +150,7 @@ func TestSelectAuthenticationMode(t *testing.T) { t.Parallel() b := newBrokerForTests(t, "", "") + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sessionID string @@ -195,7 +201,8 @@ func TestSelectAuthenticationMode(t *testing.T) { } require.NoError(t, err, "SelectAuthenticationMode should not return an error, but did") - wantUI := testutils.LoadWithUpdateFromGoldenYAML(t, gotUI) + wantUI := testutils.LoadWithUpdateFromGoldenYAML(t, gotUI, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, wantUI, gotUI, "SelectAuthenticationMode should return the expected mode UI, but did not") }) } @@ -205,6 +212,7 @@ func TestIsAuthenticated(t *testing.T) { t.Parallel() b := newBrokerForTests(t, "", "") + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sessionID string @@ -274,7 +282,8 @@ func TestIsAuthenticated(t *testing.T) { <-done gotStr := firstCallReturn + secondCallReturn - want := testutils.LoadWithUpdateFromGolden(t, gotStr) + want := testutils.LoadWithUpdateFromGolden(t, gotStr, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, gotStr, "IsAuthenticated should return the expected combined data, but did not") }) } @@ -321,6 +330,7 @@ func TestUserPreCheck(t *testing.T) { t.Parallel() b := newBrokerForTests(t, "", "") + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string @@ -342,7 +352,8 @@ func TestUserPreCheck(t *testing.T) { } require.NoError(t, err, "UserPreCheck should not return an error, but did") - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "UserPreCheck should return the expected data, but did not") }) } diff --git a/internal/brokers/internal_test.go b/internal/brokers/internal_test.go index a33cbc4b3..cda071721 100644 --- a/internal/brokers/internal_test.go +++ b/internal/brokers/internal_test.go @@ -63,6 +63,8 @@ var ( func TestUnmarshalUserInfo(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) + tests := map[string]struct { jsonInput string @@ -89,7 +91,8 @@ func TestUnmarshalUserInfo(t *testing.T) { gotJSON, err := json.Marshal(got) require.NoError(t, err, "Marshaling the result should not return an error, but did") - want := testutils.LoadWithUpdateFromGolden(t, string(gotJSON)) + want := testutils.LoadWithUpdateFromGolden(t, string(gotJSON), + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, string(gotJSON), "unmarshalUserInfo should return the expected format, but did not") }) } diff --git a/internal/brokers/manager_test.go b/internal/brokers/manager_test.go index 2fd161972..1622c4145 100644 --- a/internal/brokers/manager_test.go +++ b/internal/brokers/manager_test.go @@ -19,6 +19,7 @@ var ( ) func TestNewManager(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { brokerConfigDir string configuredBrokers []string @@ -58,7 +59,8 @@ func TestNewManager(t *testing.T) { brokers = append(brokers, broker.Name) } - want := testutils.LoadWithUpdateFromGoldenYAML(t, brokers) + want := testutils.LoadWithUpdateFromGoldenYAML(t, brokers, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, brokers, "NewManager should return the expected brokers, but did not") }) } @@ -169,6 +171,7 @@ func TestBrokerFromSessionID(t *testing.T) { func TestNewSession(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { brokerID string username string @@ -244,7 +247,8 @@ func TestNewSession(t *testing.T) { // Replaces the autogenerated part of the ID with a placeholder before saving the file. gotStr := fmt.Sprintf("ID: %s\nEncryption Key: %s\n", strings.ReplaceAll(gotID, wantBroker.ID, "BROKER_ID"), gotEKey) - wantStr := testutils.LoadWithUpdateFromGolden(t, gotStr) + wantStr := testutils.LoadWithUpdateFromGolden(t, gotStr, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, wantStr, gotStr, "NewSession should return the expected session, but did not") gotBroker, err := m.BrokerFromSessionID(gotID) diff --git a/internal/services/nss/nss_test.go b/internal/services/nss/nss_test.go index 4c4481b05..df96346d1 100644 --- a/internal/services/nss/nss_test.go +++ b/internal/services/nss/nss_test.go @@ -43,6 +43,7 @@ func TestNewService(t *testing.T) { } func TestGetPasswdByName(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string @@ -73,12 +74,14 @@ func TestGetPasswdByName(t *testing.T) { client := newNSSClient(t, tc.sourceDB, false) got, err := client.GetPasswdByName(context.Background(), &authd.GetPasswdByNameRequest{Name: tc.username, ShouldPreCheck: tc.shouldPreCheck}) - requireExpectedResult(t, "GetPasswdByName", got, err, tc.wantErr, tc.wantErrNotExists) + requireExpectedResult(t, &goldenTracker, "GetPasswdByName", got, err, tc.wantErr, tc.wantErrNotExists) }) } } +//nolint:dupl // This is not a duplicate test func TestGetPasswdByUID(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { uid uint32 @@ -101,12 +104,13 @@ func TestGetPasswdByUID(t *testing.T) { client := newNSSClient(t, tc.sourceDB, false) got, err := client.GetPasswdByUID(context.Background(), &authd.GetByIDRequest{Id: tc.uid}) - requireExpectedResult(t, "GetPasswdByUID", got, err, tc.wantErr, tc.wantErrNotExists) + requireExpectedResult(t, &goldenTracker, "GetPasswdByUID", got, err, tc.wantErr, tc.wantErrNotExists) }) } } func TestGetPasswdEntries(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sourceDB string @@ -125,12 +129,14 @@ func TestGetPasswdEntries(t *testing.T) { client := newNSSClient(t, tc.sourceDB, false) got, err := client.GetPasswdEntries(context.Background(), &authd.Empty{}) - requireExpectedEntriesResult(t, "GetPasswdEntries", got.GetEntries(), err, tc.wantErr) + requireExpectedEntriesResult(t, &goldenTracker, "GetPasswdEntries", got.GetEntries(), err, tc.wantErr) }) } } +//nolint:dupl // This is not a duplicate test func TestGetGroupByName(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { groupname string @@ -153,12 +159,14 @@ func TestGetGroupByName(t *testing.T) { client := newNSSClient(t, tc.sourceDB, false) got, err := client.GetGroupByName(context.Background(), &authd.GetGroupByNameRequest{Name: tc.groupname}) - requireExpectedResult(t, "GetGroupByName", got, err, tc.wantErr, tc.wantErrNotExists) + requireExpectedResult(t, &goldenTracker, "GetGroupByName", got, err, tc.wantErr, tc.wantErrNotExists) }) } } +//nolint:dupl // This is not a duplicate test func TestGetGroupByGID(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { gid uint32 @@ -181,12 +189,13 @@ func TestGetGroupByGID(t *testing.T) { client := newNSSClient(t, tc.sourceDB, false) got, err := client.GetGroupByGID(context.Background(), &authd.GetByIDRequest{Id: tc.gid}) - requireExpectedResult(t, "GetGroupByGID", got, err, tc.wantErr, tc.wantErrNotExists) + requireExpectedResult(t, &goldenTracker, "GetGroupByGID", got, err, tc.wantErr, tc.wantErrNotExists) }) } } func TestGetGroupEntries(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sourceDB string @@ -205,12 +214,13 @@ func TestGetGroupEntries(t *testing.T) { client := newNSSClient(t, tc.sourceDB, false) got, err := client.GetGroupEntries(context.Background(), &authd.Empty{}) - requireExpectedEntriesResult(t, "GetGroupEntries", got.GetEntries(), err, tc.wantErr) + requireExpectedEntriesResult(t, &goldenTracker, "GetGroupEntries", got.GetEntries(), err, tc.wantErr) }) } } func TestGetShadowByName(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string @@ -235,12 +245,13 @@ func TestGetShadowByName(t *testing.T) { client := newNSSClient(t, tc.sourceDB, tc.currentUserNotRoot) got, err := client.GetShadowByName(context.Background(), &authd.GetShadowByNameRequest{Name: tc.username}) - requireExpectedResult(t, "GetShadowByName", got, err, tc.wantErr, tc.wantErrNotExists) + requireExpectedResult(t, &goldenTracker, "GetShadowByName", got, err, tc.wantErr, tc.wantErrNotExists) }) } } func TestGetShadowEntries(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sourceDB string currentUserNotRoot bool @@ -261,7 +272,7 @@ func TestGetShadowEntries(t *testing.T) { client := newNSSClient(t, tc.sourceDB, tc.currentUserNotRoot) got, err := client.GetShadowEntries(context.Background(), &authd.Empty{}) - requireExpectedEntriesResult(t, "GetShadowEntries", got.GetEntries(), err, tc.wantErr) + requireExpectedEntriesResult(t, &goldenTracker, "GetShadowEntries", got.GetEntries(), err, tc.wantErr) }) } } @@ -354,7 +365,7 @@ func newBrokersManagerForTests(t *testing.T) *brokers.Manager { } // requireExpectedResult asserts expected behaviour from any get* NSS requests and can update them from golden content. -func requireExpectedResult[T authd.PasswdEntry | authd.GroupEntry | authd.ShadowEntry](t *testing.T, funcName string, got *T, err error, wantErr, wantErrNotExists bool) { +func requireExpectedResult[T authd.PasswdEntry | authd.GroupEntry | authd.ShadowEntry](t *testing.T, goldenTracker *testutils.GoldenTracker, funcName string, got *T, err error, wantErr, wantErrNotExists bool) { t.Helper() if wantErr { @@ -368,12 +379,13 @@ func requireExpectedResult[T authd.PasswdEntry | authd.GroupEntry | authd.Shadow } require.NoError(t, err, fmt.Sprintf("%s should not return an error, but did", funcName)) - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(goldenTracker)) requireExportedEquals(t, want, got, fmt.Sprintf("%s should return the expected entry, but did not", funcName)) } // requireExpectedEntriesResult asserts expected behaviour from any get* NSS request returning a list and can update them from golden content. -func requireExpectedEntriesResult[T authd.PasswdEntry | authd.GroupEntry | authd.ShadowEntry](t *testing.T, funcName string, got []*T, err error, wantErr bool) { +func requireExpectedEntriesResult[T authd.PasswdEntry | authd.GroupEntry | authd.ShadowEntry](t *testing.T, goldenTracker *testutils.GoldenTracker, funcName string, got []*T, err error, wantErr bool) { t.Helper() if wantErr { @@ -385,7 +397,8 @@ func requireExpectedEntriesResult[T authd.PasswdEntry | authd.GroupEntry | authd } require.NoError(t, err, fmt.Sprintf("%s should not return an error, but did", funcName)) - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(goldenTracker)) if len(want) != len(got) { require.Equal(t, len(want), len(got), "Not the expected number of elements in the list. Wanted: %v\nGot: %v", want, got) } diff --git a/internal/services/pam/pam_test.go b/internal/services/pam/pam_test.go index d8fbb798d..83dea7db0 100644 --- a/internal/services/pam/pam_test.go +++ b/internal/services/pam/pam_test.go @@ -78,6 +78,7 @@ func TestNewService(t *testing.T) { func TestAvailableBrokers(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { currentUserNotRoot bool @@ -106,7 +107,8 @@ func TestAvailableBrokers(t *testing.T) { for _, broker := range got { broker.Id = broker.Name + "_ID" } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "AvailableBrokers returned unexpected brokers") }) } @@ -183,6 +185,7 @@ func TestGetPreviousBroker(t *testing.T) { func TestSelectBroker(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { brokerID string username string @@ -247,7 +250,8 @@ func TestSelectBroker(t *testing.T) { got := fmt.Sprintf("ID: %s\nEncryption Key: %s\n", strings.ReplaceAll(sbResp.GetSessionId(), tc.brokerID, "BROKER_ID"), sbResp.GetEncryptionKey()) - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "SelectBroker returned an unexpected response") }) } @@ -256,6 +260,7 @@ func TestSelectBroker(t *testing.T) { func TestGetAuthenticationModes(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sessionID string supportedUILayouts []*authd.UILayout @@ -312,7 +317,8 @@ func TestGetAuthenticationModes(t *testing.T) { require.NoError(t, err, "GetAuthenticationModes should not return an error, but did") got := gamResp.GetAuthenticationModes() - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "GetAuthenticationModes returned an unexpected response") }) } @@ -321,6 +327,7 @@ func TestGetAuthenticationModes(t *testing.T) { func TestSelectAuthenticationMode(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sessionID string authMode string @@ -402,13 +409,15 @@ func TestSelectAuthenticationMode(t *testing.T) { require.NoError(t, err, "SelectAuthenticationMode should not return an error, but did") got := samResp.GetUiLayoutInfo() - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "SelectAuthenticationMode should have returned the expected UI layout") }) } } func TestIsAuthenticated(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { sessionID string existingDB string @@ -523,16 +532,22 @@ func TestIsAuthenticated(t *testing.T) { got := firstCall + secondCall got = permissionstestutils.IdempotentPermissionError(got) - want := testutils.LoadWithUpdateFromGolden(t, got, testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "IsAuthenticated"))) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "IsAuthenticated")), + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "IsAuthenticated should return the expected combined data, but did not") // Check that cache has been updated too. gotDB, err := cachetestutils.DumpToYaml(userstestutils.GetManagerCache(m)) require.NoError(t, err, "Setup: failed to dump database for comparing") - wantDB := testutils.LoadWithUpdateFromGolden(t, gotDB, testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "cache.db"))) + wantDB := testutils.LoadWithUpdateFromGolden(t, gotDB, + testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "cache.db")), + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, wantDB, gotDB, "IsAuthenticated should update the cache database as expected") - localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, filepath.Join(testutils.GoldenPath(t), "gpasswd.output")) + gpasswdGolden := filepath.Join(testutils.GoldenPath(t), "gpasswd.output") + localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, gpasswdGolden) + goldenTracker.MarkUsed(t, testutils.WithGoldenPath(gpasswdGolden)) }) } } @@ -541,6 +556,7 @@ func TestIDGeneration(t *testing.T) { t.Parallel() usernamePrefix := t.Name() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string }{ @@ -570,7 +586,8 @@ func TestIDGeneration(t *testing.T) { gotDB, err := cachetestutils.DumpToYaml(userstestutils.GetManagerCache(m)) require.NoError(t, err, "Setup: failed to dump database for comparing") - wantDB := testutils.LoadWithUpdateFromGolden(t, gotDB, testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "cache.db"))) + wantDB := testutils.LoadWithUpdateFromGolden(t, gotDB, testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "cache.db")), + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, wantDB, gotDB, "IsAuthenticated should update the cache database as expected") }) } @@ -579,6 +596,7 @@ func TestIDGeneration(t *testing.T) { func TestSetDefaultBrokerForUser(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string brokerID string @@ -630,7 +648,8 @@ func TestSetDefaultBrokerForUser(t *testing.T) { // Check that cache has been updated too. gotDB, err := cachetestutils.DumpToYaml(userstestutils.GetManagerCache(m)) require.NoError(t, err, "Setup: failed to dump database for comparing") - wantDB := testutils.LoadWithUpdateFromGolden(t, gotDB, testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "cache.db"))) + wantDB := testutils.LoadWithUpdateFromGolden(t, gotDB, testutils.WithGoldenPath(filepath.Join(testutils.GoldenPath(t), "cache.db")), + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, wantDB, gotDB, "SetDefaultBrokerForUser should update the cache database as expected") }) } diff --git a/internal/testutils/args.go b/internal/testutils/args.go index 00cdca3d5..493ad4b64 100644 --- a/internal/testutils/args.go +++ b/internal/testutils/args.go @@ -17,6 +17,8 @@ var ( isVerboseOnce sync.Once sleepMultiplier float64 sleepMultiplierOnce sync.Once + runningTests []string + runningTestsOnce sync.Once ) // IsVerbose returns whether the tests are running in verbose mode. @@ -33,6 +35,20 @@ func IsVerbose() bool { return isVerbose } +// RunningTests returns whether the tests are selected to run. +func RunningTests() []string { + runningTestsOnce.Do(func() { + for _, arg := range os.Args { + value, ok := strings.CutPrefix(arg, "-test.run=") + if !ok { + continue + } + runningTests = append(runningTests, value) + } + }) + return runningTests +} + func haveBuildFlag(flag string) bool { b, ok := debug.ReadBuildInfo() if !ok { diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index c0fcdf011..8c8d386fd 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -1,12 +1,18 @@ package testutils import ( + "context" + "io/fs" + "maps" "os" "path/filepath" + "slices" "strings" + "sync" "testing" "github.com/stretchr/testify/require" + "github.com/ubuntu/authd/internal/log" "gopkg.in/yaml.v3" ) @@ -25,7 +31,8 @@ func init() { } type goldenOptions struct { - goldenPath string + goldenPath string + goldenTracker *GoldenTracker } // GoldenOption is a supported option reference to change the golden files comparison. @@ -40,9 +47,16 @@ func WithGoldenPath(path string) GoldenOption { } } -// LoadWithUpdateFromGolden loads the element from a plaintext golden file. -// It will update the file if the update flag is used prior to loading it. -func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) string { +// WithGoldenTracker sets the golden tracker to mark the golden as used. +func WithGoldenTracker(gt *GoldenTracker) GoldenOption { + return func(o *goldenOptions) { + if gt != nil { + o.goldenTracker = gt + } + } +} + +func parseOptions(t *testing.T, opts ...GoldenOption) goldenOptions { t.Helper() o := goldenOptions{ @@ -53,6 +67,16 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) s opt(&o) } + return o +} + +// LoadWithUpdateFromGolden loads the element from a plaintext golden file. +// It will update the file if the update flag is used prior to loading it. +func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) string { + t.Helper() + + o := parseOptions(t, opts...) + if update { t.Logf("updating golden file %s", o.goldenPath) err := os.MkdirAll(filepath.Dir(o.goldenPath), 0750) @@ -64,6 +88,10 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) s want, err := os.ReadFile(o.goldenPath) require.NoError(t, err, "Cannot load golden file") + if o.goldenTracker != nil { + o.goldenTracker.MarkUsed(t, WithGoldenPath(o.goldenPath)) + } + return string(want) } @@ -121,3 +149,91 @@ func GoldenPath(t *testing.T) string { func UpdateEnabled() bool { return update } + +// GoldenTracker is a structure to track used golden files in tests. +type GoldenTracker struct { + mu *sync.Mutex + used map[string]struct{} +} + +// NewGoldenTracker create a new [GoldenTracker] that checks if golden files are used. +func NewGoldenTracker(t *testing.T) GoldenTracker { + t.Helper() + + gt := GoldenTracker{ + mu: &sync.Mutex{}, + used: make(map[string]struct{}), + } + + require.False(t, strings.Contains(t.Name(), "/"), + "Setup: %T should be used from a parent test, %s is not", gt, t.Name()) + + if slices.ContainsFunc(RunningTests(), func(r string) bool { + prefix := t.Name() + "/" + return strings.HasPrefix(r, prefix) && len(r) > len(prefix) + }) { + t.Logf("%T disabled, can't work on partial tests", gt) + return gt + } + + t.Cleanup(func() { + if t.Failed() { + return + } + + goldenPath := GoldenPath(t) + + var entries []string + err := filepath.WalkDir(goldenPath, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + log.Errorf(context.TODO(), "TearDown: Reading test golden files %s: %v", path, err) + t.FailNow() + } + if path == goldenPath { + return nil + } + entries = append(entries, path) + return nil + }) + if err != nil { + log.Errorf(context.TODO(), "TearDown: Walking test golden files %s: %v", goldenPath, err) + t.FailNow() + } + + gt.mu.Lock() + defer gt.mu.Unlock() + + t.Log("Checking golden files in", goldenPath) + var unused []string + for _, e := range entries { + if _, ok := gt.used[e]; ok { + continue + } + unused = append(unused, e) + } + if len(unused) > 0 { + log.Errorf(context.TODO(), "TearDown: Unused golden files have been found:\n %#v\n known are %#v", + unused, slices.Collect(maps.Keys(gt.used))) + t.FailNow() + } + }) + + return gt +} + +// MarkUsed marks a golden file as being used. +func (gt *GoldenTracker) MarkUsed(t *testing.T, opts ...GoldenOption) { + t.Helper() + + gt.mu.Lock() + defer gt.mu.Unlock() + + o := parseOptions(t, opts...) + require.Nil(t, o.goldenTracker, "Setup: GoldenTracker option is not supported") + gt.used[o.goldenPath] = struct{}{} + + basePath := filepath.Dir(o.goldenPath) + if basePath == GoldenPath(t) { + gt.used[basePath] = struct{}{} + } +} diff --git a/internal/users/cache/db_test.go b/internal/users/cache/db_test.go index a64d9bc63..67c09fcc8 100644 --- a/internal/users/cache/db_test.go +++ b/internal/users/cache/db_test.go @@ -18,6 +18,7 @@ func TestNew(t *testing.T) { perm0644 := os.FileMode(0644) perm0000 := os.FileMode(0000) + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -78,7 +79,8 @@ func TestNew(t *testing.T) { got, err := cachetestutils.DumpToYaml(c) require.NoError(t, err, "Created database should be valid yaml content") - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Did not get expected database content") // check database permission @@ -155,6 +157,8 @@ func TestUpdateUserEntry(t *testing.T) { "group3": cache.NewGroupDB("group3", 33333, nil), } + goldenTracker := testutils.NewGoldenTracker(t) + tests := map[string]struct { userCase string groupCases []string @@ -229,7 +233,8 @@ func TestUpdateUserEntry(t *testing.T) { got, err := cachetestutils.DumpToYaml(c) require.NoError(t, err, "Created database should be valid yaml content") - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Did not get expected database content") }) } @@ -238,6 +243,7 @@ func TestUpdateUserEntry(t *testing.T) { func TestUserByID(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -256,7 +262,7 @@ func TestUserByID(t *testing.T) { c := initCache(t, tc.dbFile) got, err := c.UserByID(1111) - requireGetAssertions(t, got, tc.wantErr, tc.wantErrType, err) + requireGetAssertions(t, &goldenTracker, got, tc.wantErr, tc.wantErrType, err) }) } } @@ -264,6 +270,7 @@ func TestUserByID(t *testing.T) { func TestUserByName(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -282,7 +289,7 @@ func TestUserByName(t *testing.T) { c := initCache(t, tc.dbFile) got, err := c.UserByName("user1") - requireGetAssertions(t, got, tc.wantErr, tc.wantErrType, err) + requireGetAssertions(t, &goldenTracker, got, tc.wantErr, tc.wantErrType, err) }) } } @@ -290,6 +297,7 @@ func TestUserByName(t *testing.T) { func TestAllUsers(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -309,7 +317,7 @@ func TestAllUsers(t *testing.T) { c := initCache(t, tc.dbFile) got, err := c.AllUsers() - requireGetAssertions(t, got, tc.wantErr, nil, err) + requireGetAssertions(t, &goldenTracker, got, tc.wantErr, nil, err) }) } } @@ -317,6 +325,7 @@ func TestAllUsers(t *testing.T) { func TestGroupByID(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -336,7 +345,7 @@ func TestGroupByID(t *testing.T) { c := initCache(t, tc.dbFile) got, err := c.GroupByID(11111) - requireGetAssertions(t, got, tc.wantErr, tc.wantErrType, err) + requireGetAssertions(t, &goldenTracker, got, tc.wantErr, tc.wantErrType, err) }) } } @@ -344,6 +353,7 @@ func TestGroupByID(t *testing.T) { func TestGroupByName(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -363,7 +373,7 @@ func TestGroupByName(t *testing.T) { c := initCache(t, tc.dbFile) got, err := c.GroupByName("group1") - requireGetAssertions(t, got, tc.wantErr, tc.wantErrType, err) + requireGetAssertions(t, &goldenTracker, got, tc.wantErr, tc.wantErrType, err) }) } } @@ -371,6 +381,7 @@ func TestGroupByName(t *testing.T) { func TestAllGroups(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -393,7 +404,7 @@ func TestAllGroups(t *testing.T) { c := initCache(t, tc.dbFile) got, err := c.AllGroups() - requireGetAssertions(t, got, tc.wantErr, tc.wantErrType, err) + requireGetAssertions(t, &goldenTracker, got, tc.wantErr, tc.wantErrType, err) }) } } @@ -451,6 +462,8 @@ func TestRemoveDb(t *testing.T) { func TestDeleteUser(t *testing.T) { t.Parallel() + goldenTracker := testutils.NewGoldenTracker(t) + tests := map[string]struct { dbFile string @@ -482,7 +495,8 @@ func TestDeleteUser(t *testing.T) { got, err := cachetestutils.DumpToYaml(c) require.NoError(t, err, "Created database should be valid yaml content") - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Did not get expected database content") }) } @@ -504,7 +518,7 @@ func initCache(t *testing.T, dbFile string) (c *cache.Cache) { return c } -func requireGetAssertions[E any](t *testing.T, got E, wantErr bool, wantErrType, err error) { +func requireGetAssertions[E any](t *testing.T, goldenTracker *testutils.GoldenTracker, got E, wantErr bool, wantErrType, err error) { t.Helper() if wantErrType != nil { @@ -517,6 +531,7 @@ func requireGetAssertions[E any](t *testing.T, got E, wantErr bool, wantErrType, } require.NoError(t, err) - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(goldenTracker)) require.Equal(t, want, got, "Did not get expected database entry") } diff --git a/internal/users/manager_test.go b/internal/users/manager_test.go index 26c0b842c..89a3947ba 100644 --- a/internal/users/manager_test.go +++ b/internal/users/manager_test.go @@ -16,6 +16,7 @@ import ( ) func TestNewManager(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string corruptedDbFile bool @@ -79,7 +80,8 @@ func TestNewManager(t *testing.T) { got, err := cachetestutils.DumpToYaml(userstestutils.GetManagerCache(m)) require.NoError(t, err, "Created database should be valid yaml content") - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Did not get expected database content") localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, testutils.GoldenPath(t)+".gpasswd.output") @@ -158,6 +160,8 @@ func TestUpdateUser(t *testing.T) { "no-groups": {}, } + goldenTracker := testutils.NewGoldenTracker(t) + tests := map[string]struct { userCase string groupsCase string @@ -234,10 +238,13 @@ func TestUpdateUser(t *testing.T) { got, err := cachetestutils.DumpToYaml(userstestutils.GetManagerCache(m)) require.NoError(t, err, "Created database should be valid yaml content") - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Did not get expected database content") - localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, testutils.GoldenPath(t)+".gpasswd.output") + gpasswdGolden := testutils.GoldenPath(t) + ".gpasswd.output" + localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, gpasswdGolden) + goldenTracker.MarkUsed(t, testutils.WithGoldenPath(gpasswdGolden)) }) } } @@ -279,6 +286,7 @@ func TestBrokerForUser(t *testing.T) { } func TestUpdateBrokerForUser(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string @@ -318,7 +326,8 @@ func TestUpdateBrokerForUser(t *testing.T) { got, err := cachetestutils.DumpToYaml(userstestutils.GetManagerCache(m)) require.NoError(t, err, "Created database should be valid yaml content") - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Did not get expected database content") }) } @@ -326,6 +335,7 @@ func TestUpdateBrokerForUser(t *testing.T) { //nolint:dupl // This is not a duplicate test func TestUserByName(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string dbFile string @@ -354,7 +364,8 @@ func TestUserByName(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "UserByName should return the expected user, but did not") }) } @@ -362,6 +373,7 @@ func TestUserByName(t *testing.T) { //nolint:dupl // This is not a duplicate test func TestUserByID(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { uid uint32 dbFile string @@ -391,13 +403,15 @@ func TestUserByID(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "UserByID should return the expected user, but did not") }) } } func TestAllUsers(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -424,7 +438,8 @@ func TestAllUsers(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "AllUsers should return the expected users, but did not") }) } @@ -432,6 +447,7 @@ func TestAllUsers(t *testing.T) { //nolint:dupl // This is not a duplicate test func TestGroupByName(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { groupname string dbFile string @@ -460,7 +476,8 @@ func TestGroupByName(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "GroupByName should return the expected group, but did not") }) } @@ -468,6 +485,7 @@ func TestGroupByName(t *testing.T) { //nolint:dupl // This is not a duplicate test func TestGroupByID(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { gid uint32 dbFile string @@ -496,13 +514,15 @@ func TestGroupByID(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "GroupByID should return the expected group, but did not") }) } } func TestAllGroups(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -530,7 +550,8 @@ func TestAllGroups(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "AllGroups should return the expected groups, but did not") }) } @@ -538,6 +559,7 @@ func TestAllGroups(t *testing.T) { //nolint:dupl // This is not a duplicate test func TestShadowByName(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { username string dbFile string @@ -567,13 +589,15 @@ func TestShadowByName(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "ShadowByName should return the expected user, but did not") }) } } func TestAllShadows(t *testing.T) { + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { dbFile string @@ -600,7 +624,8 @@ func TestAllShadows(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) + want := testutils.LoadWithUpdateFromGoldenYAML(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "AllShadows should return the expected users, but did not") }) } diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index 7b5c3ceb3..75c93e00e 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -27,6 +27,7 @@ func TestIntegration(t *testing.T) { defaultDbState := "multiple_users_and_groups" defaultOutputPath := filepath.Join(filepath.Dir(daemonPath), "gpasswd.output") defaultGroupsFilePath := filepath.Join(testutils.TestFamilyPath(t), "gpasswd.group") + goldenTracker := testutils.NewGoldenTracker(t) env := append(localgroupstestutils.AuthdIntegrationTestsEnvWithGpasswdMock(t, defaultOutputPath, defaultGroupsFilePath), "AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1") ctx, cancel := context.WithCancel(context.Background()) @@ -154,7 +155,8 @@ func TestIntegration(t *testing.T) { return } - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Outputs must match") // This is to check that some cache tasks, such as cleaning a corrupted database, work as expected. diff --git a/pam/integration-tests/cli_test.go b/pam/integration-tests/cli_test.go index 39afe73aa..4eb7bd093 100644 --- a/pam/integration-tests/cli_test.go +++ b/pam/integration-tests/cli_test.go @@ -19,6 +19,7 @@ func TestCLIAuthenticate(t *testing.T) { clientPath := t.TempDir() cliEnv := preparePamRunnerTest(t, clientPath) + goldenTracker := testutils.NewGoldenTracker(t) const socketPathEnv = "AUTHD_TESTS_CLI_AUTHENTICATE_TESTS_SOCK" tests := map[string]struct { @@ -156,10 +157,13 @@ func TestCLIAuthenticate(t *testing.T) { td.AddClientOptions(t, tc.clientOptions) td.RunVhs(t, "cli", outDir, cliEnv) got := td.ExpectedOutput(t, outDir) - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Output of tape %q does not match golden file", tc.tape) - localgroupstestutils.RequireGPasswdOutput(t, gpasswdOutput, testutils.GoldenPath(t)+".gpasswd_out") + gpasswdOutputGolden := testutils.GoldenPath(t) + ".gpasswd_out" + localgroupstestutils.RequireGPasswdOutput(t, gpasswdOutput, gpasswdOutputGolden) + goldenTracker.MarkUsed(t, testutils.WithGoldenPath(gpasswdOutputGolden)) }) } } @@ -175,6 +179,7 @@ func TestCLIChangeAuthTok(t *testing.T) { require.NoError(t, err, "Setup: Could not create gpasswd output directory") gpasswdOutput := filepath.Join(outDir, "gpasswd", "chauthtok.output") groupsFile := filepath.Join(testutils.TestFamilyPath(t), "gpasswd.group") + goldenTracker := testutils.NewGoldenTracker(t) const socketPathEnv = "AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK" defaultSocketPath := runAuthd(t, gpasswdOutput, groupsFile, true) @@ -237,7 +242,8 @@ func TestCLIChangeAuthTok(t *testing.T) { td.AddClientOptions(t, clientOptions{}) td.RunVhs(t, "cli", outDir, cliEnv) got := td.ExpectedOutput(t, outDir) - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Output of tape %q does not match golden file", tc.tape) }) } diff --git a/pam/integration-tests/native_test.go b/pam/integration-tests/native_test.go index 2a65af0b6..14e8f0343 100644 --- a/pam/integration-tests/native_test.go +++ b/pam/integration-tests/native_test.go @@ -17,6 +17,7 @@ func TestNativeAuthenticate(t *testing.T) { clientPath := t.TempDir() cliEnv := preparePamRunnerTest(t, clientPath) const socketPathEnv = "AUTHD_TESTS_CLI_AUTHENTICATE_TESTS_SOCK" + goldenTracker := testutils.NewGoldenTracker(t) tests := map[string]struct { tape string @@ -219,10 +220,13 @@ func TestNativeAuthenticate(t *testing.T) { td.AddClientOptions(t, tc.clientOptions) td.RunVhs(t, "native", outDir, cliEnv) got := td.ExpectedOutput(t, outDir) - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Output of tape %q does not match golden file", tc.tape) - localgroupstestutils.RequireGPasswdOutput(t, gpasswdOutput, testutils.GoldenPath(t)+".gpasswd_out") + gpasswdOutputGolden := testutils.GoldenPath(t) + ".gpasswd_out" + localgroupstestutils.RequireGPasswdOutput(t, gpasswdOutput, gpasswdOutputGolden) + goldenTracker.MarkUsed(t, testutils.WithGoldenPath(gpasswdOutputGolden)) }) } } @@ -238,6 +242,7 @@ func TestNativeChangeAuthTok(t *testing.T) { require.NoError(t, err, "Setup: Could not create gpasswd output directory") gpasswdOutput := filepath.Join(outDir, "gpasswd", "chauthtok.output") groupsFile := filepath.Join(testutils.TestFamilyPath(t), "gpasswd.group") + goldenTracker := testutils.NewGoldenTracker(t) const socketPathEnv = "AUTHD_TESTS_CLI_AUTHTOK_TESTS_SOCK" defaultSocketPath := runAuthd(t, gpasswdOutput, groupsFile, true) @@ -304,7 +309,8 @@ func TestNativeChangeAuthTok(t *testing.T) { td.AddClientOptions(t, clientOptions{}) td.RunVhs(t, "native", outDir, cliEnv) got := td.ExpectedOutput(t, outDir) - want := testutils.LoadWithUpdateFromGolden(t, got) + want := testutils.LoadWithUpdateFromGolden(t, got, + testutils.WithGoldenTracker(&goldenTracker)) require.Equal(t, want, got, "Output of tape %q does not match golden file", tc.tape) }) }