1- package atomicfile_test
1+ package atomicfile
22
33import (
44 "bytes"
5+ "errors"
56 "os"
67 "path/filepath"
78 "runtime"
89 "testing"
910
1011 "github.com/stretchr/testify/assert"
1112 "github.com/stretchr/testify/require"
12-
13- "github.com/docker/docker-agent/pkg/atomicfile"
1413)
1514
15+ // TestWriteCreatesFileWithMode is the plan's restored "new file gets the
16+ // requested mode" regression, inadvertently dropped when this file was
17+ // rewritten to add the mid-write cleanup tests below: Write must create a
18+ // brand-new file with exactly the requested permission bits, not whatever
19+ // natefinch/atomic.WriteFile's own default (umask-derived) mode would be.
1620func TestWriteCreatesFileWithMode (t * testing.T ) {
1721 t .Parallel ()
1822 if runtime .GOOS == "windows" {
@@ -22,7 +26,7 @@ func TestWriteCreatesFileWithMode(t *testing.T) {
2226 dir := t .TempDir ()
2327 path := filepath .Join (dir , "secret" )
2428
25- require .NoError (t , atomicfile . Write (path , bytes .NewReader ([]byte ("hello" )), 0o600 ))
29+ require .NoError (t , Write (path , bytes .NewReader ([]byte ("hello" )), 0o600 ))
2630
2731 data , err := os .ReadFile (path )
2832 require .NoError (t , err )
@@ -33,6 +37,10 @@ func TestWriteCreatesFileWithMode(t *testing.T) {
3337 assert .Equal (t , os .FileMode (0o600 ), info .Mode ().Perm ())
3438}
3539
40+ // TestWriteOverwritesAndRetightensMode is the restored counterpart for an
41+ // EXISTING destination with a looser mode (0o644): a replacement Write
42+ // must retighten it to exactly the requested (0o600) bits, not merely
43+ // preserve whatever mode the file already had.
3644func TestWriteOverwritesAndRetightensMode (t * testing.T ) {
3745 t .Parallel ()
3846 if runtime .GOOS == "windows" {
@@ -43,7 +51,7 @@ func TestWriteOverwritesAndRetightensMode(t *testing.T) {
4351 path := filepath .Join (dir , "secret" )
4452
4553 require .NoError (t , os .WriteFile (path , []byte ("old" ), 0o644 ))
46- require .NoError (t , atomicfile . Write (path , bytes .NewReader ([]byte ("new" )), 0o600 ))
54+ require .NoError (t , Write (path , bytes .NewReader ([]byte ("new" )), 0o600 ))
4755
4856 data , err := os .ReadFile (path )
4957 require .NoError (t , err )
@@ -54,11 +62,84 @@ func TestWriteOverwritesAndRetightensMode(t *testing.T) {
5462 assert .Equal (t , os .FileMode (0o600 ), info .Mode ().Perm ())
5563}
5664
65+ // TestWriteReturnsErrorForMissingDirectory is the restored regression for
66+ // a missing parent directory: Write must surface the underlying error
67+ // rather than creating the parent itself or panicking.
5768func TestWriteReturnsErrorForMissingDirectory (t * testing.T ) {
5869 t .Parallel ()
5970 dir := t .TempDir ()
6071 path := filepath .Join (dir , "missing" , "file" )
6172
62- err := atomicfile . Write (path , bytes .NewReader ([]byte ("x" )), 0o600 )
73+ err := Write (path , bytes .NewReader ([]byte ("x" )), 0o600 )
6374 assert .Error (t , err )
6475}
76+
77+ // partialFailReader returns chunk on its first Read (so the underlying
78+ // os.CreateTemp'd file genuinely receives that data before anything goes
79+ // wrong) and then fails on every subsequent Read. This reproduces a
80+ // mid-write failure AFTER a real, partially written temporary file
81+ // already exists on disk — the seam the plan requires ("deterministically
82+ // fail only after a controlled temporary/partial output has been
83+ // created"), as opposed to a permission trick that would prevent the
84+ // temp file from ever being created at all.
85+ type partialFailReader struct {
86+ chunk []byte
87+ err error
88+ sent bool
89+ }
90+
91+ func (r * partialFailReader ) Read (p []byte ) (int , error ) {
92+ if ! r .sent {
93+ r .sent = true
94+ n := copy (p , r .chunk )
95+ return n , nil
96+ }
97+ return 0 , r .err
98+ }
99+
100+ // TestWrite_CleansUpTempFileOnMidWriteFailure proves the real production
101+ // cleanup path: after Write's underlying temp file has already received
102+ // partial data, a subsequent read failure must leave no temp file behind
103+ // in the target directory, and the destination path itself must remain
104+ // untouched (never partially written, never created if it didn't already
105+ // exist).
106+ func TestWrite_CleansUpTempFileOnMidWriteFailure (t * testing.T ) {
107+ dir := t .TempDir ()
108+ target := filepath .Join (dir , "artifact.bin" )
109+ injectedErr := errors .New ("injected mid-write failure" )
110+
111+ err := Write (target , & partialFailReader {chunk : []byte ("partial-data" ), err : injectedErr }, 0o600 )
112+ require .Error (t , err )
113+ assert .Contains (t , err .Error (), injectedErr .Error ())
114+
115+ _ , statErr := os .Stat (target )
116+ assert .True (t , os .IsNotExist (statErr ), "the destination file must never be created on a mid-write failure" )
117+
118+ entries , readErr := os .ReadDir (dir )
119+ require .NoError (t , readErr )
120+ assert .Empty (t , entries , "no partial/orphan temp file must remain in the directory after a mid-write failure" )
121+ }
122+
123+ // TestWrite_CleansUpTempFileOnMidWriteFailure_ExistingDestination is the
124+ // same regression when the destination already has prior content: a
125+ // mid-write failure for a REPLACEMENT write must leave the original
126+ // content intact and, again, no temp file residue.
127+ func TestWrite_CleansUpTempFileOnMidWriteFailure_ExistingDestination (t * testing.T ) {
128+ dir := t .TempDir ()
129+ target := filepath .Join (dir , "artifact.bin" )
130+ require .NoError (t , os .WriteFile (target , []byte ("original" ), 0o600 ))
131+ injectedErr := errors .New ("injected mid-write failure" )
132+
133+ err := Write (target , & partialFailReader {chunk : []byte ("partial-data" ), err : injectedErr }, 0o600 )
134+ require .Error (t , err )
135+ assert .Contains (t , err .Error (), injectedErr .Error ())
136+
137+ data , readErr := os .ReadFile (target )
138+ require .NoError (t , readErr )
139+ assert .Equal (t , "original" , string (data ), "the original destination content must survive a failed replacement" )
140+
141+ entries , err := os .ReadDir (dir )
142+ require .NoError (t , err )
143+ require .Len (t , entries , 1 , "no partial/orphan temp file must remain alongside the untouched destination" )
144+ assert .Equal (t , "artifact.bin" , entries [0 ].Name ())
145+ }
0 commit comments