Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package google-osconfig-agent for 
openSUSE:Factory checked in at 2026-07-14 13:48:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/google-osconfig-agent (Old)
 and      /work/SRC/openSUSE:Factory/.google-osconfig-agent.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "google-osconfig-agent"

Tue Jul 14 13:48:39 2026 rev:57 rq:1365363 version:20260708.00

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/google-osconfig-agent/google-osconfig-agent.changes  
    2026-07-07 21:08:06.984492215 +0200
+++ 
/work/SRC/openSUSE:Factory/.google-osconfig-agent.new.1991/google-osconfig-agent.changes
    2026-07-14 13:49:07.329317925 +0200
@@ -1,0 +2,16 @@
+Thu Jul  9 13:29:35 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to version 20260708.00
+  * Add unit tests for policies/recipes/steps.go PART 2 (#975)
+- from version 20260707.00
+  * Bump golang.org/x/net (#1017)
+- from version 20260703.00
+  * upgrade x/net package. (#1018)
+- from version 20260702.00
+  * Add test cases for config/file_resource.go (#988)
+- from version 20260701.00
+  * Add unit tests for policies/recipes/artifacts.go (#985)
+- from version 20260630.00
+  * Add unit tests for policies/recipes/steps.go PART 1 (#974)
+
+-------------------------------------------------------------------

Old:
----
  osconfig-20260625.00.tar.gz

New:
----
  osconfig-20260708.00.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ google-osconfig-agent.spec ++++++
--- /var/tmp/diff_new_pack.uxc8Jz/_old  2026-07-14 13:49:09.521392867 +0200
+++ /var/tmp/diff_new_pack.uxc8Jz/_new  2026-07-14 13:49:09.525393004 +0200
@@ -19,7 +19,7 @@
 %define shortname osconfig
 
 Name:           google-osconfig-agent
-Version:        20260625.00
+Version:        20260708.00
 Release:        0
 Summary:        Google Cloud Guest Agent
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.uxc8Jz/_old  2026-07-14 13:49:09.601395602 +0200
+++ /var/tmp/diff_new_pack.uxc8Jz/_new  2026-07-14 13:49:09.633396696 +0200
@@ -3,8 +3,8 @@
     <param name="url">https://github.com/GoogleCloudPlatform/osconfig</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="versionformat">20260625.00</param>
-    <param name="revision">20260625.00</param>
+    <param name="versionformat">20260708.00</param>
+    <param name="revision">20260708.00</param>
     <param name="changesgenerate">enable</param>
   </service>
   <service name="recompress" mode="disabled">
@@ -15,7 +15,7 @@
     <param name="basename">osconfig</param>
   </service>
   <service name="go_modules" mode="disabled">
-    <param name="archive">osconfig-20260625.00.tar.gz</param>
+    <param name="archive">osconfig-20260708.00.tar.gz</param>
   </service>
 </services>
 

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.uxc8Jz/_old  2026-07-14 13:49:09.745400525 +0200
+++ /var/tmp/diff_new_pack.uxc8Jz/_new  2026-07-14 13:49:09.757400935 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/GoogleCloudPlatform/osconfig</param>
-              <param 
name="changesrevision">66ece98bab1d62c2366c52bbc8058ef5463db58c</param></service></servicedata>
+              <param 
name="changesrevision">c0faeb26694ede5df709110d496c6255eb216662</param></service></servicedata>
 (No newline at EOF)
 

++++++ osconfig-20260625.00.tar.gz -> osconfig-20260708.00.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/config/file_resource_test.go 
new/osconfig-20260708.00/config/file_resource_test.go
--- old/osconfig-20260625.00/config/file_resource_test.go       2026-06-25 
16:31:39.000000000 +0200
+++ new/osconfig-20260708.00/config/file_resource_test.go       2026-07-08 
10:05:56.000000000 +0200
@@ -16,12 +16,17 @@
 
 import (
        "context"
+       "errors"
+       "fmt"
        "io/ioutil"
+       "net/url"
        "os"
        "path/filepath"
+       "strconv"
        "testing"
 
        "github.com/GoogleCloudPlatform/osconfig/util"
+       "github.com/GoogleCloudPlatform/osconfig/util/utiltest"
        "github.com/google/go-cmp/cmp"
 
        "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb"
@@ -39,37 +44,42 @@
                t.Fatal(err)
        }
 
+       _, errLocalPath := os.Open("does_not_exist")
+
        var tests = []struct {
-               name   string
-               frpb   *agentendpointpb.OSPolicy_Resource_FileResource
-               wantMR ManagedFile
+               name            string
+               fileResourcePB  *agentendpointpb.OSPolicy_Resource_FileResource
+               wantManagedFile ManagedFile
+               wantErr         error
        }{
                {
-                       "Absent",
-                       &agentendpointpb.OSPolicy_Resource_FileResource{
+                       name: "state absent, want absent managed file",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
                                Path:  tmpFile,
                                State: 
agentendpointpb.OSPolicy_Resource_FileResource_ABSENT,
                        },
-                       ManagedFile{
+                       wantManagedFile: ManagedFile{
                                Path:  tmpFile,
                                State: 
agentendpointpb.OSPolicy_Resource_FileResource_ABSENT,
                        },
+                       wantErr: nil,
                },
                {
-                       "Present",
-                       &agentendpointpb.OSPolicy_Resource_FileResource{
+                       name: "state present, want present managed file",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
                                Path:  tmpFile,
                                State: 
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
                        },
-                       ManagedFile{
+                       wantManagedFile: ManagedFile{
                                Path:       tmpFile,
                                State:      
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
                                Permisions: defaultFilePerms,
                        },
+                       wantErr: nil,
                },
                {
-                       "ContentsMatch",
-                       &agentendpointpb.OSPolicy_Resource_FileResource{
+                       name: "state contents match and local path source, want 
contents match managed file with checksum",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
                                Path: tmpFile,
                                Source: 
&agentendpointpb.OSPolicy_Resource_FileResource_File{
                                        File: 
&agentendpointpb.OSPolicy_Resource_File{
@@ -80,30 +90,32 @@
                                },
                                State: 
agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH,
                        },
-                       ManagedFile{
+                       wantManagedFile: ManagedFile{
                                Path:       tmpFile,
                                source:     tmpFile,
                                checksum:   
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                                State:      
agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH,
                                Permisions: defaultFilePerms,
                        },
+                       wantErr: nil,
                },
                {
-                       "Permissions",
-                       &agentendpointpb.OSPolicy_Resource_FileResource{
+                       name: "state present and custom permissions, want 
present managed file with custom permissions",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
                                Path:        tmpFile,
                                State:       
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
                                Permissions: "0777",
                        },
-                       ManagedFile{
+                       wantManagedFile: ManagedFile{
                                Path:       tmpFile,
                                State:      
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
                                Permisions: 0777,
                        },
+                       wantErr: nil,
                },
                {
-                       "LocalPath",
-                       &agentendpointpb.OSPolicy_Resource_FileResource{
+                       name: "state present and local path source, want 
present managed file with checksum",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
                                Path: tmpFile,
                                Source: 
&agentendpointpb.OSPolicy_Resource_FileResource_File{
                                        File: 
&agentendpointpb.OSPolicy_Resource_File{
@@ -114,13 +126,70 @@
                                },
                                State: 
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
                        },
-                       ManagedFile{
+                       wantManagedFile: ManagedFile{
                                Path:       tmpFile,
                                State:      
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
                                Permisions: defaultFilePerms,
                                checksum:   
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                                source:     tmpFile,
                        },
+                       wantErr: nil,
+               },
+               {
+                       name: "invalid state, expect unrecognized desired state 
error",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                               State: 
agentendpointpb.OSPolicy_Resource_FileResource_DESIRED_STATE_UNSPECIFIED,
+                       },
+                       wantErr: fmt.Errorf("unrecognized DesiredState for 
FileResource: %q", 
agentendpointpb.OSPolicy_Resource_FileResource_DESIRED_STATE_UNSPECIFIED),
+               },
+               {
+                       name: "invalid permissions, expect fail to parse 
permissions",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                               State:       
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
+                               Permissions: "invalid",
+                       },
+                       wantErr: fmt.Errorf("can't parse permissions %q: %v", 
"invalid", &strconv.NumError{Func: "ParseUint", Num: "invalid", Err: 
strconv.ErrSyntax}),
+               },
+               {
+                       name: "local file does not exist, expect fail to open 
file",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                               Path:  "some/path",
+                               State: 
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
+                               Source: 
&agentendpointpb.OSPolicy_Resource_FileResource_File{
+                                       File: 
&agentendpointpb.OSPolicy_Resource_File{
+                                               Type: 
&agentendpointpb.OSPolicy_Resource_File_LocalPath{
+                                                       LocalPath: 
"does_not_exist",
+                                               },
+                                       },
+                               },
+                       },
+                       wantErr: errLocalPath,
+               },
+               {
+                       name: "unrecognized source, expect unrecognized source 
type error",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                               Path:   "some/path",
+                               State:  
agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH,
+                               Source: nil,
+                       },
+                       wantErr: errors.New("unrecognized Source type for 
FileResource: %!q(<nil>)"),
+               },
+               {
+                       name: "remote download error, expect fail to fetch 
remote object",
+                       fileResourcePB: 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                               Path:  "some/path",
+                               State: 
agentendpointpb.OSPolicy_Resource_FileResource_CONTENTS_MATCH,
+                               Source: 
&agentendpointpb.OSPolicy_Resource_FileResource_File{
+                                       File: 
&agentendpointpb.OSPolicy_Resource_File{
+                                               Type: 
&agentendpointpb.OSPolicy_Resource_File_Remote_{
+                                                       Remote: 
&agentendpointpb.OSPolicy_Resource_File_Remote{
+                                                               Uri: 
"doesnot/exist",
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+                       wantErr: &url.Error{Op: "Get", URL: "doesnot/exist", 
Err: errors.New("unsupported protocol scheme \"\"")},
                },
        }
        for _, tt := range tests {
@@ -128,18 +197,17 @@
                        pr := &OSPolicyResource{
                                OSPolicy_Resource: 
&agentendpointpb.OSPolicy_Resource{
                                        ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{
-                                               File: tt.frpb,
+                                               File: tt.fileResourcePB,
                                        },
                                },
                        }
-                       if err := pr.Validate(ctx); err != nil {
-                               t.Fatalf("Unexpected error: %v", err)
-                       }
+                       gotErr := pr.Validate(ctx)
+                       utiltest.AssertErrorMatchAndSkip(t, gotErr, tt.wantErr)
 
-                       if diff := cmp.Diff(pr.ManagedResources(), 
&ManagedResources{Files: []ManagedFile{tt.wantMR}}, 
cmp.AllowUnexported(ManagedFile{})); diff != "" {
+                       if diff := cmp.Diff(pr.ManagedResources(), 
&ManagedResources{Files: []ManagedFile{tt.wantManagedFile}}, 
cmp.AllowUnexported(ManagedFile{})); diff != "" {
                                t.Errorf("OSPolicyResource does not match 
expectation: (-got +want)\n%s", diff)
                        }
-                       if diff := 
cmp.Diff(pr.resource.(*fileResource).managedFile, tt.wantMR, 
cmp.AllowUnexported(ManagedFile{})); diff != "" {
+                       if diff := 
cmp.Diff(pr.resource.(*fileResource).managedFile, tt.wantManagedFile, 
cmp.AllowUnexported(ManagedFile{})); diff != "" {
                                t.Errorf("fileResource does not match 
expectation: (-got +want)\n%s", diff)
                        }
                })
@@ -164,7 +232,7 @@
 
        var tests = []struct {
                name               string
-               frpb               
*agentendpointpb.OSPolicy_Resource_FileResource
+               fileResourcePB     
*agentendpointpb.OSPolicy_Resource_FileResource
                wantInDesiredState bool
        }{
                {
@@ -279,7 +347,7 @@
                        pr := &OSPolicyResource{
                                OSPolicy_Resource: 
&agentendpointpb.OSPolicy_Resource{
                                        ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{
-                                               File: tt.frpb,
+                                               File: tt.fileResourcePB,
                                        },
                                },
                        }
@@ -310,13 +378,13 @@
                t.Fatal(err)
        }
 
-       frpb := &agentendpointpb.OSPolicy_Resource_FileResource{
+       fileResourcePB := &agentendpointpb.OSPolicy_Resource_FileResource{
                Path:  tmpFile,
                State: agentendpointpb.OSPolicy_Resource_FileResource_ABSENT,
        }
        pr := &OSPolicyResource{
                OSPolicy_Resource: &agentendpointpb.OSPolicy_Resource{
-                       ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{File: frpb},
+                       ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{File: fileResourcePB},
                },
        }
        if err := pr.Validate(ctx); err != nil {
@@ -345,7 +413,7 @@
        }
        wantFile := filepath.Join(tmpDir, "bar")
 
-       frpb := &agentendpointpb.OSPolicy_Resource_FileResource{
+       fileResourcePB := &agentendpointpb.OSPolicy_Resource_FileResource{
                Path: wantFile,
                Source: &agentendpointpb.OSPolicy_Resource_FileResource_File{
                        File: &agentendpointpb.OSPolicy_Resource_File{
@@ -358,7 +426,7 @@
        }
        pr := &OSPolicyResource{
                OSPolicy_Resource: &agentendpointpb.OSPolicy_Resource{
-                       ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{File: frpb},
+                       ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{File: fileResourcePB},
                },
        }
        if err := pr.Validate(ctx); err != nil {
@@ -377,3 +445,154 @@
                t.Fatal("Repo file contents do not match after enforcement")
        }
 }
+
+func TestFileResourceEnforceStateErrors(t *testing.T) {
+       ctx := t.Context()
+       missingPath := filepath.Join(t.TempDir(), "does_not_exist")
+       errRemove := os.Remove(missingPath)
+       tests := []struct {
+               name    string
+               setup   func(t *testing.T) *OSPolicyResource
+               wantErr error
+       }{
+               {
+                       name: "invalid state, expect unrecognized desired state 
error",
+                       setup: func(t *testing.T) *OSPolicyResource {
+                               fileResourcePB := 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                                       State: 
agentendpointpb.OSPolicy_Resource_FileResource_DESIRED_STATE_UNSPECIFIED,
+                               }
+                               return &OSPolicyResource{
+                                       OSPolicy_Resource: 
&agentendpointpb.OSPolicy_Resource{
+                                               ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{
+                                                       File: fileResourcePB,
+                                               },
+                                       },
+                                       resource: &fileResource{
+                                               OSPolicy_Resource_FileResource: 
fileResourcePB,
+                                               managedFile: ManagedFile{
+                                                       State: 
fileResourcePB.GetState(),
+                                               },
+                                       },
+                               }
+                       },
+                       wantErr: fmt.Errorf("unrecognized DesiredState for 
FileResource: %q", 
agentendpointpb.OSPolicy_Resource_FileResource_DESIRED_STATE_UNSPECIFIED),
+               },
+               {
+                       name: "invalid path remove, expect failed to remove 
path error",
+                       setup: func(t *testing.T) *OSPolicyResource {
+                               fileResourcePB := 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                                       Path:  missingPath,
+                                       State: 
agentendpointpb.OSPolicy_Resource_FileResource_ABSENT,
+                               }
+                               return &OSPolicyResource{
+                                       OSPolicy_Resource: 
&agentendpointpb.OSPolicy_Resource{
+                                               ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{
+                                                       File: fileResourcePB,
+                                               },
+                                       },
+                                       resource: &fileResource{
+                                               OSPolicy_Resource_FileResource: 
fileResourcePB,
+                                               managedFile: ManagedFile{
+                                                       Path:  
fileResourcePB.GetPath(),
+                                                       State: 
fileResourcePB.GetState(),
+                                               },
+                                       },
+                               }
+                       },
+                       wantErr: fmt.Errorf("error removing %q: %v", 
missingPath, errRemove),
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       pr := tt.setup(t)
+                       gotErr := pr.EnforceState(ctx)
+                       utiltest.AssertErrorMatch(t, gotErr, tt.wantErr)
+               })
+       }
+}
+
+func TestFileResourceEnforceStateDownload(t *testing.T) {
+       ctx := t.Context()
+       tmpDir := t.TempDir()
+
+       dst := filepath.Join(tmpDir, "dst")
+
+       tests := []struct {
+               name        string
+               setup       func(t *testing.T) (*OSPolicyResource, string)
+               wantErr     error
+               wantContent string
+       }{
+               {
+                       name: "download fails during enforce state, expect 
unrecognized source type error",
+                       setup: func(t *testing.T) (*OSPolicyResource, string) {
+                               path := filepath.Join(tmpDir, "some_path")
+                               fileResourcePB := 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                                       Path:   path,
+                                       State:  
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
+                                       Source: nil,
+                               }
+                               pr := &OSPolicyResource{
+                                       OSPolicy_Resource: 
&agentendpointpb.OSPolicy_Resource{
+                                               ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{
+                                                       File: fileResourcePB,
+                                               },
+                                       },
+                                       resource: &fileResource{
+                                               OSPolicy_Resource_FileResource: 
fileResourcePB,
+                                               managedFile: ManagedFile{
+                                                       Path:       
fileResourcePB.GetPath(),
+                                                       State:      
fileResourcePB.GetState(),
+                                                       Permisions: 
defaultFilePerms,
+                                               },
+                                       },
+                               }
+                               t.Cleanup(func() { pr.Cleanup(ctx) })
+                               return pr, path
+                       },
+                       wantErr: errors.New("unrecognized Source type for 
FileResource: %!q(<nil>)"),
+               },
+               {
+                       name: "download succeeds during enforce state, expect 
success",
+                       setup: func(t *testing.T) (*OSPolicyResource, string) {
+                               fileResourcePB := 
&agentendpointpb.OSPolicy_Resource_FileResource{
+                                       Path:  dst,
+                                       State: 
agentendpointpb.OSPolicy_Resource_FileResource_PRESENT,
+                                       Source: 
&agentendpointpb.OSPolicy_Resource_FileResource_Content{
+                                               Content: "test content",
+                                       },
+                               }
+                               pr := &OSPolicyResource{
+                                       OSPolicy_Resource: 
&agentendpointpb.OSPolicy_Resource{
+                                               ResourceType: 
&agentendpointpb.OSPolicy_Resource_File_{
+                                                       File: fileResourcePB,
+                                               },
+                                       },
+                                       resource: &fileResource{
+                                               OSPolicy_Resource_FileResource: 
fileResourcePB,
+                                               managedFile: ManagedFile{
+                                                       Path:       
fileResourcePB.GetPath(),
+                                                       State:      
fileResourcePB.GetState(),
+                                                       Permisions: 
defaultFilePerms,
+                                               },
+                                       },
+                               }
+                               t.Cleanup(func() { pr.Cleanup(ctx) })
+                               return pr, dst
+                       },
+                       wantErr:     nil,
+                       wantContent: "test content",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       pr, path := tt.setup(t)
+                       gotErr := pr.EnforceState(ctx)
+
+                       utiltest.AssertErrorMatchAndSkip(t, gotErr, tt.wantErr)
+                       utiltest.AssertEquals(t, pr.InDesiredState(), true)
+                       utiltest.AssertFileContents(t, path, tt.wantContent)
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/e2e_tests/go.mod 
new/osconfig-20260708.00/e2e_tests/go.mod
--- old/osconfig-20260625.00/e2e_tests/go.mod   2026-06-25 16:31:39.000000000 
+0200
+++ new/osconfig-20260708.00/e2e_tests/go.mod   2026-07-08 10:05:56.000000000 
+0200
@@ -50,11 +50,11 @@
        go.opentelemetry.io/otel v1.43.0 // indirect
        go.opentelemetry.io/otel/metric v1.43.0 // indirect
        go.opentelemetry.io/otel/trace v1.43.0 // indirect
-       golang.org/x/crypto v0.49.0 // indirect
-       golang.org/x/net v0.52.0 // indirect
+       golang.org/x/crypto v0.51.0 // indirect
+       golang.org/x/net v0.55.0 // indirect
        golang.org/x/oauth2 v0.36.0 // indirect
        golang.org/x/sync v0.20.0 // indirect
-       golang.org/x/sys v0.42.0 // indirect
-       golang.org/x/text v0.35.0 // indirect
+       golang.org/x/sys v0.45.0 // indirect
+       golang.org/x/text v0.37.0 // indirect
        golang.org/x/time v0.15.0 // indirect
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/e2e_tests/go.sum 
new/osconfig-20260708.00/e2e_tests/go.sum
--- old/osconfig-20260625.00/e2e_tests/go.sum   2026-06-25 16:31:39.000000000 
+0200
+++ new/osconfig-20260708.00/e2e_tests/go.sum   2026-07-08 10:05:56.000000000 
+0200
@@ -375,8 +375,8 @@
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod 
h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200707235045-ab33eee955e0/go.mod 
h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod 
h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod 
h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod 
h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod 
h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod 
h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -456,8 +456,8 @@
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod 
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod 
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -554,8 +554,8 @@
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod 
h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -565,8 +565,8 @@
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod 
h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod 
h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod 
h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod 
h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod 
h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/go.mod 
new/osconfig-20260708.00/go.mod
--- old/osconfig-20260625.00/go.mod     2026-06-25 16:31:39.000000000 +0200
+++ new/osconfig-20260708.00/go.mod     2026-07-08 10:05:56.000000000 +0200
@@ -185,7 +185,7 @@
        go.yaml.in/yaml/v3 v3.0.4 // indirect
        golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
        golang.org/x/mod v0.35.0 // indirect
-       golang.org/x/net v0.54.0 // indirect
+       golang.org/x/net v0.55.0 // indirect
        golang.org/x/sync v0.20.0 // indirect
        golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect
        golang.org/x/text v0.37.0 // indirect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/go.sum 
new/osconfig-20260708.00/go.sum
--- old/osconfig-20260625.00/go.sum     2026-06-25 16:31:39.000000000 +0200
+++ new/osconfig-20260708.00/go.sum     2026-07-08 10:05:56.000000000 +0200
@@ -653,8 +653,8 @@
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod 
h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod 
h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
-golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod 
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod 
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/osconfig-20260625.00/policies/recipes/artifacts_test.go 
new/osconfig-20260708.00/policies/recipes/artifacts_test.go
--- old/osconfig-20260625.00/policies/recipes/artifacts_test.go 2026-06-25 
16:31:39.000000000 +0200
+++ new/osconfig-20260708.00/policies/recipes/artifacts_test.go 2026-07-08 
10:05:56.000000000 +0200
@@ -16,17 +16,204 @@
 
 import (
        "context"
+       "errors"
+       "fmt"
+       "io"
+       "net/http"
+       "net/http/httptest"
        "net/url"
        "strings"
        "testing"
+
+       "github.com/GoogleCloudPlatform/osconfig/util/utiltest"
+
+       "cloud.google.com/go/osconfig/agentendpoint/apiv1beta/agentendpointpb"
 )
 
-func TestFetchArtifacts_http_InvalidURL(t *testing.T) {
-       uri := "ftp://google.com/agent.deb";
-       u, err := url.Parse(uri)
-       _, err = getHTTPArtifact(context.Background(), nil, *u)
-       if err == nil || !strings.Contains(err.Error(), "unsupported protocol 
scheme") {
-               t.Errorf("expected error (unsupported protocol); got(%v)", err)
+// TestGetHTTPArtifact verifies downloading artifacts via HTTP/HTTPS and 
handles various error scenarios.
+func TestGetHTTPArtifact(t *testing.T) {
+       ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r 
*http.Request) {
+               fmt.Fprint(w, "test data")
+       }))
+       defer ts.Close()
+
+       ts404 := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               w.WriteHeader(http.StatusNotFound)
+       }))
+       defer ts404.Close()
+
+       tests := []struct {
+               name    string
+               uri     string
+               client  *http.Client
+               want    string
+               wantErr error
+       }{
+               {
+                       name:    "valid URL, want test data and nil error",
+                       uri:     ts.URL,
+                       client:  ts.Client(),
+                       want:    "test data",
+                       wantErr: nil,
+               },
+               {
+                       name:    "unsupported protocol ftp, want unsupported 
protocol scheme error",
+                       uri:     "ftp://google.com/agent.deb";,
+                       client:  http.DefaultClient,
+                       want:    "",
+                       wantErr: errors.New("error, unsupported protocol scheme 
ftp"),
+               },
+               {
+                       name:    "http status 404, want got http status 404 
error",
+                       uri:     ts404.URL,
+                       client:  ts404.Client(),
+                       want:    "",
+                       wantErr: errors.New("got http status 404 when 
attempting to download artifact"),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       u, _ := url.Parse(tt.uri)
+                       reader, err := getHTTPArtifact(context.Background(), 
tt.client, *u)
+
+                       utiltest.AssertErrorMatchAndSkip(t, err, tt.wantErr)
+
+                       defer reader.Close()
+                       data, _ := io.ReadAll(reader)
+                       utiltest.AssertEquals(t, string(data), tt.want)
+               })
+       }
+}
+
+// setupFetchArtifactsTest sets up a mock HTTP server and GCS emulator 
environment for fetchArtifacts tests.
+func setupFetchArtifactsTest(t *testing.T) *httptest.Server {
+       t.Helper()
+       ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r 
*http.Request) {
+               if r.Method == "GET" {
+                       if strings.Contains(r.URL.Path, 
"test-bucket/test-object") {
+                               fmt.Fprint(w, "gcs data")
+                               return
+                       }
+                       if strings.Contains(r.URL.Path, "remote-artifact") {
+                               fmt.Fprint(w, "remote data")
+                               return
+                       }
+                       fmt.Fprint(w, "data")
+                       return
+               }
+               w.WriteHeader(http.StatusNotFound)
+       }))
+       t.Cleanup(ts.Close)
+
+       t.Setenv("STORAGE_EMULATOR_HOST", ts.URL)
+       return ts
+}
+
+// TestFetchArtifacts verifies that single or multiple artifacts can be 
correctly downloaded from HTTP/HTTPS sources and GCS.
+func TestFetchArtifacts(t *testing.T) {
+       ts := setupFetchArtifactsTest(t)
+
+       tests := []struct {
+               name      string
+               artifacts []*agentendpointpb.SoftwareRecipe_Artifact
+               want      map[string]string
+               wantErr   error
+       }{
+               {
+                       name: "remote artifact, want remote data and nil error",
+                       artifacts: []*agentendpointpb.SoftwareRecipe_Artifact{
+                               {
+                                       Id: "remote-art",
+                                       Artifact: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote_{
+                                               Remote: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote{
+                                                       Uri: ts.URL + 
"/remote-artifact",
+                                               },
+                                       },
+                               },
+                       },
+                       want: map[string]string{
+                               "remote-art": "remote data",
+                       },
+               },
+               {
+                       name: "GCS artifact, want gcs data and nil error",
+                       artifacts: []*agentendpointpb.SoftwareRecipe_Artifact{
+                               {
+                                       Id: "gcs-art",
+                                       Artifact: 
&agentendpointpb.SoftwareRecipe_Artifact_Gcs_{
+                                               Gcs: 
&agentendpointpb.SoftwareRecipe_Artifact_Gcs{
+                                                       Bucket: "test-bucket",
+                                                       Object: "test-object",
+                                               },
+                                       },
+                               },
+                       },
+                       want: map[string]string{
+                               "gcs-art": "gcs data",
+                       },
+               },
+               {
+                       name: "multiple artifacts, want data and nil error",
+                       artifacts: []*agentendpointpb.SoftwareRecipe_Artifact{
+                               {
+                                       Id: "art1",
+                                       Artifact: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote_{
+                                               Remote: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote{
+                                                       Uri: ts.URL,
+                                               },
+                                       },
+                               },
+                               {
+                                       Id: "art2",
+                                       Artifact: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote_{
+                                               Remote: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote{
+                                                       Uri: ts.URL,
+                                               },
+                                       },
+                               },
+                       },
+                       want: map[string]string{
+                               "art1": "data",
+                               "art2": "data",
+                       },
+               },
+               {
+                       name: "remote artifact unsupported protocol, want 
protocol error",
+                       artifacts: []*agentendpointpb.SoftwareRecipe_Artifact{
+                               {
+                                       Id: "ftp-art",
+                                       Artifact: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote_{
+                                               Remote: 
&agentendpointpb.SoftwareRecipe_Artifact_Remote{
+                                                       Uri: "ftp://localhost";,
+                                               },
+                                       },
+                               },
+                       },
+                       wantErr: fmt.Errorf("error fetching artifact %q: error, 
unsupported protocol scheme ftp", "ftp-art"),
+               },
+               {
+                       name: "unknown artifact type, want unknown artifact 
error",
+                       artifacts: []*agentendpointpb.SoftwareRecipe_Artifact{
+                               {
+                                       Id: "unknown-art",
+                               },
+                       },
+                       wantErr: fmt.Errorf("unknown artifact type for artifact 
unknown-art"),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tmpDir := t.TempDir()
+                       gotArtifacts, gotErr := 
fetchArtifacts(context.Background(), tt.artifacts, tmpDir)
+                       utiltest.AssertErrorMatch(t, gotErr, tt.wantErr)
+                       utiltest.AssertEquals(t, len(gotArtifacts), 
len(tt.want))
+                       for id, wantContent := range tt.want {
+                               path, _ := gotArtifacts[id]
+                               utiltest.AssertFileContents(t, path, 
wantContent)
+                       }
+               })
        }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/policies/recipes/steps_test.go 
new/osconfig-20260708.00/policies/recipes/steps_test.go
--- old/osconfig-20260625.00/policies/recipes/steps_test.go     2026-06-25 
16:31:39.000000000 +0200
+++ new/osconfig-20260708.00/policies/recipes/steps_test.go     2026-07-08 
10:05:56.000000000 +0200
@@ -3,15 +3,23 @@
 import (
        "archive/tar"
        "archive/zip"
+       "bytes"
+       "compress/gzip"
        "context"
        "fmt"
+       "io"
        "os"
        "path/filepath"
        "regexp"
+       "strconv"
        "testing"
        "time"
 
+       "github.com/ulikunitz/xz"
+       "github.com/ulikunitz/xz/lzma"
+
        "cloud.google.com/go/osconfig/agentendpoint/apiv1beta/agentendpointpb"
+       "github.com/GoogleCloudPlatform/osconfig/util/utiltest"
 )
 
 type fileEntry struct {
@@ -216,3 +224,372 @@
                t.Errorf("unable to close file: %s", err)
        }
 }
+
+// Test_parsePermissions verifies that octal permission strings are correctly 
parsed into os.FileMode.
+func Test_parsePermissions(t *testing.T) {
+       tests := []struct {
+               name     string
+               input    string
+               wantPerm os.FileMode
+               wantErr  error
+       }{
+               {
+                       name:     "empty string, want 0755",
+                       input:    "",
+                       wantPerm: 0755,
+                       wantErr:  nil,
+               },
+               {
+                       name:     "valid octal 0644, want 0644",
+                       input:    "0644",
+                       wantPerm: 0644,
+                       wantErr:  nil,
+               },
+               {
+                       name:     "valid octal 755, want 0755",
+                       input:    "755",
+                       wantPerm: 0755,
+                       wantErr:  nil,
+               },
+               {
+                       name:    "invalid octal, want parse error",
+                       input:   "888",
+                       wantErr: &strconv.NumError{Func: "ParseUint", Num: 
"888", Err: strconv.ErrSyntax},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       gotPerm, gotErr := parsePermissions(tt.input)
+                       utiltest.AssertErrorMatchAndSkip(t, gotErr, tt.wantErr)
+                       utiltest.AssertEquals(t, gotPerm, tt.wantPerm)
+               })
+       }
+}
+
+func setupSymlinkTest(t *testing.T) (dir, linkInside, linkOutside string) {
+       tmpDir := t.TempDir()
+       dir = filepath.Join(tmpDir, "dir")
+       otherDir := filepath.Join(tmpDir, "other")
+       if err := os.Mkdir(dir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       if err := os.Mkdir(otherDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       file := filepath.Join(dir, "file")
+       if err := os.WriteFile(file, []byte("test"), 0644); err != nil {
+               t.Fatal(err)
+       }
+       otherFile := filepath.Join(otherDir, "other_file")
+       if err := os.WriteFile(otherFile, []byte("test"), 0644); err != nil {
+               t.Fatal(err)
+       }
+
+       linkInside = filepath.Join(dir, "link_inside")
+       if err := os.Symlink(file, linkInside); err != nil {
+               t.Fatal(err)
+       }
+       linkOutside = filepath.Join(dir, "link_outside")
+       if err := os.Symlink(otherFile, linkOutside); err != nil {
+               t.Fatal(err)
+       }
+
+       return dir, linkInside, linkOutside
+}
+
+// Test_ensureSymlinkBelongsToDir ensures that symlinks do not point to 
locations outside the designated directory.
+func Test_ensureSymlinkBelongsToDir(t *testing.T) {
+       dir, linkInside, linkOutside := setupSymlinkTest(t)
+
+       tests := []struct {
+               name    string
+               link    string
+               wantErr error
+       }{
+               {
+                       name:    "link target inside dir, want nil error",
+                       link:    linkInside,
+                       wantErr: nil,
+               },
+               {
+                       name:    "link target outside dir, want outside link 
error",
+                       link:    linkOutside,
+                       wantErr: fmt.Errorf("symlink %s, does not belongs to 
dir %s, rel ../other/other_file", linkOutside, dir),
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := ensureSymlinkBelongsToDir(dir, tt.link)
+                       utiltest.AssertErrorMatch(t, err, tt.wantErr)
+               })
+       }
+}
+
+// Test_stepCopyFile verifies the file copying logic, including artifact 
resolution, overwrite behavior, and permission setting.
+func Test_stepCopyFile(t *testing.T) {
+       tmpDir := t.TempDir()
+       artifactPath := filepath.Join(tmpDir, "artifact")
+       content := "artifact content"
+       if err := os.WriteFile(artifactPath, []byte(content), 0644); err != nil 
{
+               t.Fatal(err)
+       }
+
+       destPath := filepath.Join(tmpDir, "dest")
+
+       tests := []struct {
+               name        string
+               step        *agentendpointpb.SoftwareRecipe_Step_CopyFile
+               artifacts   map[string]string
+               setupFunc   func()
+               wantErr     error
+               wantContent string
+               wantPerm    os.FileMode
+       }{
+               {
+                       name: "successful copy, want nil error and 0644 
permission",
+                       step: &agentendpointpb.SoftwareRecipe_Step_CopyFile{
+                               ArtifactId:  "art1",
+                               Destination: destPath,
+                               Permissions: "0644",
+                       },
+                       artifacts:   map[string]string{"art1": artifactPath},
+                       setupFunc:   func() {},
+                       wantContent: content,
+                       wantPerm:    0644,
+               },
+               {
+                       name: "file already exists and overwrite false, want 
file exists error",
+                       step: &agentendpointpb.SoftwareRecipe_Step_CopyFile{
+                               ArtifactId:  "art1",
+                               Destination: destPath,
+                               Overwrite:   false,
+                       },
+                       artifacts:   map[string]string{"art1": artifactPath},
+                       setupFunc:   func() { os.WriteFile(destPath, 
[]byte("old content"), 0644) },
+                       wantErr:     fmt.Errorf("file already exists at path %q 
and Overwrite = false", destPath),
+                       wantContent: "old content",
+                       wantPerm:    0644,
+               },
+               {
+                       name: "file already exists and overwrite true, want nil 
error and 0755",
+                       step: &agentendpointpb.SoftwareRecipe_Step_CopyFile{
+                               ArtifactId:  "art1",
+                               Destination: destPath,
+                               Overwrite:   true,
+                               Permissions: "0755",
+                       },
+                       artifacts:   map[string]string{"art1": artifactPath},
+                       setupFunc:   func() { os.WriteFile(destPath, 
[]byte("old content"), 0644) },
+                       wantContent: content,
+                       wantPerm:    0755,
+               },
+               {
+                       name: "invalid permissions, want parse error",
+                       step: &agentendpointpb.SoftwareRecipe_Step_CopyFile{
+                               ArtifactId:  "art1",
+                               Destination: destPath,
+                               Permissions: "888",
+                       },
+                       artifacts:   map[string]string{"art1": artifactPath},
+                       setupFunc:   func() {},
+                       wantErr:     &strconv.NumError{Func: "ParseUint", Num: 
"888", Err: strconv.ErrSyntax},
+                       wantContent: "",
+                       wantPerm:    0,
+               },
+               {
+                       name: "artifact not found, want find error",
+                       step: &agentendpointpb.SoftwareRecipe_Step_CopyFile{
+                               ArtifactId:  "unknown",
+                               Destination: destPath,
+                       },
+                       artifacts:   map[string]string{"art1": artifactPath},
+                       setupFunc:   func() {},
+                       wantErr:     fmt.Errorf("could not find location for 
artifact \"unknown\""),
+                       wantContent: "",
+                       wantPerm:    0,
+               },
+               {
+                       name: "artifact file missing, want no file error",
+                       step: &agentendpointpb.SoftwareRecipe_Step_CopyFile{
+                               ArtifactId:  "art2",
+                               Destination: destPath,
+                       },
+                       artifacts:   map[string]string{"art2": 
filepath.Join(tmpDir, "missing")},
+                       setupFunc:   func() {},
+                       wantErr:     &os.PathError{Op: "open", Path: 
filepath.Join(tmpDir, "missing"), Err: fmt.Errorf("no such file or directory")},
+                       wantContent: "",
+                       wantPerm:    0,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Cleanup(func() {
+                               os.Remove(destPath)
+                       })
+                       tt.setupFunc()
+
+                       gotErr := stepCopyFile(tt.step, tt.artifacts, nil, "")
+                       utiltest.AssertErrorMatch(t, gotErr, tt.wantErr)
+                       utiltest.AssertFileExistsAndContents(t, destPath, 
tt.wantContent)
+                       info, err := os.Stat(destPath)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       utiltest.AssertEquals(t, info.Mode().Perm(), 
tt.wantPerm)
+               })
+       }
+}
+
+// Test_checkForConflicts verifies that archive extraction does not overwrite 
existing files.
+func Test_checkForConflicts(t *testing.T) {
+       existingFilePath := utiltest.WriteToTempFileMust(t, "exists.txt", 
[]byte("content"))
+       tmpDir := filepath.Dir(existingFilePath)
+
+       tests := []struct {
+               name    string
+               files   []string
+               wantErr error
+       }{
+               {
+                       name:  "no conflicts, want nil error",
+                       files: []string{"new.txt", "another.txt"},
+               },
+               {
+                       name:    "conflict with existing file, want file exists 
error",
+                       files:   []string{"exists.txt"},
+                       wantErr: fmt.Errorf("file exists: %s", 
existingFilePath),
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       buf := createTarArchive(t, tt.files)
+                       gotTar := tar.NewReader(buf)
+
+                       gotErr := checkForConflicts(gotTar, tmpDir)
+                       utiltest.AssertErrorMatch(t, gotErr, tt.wantErr)
+               })
+       }
+}
+
+func setupTestDecompress(t *testing.T, content []byte) (gzipData, xzData, 
lzmaData, bzip2Data []byte) {
+       // GZIP
+       var gzipBuf bytes.Buffer
+       gw := gzip.NewWriter(&gzipBuf)
+       if _, err := gw.Write(content); err != nil {
+               t.Fatal(err)
+       }
+       gw.Close()
+
+       // XZ
+       var xzBuf bytes.Buffer
+       xw, err := xz.NewWriter(&xzBuf)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if _, err := xw.Write(content); err != nil {
+               t.Fatal(err)
+       }
+       xw.Close()
+
+       // LZMA
+       var lzmaBuf bytes.Buffer
+       lw, err := lzma.NewWriter2(&lzmaBuf)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if _, err := lw.Write(content); err != nil {
+               t.Fatal(err)
+       }
+       lw.Close()
+
+       // BZIP2 (pre-generated hex for "dummy content")
+       bzip2Data = []byte{
+               0x42, 0x5a, 0x68, 0x39, 0x31, 0x41, 0x59, 0x26, 0x53, 0x59, 
0x7f, 0x40, 0x3b, 0xb8, 0x00, 0x00,
+               0x01, 0x11, 0x80, 0x40, 0x00, 0x0e, 0x03, 0x86, 0x20, 0x20, 
0x00, 0x22, 0x00, 0x69, 0xea, 0x10,
+               0x03, 0x02, 0x39, 0x84, 0x84, 0x31, 0x9e, 0x2e, 0xe4, 0x8a, 
0x70, 0xa1, 0x20, 0xfe, 0x80, 0x77,
+               0x70,
+       }
+
+       return gzipBuf.Bytes(), xzBuf.Bytes(), lzmaBuf.Bytes(), bzip2Data
+}
+
+// Test_decompress verifies that the decompress function correctly identifies 
different archive formats.
+func Test_decompress(t *testing.T) {
+       content := []byte("dummy content")
+       gzipData, xzData, lzmaData, bzip2Data := setupTestDecompress(t, content)
+
+       tests := []struct {
+               name        string
+               archiveType 
agentendpointpb.SoftwareRecipe_Step_ExtractArchive_ArchiveType
+               data        []byte
+               wantErr     error
+       }{
+               {
+                       name:        "archive type TAR, want nil",
+                       archiveType: 
agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR,
+                       data:        content,
+                       wantErr:     nil,
+               },
+               {
+                       name:        "archive type TAR_GZIP, want nil",
+                       archiveType: 
agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_GZIP,
+                       data:        gzipData,
+                       wantErr:     nil,
+               },
+               {
+                       name:        "archive type TAR_BZIP, want nil",
+                       archiveType: 
agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_BZIP,
+                       data:        bzip2Data,
+                       wantErr:     nil,
+               },
+               {
+                       name:        "archive type TAR_LZMA, want nil",
+                       archiveType: 
agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_LZMA,
+                       data:        lzmaData,
+                       wantErr:     nil,
+               },
+               {
+                       name:        "archive type TAR_XZ, want nil",
+                       archiveType: 
agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_XZ,
+                       data:        xzData,
+                       wantErr:     nil,
+               },
+               {
+                       name:        "unrecognized archive type, want error",
+                       archiveType: 999,
+                       data:        content,
+                       wantErr:     fmt.Errorf("Unrecognized archive type 
\"999\" when trying to decompress tar"),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       reader := bytes.NewReader(tt.data)
+                       gotReader, gotErr := decompress(reader, tt.archiveType)
+
+                       utiltest.AssertErrorMatchAndSkip(t, gotErr, tt.wantErr)
+                       gotContent, err := io.ReadAll(gotReader)
+                       if err != nil {
+                               t.Fatalf("failed to read decompressed content: 
%v", err)
+                       }
+                       utiltest.AssertEquals(t, gotContent, content)
+               })
+       }
+}
+
+func createTarArchive(t *testing.T, files []string) *bytes.Buffer {
+       t.Helper()
+       var buf bytes.Buffer
+       tw := tar.NewWriter(&buf)
+       for _, f := range files {
+               if err := tw.WriteHeader(&tar.Header{Name: f}); err != nil {
+                       t.Fatalf("failed to write tar header for %q: %v", f, 
err)
+               }
+       }
+       if err := tw.Close(); err != nil {
+               t.Fatalf("failed to close tar writer: %v", err)
+       }
+       return &buf
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/osconfig-20260625.00/util/utiltest/utiltest.go 
new/osconfig-20260708.00/util/utiltest/utiltest.go
--- old/osconfig-20260625.00/util/utiltest/utiltest.go  2026-06-25 
16:31:39.000000000 +0200
+++ new/osconfig-20260708.00/util/utiltest/utiltest.go  2026-07-08 
10:05:56.000000000 +0200
@@ -184,6 +184,20 @@
        }
 }
 
+// AssertFileExistsAndContents verifies that the file at filePath exists and 
matches wantContent.
+// If the file does not exist and wantContent is empty, the test is skipped.
+func AssertFileExistsAndContents(t *testing.T, filePath string, wantContent 
string) {
+       t.Helper()
+       _, err := os.Stat(filePath)
+       if os.IsNotExist(err) {
+               if wantContent == "" {
+                       t.SkipNow()
+               }
+               t.Fatalf("Failed to read file %q: %v", filePath, err)
+       }
+       AssertFileContents(t, filePath, wantContent)
+}
+
 // OverrideVariable overrides the value of a variable and returns a function 
to restore it.
 func OverrideVariable[T any](t *testing.T, ptr *T, val T) {
        original := *ptr

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/google-osconfig-agent/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.google-osconfig-agent.new.1991/vendor.tar.gz 
differ: char 122, line 1

Reply via email to