diff --git a/go.mod b/go.mod index fc3a96d..282a5c4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/robindittmar/dttmr-api go 1.26 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/jackc/pgx/v5 v5.10.0 diff --git a/internal/repository/user_test.go b/internal/repository/user_test.go new file mode 100644 index 0000000..659674a --- /dev/null +++ b/internal/repository/user_test.go @@ -0,0 +1,189 @@ +package repository + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/robindittmar/dttmr-api/internal/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUserRepo_CreateUser(t *testing.T) { + db, mock, err := sqlmock.New() + assert.NoError(t, err) + defer db.Close() + + repo := NewUserRepo(db) + + ctx := context.Background() + email := "test@example.com" + name := "Test User" + passwordHash := "hashedpassword123" + + now := time.Now() + expectedUser := &domain.User{ + ID: "1", + Email: email, + Name: name, + CreatedAt: now, + } + + t.Run("success", func(t *testing.T) { + mock.ExpectBegin() + + mock.ExpectQuery(`^INSERT INTO users \(email, name, password_hash\) VALUES \(\$1, \$2, \$3\) RETURNING id, created_at$`). + WithArgs(email, name, passwordHash). + WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}).AddRow(expectedUser.ID, expectedUser.CreatedAt)) + + mock.ExpectCommit() + + user, err := repo.CreateUser(ctx, email, name, passwordHash) + assert.NoError(t, err) + assert.Equal(t, expectedUser, user) + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("begin_tx_error", func(t *testing.T) { + mock.ExpectBegin().WillReturnError(fmt.Errorf("tx error")) + + user, err := repo.CreateUser(ctx, email, name, passwordHash) + assert.Error(t, err) + assert.Contains(t, err.Error(), "begin transaction") + assert.Nil(t, user) + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("insert_error", func(t *testing.T) { + mock.ExpectBegin() + mock.ExpectQuery(`^INSERT INTO users \(email, name, password_hash\) VALUES \(\$1, \$2, \$3\) RETURNING id, created_at$`). + WithArgs(email, name, passwordHash). + WillReturnError(fmt.Errorf("insert error")) + mock.ExpectRollback() + + user, err := repo.CreateUser(ctx, email, name, passwordHash) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to insert user") + assert.Nil(t, user) + assert.NoError(t, mock.ExpectationsWereMet()) + }) + + t.Run("commit_error", func(t *testing.T) { + mock.ExpectBegin() + mock.ExpectQuery(`^INSERT INTO users \(email, name, password_hash\) VALUES \(\$1, \$2, \$3\) RETURNING id, created_at$`). + WithArgs(email, name, passwordHash). + WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}).AddRow(expectedUser.ID, expectedUser.CreatedAt)) + mock.ExpectCommit().WillReturnError(fmt.Errorf("commit error")) + + user, err := repo.CreateUser(ctx, email, name, passwordHash) + assert.Error(t, err) + assert.Contains(t, err.Error(), "commit transaction") + assert.Nil(t, user) + assert.NoError(t, mock.ExpectationsWereMet()) + }) +} + +func TestUserRepo_CreateUser2(t *testing.T) { + email := "test@example.com" + name := "Test User" + passwordHash := "hashedpassword123" + now := time.Now() + expectedID := "42" + + insertQuery := regexp.QuoteMeta( + "INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id, created_at", + ) + + testCases := []struct { + name string + setupMock func(mock sqlmock.Sqlmock) + expectedError string + }{ + { + name: "Success: User created perfectly", + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + + rows := sqlmock.NewRows([]string{"id", "created_at"}). + AddRow(expectedID, now) + + mock.ExpectQuery(insertQuery). + WithArgs(email, name, passwordHash). + WillReturnRows(rows) + + mock.ExpectCommit() + }, + expectedError: "", + }, + { + name: "Failure: Database connection fails on BeginTx", + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin().WillReturnError(errors.New("db connection failed")) + }, + expectedError: "begin transaction: db connection failed", + }, + { + name: "Failure: Query fails (e.g., duplicate email)", + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + + mock.ExpectQuery(insertQuery). + WithArgs(email, name, passwordHash). + WillReturnError(errors.New("unique constraint violation")) + + mock.ExpectRollback() + }, + expectedError: "failed to insert user: unique constraint violation", + }, + { + name: "Failure: Commit fails (e.g., network timeout)", + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + + rows := sqlmock.NewRows([]string{"id", "created_at"}). + AddRow(expectedID, now) + + mock.ExpectQuery(insertQuery). + WithArgs(email, name, passwordHash). + WillReturnRows(rows) + + mock.ExpectCommit().WillReturnError(errors.New("commit timeout")) + }, + expectedError: "commit transaction: commit timeout", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + tc.setupMock(mock) + + repo := NewUserRepo(db) + + user, err := repo.CreateUser(context.Background(), email, name, passwordHash) + + if tc.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedError) + assert.Nil(t, user) + } else { + require.NoError(t, err) + require.NotNil(t, user) + assert.Equal(t, expectedID, user.ID) + assert.Equal(t, email, user.Email) + assert.Equal(t, name, user.Name) + assert.Equal(t, now, user.CreatedAt) + } + + assert.NoError(t, mock.ExpectationsWereMet()) + }) + } +}