diff --git a/pkg/gofr/datasource/file/s3/file_test.go b/pkg/gofr/datasource/file/s3/file_test.go new file mode 100644 index 000000000..8723b830f --- /dev/null +++ b/pkg/gofr/datasource/file/s3/file_test.go @@ -0,0 +1,1005 @@ +package s3 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +var ( + errGetObject = errors.New("failed to get object from S3") + errPutObject = errors.New("failed to put object to S3") + errCloseFailed = errors.New("close failed") + errReadAllFailed = errors.New("simulated io.ReadAll error") + errS3Test = errors.New("s3 error") +) + +// Helper function to create a new S3File instance for testing. +func newTestS3File(t *testing.T, ctrl *gomock.Controller, name string, size, offset int64) *S3File { + t.Helper() + return newTestS3FileWithTime(t, ctrl, name, size, offset, time.Now()) +} + +// Helper function to create a new S3File instance for testing with custom time. +func newTestS3FileWithTime(_ *testing.T, ctrl *gomock.Controller, name string, size, offset int64, + lastModified time.Time) *S3File { + mockClient := NewMocks3Client(ctrl) + mockMetrics := NewMockMetrics(ctrl) + mockLogger := NewMockLogger(ctrl) + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + return &S3File{ + conn: mockClient, + name: name, + offset: offset, + logger: mockLogger, + metrics: mockMetrics, + size: size, + lastModified: lastModified, + } +} + +// Helper to create a successful GetObjectOutput. +func getObjectOutput(content string) *s3.GetObjectOutput { + return &s3.GetObjectOutput{ + Body: io.NopCloser(bytes.NewReader([]byte(content))), + ContentLength: aws.Int64(int64(len(content))), + } +} + +// Helper to create a successful PutObjectOutput. +func putObjectOutput() *s3.PutObjectOutput { + return &s3.PutObjectOutput{} +} + +// Define mock for io.ReadCloser to test Close. +type mockReadCloser struct { + io.Reader + closeErr error +} + +func (m *mockReadCloser) Close() error { + return m.closeErr +} + +// TestS3File_Close_Success tests the successful Close operations of S3File. +func TestS3File_Close_Success(t *testing.T) { + testCases := []struct { + name string + body io.ReadCloser + }{ + { + name: "Success_BodyNil", + body: nil, + }, + { + name: "Success_BodyNotNil", + body: &mockReadCloser{Reader: bytes.NewReader([]byte("test")), closeErr: nil}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, "test-bucket/test-file.txt", 10, 0) + f.body = tc.body + + err := f.Close() + + assert.NoError(t, err, "Expected no error") + }) + } +} + +// TestS3File_Close_Failure tests the failure cases of S3File Close operations. +func TestS3File_Close_Failure(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, "test-bucket/test-file.txt", 10, 0) + f.body = &mockReadCloser{Reader: bytes.NewReader([]byte("test")), closeErr: errCloseFailed} + + err := f.Close() + + require.Error(t, err, "Expected an error") + assert.True(t, errors.Is(err, errCloseFailed) || strings.Contains(err.Error(), errCloseFailed.Error()), + "Expected error to be %v or contain %q, got %v", errCloseFailed, errCloseFailed.Error(), err) +} + +// TestS3File_Read_Success tests the successful Read operations of S3File. +func TestS3File_Read_Success(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + content := "This is a test file content." + + testCases := []struct { + name string + offset int64 + bufferLen int + mockGetObject func(m *Mocks3ClientMockRecorder) + expectedN int + expectedP string + expectedErr error + }{ + { + name: "Success_ReadFromStart", + offset: 0, + bufferLen: 5, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Eq(&s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(fileName), + })).Return(getObjectOutput(content), nil) + }, + expectedN: 5, + expectedP: "This ", + expectedErr: nil, + }, + { + name: "Success_ReadFromOffset", + offset: 5, + bufferLen: 4, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Eq(&s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(fileName), + })).Return(getObjectOutput(content), nil) + }, + expectedN: 4, + expectedP: "is a", + expectedErr: nil, + }, + { + name: "Success_ReadToEOF", + offset: 0, + bufferLen: len(content), + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(content), nil) + }, + expectedN: len(content), + expectedP: content, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, int64(len(content)), tc.offset) + + m := f.conn.(*Mocks3Client) + + tc.mockGetObject(m.EXPECT()) + + p := make([]byte, tc.bufferLen) + + for i := range p { + p[i] = 0 + } + + n, err := f.Read(p) + + require.NoError(t, err, "Expected no error") + assert.Equal(t, tc.expectedN, n, "Expected bytes read %d, got %d", tc.expectedN, n) + assert.Equal(t, tc.expectedP[:n], string(p[:n]), "Expected content %q, got %q", tc.expectedP[:n], string(p[:n])) + }) + } +} + +// TestS3File_Read_Failure tests the failure cases of S3File Read operations. +func TestS3File_Read_Failure(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + content := "This is a test file content." + + testCases := []struct { + name string + offset int64 + bufferLen int + mockGetObject func(m *Mocks3ClientMockRecorder) + expectedN int + expectedP string + expectedErr error + }{ + { + name: "Failure_GetObjectError", + offset: 0, + bufferLen: 10, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test) + }, + expectedN: 0, + expectedP: "", + expectedErr: errS3Test, + }, + { + name: "Failure_NilResponse", + offset: 0, + bufferLen: 10, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{Body: nil}, nil) + }, + expectedN: 0, + expectedP: "", + expectedErr: ErrNilResponse, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, int64(len(content)), tc.offset) + + m := f.conn.(*Mocks3Client) + + tc.mockGetObject(m.EXPECT()) + + p := make([]byte, tc.bufferLen) + + for i := range p { + p[i] = 0 + } + + n, err := f.Read(p) + + require.Error(t, err, "Expected an error") + require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err) + assert.Equal(t, tc.expectedN, n, "Expected bytes read %d, got %d", tc.expectedN, n) + }) + } +} + +// TestS3File_ReadAt_Success tests the successful ReadAt operations of S3File. +func TestS3File_ReadAt_Success(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + content := "This is a test file content." + fileSize := int64(len(content)) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, fileSize, 10) + m := f.conn.(*Mocks3Client) + + m.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(content), nil) + + p := make([]byte, 4) + n, err := f.ReadAt(p, 5) + + require.NoError(t, err, "Expected no error") + assert.Equal(t, 4, n, "Expected bytes read 4, got %d", n) + assert.Equal(t, "is a", string(p[:n]), "Expected content %q, got %q", "is a", string(p[:n])) + assert.Equal(t, int64(10), f.offset, "ReadAt modified offset. Expected 10, got %d", f.offset) +} + +// TestS3File_ReadAt_Failure tests the failure cases of S3File ReadAt operations. +func TestS3File_ReadAt_Failure(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + content := "This is a test file content." + fileSize := int64(len(content)) + + testCases := []struct { + name string + readAtOffset int64 + bufferLen int + mockGetObject func(m *Mocks3ClientMockRecorder) + expectedN int + expectedErr error + }{ + { + name: "Failure_GetObjectError", + readAtOffset: 0, + bufferLen: 10, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test) + }, + expectedN: 0, + expectedErr: errS3Test, + }, + { + name: "Failure_NilBody", + readAtOffset: 0, + bufferLen: 10, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{Body: nil}, nil) + }, + expectedN: 0, + expectedErr: io.EOF, + }, + { + name: "Failure_OutOfRange", + readAtOffset: 25, + bufferLen: 4, + mockGetObject: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(content), nil) + }, + expectedN: 0, + expectedErr: ErrOutOfRange, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, fileSize, 10) + m := f.conn.(*Mocks3Client) + + tc.mockGetObject(m.EXPECT()) + + p := make([]byte, tc.bufferLen) + n, err := f.ReadAt(p, tc.readAtOffset) + + require.Error(t, err, "Expected an error") + require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err) + assert.Equal(t, tc.expectedN, n, "Expected bytes read %d, got %d", tc.expectedN, n) + }) + } +} + +// TestS3File_Write_Success tests the successful Write operations of S3File. +func TestS3File_Write_Success(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + initialContent := "Hello, World!" + initialSize := int64(len(initialContent)) + dataToWrite := []byte("GoFr") + + testCases := []struct { + name string + initialOffset int64 + initialSize int64 + dataToWrite []byte + mockExpectations func(m *Mocks3ClientMockRecorder) + expectedN int + expectedOffset int64 + expectedSize int64 + expectedErr error + }{ + { + name: "Success_WriteFromStart_NewFile", + initialOffset: 0, + initialSize: 0, + dataToWrite: dataToWrite, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.PutObject(gomock.Any(), gomock.Any()).Return(putObjectOutput(), nil) + }, + expectedN: len(dataToWrite), + expectedOffset: int64(len(dataToWrite)), + expectedSize: int64(len(dataToWrite)), + expectedErr: nil, + }, + { + name: "Success_WriteFromStart_Overwrite", + initialOffset: 0, + initialSize: initialSize, + dataToWrite: dataToWrite, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.PutObject(gomock.Any(), gomock.Any()).Return(putObjectOutput(), nil) + }, + expectedN: len(dataToWrite), + expectedOffset: int64(len(dataToWrite)), + expectedSize: int64(len(dataToWrite)), + expectedErr: nil, + }, + { + name: "Success_WriteFromMiddle", + initialOffset: 7, + initialSize: initialSize, + dataToWrite: dataToWrite, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(initialContent), nil) + m.PutObject(gomock.Any(), gomock.Any()).Return(putObjectOutput(), nil) + }, + expectedN: len(dataToWrite), + expectedOffset: 7 + int64(len(dataToWrite)), + expectedSize: initialSize, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, tc.initialSize, tc.initialOffset) + + m := f.conn.(*Mocks3Client) + + tc.mockExpectations(m.EXPECT()) + + n, err := f.Write(tc.dataToWrite) + + require.NoError(t, err, "Expected no error") + assert.Equal(t, tc.expectedN, n, "Expected bytes written %d, got %d", tc.expectedN, n) + assert.Equal(t, tc.expectedOffset, f.offset, "Expected offset %d, got %d", tc.expectedOffset, f.offset) + assert.Equal(t, tc.expectedSize, f.size, "Expected size %d, got %d", tc.expectedSize, f.size) + }) + } +} + +// TestS3File_Write_Failure tests the failure cases of S3File Write operations. +func TestS3File_Write_Failure(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + initialContent := "Hello, World!" + initialSize := int64(len(initialContent)) + dataToWrite := []byte("GoFr") + + testCases := []struct { + name string + initialOffset int64 + initialSize int64 + dataToWrite []byte + mockExpectations func(m *Mocks3ClientMockRecorder) + expectedN int + expectedOffset int64 + expectedSize int64 + expectedErr error + }{ + { + name: "Failure_GetObjectError", + initialOffset: 5, + initialSize: initialSize, + dataToWrite: dataToWrite, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test) + }, + expectedN: 0, + expectedOffset: 5, + expectedSize: initialSize, + expectedErr: errS3Test, + }, + { + name: "Failure_PutObjectError", + initialOffset: 0, + initialSize: initialSize, + dataToWrite: dataToWrite, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.PutObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test) + }, + expectedN: 0, + expectedOffset: 0, + expectedSize: initialSize, + expectedErr: errS3Test, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, tc.initialSize, tc.initialOffset) + + m := f.conn.(*Mocks3Client) + + tc.mockExpectations(m.EXPECT()) + + n, err := f.Write(tc.dataToWrite) + + require.Error(t, err, "Expected an error") + require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err) + assert.Equal(t, tc.expectedN, n, "Expected bytes written %d, got %d", tc.expectedN, n) + assert.Equal(t, tc.expectedOffset, f.offset, "Expected offset %d, got %d", tc.expectedOffset, f.offset) + assert.Equal(t, tc.expectedSize, f.size, "Expected size %d, got %d", tc.expectedSize, f.size) + }) + } +} + +// TestS3File_WriteAt_Success tests the successful WriteAt operations of S3File. +func TestS3File_WriteAt_Success(t *testing.T) { + bucketName, fileName := "test-bucket", "test-file.txt" + fullPath := bucketName + "/" + fileName + initialContent := "Hello, World!" + initialSize, dataToWrite := int64(len(initialContent)), []byte("GoFr") + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, initialSize, 10) + m := f.conn.(*Mocks3Client) + + m.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(initialContent), nil) + + expectedPutBody := []byte("Hello, GoFrd!") + + m.EXPECT().PutObject(gomock.Any(), gomock.Any()).Do(func(_ context.Context, params *s3.PutObjectInput, _ ...func(*s3.Options)) { + actualPutBody := getBodyContent(t, params.Body) + require.True(t, bytes.Equal(expectedPutBody, actualPutBody), + "PutObject Body mismatch. Expected: %q, Got: %q", string(expectedPutBody), string(actualPutBody)) + }).Return(putObjectOutput(), nil) + + n, err := f.WriteAt(dataToWrite, 7) + + require.NoError(t, err, "Expected no error") + assert.Equal(t, len(dataToWrite), n, "Expected bytes written %d, got %d", len(dataToWrite), n) + assert.Equal(t, int64(10), f.offset, "WriteAt modified offset. Expected 10, got %d", f.offset) + assert.Equal(t, initialSize, f.size, "Expected size %d, got %d", initialSize, f.size) +} + +// TestS3File_WriteAt_Failure tests the failure cases of S3File WriteAt operations. +func TestS3File_WriteAt_Failure(t *testing.T) { + bucketName, fileName := "test-bucket", "test-file.txt" + fullPath := bucketName + "/" + fileName + initialContent := "Hello, World!" + initialSize, dataToWrite := int64(len(initialContent)), []byte("GoFr") + + testCases := []struct { + name string + writeAtOffset int64 + initialOffset int64 + mockExpectations func(m *Mocks3ClientMockRecorder) + expectedN int + expectedOffset int64 + expectedSize int64 + expectedErr error + }{ + { + name: "Failure_GetObjectError", + initialOffset: 10, + writeAtOffset: 5, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errGetObject) + }, + expectedN: 0, + expectedOffset: 10, + expectedSize: initialSize, + expectedErr: errGetObject, + }, + { + name: "Failure_PutObjectError", + initialOffset: 10, + writeAtOffset: 0, + mockExpectations: func(m *Mocks3ClientMockRecorder) { + m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(initialContent), nil) + m.PutObject(gomock.Any(), gomock.Any()).Return(nil, errPutObject) + }, + expectedN: 0, + expectedOffset: 10, + expectedSize: initialSize, + expectedErr: errPutObject, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, initialSize, tc.initialOffset) + + m := f.conn.(*Mocks3Client) + tc.mockExpectations(m.EXPECT()) + + n, err := f.WriteAt(dataToWrite, tc.writeAtOffset) + + require.Error(t, err, "Expected an error") + require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err) + assert.Equal(t, tc.expectedN, n, "Expected bytes written %d, got %d", tc.expectedN, n) + assert.Equal(t, tc.expectedOffset, f.offset, "WriteAt modified offset. Expected %d, got %d", tc.expectedOffset, f.offset) + assert.Equal(t, tc.expectedSize, f.size, "Expected size %d, got %d", tc.expectedSize, f.size) + }) + } +} + +// Helper to read the content of the PutObjectInput Body. +func getBodyContent(t *testing.T, body io.Reader) []byte { + t.Helper() + + b, err := io.ReadAll(body) + require.NoError(t, err, "Failed to read PutObject body") + + return b +} + +// TestS3File_Seek_Success tests the successful Seek operations of S3File. +func TestS3File_Seek_Success(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + fileSize := int64(20) + + testCases := []struct { + name string + initialOffset int64 + offset int64 + whence int + expectedNewOffset int64 + }{ + { + name: "SeekStart_Success", + initialOffset: 5, + offset: 10, + whence: io.SeekStart, + expectedNewOffset: 10, + }, + { + name: "SeekCurrent_Success", + initialOffset: 5, + offset: 10, + whence: io.SeekCurrent, + expectedNewOffset: 15, + }, + { + name: "SeekEnd_Success", + initialOffset: 5, + offset: -5, + whence: io.SeekEnd, + expectedNewOffset: 15, + }, + { + name: "SeekEnd_Success_ToStart", + initialOffset: 5, + offset: -20, + whence: io.SeekEnd, + expectedNewOffset: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, fileSize, tc.initialOffset) + + newOffset, err := f.Seek(tc.offset, tc.whence) + + require.NoError(t, err, "Expected no error") + assert.Equal(t, tc.expectedNewOffset, newOffset, "Expected new offset %d, got %d", tc.expectedNewOffset, newOffset) + assert.Equal(t, tc.expectedNewOffset, f.offset, "File struct offset was not updated. "+ + "Expected %d, got %d", tc.expectedNewOffset, f.offset) + }) + } +} + +// TestS3File_Seek_Failure tests the failure cases of S3File Seek operations. +func TestS3File_Seek_Failure(t *testing.T) { + bucketName := "test-bucket" + fileName := "test-file.txt" + fullPath := bucketName + "/" + fileName + fileSize := int64(20) + + testCases := []struct { + name string + initialOffset int64 + offset int64 + whence int + expectedErr error + }{ + { + name: "SeekStart_Failure_Negative", + initialOffset: 5, + offset: -1, + whence: io.SeekStart, + expectedErr: ErrOutOfRange, + }, + { + name: "SeekStart_Failure_TooLarge", + initialOffset: 5, + offset: 21, + whence: io.SeekStart, + expectedErr: ErrOutOfRange, + }, + { + name: "SeekCurrent_Failure_NegativeResult", + initialOffset: 5, + offset: -6, + whence: io.SeekCurrent, + expectedErr: ErrOutOfRange, + }, + { + name: "SeekEnd_Failure_TooLarge", + initialOffset: 5, + offset: 1, + whence: io.SeekEnd, + expectedErr: ErrOutOfRange, + }, + { + name: "Seek_InvalidWhence", + initialOffset: 5, + offset: 0, + whence: 3, // Invalid whence + expectedErr: os.ErrInvalid, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3File(t, ctrl, fullPath, fileSize, tc.initialOffset) + + _, err := f.Seek(tc.offset, tc.whence) + + require.Error(t, err, "Expected an error") + require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err) + }) + } +} + +// TestJsonReader_ValidObjects tests reading valid JSON objects from a jsonReader. +func TestJsonReader_ValidObjects(t *testing.T) { + jsonContent := `[ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25} + ]` + reader := bytes.NewReader([]byte(jsonContent)) + decoder := json.NewDecoder(reader) + _, _ = decoder.Token() + + jReader := jsonReader{decoder: decoder} + + require.True(t, jReader.Next(), "Expected Next to be true for the first object") + + var data1 struct { + Name string + Age int + } + require.NoError(t, jReader.Scan(&data1), "Scan failed for first object") + assert.Equal(t, "Alice", data1.Name) + assert.Equal(t, 30, data1.Age) + + require.True(t, jReader.Next(), "Expected Next to be true for the second object") + + var data2 struct { + Name string + Age int + } + require.NoError(t, jReader.Scan(&data2), "Scan failed for second object") + assert.Equal(t, "Bob", data2.Name) + assert.Equal(t, 25, data2.Age) +} + +// TestJsonReader_NullAndEnd tests reading null values and end of array from a jsonReader. +func TestJsonReader_NullAndEnd(t *testing.T) { + jsonContent := `[ + {"name": "Alice", "age": 30}, + null + ]` + reader := bytes.NewReader([]byte(jsonContent)) + decoder := json.NewDecoder(reader) + _, _ = decoder.Token() + + jReader := jsonReader{decoder: decoder} + + jReader.Next() + err := jReader.Scan(&struct{}{}) + require.NoError(t, err, "Scan failed for null object") + + require.True(t, jReader.Next(), "Expected Next to be true for the null object") + + var data3 any + require.NoError(t, jReader.Scan(&data3), "Scan failed for null object") + assert.Nil(t, data3) + + assert.False(t, jReader.Next(), "Expected Next to be false at the end of the array") + + var invalidScanTarget struct{} + require.Error(t, jReader.Scan(&invalidScanTarget), "Expected Scan to fail after array end") +} + +// TestS3File_Metadata_Methods tests simple metadata methods. +func TestS3File_Metadata_Methods(t *testing.T) { + bucketName := "test-bucket" + fileName := "path/to/my-file.txt" + fullPath := bucketName + "/" + fileName + testSize := int64(4096) + testTime := time.Date(2023, 10, 10, 12, 0, 0, 0, time.UTC) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + f := newTestS3FileWithTime(t, ctrl, fullPath, testSize, 0, testTime) + + expectedName := "my-file.txt" + assert.Equal(t, expectedName, f.Name()) + + assert.Equal(t, testSize, f.Size()) + + assert.Equal(t, testTime, f.ModTime()) + + assert.False(t, f.IsDir()) + + f.name = bucketName + "/path/to/my-dir/" + assert.True(t, f.IsDir()) + + assert.Equal(t, os.FileMode(0), f.Mode()) +} + +// createMockBodyWithError creates a MockReadCloser that returns an error when reading. +func createMockBodyWithError(bodyReadError error) *MockReadCloser { + return &MockReadCloser{ + Reader: io.NopCloser(errorReader{err: bodyReadError}), + CloseFunc: func() error { + return nil + }, + } +} + +// createMockBodyWithContent creates a MockReadCloser with the provided content. +func createMockBodyWithContent(fileBody []byte) *MockReadCloser { + return &MockReadCloser{ + Reader: bytes.NewReader(fileBody), + CloseFunc: func() error { + return nil + }, + } +} + +// Helper function for creating a new S3File instance for a test. +func newS3FileForReadAll(t *testing.T, ctrl *gomock.Controller, name string, body io.ReadCloser) *S3File { + t.Helper() + f := newTestS3File(t, ctrl, name, 0, 0) + f.body = body + + return f +} + +// TestS3File_ReadAll_JSONArray_Success tests reading JSON array from S3File. +func TestS3File_ReadAll_JSONArray_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBody := createMockBodyWithContent([]byte(`[{"id": 1}, {"id": 2}]`)) + f := newS3FileForReadAll(t, ctrl, "my-bucket/path/to/data.json", mockBody) + + reader, err := f.ReadAll() + + require.NoError(t, err, "ReadAll() unexpected error") + require.NotNil(t, reader, "ReadAll() returned nil reader on success") + assert.IsType(t, &jsonReader{}, reader, "ReadAll() for JSON array expected *jsonReader") +} + +// TestS3File_ReadAll_JSONObject_Success tests reading JSON object from S3File. +func TestS3File_ReadAll_JSONObject_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBody := createMockBodyWithContent([]byte(`{"key": "value"}`)) + f := newS3FileForReadAll(t, ctrl, "my-bucket/path/to/config.json", mockBody) + + reader, err := f.ReadAll() + + require.NoError(t, err, "ReadAll() unexpected error") + require.NotNil(t, reader, "ReadAll() returned nil reader on success") + assert.IsType(t, &jsonReader{}, reader, "ReadAll() for JSON object expected *jsonReader") +} + +// TestS3File_ReadAll_Text_Success tests reading text/CSV from S3File. +func TestS3File_ReadAll_Text_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBody := createMockBodyWithContent([]byte("col1,col2\n1,2")) + f := newS3FileForReadAll(t, ctrl, "my-bucket/path/to/data.csv", mockBody) + + reader, err := f.ReadAll() + + require.NoError(t, err, "ReadAll() unexpected error") + require.NotNil(t, reader, "ReadAll() returned nil reader on success") + assert.IsType(t, &textReader{}, reader, "ReadAll() for text file expected *textReader") +} + +// TestS3File_ReadAll_JSON_Error tests ReadAll error for JSON file. +func TestS3File_ReadAll_JSON_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBody := createMockBodyWithError(errReadAllFailed) + f := newS3FileForReadAll(t, ctrl, "my-bucket/fail.json", mockBody) + + reader, err := f.ReadAll() + + require.Error(t, err, "ReadAll() expected an error, but got nil") + assert.Nil(t, reader, "ReadAll() expected nil reader on error") +} + +// TestS3File_ReadAll_Text_Error tests ReadAll error for text/CSV file. +func TestS3File_ReadAll_Text_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBody := createMockBodyWithError(errReadAllFailed) + f := newS3FileForReadAll(t, ctrl, "my-bucket/fail.txt", mockBody) + + reader, err := f.ReadAll() + + require.Error(t, err, "ReadAll() expected an error, but got nil") + assert.Nil(t, reader, "ReadAll() expected nil reader on error") +} + +// TestS3File_ReadAll_JSONInvalidToken_Error tests ReadAll error for invalid JSON. +func TestS3File_ReadAll_JSONInvalidToken_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockBody := createMockBodyWithContent([]byte(`not a json`)) + f := newS3FileForReadAll(t, ctrl, "my-bucket/invalid.json", mockBody) + + reader, err := f.ReadAll() + + require.Error(t, err, "ReadAll() expected an error, but got nil") + assert.Nil(t, reader, "ReadAll() expected nil reader on error") +} + +// errorReader is a helper to simulate an io.ReadAll failure for testing. +type errorReader struct { + err error +} + +func (er errorReader) Read(_ []byte) (n int, err error) { + return 0, er.err +} + +// MockReadCloser is a minimal mock for the io.ReadCloser field 'f.body'. +type MockReadCloser struct { + io.Reader + CloseFunc func() error +} + +func (m *MockReadCloser) Close() error { + return m.CloseFunc() +} + +func TestFileSystem_Connect(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + successConfig := &Config{ + BucketName: "test-bucket", + Region: "us-east-1", + AccessKeyID: "AKIA_SUCCESS", + SecretAccessKey: "SECRET_SUCCESS", + EndPoint: "http://localhost:9000", + } + + t.Run("SuccessCase", func(t *testing.T) { + mockLogger := NewMockLogger(ctrl) + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + + fs := &FileSystem{ + config: successConfig, + logger: mockLogger, + } + + fs.Connect() + + assert.NotNil(t, fs.conn, "Connect() failed to initialize S3 client") + }) +} diff --git a/pkg/gofr/datasource/file/s3/fs_test.go b/pkg/gofr/datasource/file/s3/fs_test.go index e719048a4..2b3828dd6 100644 --- a/pkg/gofr/datasource/file/s3/fs_test.go +++ b/pkg/gofr/datasource/file/s3/fs_test.go @@ -19,147 +19,144 @@ import ( var errMock = errors.New("mocked error") -func Test_CreateFile(t *testing.T) { - type testCase struct { - name string - createPath string - setupMocks func() - expectError bool - isRoot bool - } - - ctrl := gomock.NewController(t) - defer ctrl.Finish() +// testMocks contains all the mock objects needed for tests. +type testMocks struct { + mockS3 *Mocks3Client + mockLogger *MockLogger + mockMetrics *MockMetrics +} - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) // Replace with the actual generated mock for the S3 client. - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) - - tests := []testCase{ - {name: "Create txt file", createPath: "abc.txt", - setupMocks: func() { - mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil) - mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ - Body: io.NopCloser(strings.NewReader("test file content")), - ContentLength: aws.Int64(int64(len("test file content"))), - ContentType: aws.String("text/plain"), - LastModified: aws.Time(time.Now()), - }, nil) - }, - expectError: false, isRoot: true}, - {name: "Create file with invalid path", createPath: "abc/abc.txt", - setupMocks: func() { - mockS3.EXPECT(). - ListObjectsV2(gomock.Any(), gomock.Any()). - Return(nil, errMock) - }, - expectError: true, isRoot: false}, - {name: "Create valid file with directory existing", createPath: "abc/efg.txt", - setupMocks: func() { - mockS3.EXPECT(). - ListObjectsV2(gomock.Any(), gomock.Any()). - Return(&s3.ListObjectsV2Output{ - Contents: []types.Object{ - { - Key: aws.String("abc.txt"), - Size: aws.Int64(1), - }, - }, - }, nil) - - mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil) - mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ - Body: io.NopCloser(strings.NewReader("test file content")), - ContentLength: aws.Int64(int64(len("test file content"))), - ContentType: aws.String("text/plain"), - LastModified: aws.Time(time.Now()), - }, nil) - }, - expectError: false, isRoot: false}, +// setupTestMocks creates and returns all mock objects needed for testing. +func setupTestMocks(ctrl *gomock.Controller) *testMocks { + return &testMocks{ + mockS3: NewMocks3Client(ctrl), + mockLogger: NewMockLogger(ctrl), + mockMetrics: NewMockMetrics(ctrl), } +} - // Define the configuration for the S3 package - config := &Config{ +// defaultTestConfig returns a default Config for testing. +func defaultTestConfig() *Config { + return &Config{ EndPoint: "https://example.com", BucketName: "test-bucket", Region: "us-east-1", AccessKeyID: "dummy-access-key", SecretAccessKey: "dummy-secret-key", } +} + +// setupTestFileSystem creates and returns a FileSystem with all required dependencies. +func setupTestFileSystem(mocks *testMocks, config *Config) *FileSystem { + if config == nil { + config = defaultTestConfig() + } f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, + logger: mocks.mockLogger, + metrics: mocks.mockMetrics, + conn: mocks.mockS3, } - fs := &FileSystem{ + return &FileSystem{ s3File: f, - conn: mockS3, - logger: mockLogger, + conn: mocks.mockS3, + logger: mocks.mockLogger, config: config, - metrics: mockMetrics, + metrics: mocks.mockMetrics, } +} - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() +func Test_CreateFile_TxtFile_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - for i, tt := range tests { - tt.setupMocks() - _, err := fs.Create(tt.createPath) + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - if tt.expectError { - require.Error(t, err, "TEST[%d] Failed. Desc %v", i, "Expected error during file creation") - return - } + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - require.NoError(t, err, "TEST[%d] Failed. Desc %v", i, "Failed to create file") - } + mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil) + mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ + Body: io.NopCloser(strings.NewReader("test file content")), + ContentLength: aws.Int64(int64(len("test file content"))), + ContentType: aws.String("text/plain"), + LastModified: aws.Time(time.Now()), + }, nil) + + _, err := fs.Create("abc.txt") + require.NoError(t, err, "Failed to create file") } -func Test_OpenFile(t *testing.T) { +func Test_CreateFile_WithExistingDirectory_Success(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) // Replace with the actual generated mock for the S3 client. - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - // Define the configuration for the S3 package - config := &Config{ - EndPoint: "https://example.com", - BucketName: "test-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, - } + mocks.mockS3.EXPECT(). + ListObjectsV2(gomock.Any(), gomock.Any()). + Return(&s3.ListObjectsV2Output{ + Contents: []types.Object{ + { + Key: aws.String("abc.txt"), + Size: aws.Int64(1), + }, + }, + }, nil) + + mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil) + mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ + Body: io.NopCloser(strings.NewReader("test file content")), + ContentLength: aws.Int64(int64(len("test file content"))), + ContentType: aws.String("text/plain"), + LastModified: aws.Time(time.Now()), + }, nil) - fs := &FileSystem{ - s3File: f, - conn: mockS3, - logger: mockLogger, - config: config, - metrics: mockMetrics, - } + _, err := fs.Create("abc/efg.txt") + require.NoError(t, err, "Failed to create file with existing directory") +} + +func Test_CreateFile_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT(). + ListObjectsV2(gomock.Any(), gomock.Any()). + Return(nil, errMock) + + _, err := fs.Create("abc/abc.txt") + require.Error(t, err, "Expected error during file creation with invalid path") +} - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() +func Test_OpenFile(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ - Body: io.NopCloser(strings.NewReader("mock file content")), // Mock file content - ContentType: aws.String("text/plain"), // Mock content type - LastModified: aws.Time(time.Now()), // Mock last modified time - ContentLength: aws.Int64(123), // Mock file size in bytes + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ + Body: io.NopCloser(strings.NewReader("mock file content")), + ContentType: aws.String("text/plain"), + LastModified: aws.Time(time.Now()), + ContentLength: aws.Int64(123), }, nil).AnyTimes() _, err := fs.OpenFile("abc.json", 0, os.ModePerm) @@ -170,40 +167,15 @@ func Test_MakingDirectories(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) - - // Define the configuration for the S3 package - config := &Config{ - EndPoint: "https://example.com", - BucketName: "test-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } - - f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, - } - - fs := &FileSystem{ - s3File: f, - conn: mockS3, - logger: mockLogger, - config: config, - metrics: mockMetrics, - } + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() - mockS3.EXPECT(). + mocks.mockS3.EXPECT(). PutObject(gomock.Any(), gomock.Any()). Return(&s3.PutObjectOutput{}, nil).Times(3) @@ -215,40 +187,17 @@ func Test_RenameDirectory(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) - - // Define the configuration for the S3 package - config := &Config{ - EndPoint: "https://example.com", - BucketName: "mock-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } - - f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, - } - - fs := &FileSystem{ - s3File: f, - conn: mockS3, - logger: mockLogger, - config: config, - metrics: mockMetrics, - } + mocks := setupTestMocks(ctrl) + config := defaultTestConfig() + config.BucketName = "mock-bucket" + fs := setupTestFileSystem(mocks, config) - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() - mockS3.EXPECT(). + mocks.mockS3.EXPECT(). ListObjectsV2(gomock.Any(), gomock.Any()). Return(&s3.ListObjectsV2Output{ Contents: []types.Object{ @@ -261,15 +210,15 @@ func Test_RenameDirectory(t *testing.T) { }, }, nil).Times(1) - mockS3.EXPECT(). + mocks.mockS3.EXPECT(). CopyObject(gomock.Any(), gomock.Any()). Return(&s3.CopyObjectOutput{}, nil).Times(1) - mockS3.EXPECT(). + mocks.mockS3.EXPECT(). CopyObject(gomock.Any(), gomock.Any()). Return(&s3.CopyObjectOutput{}, nil).Times(1) - mockS3.EXPECT(). + mocks.mockS3.EXPECT(). ListObjectsV2(gomock.Any(), gomock.Any()). Return(&s3.ListObjectsV2Output{ Contents: []types.Object{ @@ -282,7 +231,7 @@ func Test_RenameDirectory(t *testing.T) { }, }, nil).Times(1) - mockS3.EXPECT(). + mocks.mockS3.EXPECT(). DeleteObjects(gomock.Any(), gomock.Any()). Return(&s3.DeleteObjectsOutput{}, nil).Times(1) @@ -300,24 +249,12 @@ func Test_ReadDir(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockS3 := NewMocks3Client(ctrl) - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) - - config := &Config{ - EndPoint: "https://example.com", - BucketName: "test-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } - - f := S3File{logger: mockLogger, metrics: mockMetrics, conn: mockS3} - fs := &FileSystem{s3File: f, conn: mockS3, logger: mockLogger, config: config, metrics: mockMetrics} + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() tests := []struct { name string @@ -333,7 +270,7 @@ func Test_ReadDir(t *testing.T) { {"hij", 0, true}, }, setupMock: func() { - mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ Contents: []types.Object{ {Key: aws.String("abc/efg/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())}, {Key: aws.String("abc/efg/file.txt"), Size: aws.Int64(1), LastModified: aws.Time(time.Now())}, @@ -349,7 +286,7 @@ func Test_ReadDir(t *testing.T) { {"efg", 0, true}, }, setupMock: func() { - mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ Contents: []types.Object{ {Key: aws.String("abc/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())}, {Key: aws.String("abc/efg/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())}, @@ -389,133 +326,82 @@ func TestRemove(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) // Replace with the actual generated mock for the S3 client. - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - // Define the configuration for the S3 package - config := &Config{ - EndPoint: "https://example.com", - BucketName: "test-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()) + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() - f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, - } + name := "testfile.txt" - fs := &FileSystem{ - s3File: f, - conn: mockS3, - logger: mockLogger, - config: config, - metrics: mockMetrics, - } + mocks.mockS3.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).Return(&s3.DeleteObjectOutput{}, nil).Times(1) - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()) - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + err := fs.Remove(name) + require.NoError(t, err, "Remove() failed") +} - name := "testfile.txt" +func Test_RenameFile_ToNewName_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - mockS3.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).Return(&s3.DeleteObjectOutput{}, nil).Times(1) + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - err := fs.Remove(name) - if err != nil { - t.Fatalf("Remove() failed: %v", err) - } + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT().CopyObject(gomock.Any(), gomock.Any()).Return(&s3.CopyObjectOutput{}, nil).Times(1) + mocks.mockS3.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).Return(&s3.DeleteObjectOutput{}, nil).Times(1) + + err := fs.Rename("abcd.json", "abc.json") + require.NoError(t, err, "Unexpected error when renaming file to new name") } -func Test_RenameFile(t *testing.T) { +func Test_RenameFile_ToSameName_Success(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) // Replace with the actual generated mock for the S3 client. - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - // Define the configuration for the S3 package - config := &Config{ - EndPoint: "https://example.com", - BucketName: "test-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, - } + err := fs.Rename("abcd.json", "abcd.json") + require.NoError(t, err, "Unexpected error when renaming file to same name") +} - fs := &FileSystem{ - s3File: f, - conn: mockS3, - logger: mockLogger, - config: config, - metrics: mockMetrics, - } +func Test_RenameFile_WithDifferentExtension_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - tests := []struct { - name string - initialName string - newName string - expectedError bool - }{ - { - name: "Rename file to new name", - initialName: "abcd.json", - newName: "abc.json", - expectedError: false, - }, - { - name: "Rename file with different extension", - initialName: "abcd.json", - newName: "abcd.txt", - expectedError: true, - }, - { - name: "Rename file to same name", - initialName: "abcd.json", - newName: "abcd.json", - expectedError: false, - }, - { - name: "Rename file to directory path (unsupported)", - initialName: "abcd.json", - newName: "abc/abcd.json", - expectedError: true, - }, - } + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{}, nil).AnyTimes() - mockS3.EXPECT().CopyObject(gomock.Any(), gomock.Any()).Return(&s3.CopyObjectOutput{}, nil).Times(1) - mockS3.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).Return(&s3.DeleteObjectOutput{}, nil).Times(1) + err := fs.Rename("abcd.json", "abcd.txt") + require.Error(t, err, "Expected error when renaming file with different extension") +} - // Iterate through each test case - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := fs.Rename(tt.initialName, tt.newName) +func Test_RenameFile_ToDirectoryPath_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() - if tt.expectedError { - require.Error(t, err, "Expected error but got none for case: %s", tt.name) - } else { - require.NoError(t, err, "Unexpected error for case: %s", tt.name) - } - }) - } + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + err := fs.Rename("abcd.json", "abc/abcd.json") + require.Error(t, err, "Expected error when renaming file to directory path") } func Test_StatFile(t *testing.T) { @@ -536,40 +422,15 @@ func Test_StatFile(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - // Create a mock S3 client - mockS3 := NewMocks3Client(ctrl) // Replace with the actual generated mock for the S3 client. - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) - - // Define the configuration for the S3 package - config := &Config{ - EndPoint: "https://example.com", - BucketName: "test-bucket", - Region: "us-east-1", - AccessKeyID: "dummy-access-key", - SecretAccessKey: "dummy-secret-key", - } - - f := S3File{ - logger: mockLogger, - metrics: mockMetrics, - conn: mockS3, - } - - fs := &FileSystem{ - s3File: f, - conn: mockS3, - logger: mockLogger, - config: config, - metrics: mockMetrics, - } + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() - mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ Contents: []types.Object{ { Key: aws.String("file.txt"), @@ -601,37 +462,23 @@ func Test_StatDirectory(t *testing.T) { } ctrl := gomock.NewController(t) - mockLogger := NewMockLogger(ctrl) - mockMetrics := NewMockMetrics(ctrl) - mockConn := NewMocks3Client(ctrl) - - mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() - mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() - - cfg := Config{ - "http://localhost:4566", - "gofr-bucket-2", - "us-east-1", - "test", - "test", - } - f := S3File{ - conn: mockConn, - logger: mockLogger, - metrics: mockMetrics, - } + mocks := setupTestMocks(ctrl) - fs := FileSystem{ - s3File: f, - logger: mockLogger, - metrics: mockMetrics, - conn: mockConn, - config: &cfg, + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + + cfg := &Config{ + EndPoint: "http://localhost:4566", + BucketName: "gofr-bucket-2", + Region: "us-east-1", + AccessKeyID: "test", + SecretAccessKey: "test", } + fs := setupTestFileSystem(mocks, cfg) - mockConn.EXPECT().ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ + mocks.mockS3.EXPECT().ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ Bucket: aws.String("gofr-bucket-2"), Prefix: aws.String("dir1/dir2"), }).Return(&s3.ListObjectsV2Output{ @@ -656,3 +503,125 @@ func Test_StatDirectory(t *testing.T) { require.NoError(t, err, "TEST[%d] Failed. Desc: %v", 0, "Error getting directory stats") assert.Equal(t, expectedResponse, response, "Mismatch in results for path: %v", expectedResponse.name) } + +func Test_CreateFile_PutObjectFails_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ + Contents: []types.Object{ + { + Key: aws.String("folder/"), + Size: aws.Int64(0), + }, + }, + }, nil) + + mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(nil, errMock) + + _, err := fs.Create("folder/test.txt") + require.Error(t, err, "Expected error when PutObject fails") +} + +func Test_CreateFile_GetObjectFails_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{ + Contents: []types.Object{ + { + Key: aws.String("folder/"), + Size: aws.Int64(0), + }, + }, + }, nil) + + mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil) + mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(nil, errMock) + + _, err := fs.Create("folder/test.txt") + require.Error(t, err, "Expected error when GetObject fails after PutObject success") +} + +func Test_Open_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Debug(gomock.Any()).Times(1) + mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{ + Body: io.NopCloser(strings.NewReader("test content")), + ContentType: aws.String("application/json"), + LastModified: aws.Time(time.Now()), + ContentLength: aws.Int64(12), + }, nil).Times(1) + + _, err := fs.Open("test.json") + require.NoError(t, err, "Unexpected error when opening file") +} + +func Test_Open_GetObjectFails_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Errorf("failed to retrieve %q: %v", "missing.json", errMock).Times(1) + mocks.mockLogger.EXPECT().Debug(gomock.Any()).Times(1) + mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(nil, errMock).Times(1) + + _, err := fs.Open("missing.json") + require.Error(t, err, "Expected error when GetObject fails") + require.Contains(t, err.Error(), "mocked error", "Expected error to contain mocked error") +} + +func Test_RenameDirectory_ListObjectsV2Fails_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(nil, errMock) + + err := fs.Rename("old-dir", "new-dir") + require.Error(t, err, "Expected error when ListObjectsV2 fails in renameDirectory") + require.Contains(t, err.Error(), "mocked error", "Expected error to contain mocked error") +} + +func Test_Stat_ListObjectsV2Fails_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mocks := setupTestMocks(ctrl) + fs := setupTestFileSystem(mocks, nil) + + mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(nil, errMock) + + _, err := fs.Stat("test-file.txt") + require.Error(t, err, "Expected error when ListObjectsV2 fails") + require.Contains(t, err.Error(), "mocked error", "Expected error to contain mocked error") +}