@@ -6,11 +6,14 @@ package awss3
66
77import (
88 "context"
9+ "io"
10+ "strings"
911 "testing"
1012 "time"
1113
1214 "github.com/elastic/beats/v7/libbeat/beat"
1315 "github.com/elastic/elastic-agent-libs/logp"
16+ "github.com/elastic/elastic-agent-libs/logp/logptest"
1417 "github.com/elastic/elastic-agent-libs/monitoring"
1518
1619 "github.com/aws/aws-sdk-go-v2/aws"
@@ -139,7 +142,7 @@ func TestS3Poller(t *testing.T) {
139142 BucketListPrefix : listPrefix ,
140143 RegionName : "region" ,
141144 }
142- log := logp . NewLogger ( inputName )
145+ log := logptest . NewTestingLogger ( t , inputName )
143146 poller := & s3PollerInput {
144147 log : log ,
145148 config : cfg ,
@@ -156,6 +159,120 @@ func TestS3Poller(t *testing.T) {
156159 poller .runPoll (ctx )
157160 })
158161
162+ t .Run ("Poll finalizes objects after ACK (backup + delete)" , func (t * testing.T ) {
163+ store := openTestStatestore ()
164+
165+ ctx , cancel := context .WithTimeout (context .Background (), 3 * testTimeout )
166+ defer cancel ()
167+
168+ ctrl , ctx := gomock .WithContext (ctx , t )
169+ defer ctrl .Finish ()
170+
171+ mockAPI := NewMockS3API (ctrl )
172+ mockPager := NewMockS3Pager (ctrl )
173+ pipeline := newFakePipeline ()
174+
175+ backupDone := make (chan struct {})
176+ deleteDone := make (chan struct {})
177+
178+ mockAPI .EXPECT ().
179+ ListObjectsPaginator (gomock .Eq (bucket ), gomock .Eq (listPrefix ), gomock .Any ()).
180+ Times (1 ).
181+ DoAndReturn (func (_ , _ , _ string ) s3Pager {
182+ return mockPager
183+ })
184+
185+ // Poller listing loop calls HasMorePages before and after NextPage.
186+ hasMoreCalls := 0
187+ mockPager .EXPECT ().
188+ HasMorePages ().
189+ Times (2 ).
190+ DoAndReturn (func () bool {
191+ hasMoreCalls ++
192+ return hasMoreCalls == 1
193+ })
194+
195+ mockPager .EXPECT ().
196+ NextPage (gomock .Any ()).
197+ Times (1 ).
198+ DoAndReturn (func (_ context.Context , optFns ... func (* s3.Options )) (* s3.ListObjectsV2Output , error ) {
199+ return & s3.ListObjectsV2Output {
200+ Contents : []types.Object {
201+ {
202+ ETag : aws .String ("etag1" ),
203+ Key : aws .String ("key1" ),
204+ LastModified : aws .Time (time .Now ()),
205+ },
206+ },
207+ }, nil
208+ })
209+
210+ mockAPI .EXPECT ().
211+ GetObject (gomock .Any (), gomock .Eq ("" ), gomock .Eq (bucket ), gomock .Eq ("key1" )).
212+ Times (1 ).
213+ DoAndReturn (func (_ context.Context , _ string , _ string , _ string ) (* s3.GetObjectOutput , error ) {
214+ // A simple text payload that produces at least one event.
215+ return & s3.GetObjectOutput {
216+ Body : io .NopCloser (strings .NewReader ("hello\n " )),
217+ }, nil
218+ })
219+
220+ mockAPI .EXPECT ().
221+ CopyObject (gomock .Any (), gomock .Eq ("" ), gomock .Eq (bucket ), gomock .Eq ("backup-bucket" ), gomock .Eq ("key1" ), gomock .Eq ("processed/key1" )).
222+ Times (1 ).
223+ DoAndReturn (func (_ context.Context , _ string , _ string , _ string , _ string , _ string ) (* s3.CopyObjectOutput , error ) {
224+ close (backupDone )
225+ return & s3.CopyObjectOutput {}, nil
226+ })
227+
228+ mockAPI .EXPECT ().
229+ DeleteObject (gomock .Any (), gomock .Eq ("" ), gomock .Eq (bucket ), gomock .Eq ("key1" )).
230+ Times (1 ).
231+ DoAndReturn (func (_ context.Context , _ string , _ string , _ string ) (* s3.DeleteObjectOutput , error ) {
232+ close (deleteDone )
233+ return & s3.DeleteObjectOutput {}, nil
234+ })
235+
236+ backupCfg := backupConfig {
237+ NonAWSBackupToBucketName : "backup-bucket" ,
238+ BackupToBucketPrefix : "processed/" ,
239+ Delete : true ,
240+ }
241+
242+ cfg := config {
243+ NumberOfWorkers : 1 ,
244+ BucketListInterval : pollInterval ,
245+ BucketARN : bucket ,
246+ BucketListPrefix : listPrefix ,
247+ BackupConfig : backupCfg ,
248+ }
249+ log := logptest .NewTestingLogger (t , inputName )
250+
251+ s3ObjProc := newS3ObjectProcessorFactory (nil , mockAPI , nil , backupCfg , logp .NewNopLogger ())
252+ registry , err := newStateRegistry (nil , store , listPrefix , cfg .LexicographicalOrdering , cfg .LexicographicalLookbackKeys )
253+ require .NoError (t , err , "registry creation must succeed" )
254+
255+ poller := & s3PollerInput {
256+ log : log ,
257+ config : cfg ,
258+ s3 : mockAPI ,
259+ pipeline : pipeline ,
260+ s3ObjectHandler : s3ObjProc ,
261+ registry : registry ,
262+ provider : "provider" ,
263+ metrics : newInputMetrics (monitoring .NewRegistry (), 0 , logp .NewNopLogger ()),
264+ filterProvider : newFilterProvider (& cfg ),
265+ strategy : newPollingStrategy (cfg .LexicographicalOrdering , log ),
266+ status : & statusReporterHelperMock {},
267+ }
268+
269+ poller .runPoll (ctx )
270+
271+ // Finalization happens asynchronously after ACK; wait until it completes.
272+ waitForChannel (t , backupDone , 3 * testTimeout )
273+ waitForChannel (t , deleteDone , 3 * testTimeout )
274+ })
275+
159276 t .Run ("restart bucket scan after paging errors" , func (t * testing.T ) {
160277 // Change the restart limit to 2 consecutive errors, so the test doesn't
161278 // take too long to run
@@ -284,7 +401,7 @@ func TestS3Poller(t *testing.T) {
284401 RegionName : "region" ,
285402 }
286403
287- log := logp . NewLogger ( inputName )
404+ log := logptest . NewTestingLogger ( t , inputName )
288405 poller := & s3PollerInput {
289406 log : log ,
290407 config : config {
@@ -390,7 +507,7 @@ func TestS3Poller(t *testing.T) {
390507 LexicographicalOrdering : true ,
391508 LexicographicalLookbackKeys : 100 ,
392509 }
393- log := logp . NewLogger ( inputName )
510+ log := logptest . NewTestingLogger ( t , inputName )
394511 poller := & s3PollerInput {
395512 log : log ,
396513 config : cfg ,
@@ -431,7 +548,7 @@ func TestS3Poller(t *testing.T) {
431548 ListObjectsPaginator (gomock .Eq (bucket ), gomock .Eq ("" ), gomock .Eq ("" )).
432549 Times (1 ).
433550 DoAndReturn (func (_ , _ , startAfterKey string ) s3Pager {
434- require .Equal ( t , "" , startAfterKey )
551+ require .Empty ( t , startAfterKey )
435552 return mockPager
436553 })
437554
@@ -479,7 +596,7 @@ func TestS3Poller(t *testing.T) {
479596 LexicographicalOrdering : true ,
480597 LexicographicalLookbackKeys : 100 ,
481598 }
482- log := logp . NewLogger ( inputName )
599+ log := logptest . NewTestingLogger ( t , inputName )
483600 poller := & s3PollerInput {
484601 log : log ,
485602 config : cfg ,
@@ -517,7 +634,7 @@ func TestS3Poller(t *testing.T) {
517634 ListObjectsPaginator (gomock .Eq (bucket ), gomock .Eq ("" ), gomock .Eq ("" )).
518635 Times (1 ).
519636 DoAndReturn (func (_ , _ , startAfterKey string ) s3Pager {
520- require .Equal ( t , "" , startAfterKey )
637+ require .Empty ( t , startAfterKey )
521638 return mockPager
522639 })
523640
@@ -564,7 +681,7 @@ func TestS3Poller(t *testing.T) {
564681 RegionName : "region" ,
565682 LexicographicalOrdering : false ,
566683 }
567- log := logp . NewLogger ( inputName )
684+ log := logptest . NewTestingLogger ( t , inputName )
568685 poller := & s3PollerInput {
569686 log : log ,
570687 config : cfg ,
@@ -702,7 +819,7 @@ func TestS3Poller(t *testing.T) {
702819 BucketListPrefix : "" ,
703820 RegionName : "region" ,
704821 }
705- log := logp . NewLogger ( inputName )
822+ log := logptest . NewTestingLogger ( t , inputName )
706823 poller := & s3PollerInput {
707824 log : log ,
708825 config : cfg ,
@@ -728,9 +845,19 @@ func TestS3Poller(t *testing.T) {
728845 })
729846}
730847
848+ func waitForChannel (t * testing.T , ch <- chan struct {}, timeout time.Duration ) {
849+ t .Helper ()
850+ select {
851+ case <- ch :
852+ return
853+ case <- time .After (timeout ):
854+ t .Fatal ("timed out waiting for async finalization" )
855+ }
856+ }
857+
731858func Test_S3StateHandling (t * testing.T ) {
732859 bucket := "bucket"
733- logger := logp . NewLogger ( inputName )
860+ logger := logptest . NewTestingLogger ( t , inputName )
734861 fixedTimeNow := time .Now ()
735862
736863 tests := []struct {
@@ -933,6 +1060,11 @@ func Test_S3StateHandling(t *testing.T) {
9331060 eventCallback (beat.Event {})
9341061 return nil
9351062 })
1063+ // FinalizeS3Object is called inside the ACK callback for every successfully
1064+ // stored object (state.Stored == true). The tests here use an empty
1065+ // backupConfig so the real implementation is a no-op, but the mock still
1066+ // needs an expectation to avoid "unexpected call" panics.
1067+ mockS3ObjectHandler .EXPECT ().FinalizeS3Object ().AnyTimes ().Return (nil )
9361068
9371069 store := openTestStatestore ()
9381070 s3Registry , err := newStateRegistry (logger , store , "" , false , 0 )
0 commit comments