Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAC-15 Retrieve Tag GET #33

Merged
merged 16 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions backend/src/controllers/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package controllers

import (
"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/services"

"github.com/gofiber/fiber/v2"
)

type TagController struct {
tagService services.TagServiceInterface
}

func NewTagController(tagService services.TagServiceInterface) *TagController {
return &TagController{tagService: tagService}
}

// CreateTag godoc
//
// @Summary Creates a tag
// @Description Creates a tag
// @ID create-tag
// @Tags tag
// @Accept json
// @Produce json
// @Success 201 {object} models.Tag
// @Failure 400 {string} string "failed to process the request"
// @Failure 400 {string} string "failed to validate the data"
// @Failure 500 {string} string "failed to create tag"
// @Router /api/v1/tags/ [post]
func (t *TagController) CreateTag(c *fiber.Ctx) error {
var tagBody models.CreateTagRequestBody

if err := c.BodyParser(&tagBody); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "failed to process the request")
}

dbTag, err := t.tagService.CreateTag(tagBody)

if err != nil {
return err
}

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)
}
14 changes: 11 additions & 3 deletions backend/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error {

var user models.User

if err := tx.Where("nuid = ?", superUser.NUID).First(&user).Error; err != nil {
if err := db.Where("nuid = ?", superUser.NUID).First(&user).Error; err != nil {
tx := db.Begin()

if err := tx.Error; err != nil {
return err
}

if err := tx.Create(&superUser).Error; err != nil {
tx.Rollback()
return err
Expand All @@ -106,7 +112,9 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error {
tx.Rollback()
return err
}
}

return tx.Commit().Error
return tx.Commit().Error

}
return nil
}
7 changes: 6 additions & 1 deletion backend/src/models/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ type Tag struct {

Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`

CategoryID uint `gorm:"foreignKey:CategoryID" json:"category_id" validate:"-"`
CategoryID uint `gorm:"foreignKey:CategoryID" json:"category_id" validate:"required"`

User []User `gorm:"many2many:user_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Club []Club `gorm:"many2many:club_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Event []Event `gorm:"many2many:event_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
}

type CreateTagRequestBody struct {
Name string `json:"name" validate:"required"`
garrettladley marked this conversation as resolved.
Show resolved Hide resolved
CategoryID uint `json:"category_id" validate:"required"`
garrettladley marked this conversation as resolved.
Show resolved Hide resolved
}
10 changes: 10 additions & 0 deletions backend/src/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func Init(db *gorm.DB) *fiber.App {

userRoutes(apiv1, &services.UserService{DB: db})
categoryRoutes(apiv1, &services.CategoryService{DB: db})
tagRoutes(apiv1, &services.TagService{DB: db})

return app
}
Expand Down Expand Up @@ -73,3 +74,12 @@ func categoryRoutes(router fiber.Router, categoryService services.CategoryServic

categories.Post("/", categoryController.CreateCategory)
}

func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) {
tagController := controllers.NewTagController(tagService)

tags := router.Group("/tags")

tags.Post("/", tagController.CreateTag)
tags.Get("/:id", tagController.GetTag)
}
41 changes: 41 additions & 0 deletions backend/src/services/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package services

import (
"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/transactions"
"github.com/GenerateNU/sac/backend/src/utilities"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)

type TagServiceInterface interface {
CreateTag(partialTag models.CreateTagRequestBody) (*models.Tag, error)
garrettladley marked this conversation as resolved.
Show resolved Hide resolved
GetTag(id string) (*models.Tag, error)
}

type TagService struct {
DB *gorm.DB
}

func (t *TagService) CreateTag(partialTag models.CreateTagRequestBody) (*models.Tag, error) {
tag := models.Tag{
Name: partialTag.Name,
CategoryID: partialTag.CategoryID,
}

if err := utilities.ValidateData(tag); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data")
}

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)
}
31 changes: 31 additions & 0 deletions backend/src/transactions/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package transactions

import (
"errors"

"github.com/GenerateNU/sac/backend/src/models"

"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)

func CreateTag(db *gorm.DB, tag models.Tag) (*models.Tag, error) {
if err := db.Create(&tag).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create tag")
}
return &tag, nil
}

func GetTag(db *gorm.DB, id uint) (*models.Tag, error) {
var tag models.Tag

if err := db.First(&tag, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "failed to find tag")
} else {
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to retrieve tag")
}
}

return &tag, nil
}
21 changes: 21 additions & 0 deletions backend/src/utilities/validator.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
56 changes: 42 additions & 14 deletions backend/tests/api/category_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,38 +73,63 @@ func TestCreateCategoryIgnoresid(t *testing.T) {
)
}

func AssertNoCategoryCreation(app TestApp, assert *assert.A, resp *http.Response) {
AssertNumCategoriesRemainsAtN(app, assert, resp, 0)
}

func AssertNumCategoriesRemainsAtN(app TestApp, assert *assert.A, resp *http.Response, n int) {
var categories []models.Category

err := app.Conn.Find(&categories).Error

assert.NilError(err)

assert.Equal(n, len(categories))
}

func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) {
TestRequest{
Method: "POST",
Path: "/api/v1/categories/",
Body: &map[string]interface{}{
"category_name": 1231,
},
}.TestOnStatusAndMessage(t, nil,
MessageWithStatus{
Status: 400,
Message: "failed to process the request",
})
}.TestOnStatusMessageAndDB(t, nil,
StatusMessageDBTester{
MessageWithStatus: MessageWithStatus{
Status: 400,
Message: "failed to process the request",
},
DBTester: AssertNoCategoryCreation,
},
)
}

func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) {
TestRequest{
Method: "POST",
Path: "/api/v1/categories/",
Body: &map[string]interface{}{},
}.TestOnStatusAndMessage(t, nil,
MessageWithStatus{
Status: 400,
Message: "failed to validate the data",
}.TestOnStatusMessageAndDB(t, nil,
StatusMessageDBTester{
MessageWithStatus: MessageWithStatus{
Status: 400,
Message: "failed to validate the data",
},
DBTester: AssertNoCategoryCreation,
},
)
}

func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) {
categoryName := "Science"
categoryName := "foo"

existingAppAssert := CreateSampleCategory(t, categoryName)

var TestNumCategoriesRemainsAt1 = func(app TestApp, assert *assert.A, resp *http.Response) {
AssertNumCategoriesRemainsAtN(app, assert, resp, 1)
}

for _, permutation := range AllCasingPermutations(categoryName) {
fmt.Println(permutation)
TestRequest{
Expand All @@ -113,10 +138,13 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) {
Body: &map[string]interface{}{
"category_name": permutation,
},
}.TestOnStatusAndMessageKeepDB(t, &existingAppAssert,
MessageWithStatus{
Status: 400,
Message: "category with that name already exists",
}.TestOnStatusMessageAndDBKeepDB(t, &existingAppAssert,
StatusMessageDBTester{
MessageWithStatus: MessageWithStatus{
Status: 400,
Message: "category with that name already exists",
},
DBTester: TestNumCategoriesRemainsAt1,
},
)
}
Expand Down
25 changes: 20 additions & 5 deletions backend/tests/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))]
Expand Down Expand Up @@ -213,10 +214,9 @@ type MessageWithStatus struct {
Message string
}

func (request TestRequest) TestOnStatusAndMessage(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) ExistingAppAssert {
func (request TestRequest) TestOnStatusAndMessage(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) {
appAssert := request.TestOnStatusAndMessageKeepDB(t, existingAppAssert, messagedStatus)
appAssert.App.DropDB()
return appAssert
}

func (request TestRequest) TestOnStatusAndMessageKeepDB(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) ExistingAppAssert {
Expand All @@ -238,17 +238,32 @@ func (request TestRequest) TestOnStatusAndMessageKeepDB(t *testing.T, existingAp
return appAssert
}

type StatusMessageDBTester struct {
MessageWithStatus MessageWithStatus
DBTester DBTester
}

func (request TestRequest) TestOnStatusMessageAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, statusMessageDBTester StatusMessageDBTester) {
appAssert := request.TestOnStatusMessageAndDBKeepDB(t, existingAppAssert, statusMessageDBTester)
appAssert.App.DropDB()
}

func (request TestRequest) TestOnStatusMessageAndDBKeepDB(t *testing.T, existingAppAssert *ExistingAppAssert, statusMessageDBTester StatusMessageDBTester) ExistingAppAssert {
appAssert := request.TestOnStatusAndMessageKeepDB(t, existingAppAssert, statusMessageDBTester.MessageWithStatus)
statusMessageDBTester.DBTester(appAssert.App, appAssert.Assert, nil)
return appAssert
}

type DBTester func(app TestApp, assert *assert.A, resp *http.Response)

type DBTesterWithStatus struct {
Status int
DBTester
}

func (request TestRequest) TestOnStatusAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert {
func (request TestRequest) TestOnStatusAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) {
appAssert := request.TestOnStatusAndDBKeepDB(t, existingAppAssert, dbTesterStatus)
appAssert.App.DropDB()
return appAssert
}

func (request TestRequest) TestOnStatusAndDBKeepDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert {
Expand Down
Loading
Loading