diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index 3d47bdac0..15ef076d6 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -43,3 +43,26 @@ func (t *TagController) CreateTag(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(&dbTag) } + +// GetTag godoc +// +// @Summary Gets a tag +// @Description Returns a tag +// @ID get-tag +// @Tags tag +// @Produce json +// @Param id path int true "Tag ID" +// @Success 200 {object} models.Tag +// @Failure 400 {string} string "failed to validate id" +// @Failure 404 {string} string "faied to find tag" +// @Failure 500 {string} string "failed to retrieve tag" +// @Router /api/v1/tags/{id} [get] +func (t *TagController) GetTag(c *fiber.Ctx) error { + tag, err := t.tagService.GetTag(c.Params("id")) + + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(&tag) +} diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index bcb48a96d..d515fb692 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -17,6 +17,6 @@ type Tag struct { } type CreateTagRequestBody struct { - Name string `json:"name" validate:"required"` - CategoryID uint `json:"category_id" validate:"required"` + Name string `json:"name" validate:"required,max=255"` + CategoryID uint `json:"category_id" validate:"required,min=1"` } diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 7711bf788..3f8682d8d 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -81,4 +81,5 @@ func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) { tags := router.Group("/tags") tags.Post("/", tagController.CreateTag) + tags.Get("/:id", tagController.GetTag) } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index a6093bdb5..7a741b994 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -9,17 +9,18 @@ import ( ) type TagServiceInterface interface { - CreateTag(partialTag models.CreateTagRequestBody) (*models.Tag, error) + CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) + GetTag(id string) (*models.Tag, error) } type TagService struct { DB *gorm.DB } -func (t *TagService) CreateTag(partialTag models.CreateTagRequestBody) (*models.Tag, error) { +func (t *TagService) CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) { tag := models.Tag{ - Name: partialTag.Name, - CategoryID: partialTag.CategoryID, + Name: tagBody.Name, + CategoryID: tagBody.CategoryID, } if err := utilities.ValidateData(tag); err != nil { @@ -28,3 +29,13 @@ func (t *TagService) CreateTag(partialTag models.CreateTagRequestBody) (*models. return transactions.CreateTag(t.DB, tag) } + +func (t *TagService) GetTag(id string) (*models.Tag, error) { + idAsUint, err := utilities.ValidateID(id) + + if err != nil { + return nil, err + } + + return transactions.GetTag(t.DB, *idAsUint) +} diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index ec261aa7e..ea844ecb5 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -21,9 +21,9 @@ func GetTag(db *gorm.DB, id uint) (*models.Tag, error) { if err := db.First(&tag, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusBadRequest, "invalid tag id") + return nil, fiber.NewError(fiber.StatusNotFound, "failed to find tag") } else { - return nil, fiber.NewError(fiber.StatusInternalServerError, "unable to retrieve tag") + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to retrieve tag") } } diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 770097268..a161587c6 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -1,7 +1,10 @@ package utilities import ( + "strconv" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" ) // Validate the data sent to the server if the data is invalid, return an error otherwise, return nil @@ -13,3 +16,21 @@ func ValidateData(model interface{}) error { return nil } + +func ValidateID(id string) (*uint, error) { + idAsInt, err := strconv.Atoi(id) + + errMsg := "failed to validate id" + + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + if idAsInt < 1 { // postgres ids start at 1 + return nil, fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + idAsUint := uint(idAsInt) + + return &idAsUint, nil +} diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index d313d4c30..a43039a8c 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -12,14 +12,14 @@ import ( "github.com/goccy/go-json" ) -func CreateSampleCategory(t *testing.T, categoryName string) ExistingAppAssert { +func CreateSampleCategory(t *testing.T, categoryName string, existingAppAssert *ExistingAppAssert) ExistingAppAssert { return TestRequest{ Method: "POST", Path: "/api/v1/categories/", Body: &map[string]interface{}{ "category_name": categoryName, }, - }.TestOnStatusAndDB(t, nil, + }.TestOnStatusAndDB(t, existingAppAssert, DBTesterWithStatus{ Status: 201, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { @@ -41,7 +41,7 @@ func CreateSampleCategory(t *testing.T, categoryName string) ExistingAppAssert { } func TestCreateCategoryWorks(t *testing.T) { - CreateSampleCategory(t, "Science") + CreateSampleCategory(t, "Science", nil) } func TestCreateCategoryIgnoresid(t *testing.T) { @@ -121,9 +121,9 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { } func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { - categoryName := "Science" + categoryName := "foo" - existingAppAssert := CreateSampleCategory(t, categoryName) + existingAppAssert := CreateSampleCategory(t, categoryName, nil) var TestNumCategoriesRemainsAt1 = func(app TestApp, assert *assert.A, resp *http.Response) { AssertNumCategoriesRemainsAtN(app, assert, resp, 1) diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 88570b644..eecc1cd53 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -78,8 +78,9 @@ func generateRandomInt(max int64) int64 { } func generateRandomDBName() string { + prefix := "sac_test_" letterBytes := "abcdefghijklmnopqrstuvwxyz" - length := 36 + length := len(prefix) + 36 result := make([]byte, length) for i := 0; i < length; i++ { result[i] = letterBytes[generateRandomInt(int64(len(letterBytes)))] @@ -229,6 +230,7 @@ type DBTesterWithStatus struct { DBTester } + func (request TestRequest) TestOnStatusAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert { appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) app, assert := appAssert.App, appAssert.Assert diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 5a0ab27c1..d119f32e1 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "net/http" "testing" @@ -11,16 +12,22 @@ import ( "github.com/goccy/go-json" ) -func TestCreateTagWorks(t *testing.T) { - existingAppAssert := CreateSampleCategory(t, "Science") - TestRequest{ +func CreateSampleTag(t *testing.T, tagName string, categoryName string, existingAppAssert *ExistingAppAssert) ExistingAppAssert { + var appAssert ExistingAppAssert + + if existingAppAssert == nil { + appAssert = CreateSampleCategory(t, categoryName, existingAppAssert) + } else { + appAssert = CreateSampleCategory(t, categoryName, existingAppAssert) + } + return TestRequest{ Method: "POST", Path: "/api/v1/tags/", Body: &map[string]interface{}{ - "name": "Generate", + "name": tagName, "category_id": 1, }, - }.TestOnStatusAndDB(t, &existingAppAssert, + }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ Status: 201, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { @@ -40,6 +47,10 @@ func TestCreateTagWorks(t *testing.T) { ) } +func TestCreateTagWorks(t *testing.T) { + CreateSampleTag(t, "Generate", "Science", nil) +} + var AssertNoTagCreation = func(app TestApp, assert *assert.A, resp *http.Response) { var tags []models.Tag @@ -105,5 +116,64 @@ func TestCreateTagFailsValidation(t *testing.T) { }, ) } +} + +func TestGetTagWorks(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + + TestRequest{ + Method: "GET", + Path: "/api/v1/tags/1", + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var respTag models.Tag + + err := json.NewDecoder(resp.Body).Decode(&respTag) + + assert.NilError(err) + + dbTag, err := transactions.GetTag(app.Conn, respTag.ID) + + assert.NilError(err) + + assert.Equal(dbTag, &respTag) + }, + }, + ) +} + +func TestGetTagFailsBadRequest(t *testing.T) { + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: "GET", + Path: fmt.Sprintf("/api/v1/tags/%s", badRequest), + }.TestOnStatusAndMessage(t, nil, + MessageWithStatus{ + Status: 400, + Message: "failed to validate id", + }, + ) + } +} +func TestGetTagFailsNotFound(t *testing.T) { + TestRequest{ + Method: "GET", + Path: "/api/v1/tags/1", + }.TestOnStatusAndMessage(t, nil, + MessageWithStatus{ + Status: 404, + Message: "failed to find tag", + }, + ) } diff --git a/scripts/clean_old_test_dbs.sh b/scripts/clean_old_test_dbs.sh new file mode 100755 index 000000000..04c97262b --- /dev/null +++ b/scripts/clean_old_test_dbs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +PGHOST="127.0.0.1" +PGPORT="5432" +PGUSER="postgres" +PGPASSWORD="postgres" +PGDATABASE="sac" + +DATABASES=$(psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -t -c "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres' AND datname != 'template0' AND datname != '$PGDATABASE' AND datname !='$(whoami)';") + + +for db in $DATABASES; do + echo "Dropping database $db" + psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -c "DROP DATABASE $db;" +done diff --git a/scripts/clean_prefixed_test_dbs.sh b/scripts/clean_prefixed_test_dbs.sh new file mode 100755 index 000000000..61797e999 --- /dev/null +++ b/scripts/clean_prefixed_test_dbs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +PGHOST="127.0.0.1" +PGPORT="5432" +PGUSER="postgres" +PGPASSWORD="postgres" +PGDATABASE="sac" +PREFIX="sac_test_" + +DATABASES=$(psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -t -c "SELECT datname FROM pg_database WHERE datistemplate = false AND datname like '$PREFIX%';") + +for db in $DATABASES; do + echo "Dropping database $db" + psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -c "DROP DATABASE $db;" +done