From 169c50fa08fa825683189d13985d443782ce578e Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 10 Oct 2024 01:30:30 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Added=20GET=20execution=20?= =?UTF-8?q?by=20ID=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/api/main.go | 5 +- cmd/api/search/criteria/errors.go | 20 ++++--- cmd/api/search/criteria/handler.go | 32 +++++++++- cmd/api/search/criteria/handler_test.go | 77 ++++++++++++++++++++++--- cmd/api/search/criteria/mocks.go | 25 ++++++++ cmd/api/search/criteria/select.go | 33 ++++++++++- cmd/api/search/criteria/select_test.go | 45 +++++++++++++++ 7 files changed, 216 insertions(+), 21 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 4e03bdc..89888cf 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -67,6 +67,8 @@ func main() { resumeCriteria := criteria.MakeResume(selectCriteriaByID, selectLastDayExecutedByCriteriaID, selectExecutionsByStatuses, scrapperEnqueueCriteria) initCriteria := criteria.MakeInit(selectExecutionsByStatuses, resumeCriteria) + selectExecutionByID := criteria.MakeSelectExecutionByID(db) + updateCriteriaExecution := criteria.MakeUpdateExecution(db) insertCriteriaExecutionDay := criteria.MakeInsertExecutionDay(db) @@ -79,8 +81,9 @@ func main() { router.HandleFunc("POST /tweets/v1", tweets.InsertHandlerV1(insertTweets)) router.HandleFunc("POST /criteria/{criteria_id}/enqueue/v1", criteria.EnqueueHandlerV1(enqueueCriteria)) router.HandleFunc("POST /criteria/init/v1", criteria.InitHandlerV1(initCriteria)) + router.HandleFunc("GET /criteria/executions/{execution_id}/v1", criteria.GetExecutionByIDHandlerV1(selectExecutionByID)) router.HandleFunc("PUT /criteria/executions/{execution_id}/v1", criteria.UpdateExecutionHandlerV1(updateCriteriaExecution)) - router.HandleFunc("POST /criteria/executions/{execution_id}/day/v1", criteria.InsertExecutionDayHandlerV1(insertCriteriaExecutionDay)) + router.HandleFunc("POST /criteria/executions/{execution_id}/day/v1", criteria.CreateExecutionDayHandlerV1(insertCriteriaExecutionDay)) log.Info(ctx, "Router initialized!") /* --- Server --- */ diff --git a/cmd/api/search/criteria/errors.go b/cmd/api/search/criteria/errors.go index 6276cb3..4b69f43 100644 --- a/cmd/api/search/criteria/errors.go +++ b/cmd/api/search/criteria/errors.go @@ -3,15 +3,15 @@ package criteria import "errors" const ( - InvalidURLParameter string = "Invalid url parameter" - InvalidQueryParameterFormat string = "Invalid query parameter format" - InvalidRequestBody string = "Invalid request body" - FailedToEnqueueCriteria string = "Failed to execute enqueue criteria" - ExecutionWithSameCriteriaIDAlreadyEnqueued string = "An execution with the same criteria id is already enqueued" - FailedToExecuteInitCriteria string = "Failed to execute init criteria" - FailedToExecuteInsertCriteriaExecution string = "Failed to execute insert criteria execution" - FailedToExecuteUpdateCriteriaExecution string = "Failed to execute update criteria execution" - FailedToEncodeInsertCriteriaExecutionResponse string = "Failed to encode insert criteria execution response" + InvalidURLParameter string = "Invalid url parameter" + InvalidQueryParameterFormat string = "Invalid query parameter format" + InvalidRequestBody string = "Invalid request body" + FailedToEnqueueCriteria string = "Failed to execute enqueue criteria" + ExecutionWithSameCriteriaIDAlreadyEnqueued string = "An execution with the same criteria id is already enqueued" + FailedToExecuteInitCriteria string = "Failed to execute init criteria" + FailedToExecuteInsertCriteriaExecution string = "Failed to execute insert criteria execution" + FailedToExecuteUpdateCriteriaExecution string = "Failed to execute update criteria execution" + FailedToExecuteGetExecutionsByStatuses string = "Failed to execute get criteria executions by statuses" ) var ( @@ -32,4 +32,6 @@ var ( FailedToInsertSearchCriteriaExecutionDay = errors.New("failed to insert search criteria execution day") FailedToRetrieveLastDayExecutedDate = errors.New("failed to retrieve last day executed date") NoExecutionDaysFoundForTheGivenCriteriaID = errors.New("no execution days found for the given criteria id") + NoExecutionFoundForTheGivenID = errors.New("no execution found for the given id") + FailedToExecuteQueryToRetrieveExecutionData = errors.New("failed to execute query to retrieve execution data") ) diff --git a/cmd/api/search/criteria/handler.go b/cmd/api/search/criteria/handler.go index c8228ea..96c47c4 100644 --- a/cmd/api/search/criteria/handler.go +++ b/cmd/api/search/criteria/handler.go @@ -70,6 +70,34 @@ func InitHandlerV1(init Init) http.HandlerFunc { } } +// GetExecutionByIDHandlerV1 HTTP Handler of the endpoint /criteria/executions/{execution_id}/v1 +func GetExecutionByIDHandlerV1(selectExecutionByID SelectExecutionByID) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + executionIDParam := r.PathValue("execution_id") + executionID, err := strconv.Atoi(executionIDParam) + if err != nil { + log.Error(ctx, err.Error()) + http.Error(w, InvalidURLParameter, http.StatusBadRequest) + return + } + ctx = log.With(ctx, log.Param("execution_id", executionIDParam)) + + executions, err := selectExecutionByID(ctx, executionID) + if err != nil { + log.Error(ctx, err.Error()) + http.Error(w, FailedToExecuteGetExecutionsByStatuses, http.StatusInternalServerError) + return + } + + log.Info(ctx, "Executions successfully obtained") + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(executions) + } +} + // UpdateExecutionHandlerV1 HTTP Handler of the endpoint /criteria/executions/{execution_id}/v1 func UpdateExecutionHandlerV1(updateExecution UpdateExecution) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -106,8 +134,8 @@ func UpdateExecutionHandlerV1(updateExecution UpdateExecution) http.HandlerFunc } } -// InsertExecutionDayHandlerV1 HTTP Handler of the endpoint /criteria/executions/{execution_id}/day/v1 -func InsertExecutionDayHandlerV1(insertExecutionDay InsertExecutionDay) http.HandlerFunc { +// CreateExecutionDayHandlerV1 HTTP Handler of the endpoint /criteria/executions/{execution_id}/day/v1 +func CreateExecutionDayHandlerV1(insertExecutionDay InsertExecutionDay) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/cmd/api/search/criteria/handler_test.go b/cmd/api/search/criteria/handler_test.go index cfb7a56..548d25d 100644 --- a/cmd/api/search/criteria/handler_test.go +++ b/cmd/api/search/criteria/handler_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "io" "net/http" "net/http/httptest" "testing" @@ -133,6 +134,66 @@ func TestInitHandlerV1_failsWhenInitThrowsError(t *testing.T) { assert.Equal(t, want, got) } +func TestGetExecutionByIDHandlerV1_success(t *testing.T) { + mockExecutionDAO := criteria.MockExecutionDAO() + mockSelectExecutionByID := criteria.MockSelectExecutionByID(mockExecutionDAO, nil) + mockResponseWriter := httptest.NewRecorder() + mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/criteria/executions/{execution_id}/v1", http.NoBody) + mockRequest.SetPathValue("execution_id", "1") + + handlerV1 := criteria.GetExecutionByIDHandlerV1(mockSelectExecutionByID) + + handlerV1(mockResponseWriter, mockRequest) + + body, err := io.ReadAll(mockResponseWriter.Result().Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + want := mockExecutionDAO + var got criteria.ExecutionDAO + err = json.Unmarshal(body, &got) + if err != nil { + t.Fatalf("Failed to parse response body as JSON: %v", err) + } + + assert.Equal(t, want, got) + assert.Equal(t, "application/json", mockResponseWriter.Header().Get("Content-Type")) + assert.Equal(t, http.StatusOK, mockResponseWriter.Result().StatusCode) +} + +func TestGetExecutionByIDHandlerV1_failsWhenTheURLParamIsEmpty(t *testing.T) { + mockExecutionDAO := criteria.MockExecutionDAO() + mockSelectExecutionByID := criteria.MockSelectExecutionByID(mockExecutionDAO, nil) + mockResponseWriter := httptest.NewRecorder() + mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/criteria/executions/{execution_id}/v1", http.NoBody) + + handlerV1 := criteria.GetExecutionByIDHandlerV1(mockSelectExecutionByID) + + handlerV1(mockResponseWriter, mockRequest) + + want := http.StatusBadRequest + got := mockResponseWriter.Result().StatusCode + + assert.Equal(t, want, got) +} + +func TestGetExecutionByIDHandlerV1_failsWhenSelectExecutionByIDThrowsError(t *testing.T) { + mockSelectExecutionByID := criteria.MockSelectExecutionByID(criteria.ExecutionDAO{}, errors.New("failed to select execution by id")) + mockResponseWriter := httptest.NewRecorder() + mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/criteria/executions/{execution_id}/v1", http.NoBody) + mockRequest.SetPathValue("execution_id", "1") + + handlerV1 := criteria.GetExecutionByIDHandlerV1(mockSelectExecutionByID) + + handlerV1(mockResponseWriter, mockRequest) + + want := http.StatusInternalServerError + got := mockResponseWriter.Result().StatusCode + + assert.Equal(t, want, got) +} + func TestUpdateExecutionHandlerV1_success(t *testing.T) { mockUpdateExecution := criteria.MockUpdateExecution(nil) mockResponseWriter := httptest.NewRecorder() @@ -203,7 +264,7 @@ func TestUpdateExecutionHandlerV1_failsWhenUpdateExecutionThrowsError(t *testing assert.Equal(t, want, got) } -func TestInsertExecutionDayHandlerV1_success(t *testing.T) { +func TestCreateExecutionDayHandlerV1_success(t *testing.T) { mockInsertExecutionDay := criteria.MockInsertExecutionDay(nil) mockResponseWriter := httptest.NewRecorder() mockExecutionDay := criteria.MockExecutionDayDTO(nil) @@ -211,7 +272,7 @@ func TestInsertExecutionDayHandlerV1_success(t *testing.T) { mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/criteria/executions/{execution_id}/day/v1", bytes.NewReader(mockBody)) mockRequest.SetPathValue("execution_id", "1") - handlerV1 := criteria.InsertExecutionDayHandlerV1(mockInsertExecutionDay) + handlerV1 := criteria.CreateExecutionDayHandlerV1(mockInsertExecutionDay) handlerV1(mockResponseWriter, mockRequest) @@ -221,14 +282,14 @@ func TestInsertExecutionDayHandlerV1_success(t *testing.T) { assert.Equal(t, want, got) } -func TestInsertExecutionDayHandlerV1_failsWhenTheURLParamIsEmpty(t *testing.T) { +func TestCreateExecutionDayHandlerV1_failsWhenTheURLParamIsEmpty(t *testing.T) { mockInsertExecutionDay := criteria.MockInsertExecutionDay(nil) mockResponseWriter := httptest.NewRecorder() mockExecutionDay := criteria.MockExecutionDayDTO(nil) mockBody, _ := json.Marshal(mockExecutionDay) mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/criteria/executions/{execution_id}/day/v1", bytes.NewReader(mockBody)) - handlerV1 := criteria.InsertExecutionDayHandlerV1(mockInsertExecutionDay) + handlerV1 := criteria.CreateExecutionDayHandlerV1(mockInsertExecutionDay) handlerV1(mockResponseWriter, mockRequest) @@ -238,14 +299,14 @@ func TestInsertExecutionDayHandlerV1_failsWhenTheURLParamIsEmpty(t *testing.T) { assert.Equal(t, want, got) } -func TestInsertExecutionDayHandlerV1_failsWhenTheBodyCantBeParsed(t *testing.T) { +func TestCreateExecutionDayHandlerV1_failsWhenTheBodyCantBeParsed(t *testing.T) { mockInsertExecutionDay := criteria.MockInsertExecutionDay(nil) mockResponseWriter := httptest.NewRecorder() mockBody, _ := json.Marshal(`{"wrong": "body"}`) mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/criteria/executions/{execution_id}/day/v1", bytes.NewReader(mockBody)) mockRequest.SetPathValue("execution_id", "1") - handlerV1 := criteria.InsertExecutionDayHandlerV1(mockInsertExecutionDay) + handlerV1 := criteria.CreateExecutionDayHandlerV1(mockInsertExecutionDay) handlerV1(mockResponseWriter, mockRequest) @@ -255,7 +316,7 @@ func TestInsertExecutionDayHandlerV1_failsWhenTheBodyCantBeParsed(t *testing.T) assert.Equal(t, want, got) } -func TestInsertExecutionDayHandlerV1_failsWhenInsertExecutionDayThrowsError(t *testing.T) { +func TestCreateExecutionDayHandlerV1_failsWhenInsertExecutionDayThrowsError(t *testing.T) { mockInsertExecutionDay := criteria.MockInsertExecutionDay(errors.New("failed to insert execution day")) mockResponseWriter := httptest.NewRecorder() mockExecutionDay := criteria.MockExecutionDayDTO(nil) @@ -263,7 +324,7 @@ func TestInsertExecutionDayHandlerV1_failsWhenInsertExecutionDayThrowsError(t *t mockRequest, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "/criteria/executions/{execution_id}/day/v1", bytes.NewReader(mockBody)) mockRequest.SetPathValue("execution_id", "1") - handlerV1 := criteria.InsertExecutionDayHandlerV1(mockInsertExecutionDay) + handlerV1 := criteria.CreateExecutionDayHandlerV1(mockInsertExecutionDay) handlerV1(mockResponseWriter, mockRequest) diff --git a/cmd/api/search/criteria/mocks.go b/cmd/api/search/criteria/mocks.go index 52e1be9..eab6f6d 100644 --- a/cmd/api/search/criteria/mocks.go +++ b/cmd/api/search/criteria/mocks.go @@ -47,6 +47,13 @@ func MockInit(err error) Init { } } +// MockSelectExecutionByID mocks SelectExecutionByID function +func MockSelectExecutionByID(executionDAO ExecutionDAO, err error) SelectExecutionByID { + return func(ctx context.Context, id int) (ExecutionDAO, error) { + return executionDAO, err + } +} + // MockInsertExecution mocks InsertExecution function func MockInsertExecution(criteriaID int, err error) InsertExecution { return func(ctx context.Context, searchCriteriaID int, forced bool) (int, error) { @@ -130,6 +137,24 @@ func MockCriteriaDAOSlice() []DAO { } } +// MockExecutionDAOValues mocks the properties of ExecutionDAO to be used in the Scan function +func MockExecutionDAOValues(dao ExecutionDAO) []any { + return []any{ + dao.ID, + dao.Status, + dao.SearchCriteriaID, + } +} + +// MockExecutionDAO mocks an ExecutionDAO +func MockExecutionDAO() ExecutionDAO { + return ExecutionDAO{ + ID: 1, + Status: DoneStatus, + SearchCriteriaID: 2, + } +} + // MockExecutionsDAO mocks a slice of ExecutionDAO func MockExecutionsDAO() []ExecutionDAO { return []ExecutionDAO{ diff --git a/cmd/api/search/criteria/select.go b/cmd/api/search/criteria/select.go index fa5dbec..f87681d 100644 --- a/cmd/api/search/criteria/select.go +++ b/cmd/api/search/criteria/select.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" - "github.com/jackc/pgx/v5" "strings" + "github.com/jackc/pgx/v5" + "ahbcc/internal/database" "ahbcc/internal/log" ) @@ -18,6 +19,9 @@ type ( // SelectAll returns all the criteria of the 'search_criteria' table SelectAll func(ctx context.Context) ([]DAO, error) + // SelectExecutionByID returns an execution seeking by its ID + SelectExecutionByID func(ctx context.Context, id int) (ExecutionDAO, error) + // SelectExecutionsByStatuses returns all the search criteria executions in certain state SelectExecutionsByStatuses func(ctx context.Context, statuses []string) ([]ExecutionDAO, error) @@ -83,6 +87,33 @@ func MakeSelectAll(db database.Connection, collectRows database.CollectRows[DAO] } } +// MakeSelectExecutionByID creates a new SelectExecutionByID +func MakeSelectExecutionByID(db database.Connection) SelectExecutionByID { + const query string = ` + SELECT id, status, search_criteria_id + FROM search_criteria_executions + WHERE id = $1 + ` + + return func(ctx context.Context, id int) (ExecutionDAO, error) { + var execution ExecutionDAO + err := db.QueryRow(ctx, query, id).Scan( + &execution.ID, + &execution.Status, + &execution.SearchCriteriaID, + ) + if errors.Is(err, pgx.ErrNoRows) { + log.Error(ctx, err.Error()) + return ExecutionDAO{}, NoExecutionFoundForTheGivenID + } else if err != nil { + log.Error(ctx, err.Error()) + return ExecutionDAO{}, FailedToExecuteQueryToRetrieveExecutionData + } + + return execution, nil + } +} + // MakeSelectExecutionsByStatuses creates a new SelectExecutionsByStatuses func MakeSelectExecutionsByStatuses(db database.Connection, collectRows database.CollectRows[ExecutionDAO]) SelectExecutionsByStatuses { const query string = ` diff --git a/cmd/api/search/criteria/select_test.go b/cmd/api/search/criteria/select_test.go index bfaa9aa..d2c85f6 100644 --- a/cmd/api/search/criteria/select_test.go +++ b/cmd/api/search/criteria/select_test.go @@ -111,6 +111,51 @@ func TestSelectAll_failsWhenCollectRowsThrowsError(t *testing.T) { mockPgxRows.AssertExpectations(t) } +func TestSelectExecutionByID_success(t *testing.T) { + mockPostgresConnection := new(database.MockPostgresConnection) + mockPgxRow := new(database.MockPgxRow) + mockExecution := criteria.MockExecutionDAO() + mockScanCriteriaDAOValues := criteria.MockExecutionDAOValues(mockExecution) + database.MockScan(mockPgxRow, mockScanCriteriaDAOValues, t) + mockPostgresConnection.On("QueryRow", mock.Anything, mock.Anything, mock.Anything).Return(mockPgxRow) + + selectExecutionByID := criteria.MakeSelectExecutionByID(mockPostgresConnection) + + want := mockExecution + got, err := selectExecutionByID(context.Background(), 1) + + assert.Nil(t, err) + assert.Equal(t, want, got) + mockPostgresConnection.AssertExpectations(t) + mockPgxRow.AssertExpectations(t) +} + +func TestSelectExecutionByID_failsWhenSelectOperationFails(t *testing.T) { + tests := []struct { + err error + expected error + }{ + {err: pgx.ErrNoRows, expected: criteria.NoExecutionFoundForTheGivenID}, + {err: errors.New("failed to execute select operation"), expected: criteria.FailedToExecuteQueryToRetrieveExecutionData}, + } + + for _, tt := range tests { + mockPostgresConnection := new(database.MockPostgresConnection) + mockPgxRow := new(database.MockPgxRow) + mockPgxRow.On("Scan", mock.Anything).Return(tt.err) + mockPostgresConnection.On("QueryRow", mock.Anything, mock.Anything, mock.Anything).Return(mockPgxRow) + + selectExecutionByID := criteria.MakeSelectExecutionByID(mockPostgresConnection) + + want := tt.expected + _, got := selectExecutionByID(context.Background(), 1) + + assert.Equal(t, want, got) + mockPostgresConnection.AssertExpectations(t) + mockPgxRow.AssertExpectations(t) + } +} + func TestSelectExecutionsByState_success(t *testing.T) { mockPostgresConnection := new(database.MockPostgresConnection) mockPgxRows := new(database.MockPgxRows)