Skip to content

Commit

Permalink
SAC-9 Create Category POST (#15)
Browse files Browse the repository at this point in the history
* SAC-9 Create Category POST

* SAC-9 Docs Fix

* GML nitpicky changes & clean up

* fixed so tests passing on local

* lots of progress

* casing permutations

* forgot to add oops

* fixed typo

* move title casing logic to category

---------

Co-authored-by: Alder Whiteford <[email protected]>
Co-authored-by: garrettladley <[email protected]>
  • Loading branch information
3 people authored Jan 19, 2024
1 parent 2391ad3 commit 0472697
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 30 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

- If this doesnt work, try running `go mod tidy` and then `go get ./...` again or delete the go.mod and go.sum files and then run `go mod init backend` and `go mod tidy` again.

### React Native Builds
## React Native Builds

1. **Create client build**

Expand Down Expand Up @@ -105,7 +105,7 @@

# Commands

### React Native
## React Native

```bash
npx expo start --dev-client // runnning dev client
Expand Down
2 changes: 1 addition & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ require (
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.14.0
golang.org/x/tools v0.13.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
49 changes: 49 additions & 0 deletions backend/src/controllers/category.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package controllers

import (
"backend/src/models"
"backend/src/services"

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

type CategoryController struct {
categoryService services.CategoryServiceInterface
}

func NewCategoryController(categoryService services.CategoryServiceInterface) *CategoryController {
return &CategoryController{categoryService: categoryService}
}

// CreateCategory godoc
//
// @Summary Create a category
// @Description Creates a category that is used to group tags
// @ID create-category
// @Tags category
// @Produce json
// @Success 201 {object} models.Category
// @Failure 400 {string} string "failed to process the request"
// @Failure 400 {string} string "failed to validate data"
// @Failure 400 {string} string "category with that name already exists"
// @Failure 500 {string} string "failed to create category"
// @Router /api/v1/category/ [post]
func (t *CategoryController) CreateCategory(c *fiber.Ctx) error {
var categoryBody models.CreateCategoryRequestBody

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

category := models.Category{
Name: categoryBody.Name,
}

newCategory, err := t.categoryService.CreateCategory(category)

if err != nil {
return err
}

return c.Status(fiber.StatusCreated).JSON(newCategory)
}
21 changes: 5 additions & 16 deletions backend/src/models/category.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,14 @@ package models

import "backend/src/types"

type CategoryName string

const (
ArtsAndDesign CategoryName = "artsAndDesign"
Business CategoryName = "business"
Cultural CategoryName = "cultural"
EquityAndInclusion CategoryName = "equityAndInclusion"
PoliticalAndLaw CategoryName = "politicalAndLaw"
Religious CategoryName = "religious"
Social CategoryName = "social"
Sports CategoryName = "sports"
Sciences CategoryName = "sciences"
ComputerScienceAndEngineering CategoryName = "computerScienceAndEngineering"
)

type Category struct {
types.Model

Name CategoryName `gorm:"type:varchar(255)" json:"category_name" validate:"required"`
Name string `gorm:"type:varchar(255)" json:"category_name" validate:"required"`

Tag []Tag `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
}

type CreateCategoryRequestBody struct {
Name string `gorm:"type:varchar(255)" json:"category_name" validate:"required"`
}
10 changes: 10 additions & 0 deletions backend/src/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ func Init(db *gorm.DB) *fiber.App {
utilityRoutes(app)

apiv1 := app.Group("/api/v1")

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

return app
}
Expand Down Expand Up @@ -63,3 +65,11 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface)

users.Get("/", userController.GetAllUsers)
}

func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) {
categoryController := controllers.NewCategoryController(categoryService)

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

categories.Post("/", categoryController.CreateCategory)
}
30 changes: 30 additions & 0 deletions backend/src/services/category.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package services

import (
"backend/src/models"
"backend/src/transactions"
"backend/src/utilities"

"github.com/gofiber/fiber/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gorm.io/gorm"
)

type CategoryServiceInterface interface {
CreateCategory(category models.Category) (*models.Category, error)
}

type CategoryService struct {
DB *gorm.DB
}

func (c *CategoryService) CreateCategory(category models.Category) (*models.Category, error) {
if err := utilities.ValidateData(category); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data")
}

category.Name = cases.Title(language.English).String(category.Name)

return transactions.CreateCategory(c.DB, category)
}
41 changes: 41 additions & 0 deletions backend/src/transactions/category.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package transactions

import (
"backend/src/models"
"errors"

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

func CreateCategory(db *gorm.DB, category models.Category) (*models.Category, error) {
var existingCategory models.Category

if err := db.Where("name = ?", category.Name).First(&existingCategory).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create category")
}
} else {
return nil, fiber.NewError(fiber.StatusBadRequest, "category with that name already exists")
}

if err := db.Create(&category).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create category")
}

return &category, nil
}

func GetCategory(db *gorm.DB, id uint) (*models.Category, error) {
var category models.Category

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

return &category, nil
}
10 changes: 10 additions & 0 deletions backend/src/utilities/contains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package utilities

func Contains[T comparable](slice []T, element T) bool {
for _, v := range slice {
if v == element {
return true
}
}
return false
}
126 changes: 126 additions & 0 deletions backend/tests/api/category_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package tests

import (
"backend/src/models"
"backend/src/transactions"
"io"
"testing"

"github.com/goccy/go-json"
)

func TestCreateCategoryWorks(t *testing.T) {
app, assert, resp := RequestTesterWithJSONBody(t, "POST", "/api/v1/categories/", &map[string]interface{}{
"category_name": "Science",
}, nil, nil, nil)
defer app.DropDB()

assert.Equal(201, resp.StatusCode)

var respCategory models.Category

err := json.NewDecoder(resp.Body).Decode(&respCategory)

assert.NilError(err)

dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID)

assert.NilError(err)

assert.Equal(dbCategory, &respCategory)
}

func TestCreateCategoryIgnoresid(t *testing.T) {
app, assert, resp := RequestTesterWithJSONBody(t, "POST", "/api/v1/categories/", &map[string]interface{}{
"id": 12,
"category_name": "Science",
}, nil, nil, nil)
defer app.DropDB()

var respCategory models.Category

err := json.NewDecoder(resp.Body).Decode(&respCategory)

assert.NilError(err)

dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID)

assert.NilError(err)

assert.NotEqual(12, dbCategory.ID)
}

func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) {
app, assert, resp := RequestTesterWithJSONBody(t, "POST", "/api/v1/categories/", &map[string]interface{}{
"category_name": 1231,
}, nil, nil, nil)
defer app.DropDB()

defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)

assert.NilError(err)

msg := string(bodyBytes)

assert.Equal("failed to process the request", msg)

assert.Equal(400, resp.StatusCode)
}

func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) {
app, assert, resp := RequestTesterWithJSONBody(t, "POST", "/api/v1/categories/", &map[string]interface{}{}, nil, nil, nil)
defer app.DropDB()

defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)

assert.NilError(err)

msg := string(bodyBytes)

assert.Equal("failed to validate the data", msg)

assert.Equal(400, resp.StatusCode)
}

func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) {
categoryName := "Science"
app, assert, resp := RequestTesterWithJSONBody(t, "POST", "/api/v1/categories/", &map[string]interface{}{
"category_name": categoryName,
}, nil, nil, nil)

assert.Equal(201, resp.StatusCode)

var respCategory models.Category

err := json.NewDecoder(resp.Body).Decode(&respCategory)

assert.NilError(err)

dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID)

assert.NilError(err)

assert.Equal(dbCategory, &respCategory)

for _, permutation := range AllCasingPermutations(categoryName) {
_, _, resp = RequestTesterWithJSONBody(t, "POST", "/api/v1/categories/", &map[string]interface{}{
"category_name": permutation,
}, nil, &app, assert)

defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)

assert.NilError(err)

msg := string(bodyBytes)

assert.Equal("category with that name already exists", msg)

assert.Equal(400, resp.StatusCode)
}
}
2 changes: 1 addition & 1 deletion backend/tests/api/health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

func TestHealthWorks(t *testing.T) {
app, assert, resp := RequestTester(t, "GET", "/health", nil, nil)
app, assert, resp := RequestTester(t, "GET", "/health", nil, nil, nil, nil)

defer app.DropDB()

Expand Down
Loading

0 comments on commit 0472697

Please sign in to comment.