Skip to content
Open
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
215 changes: 215 additions & 0 deletions ospatch/zypper_patch_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package ospatch

import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"testing"

"github.com/GoogleCloudPlatform/osconfig/packages"
utilmocks "github.com/GoogleCloudPlatform/osconfig/util/mocks"
utiltest "github.com/GoogleCloudPlatform/osconfig/util/utiltest"
"github.com/golang/mock/gomock"
)

func TestRunFilter(t *testing.T) {
Expand Down Expand Up @@ -78,6 +85,208 @@ func TestRunFilter(t *testing.T) {
}
}

func TestRunZypperPatch(t *testing.T) {
const zypperBin = "/usr/bin/zypper"
someErr := errors.New("some error")
patch2 := "patch-2"

listPatchesBaseArgs := []string{"--gpg-auto-import-keys", "-q", "list-patches"}
listPatchesAllArgs := append(listPatchesBaseArgs, "--all")
listUpdatesArgs := []string{"--gpg-auto-import-keys", "-q", "list-updates"}
installArgs := []string{"--gpg-auto-import-keys", "--non-interactive", "install", "--auto-agree-with-licenses"}

onePatchOutput := []byte(`SLE-Module | patch-1 | security | important | --- | needed | Security patch`)
twoPatchesOutput := []byte("SLE-Module | patch-1 | security | important | --- | needed | Security patch\n" +
"SLE-Module | patch-2 | recommended | moderate | --- | needed | Recommended patch")
oneUpdateOutput := []byte(`v | SLES12-SP3-Updates | pkg1 | 1.0.0 | 2.0.0 | x86_64`)

mockCtrl := gomock.NewController(t)
t.Cleanup(func() { mockCtrl.Finish() })
mockCommandRunner := utilmocks.NewMockCommandRunner(mockCtrl)
packages.SetCommandRunner(mockCommandRunner)

tests := []struct {
name string
description string
opts []ZypperPatchOption
setupMock func(ctx context.Context, mock *utilmocks.MockCommandRunner)
wantErr error
}{
{
name: "ZypperPatchesError",
description: "When listing available patches fails, RunZypperPatch should surface the wrapped command error and not attempt to install anything.",
opts: nil,
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return(nil, nil, someErr).Times(1)
},
wantErr: wrapRunErr(zypperBin, listPatchesAllArgs, someErr),
},
{
name: "NoPatches_NoUpdatesRequired",
description: "When zypper reports no needed patches and --with-update is not set, RunZypperPatch should return without running any install commands.",
opts: nil,
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return([]byte(""), nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "DryrunWithPatches_SkipsInstall",
description: "In dry-run mode, RunZypperPatch should list available patches but skip the install step even when patches are needed.",
opts: []ZypperPatchOption{ZypperUpdateDryrun(true)},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return(onePatchOutput, nil, nil).Times(1)
// No install call expected.
},
wantErr: nil,
},
{
name: "InstallsPatches",
description: "With one needed patch reported by zypper, RunZypperPatch should invoke zypper install with the patch:<name> argument.",
opts: nil,
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
listCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return(onePatchOutput, nil, nil).Times(1)
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, append(installArgs, "patch:patch-1")...))).
After(listCall).Return(nil, nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "InstallError_PropagatesError",
description: "When the install step fails, RunZypperPatch should return the wrapped command error from the install invocation.",
opts: nil,
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
listCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return(onePatchOutput, nil, nil).Times(1)
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, append(installArgs, "patch:patch-1")...))).
After(listCall).Return(nil, nil, someErr).Times(1)
},
wantErr: wrapRunErr(zypperBin, append(installArgs, "patch:patch-1"), someErr),
},
{
name: "WithUpdate_ZypperUpdatesError",
description: "With --with-update enabled and no patches to install, a failure of `zypper list-updates` should be surfaced as the wrapped command error.",
opts: []ZypperPatchOption{ZypperUpdateWithUpdate(true)},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
// Empty patches list so ZypperPackagesInPatch short-circuits.
listCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return([]byte(""), nil, nil).Times(1)
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listUpdatesArgs...))).
After(listCall).Return(nil, nil, someErr).Times(1)
},
wantErr: wrapRunErr(zypperBin, listUpdatesArgs, someErr),
},
{
name: "WithUpdate_InstallsNonPatchPackages",
description: "With --with-update enabled and no needed patches, non-patch packages reported by `zypper list-updates` should be installed using the package:<name> form.",
opts: []ZypperPatchOption{ZypperUpdateWithUpdate(true)},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
// Empty patches list so ZypperPackagesInPatch short-circuits (returns empty map, nil).
listPatchesCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return([]byte(""), nil, nil).Times(1)
listUpdatesCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listUpdatesArgs...))).
After(listPatchesCall).Return(oneUpdateOutput, nil, nil).Times(1)
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, append(installArgs, "package:pkg1")...))).
After(listUpdatesCall).Return(nil, nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "CategoryOption_PassesCorrectArgs",
description: "When a category filter is provided, list-patches should be invoked with --category=<value> and without --all.",
opts: []ZypperPatchOption{ZypperPatchCategories([]string{"security"})},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
// With a category filter, --all is NOT appended.
args := append(listPatchesBaseArgs, "--category=security")
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, args...))).
Return([]byte(""), nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "SeverityOption_PassesCorrectArgs",
description: "When a severity filter is provided, list-patches should be invoked with --severity=<value> and without --all.",
opts: []ZypperPatchOption{ZypperPatchSeverities([]string{"critical"})},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
// With a severity filter, --all is NOT appended.
args := append(listPatchesBaseArgs, "--severity=critical")
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, args...))).
Return([]byte(""), nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "WithOptionalOption_PassesCorrectArgs",
description: "When --with-optional is enabled, list-patches should be invoked with --with-optional alongside --all.",
opts: []ZypperPatchOption{ZypperUpdateWithOptional(true)},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
args := append(listPatchesBaseArgs, "--with-optional", "--all")
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, args...))).
Return([]byte(""), nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "ExclusivePatches_OnlyInstallsSpecifiedPatch",
description: "With an exclusive-patches list, only the patches named in that list should be passed to the install invocation, even if zypper reports additional needed patches.",
opts: []ZypperPatchOption{ZypperUpdateWithExclusivePatches([]string{"patch-1"})},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
listCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return(twoPatchesOutput, nil, nil).Times(1)
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, append(installArgs, "patch:patch-1")...))).
After(listCall).Return(nil, nil, nil).Times(1)
},
wantErr: nil,
},
{
name: "WithExcludes_SkipsExcludedPatch",
description: "With an excludes list, the excluded patches should not be passed to the install invocation.",
opts: []ZypperPatchOption{ZypperUpdateWithExcludes([]*Exclude{CreateStringExclude(&patch2)})},
setupMock: func(ctx context.Context, mock *utilmocks.MockCommandRunner) {
listCall := mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, listPatchesAllArgs...))).
Return(twoPatchesOutput, nil, nil).Times(1)
mock.EXPECT().
Run(ctx, utilmocks.EqCmd(exec.Command(zypperBin, append(installArgs, "patch:patch-1")...))).
After(listCall).Return(nil, nil, nil).Times(1)
},
wantErr: nil,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()

tc.setupMock(ctx, mockCommandRunner)

err := RunZypperPatch(ctx, tc.opts...)
utiltest.AssertErrorMatch(t, err, tc.wantErr)
})
}
}

func isIn(needle string, haystack []string) bool {
for _, hay := range haystack {
if strings.Compare(hay, needle) == 0 {
Expand Down Expand Up @@ -153,3 +362,9 @@ func prepareTestCase() ([]*packages.ZypperPatch, []*packages.PkgInfo, map[string

return patches, pkgUpdates, pkgToPatchesMap
}

// wrapRunErr mirrors the formatting used by packages.run to wrap the
// underlying command error, so tests can express the exact expected error.
func wrapRunErr(cmd string, args []string, err error) error {
return fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", cmd, args, err, []byte(""), []byte(""))
}
Loading