diff --git a/cover.out b/cover.out new file mode 100644 index 00000000..5f02b111 --- /dev/null +++ b/cover.out @@ -0,0 +1 @@ +mode: set diff --git a/features/drop-point/service/cover.out b/features/drop-point/service/cover.out new file mode 100644 index 00000000..d6ccd57e --- /dev/null +++ b/features/drop-point/service/cover.out @@ -0,0 +1,59 @@ +mode: set +recything/features/drop-point/service/service.go:15.106,19.2 1 1 +recything/features/drop-point/service/service.go:21.80,23.21 2 1 +recything/features/drop-point/service/service.go:23.21,25.3 1 1 +recything/features/drop-point/service/service.go:27.2,28.23 2 1 +recything/features/drop-point/service/service.go:28.23,30.3 1 1 +recything/features/drop-point/service/service.go:32.2,34.41 2 1 +recything/features/drop-point/service/service.go:34.41,36.22 2 1 +recything/features/drop-point/service/service.go:36.22,38.4 1 1 +recything/features/drop-point/service/service.go:40.3,41.20 2 1 +recything/features/drop-point/service/service.go:41.20,43.4 1 1 +recything/features/drop-point/service/service.go:45.3,46.21 2 1 +recything/features/drop-point/service/service.go:46.21,48.4 1 1 +recything/features/drop-point/service/service.go:50.3,50.47 1 1 +recything/features/drop-point/service/service.go:50.47,51.44 1 1 +recything/features/drop-point/service/service.go:51.44,53.5 1 1 +recything/features/drop-point/service/service.go:56.3,59.32 3 1 +recything/features/drop-point/service/service.go:59.32,60.44 1 1 +recything/features/drop-point/service/service.go:60.44,63.10 2 1 +recything/features/drop-point/service/service.go:67.3,67.17 1 1 +recything/features/drop-point/service/service.go:67.17,72.4 1 0 +recything/features/drop-point/service/service.go:75.2,75.37 1 1 +recything/features/drop-point/service/service.go:75.37,76.25 1 1 +recything/features/drop-point/service/service.go:76.25,81.4 1 1 +recything/features/drop-point/service/service.go:84.2,85.16 2 1 +recything/features/drop-point/service/service.go:85.16,87.3 1 1 +recything/features/drop-point/service/service.go:89.2,89.12 1 1 +recything/features/drop-point/service/service.go:92.136,93.16 1 1 +recything/features/drop-point/service/service.go:93.16,95.3 1 1 +recything/features/drop-point/service/service.go:97.2,100.16 3 1 +recything/features/drop-point/service/service.go:100.16,102.3 1 1 +recything/features/drop-point/service/service.go:104.2,104.44 1 1 +recything/features/drop-point/service/service.go:107.89,108.14 1 1 +recything/features/drop-point/service/service.go:108.14,110.3 1 1 +recything/features/drop-point/service/service.go:112.2,113.16 2 1 +recything/features/drop-point/service/service.go:113.16,115.3 1 1 +recything/features/drop-point/service/service.go:117.2,117.25 1 1 +recything/features/drop-point/service/service.go:120.95,122.21 2 1 +recything/features/drop-point/service/service.go:122.21,124.3 1 1 +recything/features/drop-point/service/service.go:126.2,127.23 2 1 +recything/features/drop-point/service/service.go:127.23,129.3 1 1 +recything/features/drop-point/service/service.go:131.2,131.41 1 1 +recything/features/drop-point/service/service.go:131.41,133.22 2 1 +recything/features/drop-point/service/service.go:133.22,135.4 1 1 +recything/features/drop-point/service/service.go:137.3,138.20 2 1 +recything/features/drop-point/service/service.go:138.20,140.4 1 1 +recything/features/drop-point/service/service.go:141.3,144.21 3 1 +recything/features/drop-point/service/service.go:144.21,146.4 1 1 +recything/features/drop-point/service/service.go:148.3,148.47 1 1 +recything/features/drop-point/service/service.go:148.47,149.44 1 1 +recything/features/drop-point/service/service.go:149.44,151.5 1 1 +recything/features/drop-point/service/service.go:155.2,156.16 2 1 +recything/features/drop-point/service/service.go:156.16,158.3 1 1 +recything/features/drop-point/service/service.go:160.2,160.12 1 1 +recything/features/drop-point/service/service.go:164.67,165.14 1 1 +recything/features/drop-point/service/service.go:165.14,167.3 1 1 +recything/features/drop-point/service/service.go:169.2,170.16 2 1 +recything/features/drop-point/service/service.go:170.16,172.3 1 1 +recything/features/drop-point/service/service.go:174.2,174.12 1 1 diff --git a/features/drop-point/service/service_test.go b/features/drop-point/service/service_test.go new file mode 100644 index 00000000..683edf40 --- /dev/null +++ b/features/drop-point/service/service_test.go @@ -0,0 +1,550 @@ +package service + +import ( + "errors" + "recything/features/drop-point/entity" + "recything/mocks" + "recything/utils/constanta" + "recything/utils/pagination" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestNewDropPointService(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := NewDropPointService(mockRepository) + assert.Equal(t, mockRepository, service.(*dropPointService).dropPointRepository) +} + +// Test case for valid drop point creation +func TestCreateDropPointValidData(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "selasa", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "rabu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "kamis", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "jumat", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "sabtu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "minggu", OpenTime: "08:00", CloseTime: "14:00"}, + }, + } + + mockRepository.On("CreateDropPoint", validData).Return(nil) + + err := service.CreateDropPoint(validData) + + assert.Nil(t, err) + mockRepository.AssertExpectations(t) +} + +// Test case for empty fields +func TestCreateDropPointEmptyFieldsDropPoints(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + emptyData := entity.DropPointsCore{} + + err := service.CreateDropPoint(emptyData) + + assert.Error(t, err) + mockRepository.AssertNotCalled(t, "CreateDropPoint") +} + +func TestCreateDropPointEmptyDataSchedules(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + emptyData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "", OpenTime: "", CloseTime: ""}, + }, + } + + err := service.CreateDropPoint(emptyData) + + assert.NotNil(t, err) + mockRepository.AssertExpectations(t) +} + +func TestCreateDropPointInvalidTime(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + invalidTimeData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "14:00", CloseTime: "08:00"}, // Invalid opening time + }, + } + + err := service.CreateDropPoint(invalidTimeData) + + assert.NotNil(t, err) + mockRepository.AssertExpectations(t) +} + +// Test case for invalid latitude/longitude +func TestCreateDropPointInvalidLatLong(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + invalidLatLongData := entity.DropPointsCore{ + Name: "Invalid Location", + Address: "456 Invalid Street", + Latitude: 91.0, + Longitude: 180.0, + } + + err := service.CreateDropPoint(invalidLatLongData) + + assert.Error(t, err) + mockRepository.AssertNotCalled(t, "CreateDropPoint") +} + +// Test case for duplicate schedule days +func TestCreateDropPointDuplicateDays(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + duplicateDaysData := entity.DropPointsCore{ + Name: "Duplicate Days", + Address: "789 Duplicate Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "senin", OpenTime: "09:00", CloseTime: "16:00"}, + }, + } + + err := service.CreateDropPoint(duplicateDaysData) + + assert.Error(t, err) + mockRepository.AssertNotCalled(t, "CreateDropPoint") +} + +func TestCreateDropPointErrorDays(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + duplicateDaysData := entity.DropPointsCore{ + Name: "Duplicate Days", + Address: "789 Duplicate Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "monday", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "kamis", OpenTime: "09:00", CloseTime: "16:00"}, + }, + } + + err := service.CreateDropPoint(duplicateDaysData) + + assert.Error(t, err) + mockRepository.AssertNotCalled(t, "CreateDropPoint") +} + +func TestCreateDropPointRepositoryError(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + data := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "selasa", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "rabu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "kamis", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "jumat", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "sabtu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "minggu", OpenTime: "08:00", CloseTime: "14:00"}, + }, + } + + // Mengatur behavior mock repository + expectedError := errors.New("Repository error") + mockRepository.On("CreateDropPoint", data).Return(expectedError) + + err := service.CreateDropPoint(data) + + assert.EqualError(t, err, expectedError.Error()) + mockRepository.AssertExpectations(t) +} + +func TestGetAllDropPoint_ValidData(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + page := 1 + limit := 10 + search := "example" + + dummyDropPoints := []entity.DropPointsCore{} + dummyPageInfo := pagination.PageInfo{} + dummyCount := 42 + + mockRepository.On("GetAllDropPoint", page, limit, search).Return(dummyDropPoints, dummyPageInfo, dummyCount, nil) + resultDropPoints, resultPageInfo, resultCount, err := service.GetAllDropPoint(page, limit, search) + + mockRepository.AssertCalled(t, "GetAllDropPoint", page, limit, search) + assert.Nil(t, err) + assert.Equal(t, dummyDropPoints, resultDropPoints) + assert.Equal(t, dummyPageInfo, resultPageInfo) + assert.Equal(t, dummyCount, resultCount) +} + +func TestGetAllDropPoint_LimitExceeds(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + page := 1 + limit := 15 + search := "example" + + _, _, _, err := service.GetAllDropPoint(page, limit, search) + assert.NotNil(t, err) + assert.Equal(t, "limit tidak boleh lebih dari 10", err.Error()) +} + +func TestGetAllDropPointRepositoryError(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + page := 1 + limit := 10 + search := "example" + + expectedError := errors.New("repository error") + mockRepository.On("GetAllDropPoint", page, limit, search).Return(nil, pagination.PageInfo{}, 0, expectedError) + _, _, _, err := service.GetAllDropPoint(page, limit, search) + + mockRepository.AssertCalled(t, "GetAllDropPoint", page, limit, search) + + assert.NotNil(t, err) + assert.Equal(t, expectedError, err) +} + +func TestGetDropPointByIdInvalidID(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + invalidID := "" + + resultDropPoint, err := service.GetDropPointById(invalidID) + mockRepository.AssertNotCalled(t, "GetDropPointById") + + assert.NotNil(t, err) + assert.Equal(t, constanta.ERROR_ID_INVALID, err.Error()) + assert.Equal(t, entity.DropPointsCore{}, resultDropPoint) +} + +func TestGetDropPointByIdRepositoryError(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validID := "validID" + + expectedError := errors.New("repository error") + mockRepository.On("GetDropPointById", validID).Return(entity.DropPointsCore{}, expectedError) + + resultDropPoint, err := service.GetDropPointById(validID) + mockRepository.AssertCalled(t, "GetDropPointById", validID) + + assert.NotNil(t, err) + assert.Equal(t, expectedError, err) + assert.Equal(t, entity.DropPointsCore{}, resultDropPoint) +} + +func TestGetDropPointByIdValidData(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validID := "validID" + dummyDropPoint := entity.DropPointsCore{} + + mockRepository.On("GetDropPointById", validID).Return(dummyDropPoint, nil) + resultDropPoint, err := service.GetDropPointById(validID) + + mockRepository.AssertCalled(t, "GetDropPointById", validID) + + assert.Nil(t, err) + assert.Equal(t, dummyDropPoint, resultDropPoint) +} + +func TestUpdateDropPointByIdEmptyData(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validID := "validID" + emptyData := entity.DropPointsCore{} + err := service.UpdateDropPointById(validID, emptyData) + + mockRepository.AssertNotCalled(t, "UpdateDropPointById") + + assert.NotNil(t, err) + assert.Equal(t, "error : harap lengkapi data dengan benar", err.Error()) +} + +func TestUpdateDropPointByIdInvalidLatLong(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validID := "validID" + invalidLatLongData := entity.DropPointsCore{ + Name: "Invalid Location", + Address: "456 Invalid Street", + Latitude: 91.0, + Longitude: 180.0, + } + + err := service.UpdateDropPointById(validID, invalidLatLongData) + mockRepository.AssertNotCalled(t, "UpdateDropPointById") + + assert.NotNil(t, err) + assert.Equal(t, "bukan latitude", err.Error()) +} + +func TestUpdateDropPointByIdInvalidDay(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validID := "validID" + invalidDayData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "monday", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "invalid", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "rabu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "kamis", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "jumat", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "sabtu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "minggu", OpenTime: "08:00", CloseTime: "14:00"}, + }, + } + + mockRepository.AssertNotCalled(t, "CreateDropPoint") + err := service.UpdateDropPointById(validID, invalidDayData) + + assert.Error(t, err) + mockRepository.AssertNotCalled(t, "UpdateDropPointById") +} + +func TestUpdateDropPointByIdEmptyDataSchedules(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validID := "validID" + emptyData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "", OpenTime: "", CloseTime: ""}, + }, + } + + err := service.UpdateDropPointById(validID, emptyData) + + assert.NotNil(t, err) + mockRepository.AssertExpectations(t) +} + +func TestUpdateDropPointByIdInvalidTime(t *testing.T) { + + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + invalidData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "25:00", CloseTime: "14:00"}, + }, + } + + err := service.UpdateDropPointById("some_id", invalidData) + assert.NotNil(t, err) + assert.Equal(t, "format waktu buka tidak valid", err.Error()) +} + +func TestUpdateDropPointByIdDuplicateDay(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + invalidData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "senin", OpenTime: "09:00", CloseTime: "15:00"}, + }, + } + + err := service.UpdateDropPointById("some_id", invalidData) + + assert.NotNil(t, err) + assert.Equal(t, "hari tidak boleh sama", err.Error()) +} + +func TestUpdateDropPointByIdSuccessfulUpdate(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "selasa", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "rabu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "kamis", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "jumat", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "sabtu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "minggu", OpenTime: "08:00", CloseTime: "14:00"}, + }, + } + + mockRepository.On("UpdateDropPointById", "some_id", validData).Return(nil) + err := service.UpdateDropPointById("some_id", validData) + + assert.Nil(t, err) + mockRepository.AssertExpectations(t) +} + +func TestUpdateDropPointById_RepositoryError(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + validData := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "selasa", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "rabu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "kamis", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "jumat", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "sabtu", OpenTime: "08:00", CloseTime: "14:00"}, + {Day: "minggu", OpenTime: "08:00", CloseTime: "14:00"}, + }, + } + + expectedError := errors.New("repository error") + + mockRepository.On("UpdateDropPointById", "some_id", validData).Return(expectedError) + err := service.UpdateDropPointById("some_id", validData) + + assert.Equal(t, expectedError, err) + mockRepository.AssertExpectations(t) +} + +func TestDeleteDropPointById_SuccessfulDelete(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + mockRepository.On("DeleteDropPointById", "some_id").Return(nil) + err := service.DeleteDropPointById("some_id") + + assert.Nil(t, err) + mockRepository.AssertExpectations(t) +} + +func TestDeleteDropPointByIdRepositoryError(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + expectedError := errors.New("repository error") + mockRepository.On("DeleteDropPointById", "some_id").Return(expectedError) + err := service.DeleteDropPointById("some_id") + + assert.Equal(t, expectedError, err) + mockRepository.AssertExpectations(t) +} + +func TestDeleteDropPointById_InvalidID(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + err := service.DeleteDropPointById("") + + assert.Equal(t, constanta.ERROR_ID_INVALID, err.Error()) +} + +func TestCreateDropPointAddNewDayScheduleSuccess(t *testing.T) { + mockRepository := mocks.NewDropPointRepositoryInterface(t) + service := dropPointService{dropPointRepository: mockRepository} + + data := entity.DropPointsCore{ + Name: "Test Drop Point", + Address: "123 Main Street", + Latitude: 12.345678, + Longitude: 98.765432, + Schedule: []entity.ScheduleCore{ + {Day: "senin", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "selasa", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "kamis", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "jumat", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "sabtu", OpenTime: "08:00", CloseTime: "15:00"}, + {Day: "minggu", OpenTime: "08:00", CloseTime: "15:00"}, + }, + } + + mockRepository.On("CreateDropPoint", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + arg := args.Get(0).(entity.DropPointsCore) + found := false + for _, schedule := range arg.Schedule { + if schedule.Day == "rabu" { + found = true + assert.True(t, schedule.Closed, "Expected Closed to be true for Wednesday, but got false") + } + } + assert.True(t, found, "Expected Schedule for Wednesday not found") + }) + + err := service.CreateDropPoint(data) + + assert.Nil(t, err) + mockRepository.AssertExpectations(t) +} + + + + + + + + + + diff --git a/features/user/service/cover.out b/features/user/service/cover.out new file mode 100644 index 00000000..c784f1cc --- /dev/null +++ b/features/user/service/cover.out @@ -0,0 +1,125 @@ +mode: set +recything/features/user/service/service.go:18.92,22.2 1 1 +recything/features/user/service/service.go:25.82,28.21 2 1 +recything/features/user/service/service.go:28.21,30.3 1 1 +recything/features/user/service/service.go:32.2,33.21 2 1 +recything/features/user/service/service.go:33.21,35.3 1 1 +recything/features/user/service/service.go:37.2,38.22 2 1 +recything/features/user/service/service.go:38.22,40.3 1 1 +recything/features/user/service/service.go:42.2,43.16 2 1 +recything/features/user/service/service.go:43.16,45.3 1 1 +recything/features/user/service/service.go:47.2,47.43 1 1 +recything/features/user/service/service.go:47.43,49.3 1 1 +recything/features/user/service/service.go:51.2,52.16 2 1 +recything/features/user/service/service.go:52.16,54.3 1 0 +recything/features/user/service/service.go:56.2,61.16 5 1 +recything/features/user/service/service.go:61.16,63.3 1 0 +recything/features/user/service/service.go:65.2,67.23 2 1 +recything/features/user/service/service.go:71.88,74.21 2 1 +recything/features/user/service/service.go:74.21,76.3 1 1 +recything/features/user/service/service.go:78.2,79.21 2 1 +recything/features/user/service/service.go:79.21,81.3 1 1 +recything/features/user/service/service.go:83.2,84.21 2 1 +recything/features/user/service/service.go:84.21,86.3 1 1 +recything/features/user/service/service.go:88.2,88.26 1 1 +recything/features/user/service/service.go:88.26,90.3 1 1 +recything/features/user/service/service.go:92.2,93.18 2 1 +recything/features/user/service/service.go:93.18,95.3 1 1 +recything/features/user/service/service.go:97.2,98.16 2 1 +recything/features/user/service/service.go:98.16,100.3 1 0 +recything/features/user/service/service.go:101.2,101.29 1 1 +recything/features/user/service/service.go:105.69,106.14 1 1 +recything/features/user/service/service.go:106.14,108.3 1 1 +recything/features/user/service/service.go:110.2,111.16 2 1 +recything/features/user/service/service.go:111.16,113.3 1 0 +recything/features/user/service/service.go:115.2,116.20 2 1 +recything/features/user/service/service.go:116.20,118.22 2 0 +recything/features/user/service/service.go:118.22,120.4 1 0 +recything/features/user/service/service.go:123.2,124.16 2 1 +recything/features/user/service/service.go:124.16,126.3 1 1 +recything/features/user/service/service.go:127.2,127.22 1 1 +recything/features/user/service/service.go:131.75,132.14 1 1 +recything/features/user/service/service.go:132.14,134.3 1 1 +recything/features/user/service/service.go:136.2,137.19 2 1 +recything/features/user/service/service.go:137.19,139.3 1 1 +recything/features/user/service/service.go:141.2,142.21 2 1 +recything/features/user/service/service.go:142.21,144.3 1 1 +recything/features/user/service/service.go:146.2,147.21 2 1 +recything/features/user/service/service.go:147.21,149.3 1 1 +recything/features/user/service/service.go:151.2,151.28 1 1 +recything/features/user/service/service.go:151.28,152.81 1 1 +recything/features/user/service/service.go:152.81,154.4 1 1 +recything/features/user/service/service.go:157.2,158.16 2 1 +recything/features/user/service/service.go:158.16,160.3 1 0 +recything/features/user/service/service.go:162.2,162.12 1 1 +recything/features/user/service/service.go:166.79,167.14 1 1 +recything/features/user/service/service.go:167.14,169.3 1 1 +recything/features/user/service/service.go:171.2,172.16 2 1 +recything/features/user/service/service.go:172.16,174.3 1 1 +recything/features/user/service/service.go:176.2,177.21 2 1 +recything/features/user/service/service.go:177.21,179.3 1 1 +recything/features/user/service/service.go:181.2,182.22 2 1 +recything/features/user/service/service.go:182.22,184.3 1 1 +recything/features/user/service/service.go:186.2,187.18 2 1 +recything/features/user/service/service.go:187.18,189.3 1 1 +recything/features/user/service/service.go:191.2,191.46 1 1 +recything/features/user/service/service.go:191.46,193.3 1 1 +recything/features/user/service/service.go:195.2,196.20 2 1 +recything/features/user/service/service.go:196.20,198.3 1 0 +recything/features/user/service/service.go:199.2,202.16 3 1 +recything/features/user/service/service.go:202.16,204.3 1 0 +recything/features/user/service/service.go:206.2,206.12 1 1 +recything/features/user/service/service.go:210.63,211.17 1 1 +recything/features/user/service/service.go:211.17,213.3 1 1 +recything/features/user/service/service.go:215.2,216.16 2 1 +recything/features/user/service/service.go:216.16,218.3 1 1 +recything/features/user/service/service.go:220.2,220.21 1 1 +recything/features/user/service/service.go:220.21,222.3 1 1 +recything/features/user/service/service.go:224.2,225.16 2 1 +recything/features/user/service/service.go:225.16,227.3 1 1 +recything/features/user/service/service.go:229.2,229.19 1 1 +recything/features/user/service/service.go:233.75,234.14 1 1 +recything/features/user/service/service.go:234.14,236.3 1 1 +recything/features/user/service/service.go:238.2,238.53 1 1 +recything/features/user/service/service.go:242.56,245.21 2 1 +recything/features/user/service/service.go:245.21,247.3 1 1 +recything/features/user/service/service.go:249.2,250.21 2 1 +recything/features/user/service/service.go:250.21,252.3 1 1 +recything/features/user/service/service.go:254.2,255.24 2 1 +recything/features/user/service/service.go:255.24,257.3 1 0 +recything/features/user/service/service.go:259.2,262.20 3 1 +recything/features/user/service/service.go:262.20,264.3 1 1 +recything/features/user/service/service.go:266.2,266.34 1 1 +recything/features/user/service/service.go:266.34,268.3 1 0 +recything/features/user/service/service.go:270.2,271.12 2 1 +recything/features/user/service/service.go:275.69,278.21 2 1 +recything/features/user/service/service.go:278.21,280.3 1 1 +recything/features/user/service/service.go:282.2,283.16 2 1 +recything/features/user/service/service.go:283.16,285.3 1 1 +recything/features/user/service/service.go:287.2,287.50 1 1 +recything/features/user/service/service.go:287.50,289.3 1 1 +recything/features/user/service/service.go:291.2,291.26 1 1 +recything/features/user/service/service.go:291.26,293.3 1 1 +recything/features/user/service/service.go:295.2,296.16 2 0 +recything/features/user/service/service.go:296.16,298.3 1 0 +recything/features/user/service/service.go:300.2,301.21 2 0 +recything/features/user/service/service.go:301.21,303.3 1 0 +recything/features/user/service/service.go:305.2,305.19 1 0 +recything/features/user/service/service.go:309.79,312.21 2 1 +recything/features/user/service/service.go:312.21,314.3 1 1 +recything/features/user/service/service.go:316.2,317.21 2 1 +recything/features/user/service/service.go:317.21,319.3 1 1 +recything/features/user/service/service.go:321.2,322.22 2 1 +recything/features/user/service/service.go:322.22,324.3 1 1 +recything/features/user/service/service.go:326.2,326.43 1 1 +recything/features/user/service/service.go:326.43,328.3 1 1 +recything/features/user/service/service.go:330.2,331.20 2 1 +recything/features/user/service/service.go:331.20,333.3 1 0 +recything/features/user/service/service.go:334.2,337.19 3 1 +recything/features/user/service/service.go:337.19,339.3 1 0 +recything/features/user/service/service.go:341.2,341.12 1 1 +recything/features/user/service/service.go:345.79,346.39 1 1 +recything/features/user/service/service.go:346.39,348.3 1 1 +recything/features/user/service/service.go:350.2,351.15 2 1 +recything/features/user/service/service.go:351.15,353.3 1 1 +recything/features/user/service/service.go:355.2,355.12 1 1 diff --git a/features/user/service/service_test.go b/features/user/service/service_test.go new file mode 100644 index 00000000..a08b17f6 --- /dev/null +++ b/features/user/service/service_test.go @@ -0,0 +1,881 @@ +package service + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "recything/features/user/entity" + "recything/mocks" + "recything/utils/constanta" + "recything/utils/helper" + "testing" + "time" + + "github.com/golang-jwt/jwt" + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func getCurrentWorkingDirectory() string { + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + return dir +} + +func findEnvFile(dir string) string { + for { + envPath := filepath.Join(dir, ".env") + if _, err := os.Stat(envPath); err == nil { + return envPath + } + + // Pindah ke direktori induk + parentDir := filepath.Dir(dir) + if parentDir == dir { + break + } + + dir = parentDir + } + + return "" +} + +// TestRegister tests the Register method of userService. +func TestRegister(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + mockEmail := new(mocks.MockUserRepository) + userSvc := NewUserService(mockRepo) + + t.Run("Registrasi Valid", func(t *testing.T) { + mockRepo.On("FindByEmail", "test@example.com").Return(entity.UsersCore{}, errors.New(constanta.ERROR_EMAIL_EXIST)) + mockRepo.On("Register", mock.Anything).Return(entity.UsersCore{VerificationToken: "some_token"}, nil) + mockEmail.On("SendVerificationEmail", "test@example.com", "some_token").Return(nil).Once() + + userData := entity.UsersCore{ + Fullname: "John Doe", + Email: "test@example.com", + Password: "password", + ConfirmPassword: "password", + } + + result, err := userSvc.Register(userData) + assert.Nil(t, err) + assert.NotEmpty(t, result.VerificationToken) + mockRepo.AssertExpectations(t) + }) + + t.Run("Data Empty", func(t *testing.T) { + + requestBody := entity.UsersCore{ + Fullname: "", + Email: "", + Password: "", + ConfirmPassword: "", + } + + _, err := userSvc.Register(requestBody) + + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Email Invalid Fomat", func(t *testing.T) { + + requestBody := entity.UsersCore{ + Fullname: "John Doe", + Email: "johnexecom", + Password: "password123", + ConfirmPassword: "password123", + } + + _, err := userSvc.Register(requestBody) + + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Password Invalid Length", func(t *testing.T) { + + requestBody := entity.UsersCore{ + Fullname: "John Doe", + Email: "john@example.com", + Password: "pass", + ConfirmPassword: "pass", + } + + _, err := userSvc.Register(requestBody) + + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Confirm Password", func(t *testing.T) { + mockRepo.On("FindByEmail", "john@example.com").Return(entity.UsersCore{}, errors.New("user not found")) + + requestBody := entity.UsersCore{ + Fullname: "John Doe", + Email: "john@example.com", + Password: "ayamgoreng123", + ConfirmPassword: "ayamgoreg12345", + } + result, err := userSvc.Register(requestBody) + assert.Error(t, err) + + assert.Equal(t, entity.UsersCore{}, result) + assert.Equal(t, constanta.ERROR_CONFIRM_PASSWORD, err.Error()) + mockRepo.AssertExpectations(t) + }) + + t.Run("Email Duplicat", func(t *testing.T) { + mockRepo.On("FindByEmail", "duplicate@example.com").Return(entity.UsersCore{}, nil) + + duplicateUserData := entity.UsersCore{ + Fullname: "Jane Doe", + Email: "duplicate@example.com", + Password: "password", + ConfirmPassword: "password", + } + + mockEmail.On("SendVerificationEmail", mock.Anything, mock.Anything).Return(nil).Once() + + _, err := userSvc.Register(duplicateUserData) + assert.NotNil(t, err) + assert.EqualError(t, err, constanta.ERROR_EMAIL_EXIST) + mockRepo.AssertExpectations(t) + }) +} + +// TestLogin tests the Login method of userService. +func TestLogin(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + mockUser := new(mocks.MockUserRepository) + userSvc := NewUserService(mockRepo) + + currentDir := getCurrentWorkingDirectory() + + envFile := findEnvFile(currentDir) + if envFile == "" { + t.Fatal("Error: .env file not found") + } + + if err := godotenv.Load(envFile); err != nil { + t.Fatal("Error loading .env file:", err) + } + + t.Run("Login Success", func(t *testing.T) { + mockUserData := entity.UsersCore{ + Id: "user_id", + Fullname: "John Doe", + Email: "test@example.com", + Password: "", + IsVerified: true, + VerificationToken: "", + } + + password, err := helper.HashPassword("correct_password") + assert.Nil(t, err) + mockUserData.Password = password + + mockRepo.On("FindByEmail", "test@example.com").Return(mockUserData, nil) + mockUser.On("CreateToken", mock.Anything, mock.Anything).Return("some_token", nil) + + email := "test@example.com" + passwordInput := "correct_password" + + isPasswordCorrect := helper.CompareHash(mockUserData.Password, passwordInput) + assert.True(t, isPasswordCorrect) + + dataUser, token, err := userSvc.Login(email, passwordInput) + + mockRepo.AssertExpectations(t) + assert.Nil(t, err) + assert.Equal(t, "user_id", dataUser.Id) + + actualPayload, err := helper.DecodeJWTToken(token) + if err != nil { + if ve, ok := err.(*jwt.ValidationError); ok { + t.Errorf("JWT validation error: %v", ve) + } + } + + assert.Equal(t, "", actualPayload) + }) + + t.Run("Email Not Found", func(t *testing.T) { + mockRepo.On("FindByEmail", "nonexistent@example.com").Return(entity.UsersCore{}, errors.New(constanta.EMAIL_NOT_REGISTER)) + + email := "nonexistent@example.com" + password := "password" + + _, _, err := userSvc.Login(email, password) + mockRepo.AssertExpectations(t) + + // Periksa hasil fungsi Login + assert.NotNil(t, err) + assert.EqualError(t, err, constanta.EMAIL_NOT_REGISTER) + }) + + t.Run("Data Empty", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "", + Password: "", + } + _, _, err := userSvc.Login(requestBody.Email, requestBody.Password) + + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Account Has Not Been Verified", func(t *testing.T) { + mockUserData := entity.UsersCore{ + Id: "user_id", + Fullname: "John Doe", + Email: "unverified@example.com", + Password: "correct_password_hash", + IsVerified: false, + VerificationToken: "some_verification_token", + } + + mockRepo.On("FindByEmail", "unverified@example.com").Return(mockUserData, nil) + email := "unverified@example.com" + password := "password" + + _, _, err := userSvc.Login(email, password) + + mockRepo.AssertExpectations(t) + assert.NotNil(t, err) + assert.EqualError(t, err, "akun belum terverifikasi") + }) + + t.Run("Wrong Password ", func(t *testing.T) { + mockUserData := entity.UsersCore{ + Id: "user_id", + Fullname: "John Doe", + Email: "test@example.com", + Password: "correct_password_hash", + IsVerified: true, + VerificationToken: "", + } + + mockRepo.On("FindByEmail", "test@example.com").Return(mockUserData, nil) + email := "test@example.com" + password := "wrong_password" + + _, _, err := userSvc.Login(email, password) + + mockRepo.AssertExpectations(t) + assert.NotNil(t, err) + assert.EqualError(t, err, "email atau password salah") + }) + + t.Run("Email Invalid Fomat", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "johnexecom", + Password: "password123", + } + + _, _, err := userSvc.Login(requestBody.Email, requestBody.Password) + + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) +} + +// TestGetById tests the GetById method of userService. +func TestGetById(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Get ById", func(t *testing.T) { + userID := "user123" + + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + user, err := userSvc.GetById(userID) + mockRepo.AssertExpectations(t) + + assert.Nil(t, err) + assert.NotNil(t, user) + }) + + t.Run("Invalid Get ById", func(t *testing.T) { + userID := "" + _, err := userSvc.GetById(userID) + assert.NotNil(t, err) + assert.EqualError(t, err, constanta.ERROR_ID_INVALID) + mockRepo.AssertExpectations(t) + }) + + t.Run("Valid Update Badge", func(t *testing.T) { + userID := "user123" + mockRepo.On("UpdateBadge", userID).Return(nil).Once() + + err := mockRepo.UpdateBadge(userID) + + mockRepo.AssertExpectations(t) + assert.Nil(t, err) + }) + + t.Run("Error Repository", func(t *testing.T) { + userID := "12345asd" + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, errors.New("data user tidak ada")).Once() + + _, err := userSvc.GetById(userID) + assert.Error(t, err) + assert.EqualError(t, err, "data user tidak ada") + mockRepo.AssertExpectations(t) + }) +} + +// TestUpdateById tests the UpdateById method of userService. +func TestUpdateById(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Update By Id", func(t *testing.T) { + userID := "user123" + userData := entity.UsersCore{ + Fullname: "Updated Name", + Phone: "082189638011", + Address: "Updated Address", + DateOfBirth: "1990-01-01", + Purpose: "Updated Purpose", + } + + // Mock pemanggilan GetById dengan mengembalikan data yang sesuai + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + mockRepo.On("UpdateById", userID, userData).Return(nil) + + err := userSvc.UpdateById(userID, userData) + assert.Nil(t, err) + + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Update ById", func(t *testing.T) { + userID := "" + userData := entity.UsersCore{} + err := userSvc.UpdateById(userID, userData) + assert.NotNil(t, err) + assert.EqualError(t, err, constanta.ERROR_ID_INVALID) + mockRepo.AssertExpectations(t) + }) + + t.Run("Error Get By Id", func(t *testing.T) { + userID := "user123" + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, errors.New("failed to get user by ID")).Once() + err := userSvc.UpdateById(userID, entity.UsersCore{}) + + assert.Error(t, err) + assert.EqualError(t, err, "failed to get user by ID") + mockRepo.AssertExpectations(t) + mockRepo.AssertCalled(t, "GetById", userID) + mockRepo.AssertNotCalled(t, "UpdateBadge", userID) + }) + + t.Run("Data Empty", func(t *testing.T) { + userID := "user123" + requestBody := entity.UsersCore{ + Fullname: "", + Phone: "", + Address: "", + DateOfBirth: "", + Purpose: "", + } + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + + err := userSvc.UpdateById(userID, requestBody) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Phone Number", func(t *testing.T) { + userID := "user123" + requestBody := entity.UsersCore{ + Fullname: "budiawan", + Phone: "1234567", + Address: "minasaupa", + DateOfBirth: "2023-04-01", + Purpose: "pake nanya", + } + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + + err := userSvc.UpdateById(userID, requestBody) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Date", func(t *testing.T) { + userID := "user123" + requestBody := entity.UsersCore{ + Fullname: "budiawan", + Phone: "082189638011", + Address: "minasaupa", + DateOfBirth: "01-04-2023", + Purpose: "pake nanya", + } + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + + err := userSvc.UpdateById(userID, requestBody) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Data Not Found", func(t *testing.T) { + requestBody := entity.UsersCore{ + Fullname: "budiawan", + Phone: "082189638011", + Address: "minasaupa", + DateOfBirth: "01-04-2023", + Purpose: "pake nanya", + } + + userID := "2" + mockRepo.On("GetById", mock.AnythingOfType("string")).Return(requestBody, errors.New("data user tidak ada")) + user, err := userSvc.GetById(userID) + + assert.Error(t, err) + assert.NotEqual(t, userID, requestBody.Id) + assert.EqualError(t, err, "data user tidak ada") + assert.Empty(t, user) + mockRepo.AssertExpectations(t) + }) + +} + +// TestUpdatePassword tests the UpdatePassword method of userService. +func TestUpdatePassword(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Update Password", func(t *testing.T) { + userID := "user123" + passwordData := entity.UsersCore{ + Password: "old_password", + NewPassword: "new_password", + ConfirmPassword: "new_password", + } + + oldPasswordHash, _ := helper.HashPassword(passwordData.Password) + + mockRepo.On("GetById", userID).Return(entity.UsersCore{Password: oldPasswordHash}, nil) + mockRepo.On("UpdatePassword", userID, mock.Anything).Return(nil) + + err := userSvc.UpdatePassword(userID, passwordData) + assert.Nil(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Error Get By Id", func(t *testing.T) { + userID := "user123" + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, errors.New("data user tidak ada")).Once() + err := userSvc.UpdatePassword(userID, entity.UsersCore{}) + + assert.Error(t, err) + assert.EqualError(t, err, "data user tidak ada") + mockRepo.AssertExpectations(t) + mockRepo.AssertCalled(t, "GetById", userID) + mockRepo.AssertNotCalled(t, "UpdateBadge", userID) + }) + + t.Run("Data Empty", func(t *testing.T) { + userID := "user123" + requestBody := entity.UsersCore{ + Fullname: "", + Phone: "", + Address: "", + DateOfBirth: "", + Purpose: "", + } + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + + err := userSvc.UpdatePassword(userID, requestBody) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Password Invalid Length", func(t *testing.T) { + userID := "user123" + requestBody := entity.UsersCore{ + Password: "123456asd", + NewPassword: "pass", + ConfirmPassword: "pass", + } + + mockRepo.On("GetById", userID).Return(entity.UsersCore{}, nil) + err := userSvc.UpdatePassword(userID, requestBody) + + assert.Error(t, err) + assert.EqualError(t, err, "minimal 8 karakter, ulangi kembali!") + mockRepo.AssertExpectations(t) + }) + + t.Run("Password Mismatch", func(t *testing.T) { + userID := "user123" + requestBody := entity.UsersCore{ + Password: "currentPassword", + NewPassword: "wrongNewPassword", + ConfirmPassword: "wrongNewPasswordConfirmation", + } + + mockRepo.On("GetById", userID).Return(entity.UsersCore{Password: "currentPassword"}, nil) + err := userSvc.UpdatePassword(userID, requestBody) + assert.Error(t, err) + assert.EqualError(t, err, constanta.ERROR_PASSWORD) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Confirm Password", func(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + userID := "user123" + requestBody := entity.UsersCore{ + Password: "currentPassword", + NewPassword: "newPassword", + ConfirmPassword: "mismatchedPassword", + } + hashedPassword, _ := helper.HashPassword("currentPassword") + mockRepo.On("GetById", userID).Return(entity.UsersCore{ + Password: hashedPassword, + }, nil) + + err := userSvc.UpdatePassword(userID, requestBody) + assert.Error(t, err) + assert.EqualError(t, err, constanta.ERROR_CONFIRM_PASSWORD) + mockRepo.AssertExpectations(t) + }) + t.Run("Invalid Update Password", func(t *testing.T) { + userID := "" + passwordData := entity.UsersCore{} + err := userSvc.UpdatePassword(userID, passwordData) + assert.NotNil(t, err) + assert.EqualError(t, err, constanta.ERROR_ID_INVALID) + mockRepo.AssertExpectations(t) + }) +} + +// TestVerifyUser tests the VerifyUser method of userService. +func TestVerifyUser(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Verify User", func(t *testing.T) { + token := "valid_token" + mockRepo.On("GetByVerificationToken", token).Return(entity.UsersCore{IsVerified: false}, nil) + mockRepo.On("UpdateIsVerified", mock.Anything, true).Return(nil) + + isVerified, err := userSvc.VerifyUser(token) + fmt.Println("isVerified:", isVerified) // Add this line to print the value + + assert.False(t, isVerified) + assert.Nil(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Verify User", func(t *testing.T) { + token := "invalid_token" + mockRepo.On("GetByVerificationToken", token).Return(entity.UsersCore{IsVerified: true}, nil) + + isVerified, err := userSvc.VerifyUser(token) + assert.True(t, isVerified) + assert.Nil(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Token", func(t *testing.T) { + isVerified, err := userSvc.VerifyUser("") + + assert.Error(t, err) + assert.EqualError(t, err, "invalid token") + assert.False(t, isVerified) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Token", func(t *testing.T) { + token := "test_token" + + mockRepo.On("GetByVerificationToken", token).Return(entity.UsersCore{}, errors.New("database error")).Once() + isVerified, err := userSvc.VerifyUser(token) + + assert.Error(t, err) + assert.EqualError(t, err, "gagal mendapatkan data") + assert.False(t, isVerified) + mockRepo.AssertExpectations(t) + }) + + t.Run("Error Verified", func(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + token := "test_token" + mockRepo.On("GetByVerificationToken", token).Return(entity.UsersCore{Id: "user123"}, nil).Once() + mockRepo.On("UpdateIsVerified", "user123", true).Return(errors.New("database error")).Once() + + isVerified, err := userSvc.VerifyUser(token) + assert.Error(t, err) + assert.EqualError(t, err, "gagal untuk mengaktifkan user") + assert.False(t, isVerified) + + mockRepo.AssertExpectations(t) + }) + +} + +// TestUpdateIsVerified tests the UpdateIsVerified method of userService. +func TestUpdateIsVerified(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Update Verified", func(t *testing.T) { + userID := "user123" + isVerified := true + mockRepo.On("UpdateIsVerified", userID, isVerified).Return(nil) + + err := userSvc.UpdateIsVerified(userID, isVerified) + assert.Nil(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Update Verified", func(t *testing.T) { + userID := "" + isVerified := false + err := userSvc.UpdateIsVerified(userID, isVerified) + assert.NotNil(t, err) + assert.EqualError(t, err, constanta.ERROR_ID_INVALID) + mockRepo.AssertExpectations(t) + }) +} + +// TestSendOTP tests the SendOTP method of userService. +func TestSendOTP(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Send OTP", func(t *testing.T) { + email := "test@example.com" + mockRepo.On("SendOTP", email, mock.Anything, mock.Anything).Return(entity.UsersCore{}, nil) + err := userSvc.SendOTP(email) + assert.Nil(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Data Empty", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "", + } + err := userSvc.SendOTP(requestBody.Email) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Email Format", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "ayamgoreng", + } + err := userSvc.SendOTP(requestBody.Email) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Error Generate OTP", func(t *testing.T) { + mockRepo := new(mocks.MockUserRepository) + + mockRepo.On("GenerateOTP", 4).Return("", errors.New("generate otp gagal")).Once() + result, err := mockRepo.GenerateOTP(4) + + assert.Error(t, err) + assert.EqualError(t, err, "generate otp gagal") + assert.Empty(t, result) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Send OTP", func(t *testing.T) { + email := "invalid@example.com" + // Mock user not found scenario + mockRepo.On("SendOTP", email, mock.Anything, mock.Anything).Return(entity.UsersCore{}, errors.New("user not found")) + err := userSvc.SendOTP(email) + assert.NotNil(t, err) + assert.EqualError(t, err, "user not found") + mockRepo.AssertExpectations(t) + }) + +} + +// TestVerifyOTP tests the VerifyOTP method of userService. +func TestVerifyOTP(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Verify OTP", func(t *testing.T) { + email := "test@example.com" + // Mock user data for verification + userData := entity.UsersCore{Email: email, OtpExpiration: time.Now().Add(time.Minute).Unix()} + mockRepo.On("VerifyOTP", email, "123456").Return(userData, nil) + + token, err := userSvc.VerifyOTP(email, "123456") + assert.NotNil(t, err) + assert.EqualError(t, err, "otp tidak valid") + assert.Empty(t, token) + + // Ensure that ResetOTP is called once + mockRepo.AssertExpectations(t) + }) + + t.Run("Data Empty", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "", + Otp: "", + } + _, err := userSvc.VerifyOTP(requestBody.Email, requestBody.Otp) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Email or OTP Invalid", func(t *testing.T) { + mockRepo.On("VerifyOTP", mock.Anything, mock.Anything).Return(entity.UsersCore{}, errors.New("email atau otp salah")).Once() + requestBody := entity.UsersCore{ + Email: "asdasd", + Otp: "aVC5", + } + + _, err := userSvc.VerifyOTP(requestBody.Email, requestBody.Otp) + assert.Error(t, err) + assert.EqualError(t, err, "email atau otp salah") + + mockRepo.AssertExpectations(t) + }) + + t.Run("Otp Expired", func(t *testing.T) { + expiredOTP := "123456" + expirationTime := time.Now().Add(-time.Hour) + mockRepo.On("VerifyOTP", mock.Anything, mock.Anything).Return(entity.UsersCore{Otp: expiredOTP, OtpExpiration: expirationTime.Unix()}, nil).Once() + + userSvc := NewUserService(mockRepo) + _, err := userSvc.VerifyOTP("anyemail@example.com", "anyOTP") + assert.Error(t, err) + assert.EqualError(t, err, "otp sudah kadaluwarsa") + mockRepo.AssertExpectations(t) + }) + +} + +func TestUpdateNewPassword(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid New Password", func(t *testing.T) { + mockRepo.On("NewPassword", mock.Anything, mock.Anything).Return(entity.UsersCore{}, nil).Once() + email := "test@example.com" + password := "NewSecurePass" + confirmPassword := "NewSecurePass" + + err := userSvc.NewPassword(email, entity.UsersCore{ + Password: password, + ConfirmPassword: confirmPassword, + }) + + assert.NoError(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Password Mismatch", func(t *testing.T) { + us := &userService{userRepo: mockRepo} + + err := us.NewPassword("mismatch@email.com", entity.UsersCore{ + Password: "password1", + ConfirmPassword: "password2", + }) + + assert.EqualError(t, err, constanta.ERROR_PASSWORD) + }) + + t.Run("Data Empty", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "", + Password: "", + ConfirmPassword: "", + } + + err := userSvc.NewPassword(requestBody.Email, requestBody) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Password Invalid Length", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "bidadari@gmail.com", + Password: "pass", + ConfirmPassword: "pass", + } + + err := userSvc.NewPassword(requestBody.Email, requestBody) + + assert.Error(t, err) + assert.EqualError(t, err, "minimal 8 karakter, ulangi kembali!") + mockRepo.AssertExpectations(t) + }) + + t.Run("Email Format", func(t *testing.T) { + requestBody := entity.UsersCore{ + Email: "ayamgoreng", + Password: "123456asd", + ConfirmPassword: "123456asd", + } + err := userSvc.NewPassword(requestBody.Email, requestBody) + assert.Error(t, err) + mockRepo.AssertExpectations(t) + }) +} + +// TestJoinCommunity tests the JoinCommunity method of userService. +func TestJoinCommunity(t *testing.T) { + mockRepo := new(mocks.UsersRepositoryInterface) + userSvc := NewUserService(mockRepo) + + t.Run("Valid Join Community", func(t *testing.T) { + userID := "user123" + communityID := "community456" + mockRepo.On("JoinCommunity", userID, communityID).Return(nil) + + err := userSvc.JoinCommunity(userID, communityID) + assert.Nil(t, err) + mockRepo.AssertExpectations(t) + }) + + t.Run("Invalid Join Community", func(t *testing.T) { + userID := "" + communityID := "community456" + err := userSvc.JoinCommunity(userID, communityID) + assert.NotNil(t, err) + assert.EqualError(t, err, "id tidak boleh kosong") + mockRepo.AssertExpectations(t) + }) + + t.Run("Error Join Community", func(t *testing.T) { + mockRepo.On("JoinCommunity", mock.Anything, mock.Anything).Return(errors.New("failed to join community")).Once() + + communityID := "community123" + userID := "user123" + + err := userSvc.JoinCommunity(communityID, userID) + + assert.Error(t, err) + assert.EqualError(t, err, "failed to join community") + mockRepo.AssertExpectations(t) + }) +} diff --git a/mocks/UsersRepositoryInterface.go b/mocks/UsersRepositoryInterface.go index a521129a..0c82c691 100644 --- a/mocks/UsersRepositoryInterface.go +++ b/mocks/UsersRepositoryInterface.go @@ -13,6 +13,35 @@ type UsersRepositoryInterface struct { mock.Mock } +type MockUserRepository struct { + mock.Mock +} + +func (m *MockUserRepository) VerifyOTP(email, otp string) (entity.UsersCore, error) { + args := m.Called(email, otp) + return args.Get(0).(entity.UsersCore), args.Error(1) +} + +func (m *MockUserRepository) ResetOTP(otp string) error { + args := m.Called(otp) + return args.Error(0) +} + +func (m *MockUserRepository) SendVerificationEmail(email string, token string) error { + args := m.Called(email, token) + return args.Error(0) +} + +func (_m *MockUserRepository) CreateToken(userID string, role string) (string, error) { + ret := _m.Called(userID, role) + return ret.String(0), ret.Error(1) +} + +func (_m *MockUserRepository) GenerateOTP(length int) (string, error) { + ret := _m.Called(length) + return ret.String(0), ret.Error(1) +} + // FindByEmail provides a mock function with given fields: email func (_m *UsersRepositoryInterface) FindByEmail(email string) (entity.UsersCore, error) { ret := _m.Called(email) diff --git a/utils/helper/bcrypt.go b/utils/helper/bcrypt.go index 3a6130a1..daa77caa 100644 --- a/utils/helper/bcrypt.go +++ b/utils/helper/bcrypt.go @@ -1,6 +1,10 @@ package helper import ( + "errors" + "os" + + "github.com/golang-jwt/jwt" "golang.org/x/crypto/bcrypt" ) @@ -12,4 +16,38 @@ func HashPassword(password string) (string, error) { func CompareHash(hash, password string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil -} \ No newline at end of file +} + +func DecodeJWTToken(tokenString string) (string, error) { + // Parse token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Token verification key (harus sesuai dengan key yang digunakan untuk sign token) + secretKey := os.Getenv("JWT_SECRET") + if secretKey == "" { + return nil, errors.New("secret key not found in environment variable") + } + return []byte(secretKey), nil + }) + + if err != nil { + return "", err + } + + // Memeriksa apakah token valid + if !token.Valid { + return "", errors.New("token tidak valid") + } + + // Memeriksa apakah token memiliki klaim "sub" + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return "", errors.New("gagal mendapatkan claims dari token") + } + + userID, ok := claims["sub"].(string) + if !ok || userID == "" { + return "", errors.New("tidak dapat mendapatkan user ID dari token") + } + + return userID, nil +}