From 91dea77351c5c1262983e6d804bf1dbfb2546d1a Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:13:14 -0500 Subject: [PATCH 01/12] Argon2 Password Hashing (#37) --- backend/go.mod | 2 +- backend/src/auth/password.go | 119 +++++++++++++++++++++++++++++++++++ backend/src/config/config.go | 4 +- backend/src/database/db.go | 9 ++- backend/src/models/user.go | 2 +- 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 backend/src/auth/password.go diff --git a/backend/go.mod b/backend/go.mod index 2273d3c0c..2730f8f61 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -61,7 +61,7 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.17.0 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 diff --git a/backend/src/auth/password.go b/backend/src/auth/password.go new file mode 100644 index 000000000..109980d02 --- /dev/null +++ b/backend/src/auth/password.go @@ -0,0 +1,119 @@ +package auth + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +type params struct { + memory uint32 + iterations uint32 + parallelism uint8 + saltLength uint32 + keyLength uint32 +} + +func ComputePasswordHash(password string) (*string, error) { + p := ¶ms{ + memory: 64 * 1024, + iterations: 3, + parallelism: 2, + saltLength: 16, + keyLength: 32, + } + + salt := make([]byte, p.saltLength) + + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + hash := argon2.IDKey([]byte(password), + salt, + p.iterations, + p.memory, + p.parallelism, + p.keyLength, + ) + + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + + encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p.parallelism, b64Salt, b64Hash) + + return &encodedHash, nil +} + +var ( + ErrInvalidHash = errors.New("the encoded hash is not in the correct format") + ErrIncompatibleVersion = errors.New("incompatible version of argon2") +) + +func ComparePasswordAndHash(password, encodedHash string) (bool, error) { + p, salt, hash, err := decodeHash(encodedHash) + + if err != nil { + return false, err + } + + otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) + + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return true, nil + } + + return false, nil +} + +func decodeHash(encodedHash string) (p *params, salt []byte, hash []byte, err error) { + vals := strings.Split(encodedHash, "$") + + if len(vals) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + var version int + + _, err = fmt.Sscanf(vals[2], "v=%d", &version) + + if err != nil { + return nil, nil, nil, err + } + + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + p = ¶ms{} + + _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism) + + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4]) + + if err != nil { + return nil, nil, nil, err + } + + p.saltLength = uint32(len(salt)) + + hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) + + if err != nil { + return nil, nil, nil, err + } + + p.keyLength = uint32(len(hash)) + + return p, salt, hash, nil +} diff --git a/backend/src/config/config.go b/backend/src/config/config.go index 62197e9bc..9b3f85c67 100644 --- a/backend/src/config/config.go +++ b/backend/src/config/config.go @@ -116,7 +116,7 @@ func GetConfiguration(path string) (Settings, error) { superUserPrefix := fmt.Sprintf("%sSUPERUSER__", appPrefix) portStr := os.Getenv(fmt.Sprintf("%sPORT", appPrefix)) - portInt, err := strconv.Atoi(portStr) + portInt, err := strconv.ParseUint(portStr, 10, 16) if err != nil { return Settings{}, fmt.Errorf("failed to parse port: %w", err) @@ -124,7 +124,7 @@ func GetConfiguration(path string) (Settings, error) { return Settings{ Application: ApplicationSettings{ - Port: prodSettings.Application.Port, + Port: uint16(portInt), Host: prodSettings.Application.Host, BaseUrl: os.Getenv(fmt.Sprintf("%sBASE_URL", applicationPrefix)), }, diff --git a/backend/src/database/db.go b/backend/src/database/db.go index 7e86de0cf..975c6b57e 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -1,6 +1,7 @@ package database import ( + "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/config" "github.com/GenerateNU/sac/backend/src/models" @@ -61,11 +62,17 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error { return err } + passwordHash, err := auth.ComputePasswordHash(settings.SuperUser.Password) + + if err != nil { + return err + } + superUser := models.User{ Role: models.Super, NUID: "000000000", Email: "generatesac@gmail.com", - PasswordHash: settings.SuperUser.Password, // TODO: hash this + PasswordHash: *passwordHash, FirstName: "SAC", LastName: "Super", College: models.KCCS, diff --git a/backend/src/models/user.go b/backend/src/models/user.go index 792fb8181..9bf454057 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -43,7 +43,7 @@ type User struct { FirstName string `gorm:"type:varchar(255)" json:"first_name" validate:"required,max=255"` LastName string `gorm:"type:varchar(255)" json:"last_name" validate:"required,max=255"` Email string `gorm:"type:varchar(255);unique" json:"email" validate:"required,email,max=255"` - PasswordHash string `gorm:"type:text" json:"-" validate:"required"` + PasswordHash string `gorm:"type:varchar(97)" json:"-" validate:"required,len=97"` College College `gorm:"type:varchar(255)" json:"college" validate:"required,max=255"` Year Year `gorm:"type:smallint" json:"year" validate:"required,min=1,max=6"` From cde14a82b00314bcdd7f164e7e1f51728df778a9 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sat, 20 Jan 2024 23:53:30 -0500 Subject: [PATCH 02/12] SAC-13 Create Tag POST (#9) --- backend/src/controllers/tag.go | 45 ++++++++++++ backend/src/database/db.go | 14 +++- backend/src/models/tag.go | 7 +- backend/src/server/server.go | 9 +++ backend/src/services/tag.go | 30 ++++++++ backend/src/transactions/tag.go | 31 ++++++++ backend/tests/api/category_test.go | 61 +++++++++++----- backend/tests/api/helpers.go | 46 ++++-------- backend/tests/api/tag_test.go | 109 +++++++++++++++++++++++++++++ 9 files changed, 296 insertions(+), 56 deletions(-) create mode 100644 backend/src/controllers/tag.go create mode 100644 backend/src/services/tag.go create mode 100644 backend/src/transactions/tag.go create mode 100644 backend/tests/api/tag_test.go diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go new file mode 100644 index 000000000..3d47bdac0 --- /dev/null +++ b/backend/src/controllers/tag.go @@ -0,0 +1,45 @@ +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) +} diff --git a/backend/src/database/db.go b/backend/src/database/db.go index 975c6b57e..d0159feeb 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -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 @@ -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 } diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index 25c26b25b..bcb48a96d 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -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"` + CategoryID uint `json:"category_id" validate:"required"` +} diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 2a23e9a0f..7711bf788 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -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 } @@ -73,3 +74,11 @@ 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) +} diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go new file mode 100644 index 000000000..a6093bdb5 --- /dev/null +++ b/backend/src/services/tag.go @@ -0,0 +1,30 @@ +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) +} + +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) +} diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go new file mode 100644 index 000000000..ec261aa7e --- /dev/null +++ b/backend/src/transactions/tag.go @@ -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.StatusBadRequest, "invalid tag id") + } else { + return nil, fiber.NewError(fiber.StatusInternalServerError, "unable to retrieve tag") + } + } + + return &tag, nil +} diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index 4119dc3cb..d313d4c30 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -19,7 +19,7 @@ func CreateSampleCategory(t *testing.T, categoryName string) ExistingAppAssert { Body: &map[string]interface{}{ "category_name": categoryName, }, - }.TestOnStatusAndDBKeepDB(t, nil, + }.TestOnStatusAndDB(t, nil, DBTesterWithStatus{ Status: 201, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { @@ -41,8 +41,7 @@ func CreateSampleCategory(t *testing.T, categoryName string) ExistingAppAssert { } func TestCreateCategoryWorks(t *testing.T) { - appAssert := CreateSampleCategory(t, "Science") - appAssert.App.DropDB() + CreateSampleCategory(t, "Science") } func TestCreateCategoryIgnoresid(t *testing.T) { @@ -73,6 +72,20 @@ 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", @@ -80,11 +93,15 @@ func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) { 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) { @@ -92,10 +109,13 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { 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, }, ) } @@ -105,6 +125,10 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { 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{ @@ -113,13 +137,14 @@ 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", + }.TestOnStatusMessageAndDB(t, &existingAppAssert, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "category with that name already exists", + }, + DBTester: TestNumCategoriesRemainsAt1, }, ) } - - existingAppAssert.App.DropDB() } diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 534f00f4e..88570b644 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -116,24 +116,6 @@ func configureDatabase(config config.Settings) (*gorm.DB, error) { return dbWithDB, nil } -func (app TestApp) DropDB() { - db, err := app.Conn.DB() - - if err != nil { - panic(err) - } - - db.Close() - - app.Conn, err = gorm.Open(gormPostgres.Open(app.Settings.Database.WithoutDb()), &gorm.Config{SkipDefaultTransaction: true}) - - if err != nil { - panic(err) - } - - app.Conn.Exec(fmt.Sprintf("DROP DATABASE %s;", app.Settings.Database.DatabaseName)) -} - type ExistingAppAssert struct { App TestApp Assert *assert.A @@ -188,10 +170,7 @@ func (request TestRequest) Test(t *testing.T, existingAppAssert *ExistingAppAsse func (request TestRequest) TestOnStatus(t *testing.T, existingAppAssert *ExistingAppAssert, status int) ExistingAppAssert { appAssert, resp := request.Test(t, existingAppAssert) - app, assert := appAssert.App, appAssert.Assert - if existingAppAssert != nil { - defer app.DropDB() - } + _, assert := appAssert.App, appAssert.Assert assert.Equal(status, resp.StatusCode) @@ -214,12 +193,6 @@ type MessageWithStatus struct { } func (request TestRequest) TestOnStatusAndMessage(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) ExistingAppAssert { - appAssert := request.TestOnStatusAndMessageKeepDB(t, existingAppAssert, messagedStatus) - appAssert.App.DropDB() - return appAssert -} - -func (request TestRequest) TestOnStatusAndMessageKeepDB(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) ExistingAppAssert { appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) assert := appAssert.Assert @@ -238,6 +211,17 @@ 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) ExistingAppAssert { + appAssert := request.TestOnStatusAndMessage(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 { @@ -246,12 +230,6 @@ type DBTesterWithStatus struct { } func (request TestRequest) TestOnStatusAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert { - appAssert := request.TestOnStatusAndDBKeepDB(t, existingAppAssert, dbTesterStatus) - appAssert.App.DropDB() - return appAssert -} - -func (request TestRequest) TestOnStatusAndDBKeepDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert { appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) app, assert := appAssert.App, appAssert.Assert defer resp.Body.Close() diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go new file mode 100644 index 000000000..5a0ab27c1 --- /dev/null +++ b/backend/tests/api/tag_test.go @@ -0,0 +1,109 @@ +package tests + +import ( + "net/http" + "testing" + + "github.com/GenerateNU/sac/backend/src/models" + "github.com/GenerateNU/sac/backend/src/transactions" + "github.com/huandu/go-assert" + + "github.com/goccy/go-json" +) + +func TestCreateTagWorks(t *testing.T) { + existingAppAssert := CreateSampleCategory(t, "Science") + TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &map[string]interface{}{ + "name": "Generate", + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 201, + 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) + }, + }, + ) +} + +var AssertNoTagCreation = func(app TestApp, assert *assert.A, resp *http.Response) { + var tags []models.Tag + + err := app.Conn.Find(&tags).Error + + assert.NilError(err) + + assert.Equal(0, len(tags)) +} + +func TestCreateTagFailsBadRequest(t *testing.T) { + badBodys := []map[string]interface{}{ + { + "name": "Generate", + "category_id": "1", + }, + { + "name": 1, + "category_id": 1, + }, + } + + for _, badBody := range badBodys { + TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &badBody, + }.TestOnStatusMessageAndDB(t, nil, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "failed to process the request", + }, + DBTester: AssertNoTagCreation, + }, + ) + } +} + +func TestCreateTagFailsValidation(t *testing.T) { + badBodys := []map[string]interface{}{ + { + "name": "Generate", + }, + { + "category_id": 1, + }, + {}, + } + + for _, badBody := range badBodys { + TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &badBody, + }.TestOnStatusMessageAndDB(t, nil, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "failed to validate the data", + }, + DBTester: AssertNoTagCreation, + }, + ) + } + +} From d71fcce6c4b15908f761f1810e538c6e816cd7e1 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:06:33 -0500 Subject: [PATCH 03/12] SAC-15 Retrieve Tag GET (#33) --- backend/src/controllers/tag.go | 23 +++++++++ backend/src/models/tag.go | 4 +- backend/src/server/server.go | 1 + backend/src/services/tag.go | 19 +++++-- backend/src/transactions/tag.go | 4 +- backend/src/utilities/validator.go | 21 ++++++++ backend/tests/api/category_test.go | 10 ++-- backend/tests/api/helpers.go | 4 +- backend/tests/api/tag_test.go | 80 ++++++++++++++++++++++++++++-- scripts/clean_old_test_dbs.sh | 17 +++++++ scripts/clean_prefixed_test_dbs.sh | 17 +++++++ 11 files changed, 181 insertions(+), 19 deletions(-) create mode 100755 scripts/clean_old_test_dbs.sh create mode 100755 scripts/clean_prefixed_test_dbs.sh 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 From 77ddd0bb510007eeabd7344e93aad76b3e913445 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:13:32 -0500 Subject: [PATCH 04/12] SAC-16 Delete Tag DELETE (#34) --- backend/src/controllers/tag.go | 22 +++++++++++++ backend/src/database/db.go | 1 + backend/src/server/server.go | 1 + backend/src/services/tag.go | 11 +++++++ backend/src/transactions/tag.go | 12 +++++++ backend/tests/api/helpers.go | 3 +- backend/tests/api/tag_test.go | 56 +++++++++++++++++++++++++++++---- 7 files changed, 98 insertions(+), 8 deletions(-) diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index 15ef076d6..58e55afc8 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -66,3 +66,25 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(&tag) } + +// DeleteTag godoc +// +// @Summary Deletes a tag +// @Description Deletes a tag +// @ID delete-tag +// @Tags tag +// @Param id path int true "Tag ID" +// @Success 204 {string} string "No Content" +// @Failure 400 {string} string "failed to validate id" +// @Failure 404 {string} string "failed to find tag" +// @Failure 500 {string} string "failed to delete tag" +// @Router /api/v1/tags/{id} [delete] +func (t *TagController) DeleteTag(c *fiber.Ctx) error { + err := t.tagService.DeleteTag(c.Params("id")) + + if err != nil { + return err + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/src/database/db.go b/backend/src/database/db.go index d0159feeb..182074ade 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -56,6 +56,7 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error { return err } + tx := db.Begin() if err := tx.Error; err != nil { diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 3f8682d8d..4f02f90a3 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -82,4 +82,5 @@ func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) { tags.Post("/", tagController.CreateTag) tags.Get("/:id", tagController.GetTag) + tags.Delete("/:id", tagController.DeleteTag) } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index 7a741b994..f76309ec7 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -11,6 +11,7 @@ import ( type TagServiceInterface interface { CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) GetTag(id string) (*models.Tag, error) + DeleteTag(id string) error } type TagService struct { @@ -39,3 +40,13 @@ func (t *TagService) GetTag(id string) (*models.Tag, error) { return transactions.GetTag(t.DB, *idAsUint) } + +func (t *TagService) DeleteTag(id string) error { + idAsUint, err := utilities.ValidateID(id) + + if err != nil { + return err + } + + return transactions.DeleteTag(t.DB, *idAsUint) +} diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index ea844ecb5..6f033babe 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -29,3 +29,15 @@ func GetTag(db *gorm.DB, id uint) (*models.Tag, error) { return &tag, nil } + +func DeleteTag(db *gorm.DB, id uint) error { + if result := db.Delete(&models.Tag{}, id); result.RowsAffected == 0 { + if result.Error != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to delete tag") + } else { + return fiber.NewError(fiber.StatusNotFound, "failed to find tag") + } + } + + return nil +} diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index eecc1cd53..0d552d933 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -86,7 +86,7 @@ func generateRandomDBName() string { result[i] = letterBytes[generateRandomInt(int64(len(letterBytes)))] } - return string(result) + return fmt.Sprintf("%s%s", prefix, string(result)) } func configureDatabase(config config.Settings) (*gorm.DB, error) { @@ -230,7 +230,6 @@ 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 d119f32e1..61675eb1a 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -47,11 +47,8 @@ func CreateSampleTag(t *testing.T, tagName string, categoryName string, existing ) } -func TestCreateTagWorks(t *testing.T) { - CreateSampleTag(t, "Generate", "Science", nil) -} +var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { -var AssertNoTagCreation = func(app TestApp, assert *assert.A, resp *http.Response) { var tags []models.Tag err := app.Conn.Find(&tags).Error @@ -84,7 +81,7 @@ func TestCreateTagFailsBadRequest(t *testing.T) { Status: 400, Message: "failed to process the request", }, - DBTester: AssertNoTagCreation, + DBTester: AssertNoTags, }, ) } @@ -112,7 +109,7 @@ func TestCreateTagFailsValidation(t *testing.T) { Status: 400, Message: "failed to validate the data", }, - DBTester: AssertNoTagCreation, + DBTester: AssertNoTags, }, ) } @@ -177,3 +174,50 @@ func TestGetTagFailsNotFound(t *testing.T) { }, ) } +func TestDeleteTagWorks(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + + TestRequest{ + Method: "DELETE", + Path: "/api/v1/tags/1", + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 204, + DBTester: AssertNoTags, + }, + ) +} + +func TestDeleteTagFailsBadRequest(t *testing.T) { + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: "DELETE", + Path: fmt.Sprintf("/api/v1/tags/%s", badRequest), + }.TestOnStatusAndMessage(t, nil, + MessageWithStatus{ + Status: 400, + Message: "failed to validate id", + }, + ) + } +} + +func TestDeleteTagFailsNotFound(t *testing.T) { + TestRequest{ + Method: "DELETE", + Path: "/api/v1/tags/1", + }.TestOnStatusAndMessage(t, nil, + MessageWithStatus{ + Status: 404, + Message: "failed to find tag", + }, + ) +} From 62e629381ddee1322d6d0e2b7d43e5191011af74 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 21 Jan 2024 11:55:06 -0500 Subject: [PATCH 05/12] SAC-14 Update Tag Patch (#35) --- backend/src/controllers/tag.go | 34 ++++++- backend/src/models/tag.go | 12 ++- backend/src/server/server.go | 1 + backend/src/services/tag.go | 22 ++++- backend/src/transactions/tag.go | 13 +++ backend/src/utilities/validator.go | 1 + backend/tests/api/category_test.go | 8 +- backend/tests/api/tag_test.go | 146 ++++++++++++++++++++++------- 8 files changed, 193 insertions(+), 44 deletions(-) diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index 58e55afc8..fda5186f9 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -67,6 +67,38 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(&tag) } +// UpdateTag godoc +// +// @Summary Updates a tag +// @Description Updates a tag +// @ID update-tag +// @Tags tag +// @Accept json +// @Produce json +// @Param id path int true "Tag ID" +// @Success 200 {object} models.Tag +// @Failure 400 {string} string "failed to process the request" +// @Failure 400 {string} string "failed to validate id" +// @Failure 400 {string} string "failed to validate the data" +// @Failure 404 {string} string "failed to find tag" +// @Failure 500 {string} string "failed to update tag" +// @Router /api/v1/tags/{id} [patch] +func (t *TagController) UpdateTag(c *fiber.Ctx) error { + var tagBody models.UpdateTagRequestBody + + if err := c.BodyParser(&tagBody); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") + } + + tag, err := t.tagService.UpdateTag(c.Params("id"), tagBody) + + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(&tag) +} + // DeleteTag godoc // // @Summary Deletes a tag @@ -74,7 +106,7 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { // @ID delete-tag // @Tags tag // @Param id path int true "Tag ID" -// @Success 204 {string} string "No Content" +// @Success 204 // @Failure 400 {string} string "failed to validate id" // @Failure 404 {string} string "failed to find tag" // @Failure 500 {string} string "failed to delete tag" diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index d515fb692..767b3d37b 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -9,14 +9,22 @@ type Tag struct { Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"` - CategoryID uint `gorm:"foreignKey:CategoryID" json:"category_id" validate:"required"` + CategoryID uint `gorm:"foreignKey:CategoryID" json:"category_id" validate:"required,min=1"` 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 { +type PartialTag struct { Name string `json:"name" validate:"required,max=255"` CategoryID uint `json:"category_id" validate:"required,min=1"` } + +type CreateTagRequestBody struct { + PartialTag +} + +type UpdateTagRequestBody struct { + PartialTag +} diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 4f02f90a3..a59011369 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -82,5 +82,6 @@ func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) { tags.Post("/", tagController.CreateTag) tags.Get("/:id", tagController.GetTag) + tags.Patch("/:id", tagController.UpdateTag) tags.Delete("/:id", tagController.DeleteTag) } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index f76309ec7..dac2203b3 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -11,7 +11,8 @@ import ( type TagServiceInterface interface { CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) GetTag(id string) (*models.Tag, error) - DeleteTag(id string) error + UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, error) + DeleteTag(id string) error } type TagService struct { @@ -41,6 +42,25 @@ func (t *TagService) GetTag(id string) (*models.Tag, error) { return transactions.GetTag(t.DB, *idAsUint) } +func (t *TagService) UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, error) { + idAsUint, err := utilities.ValidateID(id) + + if err != nil { + return nil, err + } + + tag := models.Tag{ + Name: tagBody.Name, + CategoryID: tagBody.CategoryID, + } + + if err := utilities.ValidateData(tag); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") + } + + return transactions.UpdateTag(t.DB, *idAsUint, tag) +} + func (t *TagService) DeleteTag(id string) error { idAsUint, err := utilities.ValidateID(id) diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index 6f033babe..8ba91210a 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -30,6 +30,19 @@ func GetTag(db *gorm.DB, id uint) (*models.Tag, error) { return &tag, nil } +func UpdateTag(db *gorm.DB, id uint, tag models.Tag) (*models.Tag, error) { + if err := db.Model(&models.Tag{}).Where("id = ?", id).Updates(tag).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 update tag") + } + } + + return &tag, nil + +} + func DeleteTag(db *gorm.DB, id uint) error { if result := db.Delete(&models.Tag{}, id); result.RowsAffected == 0 { if result.Error != nil { diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index a161587c6..07ef87d87 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -17,6 +17,7 @@ func ValidateData(model interface{}) error { return nil } +// Validates that an id follows postgres uint format, returns a uint otherwise returns an error func ValidateID(id string) (*uint, error) { idAsInt, err := strconv.Atoi(id) diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index a43039a8c..83e6e0b2d 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -1,7 +1,6 @@ package tests import ( - "fmt" "net/http" "testing" @@ -72,7 +71,7 @@ func TestCreateCategoryIgnoresid(t *testing.T) { ) } -func AssertNoCategoryCreation(app TestApp, assert *assert.A, resp *http.Response) { +func AssertNoCategories(app TestApp, assert *assert.A, resp *http.Response) { AssertNumCategoriesRemainsAtN(app, assert, resp, 0) } @@ -99,7 +98,7 @@ func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) { Status: 400, Message: "failed to process the request", }, - DBTester: AssertNoCategoryCreation, + DBTester: AssertNoCategories, }, ) } @@ -115,7 +114,7 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { Status: 400, Message: "failed to validate the data", }, - DBTester: AssertNoCategoryCreation, + DBTester: AssertNoCategories, }, ) } @@ -130,7 +129,6 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { } for _, permutation := range AllCasingPermutations(categoryName) { - fmt.Println(permutation) TestRequest{ Method: "POST", Path: "/api/v1/categories/", diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 61675eb1a..2b35f7820 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -12,14 +12,26 @@ import ( "github.com/goccy/go-json" ) +var AssertRespTagSameAsDBTag = func(app TestApp, assert *assert.A, resp *http.Response) { + var respTag models.Tag + + err := json.NewDecoder(resp.Body).Decode(&respTag) + + assert.NilError(err) + + fmt.Printf("respTag: %+v\n", respTag) + fmt.Printf("respTag.ID: %+v\n", respTag.ID) + + dbTag, err := transactions.GetTag(app.Conn, respTag.ID) + + assert.NilError(err) + + assert.Equal(dbTag, &respTag) +} + func CreateSampleTag(t *testing.T, tagName string, categoryName string, existingAppAssert *ExistingAppAssert) ExistingAppAssert { - var appAssert ExistingAppAssert + appAssert := CreateSampleCategory(t, categoryName, existingAppAssert) - if existingAppAssert == nil { - appAssert = CreateSampleCategory(t, categoryName, existingAppAssert) - } else { - appAssert = CreateSampleCategory(t, categoryName, existingAppAssert) - } return TestRequest{ Method: "POST", Path: "/api/v1/tags/", @@ -29,26 +41,17 @@ func CreateSampleTag(t *testing.T, tagName string, categoryName string, existing }, }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ - Status: 201, - 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) - }, + Status: 201, + DBTester: AssertRespTagSameAsDBTag, }, ) } -var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { +func TestCreateTagWorks(t *testing.T) { + CreateSampleTag(t, "Generate", "Science", nil) +} +var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { var tags []models.Tag err := app.Conn.Find(&tags).Error @@ -123,20 +126,8 @@ func TestGetTagWorks(t *testing.T) { 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) - }, + Status: 200, + DBTester: AssertRespTagSameAsDBTag, }, ) } @@ -174,6 +165,91 @@ func TestGetTagFailsNotFound(t *testing.T) { }, ) } + +func TestUpdateTagWorksUpdateName(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &map[string]interface{}{ + "name": "GenerateNU", + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +} + +func TestUpdateTagWorksUpdateCategory(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + existingAppAssert = CreateSampleCategory(t, "Technology", &existingAppAssert) + + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &map[string]interface{}{ + "name": "Generate", + "category_id": 2, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +} + +func TestUpdateTagWorksWithSameDetails(t *testing.T) { + existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) + + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &map[string]interface{}{ + "name": "Generate", + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &existingAppAssert, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +} + +func TestUpdateTagFailsBadRequest(t *testing.T) { + badBodys := []map[string]interface{}{ + { + "name": "Generate", + "category_id": "1", + }, + { + "name": 1, + "category_id": 1, + }, + } + + for _, badBody := range badBodys { + TestRequest{ + Method: "PATCH", + Path: "/api/v1/tags/1", + Body: &badBody, + }.TestOnStatusMessageAndDB(t, nil, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "failed to process the request", + }, + DBTester: AssertNoTags, + }, + ) + } +} + func TestDeleteTagWorks(t *testing.T) { existingAppAssert := CreateSampleTag(t, "Generate", "Science", nil) From 4ca58e64161b7570be5a3a27a87e831f47c7e989 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:34:41 -0500 Subject: [PATCH 06/12] Testing Documentation | Testing API Clean Up (#39) --- backend/src/controllers/category.go | 3 + backend/tests/api/README.md | 140 ++++++++++++++++++++++++++++ backend/tests/api/category_test.go | 47 ++++------ backend/tests/api/helpers.go | 32 +++---- backend/tests/api/tag_test.go | 3 - 5 files changed, 176 insertions(+), 49 deletions(-) create mode 100644 backend/tests/api/README.md diff --git a/backend/src/controllers/category.go b/backend/src/controllers/category.go index 321b83621..02ad115bf 100644 --- a/backend/src/controllers/category.go +++ b/backend/src/controllers/category.go @@ -1,6 +1,8 @@ package controllers import ( + "fmt" + "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" @@ -32,6 +34,7 @@ func (t *CategoryController) CreateCategory(c *fiber.Ctx) error { var categoryBody models.CreateCategoryRequestBody if err := c.BodyParser(&categoryBody); err != nil { + fmt.Print(err) return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") } diff --git a/backend/tests/api/README.md b/backend/tests/api/README.md new file mode 100644 index 000000000..6621648c3 --- /dev/null +++ b/backend/tests/api/README.md @@ -0,0 +1,140 @@ +# Using the Integration Testing Helpers + +The integration testing helpers are a set of functions that reduce the boilerplate code required to write integration tests. They are located in the `backend/tests/helpers.go`. + +## Modeling a Request with `TestRequest` + +You can model a request with the `TestRequest` struct: + +```go +type TestRequest struct { + Method string + Path string + Body *map[string]interface{} + Headers *map[string]string +} +``` + +Since `Body` and `Headers` are pointers, if they don't set them when creating a `TestRequest`, they will be `nil`. + +Here is an example of creating a `TestRequest`, notice how instead of saying `Headers: nil`, we can simply omit the `Headers` field. + +```go +TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &map[string]interface{}{ + "name": tagName, + "category_id": 1, + }, +} +``` + +This handles a lot of the logic for you, for example, if the body is not nil, it will be marshalled into JSON and the `Content-Type` header will be set to `application/json`. + +## Testing that a Request Returns a XXX Status Code + +Say you want to test hitting the `[APP_ADDRESS]/health` endpoint with a GET request returns a `200` status code. + +```go +TestRequest{ + Method: "GET", + Path: "/health", + }.TestOnStatus(t, nil, 200) +``` + +## Testing that a Request Returns a XXX Status Code and Assert Something About the Database + +Say you want to test that a creating a catgory with POST `[APP_ADDRESS]/api/v1/categories/` returns a `201` + +```go +TestRequest{ + Method: "POST", + Path: "/api/v1/categories/", + Body: &map[string]interface{}{ + "category_name": categoryName, + }, + }.TestOnStatusAndDB(t, nil, + DBTesterWithStatus{ + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, + }, + ) +``` + +### DBTesters + +Often times there are common assertions you want to make about the database, for example, if the object in the response is the same as the object in the database. We can create a lambda function that takes in the `TestApp`, `*assert.A`, and `*http.Response` and makes the assertions we want. We can then pass this function to the `DBTesterWithStatus` struct. + +```go +var AssertRespCategorySameAsDBCategory = func(app TestApp, assert *assert.A, resp *http.Response) { + 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) +} +``` + +### Existing App Asserts + +Since the test suite creates a new database for each test, we can have a deterministic database state for each test. However, what if we have a multi step test that depends on the previous steps database state? That is where `ExistingAppAssert` comes in! This will allow us to keep using the database from a previous step in the test. + +Consider this example, to create a tag, we need to create a category first. This is a multi step test, so we need to use `ExistingAppAssert` to keep the database state from the previous step. + +```go +appAssert := TestRequest{ + Method: "POST", + Path: "/api/v1/categories/", + Body: &map[string]interface{}{ + "category_name": categoryName, + }, + }.TestOnStatusAndDB(t, nil, + DBTesterWithStatus{ + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, + }, + ) + +TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &map[string]interface{}{ + "name": tagName, + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: 201, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +``` + +## Testing that a Request Returns a XXX Status Code, Assert Something About the Message, and Assert Something About the Database + +Say you want to test a bad request to POST `[APP_ADDRESS]/api/v1/categories/` endpoint returns a `400` status code, the message is `failed to process the request`, and that a category was not created. + +```go +TestRequest{ + Method: "POST", + Path: "/api/v1/categories/", + Body: &map[string]interface{}{ + "category_name": 1231, + }, + }.TestOnStatusMessageAndDB(t, nil, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "failed to process the request", + }, + DBTester: AssertNoCategories, + }, + ) +``` diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index 83e6e0b2d..6a11fa7a3 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -11,6 +11,20 @@ import ( "github.com/goccy/go-json" ) +var AssertRespCategorySameAsDBCategory = func(app TestApp, assert *assert.A, resp *http.Response) { + 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 CreateSampleCategory(t *testing.T, categoryName string, existingAppAssert *ExistingAppAssert) ExistingAppAssert { return TestRequest{ Method: "POST", @@ -20,21 +34,8 @@ func CreateSampleCategory(t *testing.T, categoryName string, existingAppAssert * }, }.TestOnStatusAndDB(t, existingAppAssert, DBTesterWithStatus{ - Status: 201, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - - 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) - }, + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, }, ) } @@ -53,20 +54,8 @@ func TestCreateCategoryIgnoresid(t *testing.T) { }, }.TestOnStatusAndDB(t, nil, DBTesterWithStatus{ - Status: 201, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - 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) - }, + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, }, ) } diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 0d552d933..7cd5f131c 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -152,10 +152,18 @@ func (request TestRequest) Test(t *testing.T, existingAppAssert *ExistingAppAsse req = httptest.NewRequest(request.Method, address, bytes.NewBuffer(bodyBytes)) - if request.Headers != nil { - for key, value := range *request.Headers { - req.Header.Set(key, value) - } + if request.Headers == nil { + request.Headers = &map[string]string{} + } + + if _, ok := (*request.Headers)["Content-Type"]; !ok { + (*request.Headers)["Content-Type"] = "application/json" + } + } + + if request.Headers != nil { + for key, value := range *request.Headers { + req.Header.Add(key, value) } } @@ -171,6 +179,7 @@ func (request TestRequest) Test(t *testing.T, existingAppAssert *ExistingAppAsse func (request TestRequest) TestOnStatus(t *testing.T, existingAppAssert *ExistingAppAssert, status int) ExistingAppAssert { appAssert, resp := request.Test(t, existingAppAssert) + _, assert := appAssert.App, appAssert.Assert assert.Equal(status, resp.StatusCode) @@ -178,23 +187,13 @@ func (request TestRequest) TestOnStatus(t *testing.T, existingAppAssert *Existin return appAssert } -func (request TestRequest) TestWithJSONBody(t *testing.T, existingAppAssert *ExistingAppAssert) (ExistingAppAssert, *http.Response) { - if request.Headers == nil { - request.Headers = &map[string]string{"Content-Type": "application/json"} - } else if _, ok := (*request.Headers)["Content-Type"]; !ok { - (*request.Headers)["Content-Type"] = "application/json" - } - - return request.Test(t, existingAppAssert) -} - type MessageWithStatus struct { Status int Message string } func (request TestRequest) TestOnStatusAndMessage(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) ExistingAppAssert { - appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) + appAssert, resp := request.Test(t, existingAppAssert) assert := appAssert.Assert defer resp.Body.Close() @@ -231,9 +230,8 @@ type DBTesterWithStatus struct { } func (request TestRequest) TestOnStatusAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert { - appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) + appAssert, resp := request.Test(t, existingAppAssert) app, assert := appAssert.App, appAssert.Assert - defer resp.Body.Close() assert.Equal(dbTesterStatus.Status, resp.StatusCode) diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 2b35f7820..169152fb4 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -19,9 +19,6 @@ var AssertRespTagSameAsDBTag = func(app TestApp, assert *assert.A, resp *http.Re assert.NilError(err) - fmt.Printf("respTag: %+v\n", respTag) - fmt.Printf("respTag.ID: %+v\n", respTag.ID) - dbTag, err := transactions.GetTag(app.Conn, respTag.ID) assert.NilError(err) From 5c57959cefc8859b177a5f7906dc1060be1632b3 Mon Sep 17 00:00:00 2001 From: Zackary Lassetter <93090968+zacklassetter@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:04:45 -0500 Subject: [PATCH 07/12] SAC-6 Get User GET (#14) Co-authored-by: edwinliiiii Co-authored-by: edwinliiiii <91173669+edwinliiiii@users.noreply.github.com> Co-authored-by: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Co-authored-by: garrettladley --- .github/workflows/go.yml | 5 +-- backend/src/controllers/user.go | 21 +++++++++ backend/src/models/user.go | 2 +- backend/src/server/server.go | 1 + backend/src/services/user.go | 13 +++++- backend/src/transactions/user.go | 17 +++++++- backend/tests/api/README.md | 12 ++++-- backend/tests/api/category_test.go | 10 +++-- backend/tests/api/health_test.go | 2 +- backend/tests/api/helpers.go | 14 ++++++ backend/tests/api/tag_test.go | 26 +++++------ backend/tests/api/user_test.go | 69 +++++++++++++++++++++++++++++- cli/commands/test.go | 5 +-- 13 files changed, 164 insertions(+), 33 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 34c75bd09..1801e281f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -69,9 +69,8 @@ jobs: - name: Install Dependencies run: cd backend && go get ./... - name: Migrate DB - run: | - cd backend/src && go run main.go --only-migrate + run: cd backend/src && go run main.go --only-migrate - name: Run Tests with Coverage - run: cd backend && go test -race -coverprofile=coverage.txt -covermode=atomic ./... + run: cd backend && go test -failfast -benchmem -race -coverprofile=coverage.txt ./... - name: Print Coverage run: cd backend && go tool cover -func=coverage.txt diff --git a/backend/src/controllers/user.go b/backend/src/controllers/user.go index c23ce0c0c..57e6802be 100644 --- a/backend/src/controllers/user.go +++ b/backend/src/controllers/user.go @@ -33,3 +33,24 @@ func (u *UserController) GetAllUsers(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(users) } + +// GetUser godoc +// +// @Summary Gets specific user +// @Description Returns specific user +// @ID get-user +// @Tags user +// @Produce json +// @Success 200 {object} models.User +// @Failure 400 {string} string "failed to validate id" +// @Failure 404 {string} string "failed to find user" +// @Failure 500 {string} string +// @Router /api/v1/users/ [get] +func (u *UserController) GetUser(c *fiber.Ctx) error { + user, err := u.userService.GetUser(c.Params("id")) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(user) +} diff --git a/backend/src/models/user.go b/backend/src/models/user.go index 9bf454057..f5e4433e6 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -38,7 +38,7 @@ const ( type User struct { types.Model - Role UserRole `gorm:"type:varchar(255);" json:"user_role" validate:"required,max=255"` + Role UserRole `gorm:"type:varchar(255);" json:"user_role,omitempty" validate:"required,max=255"` NUID string `gorm:"column:nuid;type:varchar(9);unique" json:"nuid" validate:"required,numeric,len=9"` FirstName string `gorm:"type:varchar(255)" json:"first_name" validate:"required,max=255"` LastName string `gorm:"type:varchar(255)" json:"last_name" validate:"required,max=255"` diff --git a/backend/src/server/server.go b/backend/src/server/server.go index a59011369..1f6a8e20b 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -65,6 +65,7 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface) users := router.Group("/users") users.Get("/", userController.GetAllUsers) + users.Get("/:id", userController.GetUser) } func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) { diff --git a/backend/src/services/user.go b/backend/src/services/user.go index fe27eb731..8e9e60ac1 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -3,19 +3,30 @@ package services import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" + "github.com/GenerateNU/sac/backend/src/utilities" "gorm.io/gorm" ) type UserServiceInterface interface { GetAllUsers() ([]models.User, error) + GetUser(string) (*models.User, error) } type UserService struct { DB *gorm.DB } -// Gets all users (including soft deleted users) for testing func (u *UserService) GetAllUsers() ([]models.User, error) { return transactions.GetAllUsers(u.DB) } + +func (u *UserService) GetUser(userID string) (*models.User, error) { + idAsUint, err := utilities.ValidateID(userID) + + if err != nil { + return nil, err + } + + return transactions.GetUser(u.DB, *idAsUint) +} diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 3d15f1385..54792b068 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -1,6 +1,8 @@ package transactions import ( + "errors" + "github.com/GenerateNU/sac/backend/src/models" "github.com/gofiber/fiber/v2" @@ -10,9 +12,22 @@ import ( func GetAllUsers(db *gorm.DB) ([]models.User, error) { var users []models.User - if err := db.Unscoped().Omit("password_hash").Find(&users).Error; err != nil { + if err := db.Omit("password_hash").Find(&users).Error; err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to get all users") } return users, nil } + +func GetUser(db *gorm.DB, id uint) (*models.User, error) { + var user models.User + + if err := db.Omit("password_hash").First(&user, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "failed to find tag") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to get user") + } + + return &user, nil +} diff --git a/backend/tests/api/README.md b/backend/tests/api/README.md index 6621648c3..317eb2800 100644 --- a/backend/tests/api/README.md +++ b/backend/tests/api/README.md @@ -40,7 +40,7 @@ Say you want to test hitting the `[APP_ADDRESS]/health` endpoint with a GET requ TestRequest{ Method: "GET", Path: "/health", - }.TestOnStatus(t, nil, 200) + }.TestOnStatus(t, nil, 200).Close() ``` ## Testing that a Request Returns a XXX Status Code and Assert Something About the Database @@ -59,7 +59,7 @@ TestRequest{ Status: 201, DBTester: AssertRespCategorySameAsDBCategory, }, - ) + ).Close() ``` ### DBTesters @@ -114,9 +114,13 @@ TestRequest{ Status: 201, DBTester: AssertRespTagSameAsDBTag, }, - ) + ).Close() ``` +### Why Close? + +This closes the connection to the database. This is important because if you don't close the connection, we will run out of available connections and the tests will fail. **Call this on the last test request of a test** + ## Testing that a Request Returns a XXX Status Code, Assert Something About the Message, and Assert Something About the Database Say you want to test a bad request to POST `[APP_ADDRESS]/api/v1/categories/` endpoint returns a `400` status code, the message is `failed to process the request`, and that a category was not created. @@ -136,5 +140,5 @@ TestRequest{ }, DBTester: AssertNoCategories, }, - ) + ).Close() ``` diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index 6a11fa7a3..5ba99410c 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -41,7 +41,7 @@ func CreateSampleCategory(t *testing.T, categoryName string, existingAppAssert * } func TestCreateCategoryWorks(t *testing.T) { - CreateSampleCategory(t, "Science", nil) + CreateSampleCategory(t, "Science", nil).Close() } func TestCreateCategoryIgnoresid(t *testing.T) { @@ -57,7 +57,7 @@ func TestCreateCategoryIgnoresid(t *testing.T) { Status: 201, DBTester: AssertRespCategorySameAsDBCategory, }, - ) + ).Close() } func AssertNoCategories(app TestApp, assert *assert.A, resp *http.Response) { @@ -89,7 +89,7 @@ func TestCreateCategoryFailsIfNameIsNotString(t *testing.T) { }, DBTester: AssertNoCategories, }, - ) + ).Close() } func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { @@ -105,7 +105,7 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { }, DBTester: AssertNoCategories, }, - ) + ).Close() } func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { @@ -134,4 +134,6 @@ func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { }, ) } + + existingAppAssert.Close() } diff --git a/backend/tests/api/health_test.go b/backend/tests/api/health_test.go index a44d19d2c..71fe536dd 100644 --- a/backend/tests/api/health_test.go +++ b/backend/tests/api/health_test.go @@ -10,5 +10,5 @@ func TestHealthWorks(t *testing.T) { Path: "/health", }.TestOnStatus(t, nil, 200, - ) + ).Close() } diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 7cd5f131c..25d2d5a93 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -122,6 +122,20 @@ type ExistingAppAssert struct { Assert *assert.A } +func (eaa ExistingAppAssert) Close() { + db, err := eaa.App.Conn.DB() + + if err != nil { + panic(err) + } + + err = db.Close() + + if err != nil { + panic(err) + } +} + type TestRequest struct { Method string Path string diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 169152fb4..45c16d94d 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -45,7 +45,7 @@ func CreateSampleTag(t *testing.T, tagName string, categoryName string, existing } func TestCreateTagWorks(t *testing.T) { - CreateSampleTag(t, "Generate", "Science", nil) + CreateSampleTag(t, "Generate", "Science", nil).Close() } var AssertNoTags = func(app TestApp, assert *assert.A, resp *http.Response) { @@ -83,7 +83,7 @@ func TestCreateTagFailsBadRequest(t *testing.T) { }, DBTester: AssertNoTags, }, - ) + ).Close() } } @@ -111,7 +111,7 @@ func TestCreateTagFailsValidation(t *testing.T) { }, DBTester: AssertNoTags, }, - ) + ).Close() } } @@ -126,7 +126,7 @@ func TestGetTagWorks(t *testing.T) { Status: 200, DBTester: AssertRespTagSameAsDBTag, }, - ) + ).Close() } func TestGetTagFailsBadRequest(t *testing.T) { @@ -147,7 +147,7 @@ func TestGetTagFailsBadRequest(t *testing.T) { Status: 400, Message: "failed to validate id", }, - ) + ).Close() } } @@ -160,7 +160,7 @@ func TestGetTagFailsNotFound(t *testing.T) { Status: 404, Message: "failed to find tag", }, - ) + ).Close() } func TestUpdateTagWorksUpdateName(t *testing.T) { @@ -178,7 +178,7 @@ func TestUpdateTagWorksUpdateName(t *testing.T) { Status: 200, DBTester: AssertRespTagSameAsDBTag, }, - ) + ).Close() } func TestUpdateTagWorksUpdateCategory(t *testing.T) { @@ -197,7 +197,7 @@ func TestUpdateTagWorksUpdateCategory(t *testing.T) { Status: 200, DBTester: AssertRespTagSameAsDBTag, }, - ) + ).Close() } func TestUpdateTagWorksWithSameDetails(t *testing.T) { @@ -215,7 +215,7 @@ func TestUpdateTagWorksWithSameDetails(t *testing.T) { Status: 200, DBTester: AssertRespTagSameAsDBTag, }, - ) + ).Close() } func TestUpdateTagFailsBadRequest(t *testing.T) { @@ -243,7 +243,7 @@ func TestUpdateTagFailsBadRequest(t *testing.T) { }, DBTester: AssertNoTags, }, - ) + ).Close() } } @@ -258,7 +258,7 @@ func TestDeleteTagWorks(t *testing.T) { Status: 204, DBTester: AssertNoTags, }, - ) + ).Close() } func TestDeleteTagFailsBadRequest(t *testing.T) { @@ -279,7 +279,7 @@ func TestDeleteTagFailsBadRequest(t *testing.T) { Status: 400, Message: "failed to validate id", }, - ) + ).Close() } } @@ -292,5 +292,5 @@ func TestDeleteTagFailsNotFound(t *testing.T) { Status: 404, Message: "failed to find tag", }, - ) + ).Close() } diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index 8307c0dc9..693d93c8e 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "net/http" "testing" @@ -43,5 +44,71 @@ func TestGetAllUsersWorks(t *testing.T) { assert.Equal(dbUser, respUser) }, }, - ) + ).Close() +} + +var AssertRespUserSameAsDBUser = func(app TestApp, assert *assert.A, resp *http.Response) { + var respUser models.User + + err := json.NewDecoder(resp.Body).Decode(&respUser) + + assert.NilError(err) + + dbUser, err := transactions.GetUser(app.Conn, respUser.ID) + + assert.NilError(err) + + assert.Equal(dbUser.Role, respUser.Role) + assert.Equal(dbUser.NUID, respUser.NUID) + assert.Equal(dbUser.FirstName, respUser.FirstName) + assert.Equal(dbUser.LastName, respUser.LastName) + assert.Equal(dbUser.Email, respUser.Email) + assert.Equal(dbUser.College, respUser.College) + assert.Equal(dbUser.Year, respUser.Year) +} + +func TestGetUserWorks(t *testing.T) { + TestRequest{ + Method: "GET", + Path: "/api/v1/users/1", + }.TestOnStatusAndDB(t, nil, + DBTesterWithStatus{ + Status: 200, + DBTester: AssertRespUserSameAsDBUser, + }, + ).Close() +} + +func TestGetUserFailsBadRequest(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", + }, + ).Close() + } +} + +func TestGetUserFailsNotFound(t *testing.T) { + TestRequest{ + Method: "GET", + Path: "/api/v1/users/69", + }.TestOnStatusAndMessage(t, nil, + MessageWithStatus{ + Status: 404, + Message: "failed to find tag", + }, + ).Close() } diff --git a/cli/commands/test.go b/cli/commands/test.go index a4ea32ac3..65b5fe4ad 100644 --- a/cli/commands/test.go +++ b/cli/commands/test.go @@ -8,7 +8,6 @@ import ( "github.com/urfave/cli/v2" ) - func TestCommand(backendDir string, frontendDir string) *cli.Command { command := cli.Command{ Name: "test", @@ -36,7 +35,7 @@ func TestCommand(backendDir string, frontendDir string) *cli.Command { } fmt.Println("Frontend", c.String("frontend")) - + folder := c.String("frontend") runFrontend := folder != "" runBackend := c.Bool("backend") @@ -50,7 +49,6 @@ func TestCommand(backendDir string, frontendDir string) *cli.Command { return &command } - func Test(backendDir string, frontendDir string, folder string, runFrontend bool, runBackend bool) error { var wg sync.WaitGroup @@ -106,4 +104,3 @@ func FrontendTest(frontendDir string, folder string) error { return nil } - From adbeeff003cb70c32dc65bc2313c7e49f1ab5ff9 Mon Sep 17 00:00:00 2001 From: David Oduneye <44040421+DOOduneye@users.noreply.github.com> Date: Sun, 21 Jan 2024 21:42:04 -0500 Subject: [PATCH 08/12] CLI Updates (#40) Co-authored-by: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> --- .gitignore | 2 +- CONTRIBUTING.md | 42 +- backend/src/database/db.go | 11 + backend/src/docs/docs.go | 726 ++++++++++------------------ backend/src/docs/swagger.json | 726 ++++++++++------------------ backend/src/docs/swagger.yaml | 504 ++++++------------- backend/src/main.go | 10 +- backend/src/models/category.go | 3 +- backend/src/server/server.go | 1 - cli/commands/config.go | 10 + cli/commands/drop.go | 43 ++ cli/commands/lint.go | 2 +- cli/commands/migrate.go | 44 +- cli/commands/reset.go | 29 +- cli/commands/swagger.go | 11 +- cli/commands/test.go | 29 +- cli/main.go | 21 +- cli/utils/path.go | 44 ++ go.work.sum | 13 +- scripts/{reset_db.sh => drop_db.sh} | 0 scripts/insert_db.sh | 16 +- 21 files changed, 880 insertions(+), 1407 deletions(-) create mode 100644 cli/commands/config.go create mode 100644 cli/commands/drop.go create mode 100644 cli/utils/path.go rename scripts/{reset_db.sh => drop_db.sh} (100%) diff --git a/.gitignore b/.gitignore index 807d5c6b6..df5f21020 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .DS_Store .env -sac-cli \ No newline at end of file +sac-cli diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d51af442..28401a7f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,6 +96,39 @@ psql // opens psql shell CREATE DATABASE sac; ``` +# Commands + +### React Native + + ```bash + npx expo start --dev-client // runnning dev client + npx expo start --dev-client --ios // specific platform + yarn format // format code + yarn lint // lint code + yarn test // run tests + ``` + +### Go + + ```bash + go run main.go // run server + go test ./... // run tests + go fmt ./... // format code + go vet ./... // lint code + ``` + +### SAC CLI + + To install use `./install.sh` and then run `sac-cli` to see all commands. + + ```bash + sac-cli migrate // run migrations + sac-cli reset // reset database + sac-cli swagger // generate swagger docs + sac-cli lint // lint code + sac-cli format // format code + sac-cli test // run tests + ``` 4. **Create a user** @@ -124,15 +157,16 @@ go vet ./... // lint code ``` -### Others (WIP) +### Others ```bash sac-cli migrate // run migrations + sac-cli drop // drop database sac-cli reset // reset database sac-cli swagger // generate swagger docs - sac-cli lint // lint code - sac-cli format // format code - sac-cli test // run tests + sac-cli lint // lint code (WIP) + sac-cli format // format code (WIP) + sac-cli test // run tests (WIP) ``` # Git Flow diff --git a/backend/src/database/db.go b/backend/src/database/db.go index 182074ade..4b0fba3ad 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -56,7 +56,18 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error { return err } + // Check if the database already has a super user + var superUser models.User + if err := db.Where("role = ?", models.Super).First(&superUser).Error; err != nil { + if err := createSuperUser(settings, db); err != nil { + return err + } + } + + return nil +} +func createSuperUser(settings config.Settings, db *gorm.DB) error { tx := db.Begin() if err := tx.Error; err != nil { diff --git a/backend/src/docs/docs.go b/backend/src/docs/docs.go index 8e021b92b..574baac3f 100644 --- a/backend/src/docs/docs.go +++ b/backend/src/docs/docs.go @@ -18,6 +18,212 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/v1/category/": { + "post": { + "description": "Creates a category that is used to group tags", + "produces": [ + "application/json" + ], + "tags": [ + "category" + ], + "summary": "Create a category", + "operationId": "create-category", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Category" + } + }, + "400": { + "description": "category with that name already exists", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to create category", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/tags/": { + "post": { + "description": "Creates a tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Creates a tag", + "operationId": "create-tag", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "failed to validate the data", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to create tag", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/tags/{id}": { + "get": { + "description": "Returns a tag", + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Gets a tag", + "operationId": "get-tag", + "parameters": [ + { + "type": "integer", + "description": "Tag ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "failed to validate id", + "schema": { + "type": "string" + } + }, + "404": { + "description": "faied to find tag", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to retrieve tag", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Deletes a tag", + "tags": [ + "tag" + ], + "summary": "Deletes a tag", + "operationId": "delete-tag", + "parameters": [ + { + "type": "integer", + "description": "Tag ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "failed to validate id", + "schema": { + "type": "string" + } + }, + "404": { + "description": "failed to find tag", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to delete tag", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "description": "Updates a tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Updates a tag", + "operationId": "update-tag", + "parameters": [ + { + "type": "integer", + "description": "Tag ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "failed to validate the data", + "schema": { + "type": "string" + } + }, + "404": { + "description": "failed to find tag", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to update tag", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/users/": { "get": { "description": "Returns all users", @@ -40,7 +246,7 @@ const docTemplate = `{ } }, "500": { - "description": "Failed to fetch users", + "description": "failed to get all users", "schema": { "type": "string" } @@ -50,118 +256,24 @@ const docTemplate = `{ } }, "definitions": { - "models.Club": { + "models.Category": { "type": "object", "required": [ - "application_link", - "club_members", - "contacts", - "description", - "is_recruiting", - "name", - "num_members", - "point_of_contacts", - "preview", - "recruitment_cycle", - "recruitment_type" + "category_name" ], "properties": { - "application_link": { - "type": "string" - }, - "club_followers": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "club_intended_applicants": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "club_members": { - "description": "User", - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "comments": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Comment" - } - }, - "contacts": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Contact" - } + "category_name": { + "type": "string", + "maxLength": 255 }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, - "description": { - "description": "MongoDB URI", - "type": "string" - }, - "events": { - "description": "Event", - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } - }, "id": { "type": "integer", "example": 1 }, - "is_recruiting": { - "type": "boolean" - }, - "logo": { - "description": "S3 URI", - "type": "string" - }, - "name": { - "type": "string" - }, - "notifications": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Notification" - } - }, - "num_members": { - "type": "integer" - }, - "parent_club": { - "type": "integer" - }, - "point_of_contacts": { - "type": "array", - "items": { - "$ref": "#/definitions/models.PointOfContact" - } - }, - "preview": { - "type": "string" - }, - "recruitment_cycle": { - "$ref": "#/definitions/models.RecruitmentCycle" - }, - "recruitment_type": { - "$ref": "#/definitions/models.RecruitmentType" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Tag" - } - }, "updated_at": { "type": "string", "example": "2023-09-20T16:34:50Z" @@ -204,361 +316,38 @@ const docTemplate = `{ "CSSH" ] }, - "models.Comment": { - "type": "object", - "required": [ - "question" - ], - "properties": { - "answer": { - "type": "string" - }, - "answered_by": { - "$ref": "#/definitions/models.User" - }, - "asked_by": { - "$ref": "#/definitions/models.User" - }, - "asked_by_id": { - "type": "integer" - }, - "club": { - "$ref": "#/definitions/models.Club" - }, - "club_id": { - "type": "integer" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "id": { - "type": "integer", - "example": 1 - }, - "num_found_helpful": { - "type": "integer" - }, - "question": { - "type": "string" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "user_id": { - "type": "integer" - } - } - }, - "models.Contact": { - "type": "object", - "required": [ - "content", - "type" - ], - "properties": { - "content": { - "description": "media URI", - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "id": { - "type": "integer", - "example": 1 - }, - "type": { - "$ref": "#/definitions/models.Media" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.Event": { - "type": "object", - "required": [ - "clubs", - "content", - "end_time", - "event_type", - "location", - "name", - "preview", - "start_time" - ], - "properties": { - "clubs": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, - "content": { - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "end_time": { - "type": "string" - }, - "event_type": { - "$ref": "#/definitions/models.EventType" - }, - "id": { - "type": "integer", - "example": 1 - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "notifications": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Notification" - } - }, - "preview": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Tag" - } - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "user_rsvps": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "user_waitlists": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - } - } - }, - "models.EventType": { - "type": "string", - "enum": [ - "open", - "membersOnly" - ], - "x-enum-varnames": [ - "Open", - "MembersOnly" - ] - }, - "models.Media": { - "type": "string", - "enum": [ - "facebook", - "instagram", - "twitter", - "linkedin", - "youtube", - "github", - "custom" - ], - "x-enum-varnames": [ - "Facebook", - "Instagram", - "Twitter", - "LinkedIn", - "YouTube", - "GitHub", - "Custom" - ] - }, - "models.Notification": { - "type": "object", - "required": [ - "content", - "deep_link", - "icon", - "reference_id", - "reference_type", - "send_at", - "title" - ], - "properties": { - "content": { - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "deep_link": { - "type": "string" - }, - "icon": { - "description": "S3 URI", - "type": "string" - }, - "id": { - "type": "integer", - "example": 1 - }, - "reference_id": { - "type": "integer" - }, - "reference_type": { - "$ref": "#/definitions/models.NotificationType" - }, - "send_at": { - "type": "string" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.NotificationType": { - "type": "string", - "enum": [ - "event", - "club" - ], - "x-enum-varnames": [ - "EventNotification", - "ClubNotification" - ] - }, - "models.PointOfContact": { - "type": "object", - "required": [ - "email", - "name", - "position" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "email": { - "type": "string" - }, - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string" - }, - "photo": { - "description": "S3 URI, fallback to default logo if null", - "type": "string" - }, - "position": { - "type": "string" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.RecruitmentCycle": { - "type": "string", - "enum": [ - "fall", - "spring", - "fallSpring", - "always" - ], - "x-enum-varnames": [ - "Fall", - "Spring", - "FallSpring", - "Always" - ] - }, - "models.RecruitmentType": { - "type": "string", - "enum": [ - "unrestricted", - "tryout", - "application" - ], - "x-enum-varnames": [ - "Unrestricted", - "Tryout", - "Application" - ] - }, "models.Tag": { "type": "object", "required": [ + "category_id", "name" ], "properties": { "category_id": { - "type": "integer" - }, - "clubs": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } + "type": "integer", + "minimum": 1 }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, - "events": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } - }, "id": { "type": "integer", "example": 1 }, "name": { - "type": "string" + "type": "string", + "maxLength": 255 }, "updated_at": { "type": "string", "example": "2023-09-20T16:34:50Z" - }, - "users": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } } } }, "models.User": { "type": "object", "required": [ - "club_members", "college", "email", "first_name", @@ -568,86 +357,57 @@ const docTemplate = `{ "year" ], "properties": { - "answered": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Comment" - } - }, - "club_followers": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, - "club_intended_applicants": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, - "club_members": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, "college": { - "$ref": "#/definitions/models.College" - }, - "comments": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Comment" - } + "maxLength": 255, + "allOf": [ + { + "$ref": "#/definitions/models.College" + } + ] }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, "email": { - "type": "string" - }, - "event_rsvps": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } - }, - "event_waitlist": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } + "type": "string", + "maxLength": 255 }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 255 }, "id": { "type": "integer", "example": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 255 }, "nuid": { "type": "string" }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Tag" - } - }, "updated_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, "user_role": { - "$ref": "#/definitions/models.UserRole" + "maxLength": 255, + "allOf": [ + { + "$ref": "#/definitions/models.UserRole" + } + ] }, "year": { - "$ref": "#/definitions/models.Year" + "maximum": 6, + "minimum": 1, + "allOf": [ + { + "$ref": "#/definitions/models.Year" + } + ] } } }, @@ -690,7 +450,7 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "127.0.0.1:8080", - BasePath: "/", + BasePath: "/api/v1", Schemes: []string{}, Title: "SAC API", Description: "Backend Server for SAC App", diff --git a/backend/src/docs/swagger.json b/backend/src/docs/swagger.json index 0e6b0c012..06f90ea40 100644 --- a/backend/src/docs/swagger.json +++ b/backend/src/docs/swagger.json @@ -10,8 +10,214 @@ "version": "1.0" }, "host": "127.0.0.1:8080", - "basePath": "/", + "basePath": "/api/v1", "paths": { + "/api/v1/category/": { + "post": { + "description": "Creates a category that is used to group tags", + "produces": [ + "application/json" + ], + "tags": [ + "category" + ], + "summary": "Create a category", + "operationId": "create-category", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Category" + } + }, + "400": { + "description": "category with that name already exists", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to create category", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/tags/": { + "post": { + "description": "Creates a tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Creates a tag", + "operationId": "create-tag", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "failed to validate the data", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to create tag", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/tags/{id}": { + "get": { + "description": "Returns a tag", + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Gets a tag", + "operationId": "get-tag", + "parameters": [ + { + "type": "integer", + "description": "Tag ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "failed to validate id", + "schema": { + "type": "string" + } + }, + "404": { + "description": "faied to find tag", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to retrieve tag", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Deletes a tag", + "tags": [ + "tag" + ], + "summary": "Deletes a tag", + "operationId": "delete-tag", + "parameters": [ + { + "type": "integer", + "description": "Tag ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "failed to validate id", + "schema": { + "type": "string" + } + }, + "404": { + "description": "failed to find tag", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to delete tag", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "description": "Updates a tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Updates a tag", + "operationId": "update-tag", + "parameters": [ + { + "type": "integer", + "description": "Tag ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "failed to validate the data", + "schema": { + "type": "string" + } + }, + "404": { + "description": "failed to find tag", + "schema": { + "type": "string" + } + }, + "500": { + "description": "failed to update tag", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/users/": { "get": { "description": "Returns all users", @@ -34,7 +240,7 @@ } }, "500": { - "description": "Failed to fetch users", + "description": "failed to get all users", "schema": { "type": "string" } @@ -44,118 +250,24 @@ } }, "definitions": { - "models.Club": { + "models.Category": { "type": "object", "required": [ - "application_link", - "club_members", - "contacts", - "description", - "is_recruiting", - "name", - "num_members", - "point_of_contacts", - "preview", - "recruitment_cycle", - "recruitment_type" + "category_name" ], "properties": { - "application_link": { - "type": "string" - }, - "club_followers": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "club_intended_applicants": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "club_members": { - "description": "User", - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "comments": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Comment" - } - }, - "contacts": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Contact" - } + "category_name": { + "type": "string", + "maxLength": 255 }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, - "description": { - "description": "MongoDB URI", - "type": "string" - }, - "events": { - "description": "Event", - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } - }, "id": { "type": "integer", "example": 1 }, - "is_recruiting": { - "type": "boolean" - }, - "logo": { - "description": "S3 URI", - "type": "string" - }, - "name": { - "type": "string" - }, - "notifications": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Notification" - } - }, - "num_members": { - "type": "integer" - }, - "parent_club": { - "type": "integer" - }, - "point_of_contacts": { - "type": "array", - "items": { - "$ref": "#/definitions/models.PointOfContact" - } - }, - "preview": { - "type": "string" - }, - "recruitment_cycle": { - "$ref": "#/definitions/models.RecruitmentCycle" - }, - "recruitment_type": { - "$ref": "#/definitions/models.RecruitmentType" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Tag" - } - }, "updated_at": { "type": "string", "example": "2023-09-20T16:34:50Z" @@ -198,361 +310,38 @@ "CSSH" ] }, - "models.Comment": { - "type": "object", - "required": [ - "question" - ], - "properties": { - "answer": { - "type": "string" - }, - "answered_by": { - "$ref": "#/definitions/models.User" - }, - "asked_by": { - "$ref": "#/definitions/models.User" - }, - "asked_by_id": { - "type": "integer" - }, - "club": { - "$ref": "#/definitions/models.Club" - }, - "club_id": { - "type": "integer" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "id": { - "type": "integer", - "example": 1 - }, - "num_found_helpful": { - "type": "integer" - }, - "question": { - "type": "string" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "user_id": { - "type": "integer" - } - } - }, - "models.Contact": { - "type": "object", - "required": [ - "content", - "type" - ], - "properties": { - "content": { - "description": "media URI", - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "id": { - "type": "integer", - "example": 1 - }, - "type": { - "$ref": "#/definitions/models.Media" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.Event": { - "type": "object", - "required": [ - "clubs", - "content", - "end_time", - "event_type", - "location", - "name", - "preview", - "start_time" - ], - "properties": { - "clubs": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, - "content": { - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "end_time": { - "type": "string" - }, - "event_type": { - "$ref": "#/definitions/models.EventType" - }, - "id": { - "type": "integer", - "example": 1 - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "notifications": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Notification" - } - }, - "preview": { - "type": "string" - }, - "start_time": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Tag" - } - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "user_rsvps": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - }, - "user_waitlists": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } - } - } - }, - "models.EventType": { - "type": "string", - "enum": [ - "open", - "membersOnly" - ], - "x-enum-varnames": [ - "Open", - "MembersOnly" - ] - }, - "models.Media": { - "type": "string", - "enum": [ - "facebook", - "instagram", - "twitter", - "linkedin", - "youtube", - "github", - "custom" - ], - "x-enum-varnames": [ - "Facebook", - "Instagram", - "Twitter", - "LinkedIn", - "YouTube", - "GitHub", - "Custom" - ] - }, - "models.Notification": { - "type": "object", - "required": [ - "content", - "deep_link", - "icon", - "reference_id", - "reference_type", - "send_at", - "title" - ], - "properties": { - "content": { - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "deep_link": { - "type": "string" - }, - "icon": { - "description": "S3 URI", - "type": "string" - }, - "id": { - "type": "integer", - "example": 1 - }, - "reference_id": { - "type": "integer" - }, - "reference_type": { - "$ref": "#/definitions/models.NotificationType" - }, - "send_at": { - "type": "string" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.NotificationType": { - "type": "string", - "enum": [ - "event", - "club" - ], - "x-enum-varnames": [ - "EventNotification", - "ClubNotification" - ] - }, - "models.PointOfContact": { - "type": "object", - "required": [ - "email", - "name", - "position" - ], - "properties": { - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "email": { - "type": "string" - }, - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string" - }, - "photo": { - "description": "S3 URI, fallback to default logo if null", - "type": "string" - }, - "position": { - "type": "string" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.RecruitmentCycle": { - "type": "string", - "enum": [ - "fall", - "spring", - "fallSpring", - "always" - ], - "x-enum-varnames": [ - "Fall", - "Spring", - "FallSpring", - "Always" - ] - }, - "models.RecruitmentType": { - "type": "string", - "enum": [ - "unrestricted", - "tryout", - "application" - ], - "x-enum-varnames": [ - "Unrestricted", - "Tryout", - "Application" - ] - }, "models.Tag": { "type": "object", "required": [ + "category_id", "name" ], "properties": { "category_id": { - "type": "integer" - }, - "clubs": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } + "type": "integer", + "minimum": 1 }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, - "events": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } - }, "id": { "type": "integer", "example": 1 }, "name": { - "type": "string" + "type": "string", + "maxLength": 255 }, "updated_at": { "type": "string", "example": "2023-09-20T16:34:50Z" - }, - "users": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } } } }, "models.User": { "type": "object", "required": [ - "club_members", "college", "email", "first_name", @@ -562,86 +351,57 @@ "year" ], "properties": { - "answered": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Comment" - } - }, - "club_followers": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, - "club_intended_applicants": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, - "club_members": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Club" - } - }, "college": { - "$ref": "#/definitions/models.College" - }, - "comments": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Comment" - } + "maxLength": 255, + "allOf": [ + { + "$ref": "#/definitions/models.College" + } + ] }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, "email": { - "type": "string" - }, - "event_rsvps": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } - }, - "event_waitlist": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Event" - } + "type": "string", + "maxLength": 255 }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 255 }, "id": { "type": "integer", "example": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 255 }, "nuid": { "type": "string" }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Tag" - } - }, "updated_at": { "type": "string", "example": "2023-09-20T16:34:50Z" }, "user_role": { - "$ref": "#/definitions/models.UserRole" + "maxLength": 255, + "allOf": [ + { + "$ref": "#/definitions/models.UserRole" + } + ] }, "year": { - "$ref": "#/definitions/models.Year" + "maximum": 6, + "minimum": 1, + "allOf": [ + { + "$ref": "#/definitions/models.Year" + } + ] } } }, diff --git a/backend/src/docs/swagger.yaml b/backend/src/docs/swagger.yaml index 0ec1252af..698351905 100644 --- a/backend/src/docs/swagger.yaml +++ b/backend/src/docs/swagger.yaml @@ -1,88 +1,21 @@ -basePath: / +basePath: /api/v1 definitions: - models.Club: + models.Category: properties: - application_link: + category_name: + maxLength: 255 type: string - club_followers: - items: - $ref: '#/definitions/models.User' - type: array - club_intended_applicants: - items: - $ref: '#/definitions/models.User' - type: array - club_members: - description: User - items: - $ref: '#/definitions/models.User' - type: array - comments: - items: - $ref: '#/definitions/models.Comment' - type: array - contacts: - items: - $ref: '#/definitions/models.Contact' - type: array created_at: example: "2023-09-20T16:34:50Z" type: string - description: - description: MongoDB URI - type: string - events: - description: Event - items: - $ref: '#/definitions/models.Event' - type: array id: example: 1 type: integer - is_recruiting: - type: boolean - logo: - description: S3 URI - type: string - name: - type: string - notifications: - items: - $ref: '#/definitions/models.Notification' - type: array - num_members: - type: integer - parent_club: - type: integer - point_of_contacts: - items: - $ref: '#/definitions/models.PointOfContact' - type: array - preview: - type: string - recruitment_cycle: - $ref: '#/definitions/models.RecruitmentCycle' - recruitment_type: - $ref: '#/definitions/models.RecruitmentType' - tags: - items: - $ref: '#/definitions/models.Tag' - type: array updated_at: example: "2023-09-20T16:34:50Z" type: string required: - - application_link - - club_members - - contacts - - description - - is_recruiting - - name - - num_members - - point_of_contacts - - preview - - recruitment_cycle - - recruitment_type + - category_name type: object models.College: enum: @@ -116,318 +49,63 @@ definitions: - CPS - CS - CSSH - models.Comment: - properties: - answer: - type: string - answered_by: - $ref: '#/definitions/models.User' - asked_by: - $ref: '#/definitions/models.User' - asked_by_id: - type: integer - club: - $ref: '#/definitions/models.Club' - club_id: - type: integer - created_at: - example: "2023-09-20T16:34:50Z" - type: string - id: - example: 1 - type: integer - num_found_helpful: - type: integer - question: - type: string - updated_at: - example: "2023-09-20T16:34:50Z" - type: string - user_id: - type: integer - required: - - question - type: object - models.Contact: - properties: - content: - description: media URI - type: string - created_at: - example: "2023-09-20T16:34:50Z" - type: string - id: - example: 1 - type: integer - type: - $ref: '#/definitions/models.Media' - updated_at: - example: "2023-09-20T16:34:50Z" - type: string - required: - - content - - type - type: object - models.Event: - properties: - clubs: - items: - $ref: '#/definitions/models.Club' - type: array - content: - type: string - created_at: - example: "2023-09-20T16:34:50Z" - type: string - end_time: - type: string - event_type: - $ref: '#/definitions/models.EventType' - id: - example: 1 - type: integer - location: - type: string - name: - type: string - notifications: - items: - $ref: '#/definitions/models.Notification' - type: array - preview: - type: string - start_time: - type: string - tags: - items: - $ref: '#/definitions/models.Tag' - type: array - updated_at: - example: "2023-09-20T16:34:50Z" - type: string - user_rsvps: - items: - $ref: '#/definitions/models.User' - type: array - user_waitlists: - items: - $ref: '#/definitions/models.User' - type: array - required: - - clubs - - content - - end_time - - event_type - - location - - name - - preview - - start_time - type: object - models.EventType: - enum: - - open - - membersOnly - type: string - x-enum-varnames: - - Open - - MembersOnly - models.Media: - enum: - - facebook - - instagram - - twitter - - linkedin - - youtube - - github - - custom - type: string - x-enum-varnames: - - Facebook - - Instagram - - Twitter - - LinkedIn - - YouTube - - GitHub - - Custom - models.Notification: - properties: - content: - type: string - created_at: - example: "2023-09-20T16:34:50Z" - type: string - deep_link: - type: string - icon: - description: S3 URI - type: string - id: - example: 1 - type: integer - reference_id: - type: integer - reference_type: - $ref: '#/definitions/models.NotificationType' - send_at: - type: string - title: - type: string - updated_at: - example: "2023-09-20T16:34:50Z" - type: string - required: - - content - - deep_link - - icon - - reference_id - - reference_type - - send_at - - title - type: object - models.NotificationType: - enum: - - event - - club - type: string - x-enum-varnames: - - EventNotification - - ClubNotification - models.PointOfContact: - properties: - created_at: - example: "2023-09-20T16:34:50Z" - type: string - email: - type: string - id: - example: 1 - type: integer - name: - type: string - photo: - description: S3 URI, fallback to default logo if null - type: string - position: - type: string - updated_at: - example: "2023-09-20T16:34:50Z" - type: string - required: - - email - - name - - position - type: object - models.RecruitmentCycle: - enum: - - fall - - spring - - fallSpring - - always - type: string - x-enum-varnames: - - Fall - - Spring - - FallSpring - - Always - models.RecruitmentType: - enum: - - unrestricted - - tryout - - application - type: string - x-enum-varnames: - - Unrestricted - - Tryout - - Application models.Tag: properties: category_id: + minimum: 1 type: integer - clubs: - items: - $ref: '#/definitions/models.Club' - type: array created_at: example: "2023-09-20T16:34:50Z" type: string - events: - items: - $ref: '#/definitions/models.Event' - type: array id: example: 1 type: integer name: + maxLength: 255 type: string updated_at: example: "2023-09-20T16:34:50Z" type: string - users: - items: - $ref: '#/definitions/models.User' - type: array required: + - category_id - name type: object models.User: properties: - answered: - items: - $ref: '#/definitions/models.Comment' - type: array - club_followers: - items: - $ref: '#/definitions/models.Club' - type: array - club_intended_applicants: - items: - $ref: '#/definitions/models.Club' - type: array - club_members: - items: - $ref: '#/definitions/models.Club' - type: array college: - $ref: '#/definitions/models.College' - comments: - items: - $ref: '#/definitions/models.Comment' - type: array + allOf: + - $ref: '#/definitions/models.College' + maxLength: 255 created_at: example: "2023-09-20T16:34:50Z" type: string email: + maxLength: 255 type: string - event_rsvps: - items: - $ref: '#/definitions/models.Event' - type: array - event_waitlist: - items: - $ref: '#/definitions/models.Event' - type: array first_name: + maxLength: 255 type: string id: example: 1 type: integer last_name: + maxLength: 255 type: string nuid: type: string - tags: - items: - $ref: '#/definitions/models.Tag' - type: array updated_at: example: "2023-09-20T16:34:50Z" type: string user_role: - $ref: '#/definitions/models.UserRole' + allOf: + - $ref: '#/definitions/models.UserRole' + maxLength: 255 year: - $ref: '#/definitions/models.Year' + allOf: + - $ref: '#/definitions/models.Year' + maximum: 6 + minimum: 1 required: - - club_members - college - email - first_name @@ -471,6 +149,144 @@ info: title: SAC API version: "1.0" paths: + /api/v1/category/: + post: + description: Creates a category that is used to group tags + operationId: create-category + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Category' + "400": + description: category with that name already exists + schema: + type: string + "500": + description: failed to create category + schema: + type: string + summary: Create a category + tags: + - category + /api/v1/tags/: + post: + consumes: + - application/json + description: Creates a tag + operationId: create-tag + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Tag' + "400": + description: failed to validate the data + schema: + type: string + "500": + description: failed to create tag + schema: + type: string + summary: Creates a tag + tags: + - tag + /api/v1/tags/{id}: + delete: + description: Deletes a tag + operationId: delete-tag + parameters: + - description: Tag ID + in: path + name: id + required: true + type: integer + responses: + "204": + description: No Content + "400": + description: failed to validate id + schema: + type: string + "404": + description: failed to find tag + schema: + type: string + "500": + description: failed to delete tag + schema: + type: string + summary: Deletes a tag + tags: + - tag + get: + description: Returns a tag + operationId: get-tag + parameters: + - description: Tag ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Tag' + "400": + description: failed to validate id + schema: + type: string + "404": + description: faied to find tag + schema: + type: string + "500": + description: failed to retrieve tag + schema: + type: string + summary: Gets a tag + tags: + - tag + patch: + consumes: + - application/json + description: Updates a tag + operationId: update-tag + parameters: + - description: Tag ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Tag' + "400": + description: failed to validate the data + schema: + type: string + "404": + description: failed to find tag + schema: + type: string + "500": + description: failed to update tag + schema: + type: string + summary: Updates a tag + tags: + - tag /api/v1/users/: get: description: Returns all users @@ -485,7 +301,7 @@ paths: $ref: '#/definitions/models.User' type: array "500": - description: Failed to fetch users + description: failed to get all users schema: type: string summary: Gets all users diff --git a/backend/src/main.go b/backend/src/main.go index edaa150a1..d1c5d6bee 100644 --- a/backend/src/main.go +++ b/backend/src/main.go @@ -19,19 +19,18 @@ import ( // @BasePath /api/v1 func main() { onlyMigrate := flag.Bool("only-migrate", false, "Specify if you want to only perform the database migration") + configPath := flag.String("config", "../../config", "Specify the path to the config directory") flag.Parse() - config, err := config.GetConfiguration("../../config") - + config, err := config.GetConfiguration(*configPath) if err != nil { - panic(err) + panic(fmt.Sprintf("Error getting configuration: %s", err.Error())) } db, err := database.ConfigureDB(config) - if err != nil { - panic(err) + panic(fmt.Sprintf("Error configuring database: %s", err.Error())) } if *onlyMigrate { @@ -39,7 +38,6 @@ func main() { } err = database.ConnPooling(db) - if err != nil { panic(err) } diff --git a/backend/src/models/category.go b/backend/src/models/category.go index b1a27c52c..5705ed9d8 100644 --- a/backend/src/models/category.go +++ b/backend/src/models/category.go @@ -6,8 +6,7 @@ type Category struct { types.Model Name string `gorm:"type:varchar(255)" json:"category_name" validate:"required,max=255"` - - Tag []Tag `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` + Tag []Tag `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } type CreateCategoryRequestBody struct { diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 1f6a8e20b..c98afadfc 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -22,7 +22,6 @@ import ( // @contact.email oduneye.d@northeastern.edu and ladley.g@northeastern.edu // @host 127.0.0.1:8080 // @BasePath / - func Init(db *gorm.DB) *fiber.App { app := newFiberApp() diff --git a/cli/commands/config.go b/cli/commands/config.go new file mode 100644 index 000000000..18cf4653c --- /dev/null +++ b/cli/commands/config.go @@ -0,0 +1,10 @@ +package commands + +import ( + "os" + "path/filepath" +) + +var ROOT_DIR, _ = os.Getwd() +var FRONTEND_DIR = filepath.Join(ROOT_DIR, "/frontend") +var BACKEND_DIR = filepath.Join(ROOT_DIR, "/backend/src") diff --git a/cli/commands/drop.go b/cli/commands/drop.go new file mode 100644 index 000000000..527287d3d --- /dev/null +++ b/cli/commands/drop.go @@ -0,0 +1,43 @@ +package commands + +import ( + "fmt" + "os/exec" + + "github.com/urfave/cli/v2" +) + +func DropDBCommand() *cli.Command { + command := cli.Command{ + Name: "drop", + Usage: "Drops the database", + Action: func(c *cli.Context) error { + if c.Args().Len() > 0 { + return cli.Exit("Invalid arguments", 1) + } + + DropDB() + return nil + }, + } + + return &command +} + +func DropDB() error { + fmt.Println("Droping database") + + cmd := exec.Command("../../scripts/drop_db.sh") + cmd.Dir = BACKEND_DIR + + output, err := cmd.CombinedOutput() + if err != nil { + return cli.Exit("Error running drop_db.sh", 1) + } + + fmt.Println(string(output)) + + fmt.Println("Done droping database") + + return nil +} \ No newline at end of file diff --git a/cli/commands/lint.go b/cli/commands/lint.go index 6ee52b49f..7d327a99a 100644 --- a/cli/commands/lint.go +++ b/cli/commands/lint.go @@ -30,4 +30,4 @@ func LintCommand() *cli.Command { } return &command -} \ No newline at end of file +} diff --git a/cli/commands/migrate.go b/cli/commands/migrate.go index 0e10c320f..7f1f08802 100644 --- a/cli/commands/migrate.go +++ b/cli/commands/migrate.go @@ -3,12 +3,11 @@ package commands import ( "fmt" "os/exec" - "sync" "github.com/urfave/cli/v2" ) -func MigrateCommand(backendDir string) *cli.Command { +func MigrateCommand() *cli.Command { command := cli.Command{ Name: "migrate", Usage: "Migrate the database", @@ -17,7 +16,7 @@ func MigrateCommand(backendDir string) *cli.Command { return cli.Exit("Invalid arguments", 1) } - Migrate(backendDir) + Migrate() return nil }, } @@ -25,35 +24,36 @@ func MigrateCommand(backendDir string) *cli.Command { return &command } -func Migrate(backendDir string) error { - var wg sync.WaitGroup +func Migrate() error { + // var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + fmt.Println("Migrating database") - cmd := exec.Command("go", "run", "main.go", "--only-migrate") - cmd.Dir = backendDir + goCmd := exec.Command("go", "run", "main.go", "--only-migrate") + goCmd.Dir = BACKEND_DIR - fmt.Println("Running main.go") - err := cmd.Run() - if err != nil { - fmt.Println("Error running main.go:", err) - } - }() + fmt.Println("Running main.go") + fmt.Println("Command:", goCmd.String()) - cmd := exec.Command("../../scripts/insert_db.sh") - cmd.Dir = backendDir + output, err := goCmd.CombinedOutput() + if err != nil { + fmt.Println("Error running main.go:", err) + } + + fmt.Println(string(output)) + + fmt.Println("Running insert_db.sh") + scriptCmd := exec.Command("./scripts/insert_db.sh") + scriptCmd.Dir = ROOT_DIR - output, err := cmd.CombinedOutput() + output, err = scriptCmd.CombinedOutput() if err != nil { return cli.Exit("Error running insert_db.sh", 1) } fmt.Println(string(output)) - wg.Wait() - fmt.Println("Insert_db.sh completed") - + fmt.Println("Done migrating database") + return nil } diff --git a/cli/commands/reset.go b/cli/commands/reset.go index a62037370..61fc0ba0d 100644 --- a/cli/commands/reset.go +++ b/cli/commands/reset.go @@ -2,19 +2,44 @@ package commands import ( "fmt" + "os/exec" "github.com/urfave/cli/v2" ) func ResetDBCommand() *cli.Command { command := cli.Command{ - Name: "resetdb", + Name: "reset", Usage: "Resets the database", Action: func(c *cli.Context) error { - fmt.Println("resetdb") + if c.Args().Len() > 0 { + return cli.Exit("Invalid arguments", 1) + } + + ResetDB() return nil }, } return &command +} + +func ResetDB() error { + fmt.Println("Resetting database") + + DropDB() + + cmd := exec.Command("sleep", "1") + cmd.Dir = BACKEND_DIR + + err := cmd.Run() + if err != nil { + return cli.Exit("Error running sleep", 1) + } + + Migrate() + + fmt.Println("Done resetting database") + + return nil } \ No newline at end of file diff --git a/cli/commands/swagger.go b/cli/commands/swagger.go index cfaefe531..467c58b2c 100644 --- a/cli/commands/swagger.go +++ b/cli/commands/swagger.go @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli/v2" ) -func SwaggerCommand(backendDir string) *cli.Command { +func SwaggerCommand() *cli.Command { command := cli.Command{ Name: "swagger", Usage: "Updates the swagger documentation", @@ -16,7 +16,7 @@ func SwaggerCommand(backendDir string) *cli.Command { return cli.Exit("Invalid arguments", 1) } - Swagger(backendDir) + Swagger() return nil }, } @@ -24,10 +24,9 @@ func SwaggerCommand(backendDir string) *cli.Command { return &command } - -func Swagger(backendDir string) error { +func Swagger() error { cmd := exec.Command("swag", "init") - cmd.Dir = backendDir + cmd.Dir = BACKEND_DIR out, err := cmd.CombinedOutput() if err != nil { @@ -37,4 +36,4 @@ func Swagger(backendDir string) error { fmt.Println(string(out)) return nil -} \ No newline at end of file +} diff --git a/cli/commands/test.go b/cli/commands/test.go index 65b5fe4ad..4a7189205 100644 --- a/cli/commands/test.go +++ b/cli/commands/test.go @@ -8,7 +8,7 @@ import ( "github.com/urfave/cli/v2" ) -func TestCommand(backendDir string, frontendDir string) *cli.Command { +func TestCommand() *cli.Command { command := cli.Command{ Name: "test", Usage: "Runs tests", @@ -34,13 +34,11 @@ func TestCommand(backendDir string, frontendDir string) *cli.Command { return cli.Exit("Must specify frontend or backend", 1) } - fmt.Println("Frontend", c.String("frontend")) - folder := c.String("frontend") runFrontend := folder != "" runBackend := c.Bool("backend") - Test(backendDir, frontendDir, folder, runFrontend, runBackend) + Test(folder, runFrontend, runBackend) return nil }, @@ -49,7 +47,7 @@ func TestCommand(backendDir string, frontendDir string) *cli.Command { return &command } -func Test(backendDir string, frontendDir string, folder string, runFrontend bool, runBackend bool) error { +func Test(folder string, runFrontend bool, runBackend bool) error { var wg sync.WaitGroup // Start the backend if specified @@ -57,7 +55,7 @@ func Test(backendDir string, frontendDir string, folder string, runFrontend bool wg.Add(1) go func() { defer wg.Done() - BackendTest(backendDir) + BackendTest() }() } @@ -66,7 +64,7 @@ func Test(backendDir string, frontendDir string, folder string, runFrontend bool wg.Add(1) go func() { defer wg.Done() - FrontendTest(frontendDir, folder) + FrontendTest(folder) }() } @@ -75,9 +73,9 @@ func Test(backendDir string, frontendDir string, folder string, runFrontend bool return nil } -func BackendTest(backendDir string) error { +func BackendTest() error { cmd := exec.Command("go", "test", "./...") - cmd.Dir = backendDir + "/.." + cmd.Dir = BACKEND_DIR + "/.." out, err := cmd.CombinedOutput() if err != nil { @@ -87,12 +85,21 @@ func BackendTest(backendDir string) error { fmt.Println(string(out)) + cmd = exec.Command("./scripts/clean_old_test_dbs.sh") + cmd.Dir = ROOT_DIR + + out, err = cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + return cli.Exit("Failed to clean old test databases", 1) + } + return nil } -func FrontendTest(frontendDir string, folder string) error { +func FrontendTest(folder string) error { cmd := exec.Command("yarn", "run", "test") - cmd.Dir = frontendDir + cmd.Dir = FRONTEND_DIR + "/" + folder out, err := cmd.CombinedOutput() if err != nil { diff --git a/cli/main.go b/cli/main.go index 2f309fb11..e374c545f 100755 --- a/cli/main.go +++ b/cli/main.go @@ -3,34 +3,25 @@ package main import ( "log" "os" - "path/filepath" "github.com/GenerateNU/sac/cli/commands" - "github.com/urfave/cli/v2" ) func main() { - ROOT_DIR, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - - BACKEND_DIR := filepath.Join(ROOT_DIR, "/backend/src") - FRONTEND_DIR := filepath.Join(ROOT_DIR, "/frontend") - app := &cli.App{ Name: "sac-cli", Usage: "CLI for SAC", Commands: []*cli.Command{ - commands.SwaggerCommand(BACKEND_DIR), - // commands.StartCommand(BACKEND_DIR, FRONTEND_DIR), // Dont use - commands.MigrateCommand(BACKEND_DIR), - commands.TestCommand(BACKEND_DIR, FRONTEND_DIR), + commands.SwaggerCommand(), + commands.MigrateCommand(), + commands.ResetDBCommand(), + commands.DropDBCommand(), + commands.TestCommand(), // TODO: frontend tests }, } - err = app.Run(os.Args) + err := app.Run(os.Args) if err != nil { log.Fatal(err) } diff --git a/cli/utils/path.go b/cli/utils/path.go new file mode 100644 index 000000000..44f21e27d --- /dev/null +++ b/cli/utils/path.go @@ -0,0 +1,44 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" +) + + +func GetRootDir() (string, error) { + // Get the current working directory + currentDir, err := os.Getwd() + if err != nil { + return "", err + } + + // Find the closest directory containing "install.sh" (the root directory) + rootDir, err := FindRootDir(currentDir) + if err != nil { + return "", err + } + + return rootDir, nil +} + +func FindRootDir(dir string) (string, error) { + // Check if "main.go" exists in the current directory + mainGoPath := filepath.Join(dir, "install.sh") + _, err := os.Stat(mainGoPath) + if err == nil { + // "main.go" found, this is the root directory + return dir, nil + } + + // If not found, go up one level + parentDir := filepath.Dir(dir) + if parentDir == dir { + // Reached the top without finding "main.go" + return "", fmt.Errorf("could not find root directory containing main.go") + } + + // Recursively search in the parent directory + return FindRootDir(parentDir) +} \ No newline at end of file diff --git a/go.work.sum b/go.work.sum index 3bc8690e6..164d9dd49 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,14 +1,3 @@ -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/scripts/reset_db.sh b/scripts/drop_db.sh similarity index 100% rename from scripts/reset_db.sh rename to scripts/drop_db.sh diff --git a/scripts/insert_db.sh b/scripts/insert_db.sh index 36f5b7f28..7e3de0655 100755 --- a/scripts/insert_db.sh +++ b/scripts/insert_db.sh @@ -17,19 +17,7 @@ if psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -t -c "$CHECK_TA echo "Database $PGDATABASE exists with tables." else echo "Error: Database $PGDATABASE does not exist or has no tables. Running database migration." - go run ../backend/src/main.go - sleep 3 - - # Find the process running on port 8080 and kill it - PROCESS_ID=$(lsof -i :8080 | awk 'NR==2{print $2}') - if [ -n "$PROCESS_ID" ]; then - kill -INT $PROCESS_ID - echo "Killed process $PROCESS_ID running on port 8080." - else - echo "No process running on port 8080." - exit 0 - fi -fi + go run ../backend/src/main.go --only-migrate # Insert data from data.sql if psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -a -f "$INSERTSQL" > /dev/null 2>&1; then @@ -37,4 +25,4 @@ if psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -a -f "$INSERTSQ else echo "Error: Failed to insert data." exit 1 -fi \ No newline at end of file +fi From 048def1e66bf4918e5425324f28b2b6ebd3c4bd4 Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Mon, 22 Jan 2024 02:30:50 -0500 Subject: [PATCH 09/12] relative paths for cli, can run anywhere --- backend/src/docs/docs.go | 25 ++++++--- backend/src/docs/swagger.json | 25 ++++++--- backend/src/docs/swagger.yaml | 20 ++++--- cli/commands/clean_tests.go | 43 ++++++++++++++ cli/commands/config.go | 5 +- cli/commands/format.go | 102 ++++++++++++++++------------------ cli/commands/lint.go | 70 +++++++++++++++++++++-- cli/commands/migrate.go | 9 +-- cli/commands/reset.go | 2 +- cli/commands/test.go | 20 +++---- cli/main.go | 3 + cli/utils/path.go | 12 ++-- scripts/insert_db.sh | 1 + 13 files changed, 228 insertions(+), 109 deletions(-) create mode 100644 cli/commands/clean_tests.go diff --git a/backend/src/docs/docs.go b/backend/src/docs/docs.go index 574baac3f..5eec86876 100644 --- a/backend/src/docs/docs.go +++ b/backend/src/docs/docs.go @@ -226,27 +226,36 @@ const docTemplate = `{ }, "/api/v1/users/": { "get": { - "description": "Returns all users", + "description": "Returns specific user", "produces": [ "application/json" ], "tags": [ "user" ], - "summary": "Gets all users", - "operationId": "get-all-users", + "summary": "Gets specific user", + "operationId": "get-user", "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "failed to validate id", + "schema": { + "type": "string" + } + }, + "404": { + "description": "failed to find user", + "schema": { + "type": "string" } }, "500": { - "description": "failed to get all users", + "description": "Internal Server Error", "schema": { "type": "string" } diff --git a/backend/src/docs/swagger.json b/backend/src/docs/swagger.json index 06f90ea40..39439eeed 100644 --- a/backend/src/docs/swagger.json +++ b/backend/src/docs/swagger.json @@ -220,27 +220,36 @@ }, "/api/v1/users/": { "get": { - "description": "Returns all users", + "description": "Returns specific user", "produces": [ "application/json" ], "tags": [ "user" ], - "summary": "Gets all users", - "operationId": "get-all-users", + "summary": "Gets specific user", + "operationId": "get-user", "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "failed to validate id", + "schema": { + "type": "string" + } + }, + "404": { + "description": "failed to find user", + "schema": { + "type": "string" } }, "500": { - "description": "failed to get all users", + "description": "Internal Server Error", "schema": { "type": "string" } diff --git a/backend/src/docs/swagger.yaml b/backend/src/docs/swagger.yaml index 698351905..7315e5fd2 100644 --- a/backend/src/docs/swagger.yaml +++ b/backend/src/docs/swagger.yaml @@ -289,22 +289,28 @@ paths: - tag /api/v1/users/: get: - description: Returns all users - operationId: get-all-users + description: Returns specific user + operationId: get-user produces: - application/json responses: "200": description: OK schema: - items: - $ref: '#/definitions/models.User' - type: array + $ref: '#/definitions/models.User' + "400": + description: failed to validate id + schema: + type: string + "404": + description: failed to find user + schema: + type: string "500": - description: failed to get all users + description: Internal Server Error schema: type: string - summary: Gets all users + summary: Gets specific user tags: - user swagger: "2.0" diff --git a/cli/commands/clean_tests.go b/cli/commands/clean_tests.go new file mode 100644 index 000000000..a4c3ad3c6 --- /dev/null +++ b/cli/commands/clean_tests.go @@ -0,0 +1,43 @@ +package commands + +import ( + "fmt" + "os/exec" + + "github.com/urfave/cli/v2" +) + +func ClearDBCommand() *cli.Command { + command := cli.Command{ + Name: "clean", + Usage: "Remove databases used for testing", + Action: func(c *cli.Context) error { + if c.Args().Len() > 0 { + return cli.Exit("Invalid arguments", 1) + } + + ClearDB() + return nil + }, + } + + return &command +} + +func ClearDB() error { + + fmt.Println("Clearing databases") + + cmd := exec.Command("./scripts/clean_old_test_dbs.sh") + cmd.Dir = ROOT_DIR + + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + return cli.Exit("Failed to clean old test databases", 1) + } + + fmt.Println("Databases cleared") + + return nil +} diff --git a/cli/commands/config.go b/cli/commands/config.go index 18cf4653c..06ca14e6b 100644 --- a/cli/commands/config.go +++ b/cli/commands/config.go @@ -1,10 +1,11 @@ package commands import ( - "os" "path/filepath" + + "github.com/GenerateNU/sac/cli/utils" ) -var ROOT_DIR, _ = os.Getwd() +var ROOT_DIR, _ = utils.GetRootDir() var FRONTEND_DIR = filepath.Join(ROOT_DIR, "/frontend") var BACKEND_DIR = filepath.Join(ROOT_DIR, "/backend/src") diff --git a/cli/commands/format.go b/cli/commands/format.go index c3dc305ab..694e3fe57 100644 --- a/cli/commands/format.go +++ b/cli/commands/format.go @@ -2,9 +2,8 @@ package commands import ( "fmt" - "os" "os/exec" - "path/filepath" + "sync" "github.com/urfave/cli/v2" ) @@ -12,87 +11,84 @@ import ( func FormatCommand() *cli.Command { command := cli.Command{ Name: "format", - Usage: "Runs formatter", + Usage: "Runs formatting tools", Flags: []cli.Flag{ &cli.StringFlag{ Name: "frontend", Aliases: []string{"f"}, - Usage: "Runs frontend formatter", Value: "", + Usage: "Formats a specific frontend folder", }, &cli.BoolFlag{ Name: "backend", Aliases: []string{"b"}, - Usage: "Runs backend formatter", + Usage: "Formats the backend", }, }, - Action: Format, - } + Action: func(c *cli.Context) error { + if c.Args().Len() > 0 { + return cli.Exit("Invalid arguments", 1) + } - return &command -} + if c.String("frontend") == "" && !c.Bool("backend") { + return cli.Exit("Must specify frontend or backend", 1) + } -func Format(c *cli.Context) error { - if c.Args().Len() > 0 { - return cli.Exit("Invalid arguments", 1) - } + folder := c.String("frontend") + runFrontend := folder != "" + runBackend := c.Bool("backend") - currentDir, err := os.Getwd() - if err != nil { - return cli.Exit("Error getting current directory", 1) - } + Format(folder, runFrontend, runBackend) - frontendDir := filepath.Join(currentDir, "frontend/") - backendDir := filepath.Join(currentDir, "github.com/GenerateNU/sac/backend/") - list, err := os.ReadDir(frontendDir) - if err != nil { - return cli.Exit("Error reading frontend directory", 1) + return nil + }, } - if !c.IsSet("frontend") && !c.IsSet("backend") { - formatBackend(backendDir) - for _, f := range list { - if f.IsDir() { - formatFrontend(frontendDir, f.Name()) - } - } - } + return &command +} - if c.IsSet("frontend") && c.IsSet("backend") { - formatFrontend(frontendDir, c.String("frontend")) - formatBackend(backendDir) - } +func Format(folder string, runFrontend bool, runBackend bool) error { + var wg sync.WaitGroup - if c.IsSet("frontend") { - formatFrontend(frontendDir, c.String("frontend")) + // Start the backend if specified + if runBackend { + wg.Add(1) + go func() { + defer wg.Done() + BackendFormat() + }() } - if c.IsSet("backend") { - formatBackend(backendDir) + + // Start the frontend if specified + if runFrontend { + wg.Add(1) + go func() { + defer wg.Done() + FrontendFormat(folder) + }() } + wg.Wait() + return nil } -func formatFrontend(frontendDir string, folder string) { - cmd := exec.Command("yarn", "format") - cmd.Dir = filepath.Join(frontendDir, folder) - - err := cmd.Run() - if err != nil { - fmt.Println("Error formatting frontend, run yarn format in frontend folder") - } - - fmt.Println("frontend", cmd.Dir) // remove -} +func BackendFormat() error { + fmt.Println("Formatting backend") -func formatBackend(backendDir string) { cmd := exec.Command("go", "fmt", "./...") - cmd.Dir = filepath.Join(backendDir) + cmd.Dir = BACKEND_DIR err := cmd.Run() if err != nil { - fmt.Println("Error formatting backend, run go fmt ./... in backend folder") + return cli.Exit("Failed to format backend", 1) } - fmt.Println("backend", cmd.Dir) // remove + fmt.Println("Backend formatted") + return nil } + +func FrontendFormat(folder string) error { + fmt.Println("UNIMPLEMENTED") + return nil +} \ No newline at end of file diff --git a/cli/commands/lint.go b/cli/commands/lint.go index 7d327a99a..ea1680fb8 100644 --- a/cli/commands/lint.go +++ b/cli/commands/lint.go @@ -2,6 +2,8 @@ package commands import ( "fmt" + "os/exec" + "sync" "github.com/urfave/cli/v2" ) @@ -9,25 +11,85 @@ import ( func LintCommand() *cli.Command { command := cli.Command{ Name: "lint", - Usage: "Runs linter", + Usage: "Runs linting tools", Flags: []cli.Flag{ &cli.StringFlag{ Name: "frontend", Aliases: []string{"f"}, - Usage: "Runs frontend linter", Value: "", + Usage: "Lint a specific frontend folder", }, &cli.BoolFlag{ Name: "backend", Aliases: []string{"b"}, - Usage: "Runs backend linter", + Usage: "Lint the backend", }, }, Action: func(c *cli.Context) error { - fmt.Println("lint", c.String("frontend"), c.Bool("backend")) + if c.Args().Len() > 0 { + return cli.Exit("Invalid arguments", 1) + } + + if c.String("frontend") == "" && !c.Bool("backend") { + return cli.Exit("Must specify frontend or backend", 1) + } + + folder := c.String("frontend") + runFrontend := folder != "" + runBackend := c.Bool("backend") + + Lint(folder, runFrontend, runBackend) + return nil }, } return &command } + +func Lint(folder string, runFrontend bool, runBackend bool) error { + var wg sync.WaitGroup + + // Start the backend if specified + if runBackend { + wg.Add(1) + go func() { + defer wg.Done() + BackendLint() + }() + } + + // Start the frontend if specified + if runFrontend { + wg.Add(1) + go func() { + defer wg.Done() + FrontendLint(folder) + }() + } + + wg.Wait() + + return nil +} + +func BackendLint() error { + fmt.Println("Linting backend") + + cmd := exec.Command("go", "vet", "./...") + cmd.Dir = BACKEND_DIR + + err := cmd.Run() + if err != nil { + return cli.Exit("Failed to lint backend", 1) + } + + fmt.Println("Backend linted") + + return nil +} + +func FrontendLint(folder string) error { + fmt.Println("UNIMPLEMENTED") + return nil +} diff --git a/cli/commands/migrate.go b/cli/commands/migrate.go index 7f1f08802..2ecc2b7a1 100644 --- a/cli/commands/migrate.go +++ b/cli/commands/migrate.go @@ -25,16 +25,11 @@ func MigrateCommand() *cli.Command { } func Migrate() error { - // var wg sync.WaitGroup - fmt.Println("Migrating database") goCmd := exec.Command("go", "run", "main.go", "--only-migrate") goCmd.Dir = BACKEND_DIR - fmt.Println("Running main.go") - fmt.Println("Command:", goCmd.String()) - output, err := goCmd.CombinedOutput() if err != nil { fmt.Println("Error running main.go:", err) @@ -42,7 +37,8 @@ func Migrate() error { fmt.Println(string(output)) - fmt.Println("Running insert_db.sh") + fmt.Println("Inserting data into database") + scriptCmd := exec.Command("./scripts/insert_db.sh") scriptCmd.Dir = ROOT_DIR @@ -52,7 +48,6 @@ func Migrate() error { } fmt.Println(string(output)) - fmt.Println("Done migrating database") return nil diff --git a/cli/commands/reset.go b/cli/commands/reset.go index 61fc0ba0d..44e16fd30 100644 --- a/cli/commands/reset.go +++ b/cli/commands/reset.go @@ -30,7 +30,7 @@ func ResetDB() error { DropDB() cmd := exec.Command("sleep", "1") - cmd.Dir = BACKEND_DIR + cmd.Dir = BACKEND_DIR err := cmd.Run() if err != nil { diff --git a/cli/commands/test.go b/cli/commands/test.go index 4a7189205..50a58c934 100644 --- a/cli/commands/test.go +++ b/cli/commands/test.go @@ -74,6 +74,11 @@ func Test(folder string, runFrontend bool, runBackend bool) error { } func BackendTest() error { + // rootDir, err := utils.GetRootDir() + // if err != nil { + // return cli.Exit("Couldn't find the project root", 1) + // } + cmd := exec.Command("go", "test", "./...") cmd.Dir = BACKEND_DIR + "/.." @@ -88,9 +93,8 @@ func BackendTest() error { cmd = exec.Command("./scripts/clean_old_test_dbs.sh") cmd.Dir = ROOT_DIR - out, err = cmd.CombinedOutput() + err = cmd.Run() if err != nil { - fmt.Println(string(out)) return cli.Exit("Failed to clean old test databases", 1) } @@ -98,16 +102,6 @@ func BackendTest() error { } func FrontendTest(folder string) error { - cmd := exec.Command("yarn", "run", "test") - cmd.Dir = FRONTEND_DIR + "/" + folder - - out, err := cmd.CombinedOutput() - if err != nil { - fmt.Println(string(out)) - return cli.Exit("Failed to run frontend tests", 1) - } - - fmt.Println(string(out)) - + fmt.Println("UNIMPLEMENTED") return nil } diff --git a/cli/main.go b/cli/main.go index e374c545f..df7221ad8 100755 --- a/cli/main.go +++ b/cli/main.go @@ -14,10 +14,13 @@ func main() { Usage: "CLI for SAC", Commands: []*cli.Command{ commands.SwaggerCommand(), + commands.ClearDBCommand(), commands.MigrateCommand(), commands.ResetDBCommand(), commands.DropDBCommand(), commands.TestCommand(), // TODO: frontend tests + commands.FormatCommand(), // TODO: frontend format + commands.LintCommand(), // TODO: frontend lint }, } diff --git a/cli/utils/path.go b/cli/utils/path.go index 44f21e27d..d91a6b504 100644 --- a/cli/utils/path.go +++ b/cli/utils/path.go @@ -14,7 +14,7 @@ func GetRootDir() (string, error) { return "", err } - // Find the closest directory containing "install.sh" (the root directory) + // Find the closest directory containing "sac-cli" (the root directory) rootDir, err := FindRootDir(currentDir) if err != nil { return "", err @@ -24,19 +24,19 @@ func GetRootDir() (string, error) { } func FindRootDir(dir string) (string, error) { - // Check if "main.go" exists in the current directory - mainGoPath := filepath.Join(dir, "install.sh") + // Check if "sac-cli" exists in the current directory + mainGoPath := filepath.Join(dir, "sac-cli") _, err := os.Stat(mainGoPath) if err == nil { - // "main.go" found, this is the root directory + // "sac-cli" found, this is the root directory return dir, nil } // If not found, go up one level parentDir := filepath.Dir(dir) if parentDir == dir { - // Reached the top without finding "main.go" - return "", fmt.Errorf("could not find root directory containing main.go") + // Reached the top without finding "sac-cli" + return "", fmt.Errorf("could not find root directory containing sac-cli") } // Recursively search in the parent directory diff --git a/scripts/insert_db.sh b/scripts/insert_db.sh index 7e3de0655..ded8d2031 100755 --- a/scripts/insert_db.sh +++ b/scripts/insert_db.sh @@ -18,6 +18,7 @@ if psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -t -c "$CHECK_TA else echo "Error: Database $PGDATABASE does not exist or has no tables. Running database migration." go run ../backend/src/main.go --only-migrate +fi # Insert data from data.sql if psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" -a -f "$INSERTSQL" > /dev/null 2>&1; then From e76a3170c36fe6c91e4982b63b490dd33049bf60 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:56:33 -0500 Subject: [PATCH 10/12] Update auto_request_review.yml --- .github/workflows/auto_request_review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml index a1175e8c8..0f5eda810 100644 --- a/.github/workflows/auto_request_review.yml +++ b/.github/workflows/auto_request_review.yml @@ -8,7 +8,7 @@ jobs: auto-request-review: runs-on: ubuntu-latest steps: - - name: Request review from the TLs and 1 random team member + - name: Request review from the TLs and random team members uses: necojackarc/auto-request-review@v0.12.0 with: token: ${{ secrets.PAT_FOR_AUTO_REQUEST_REVIEW }} From 83bb7cf99d54ea9096f54d36b7e870970d220ee1 Mon Sep 17 00:00:00 2001 From: Michael Brennan <76018881+michael-brennan2005@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:12:51 -0500 Subject: [PATCH 11/12] Validator singleton (#43) --- backend/src/server/server.go | 12 ++-- backend/src/services/category.go | 7 +- backend/src/services/tag.go | 8 ++- backend/src/services/user.go | 4 +- go.work.sum | 119 +++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 11 deletions(-) diff --git a/backend/src/server/server.go b/backend/src/server/server.go index c98afadfc..8388e82a2 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -3,7 +3,7 @@ package server import ( "github.com/GenerateNU/sac/backend/src/controllers" "github.com/GenerateNU/sac/backend/src/services" - + "github.com/go-playground/validator/v10" "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" @@ -25,13 +25,17 @@ import ( func Init(db *gorm.DB) *fiber.App { app := newFiberApp() + validate := validator.New(validator.WithRequiredStructEnabled()) + // MARK: Custom validator tags can be registered here. + // validate.RegisterValidation("my_custom_validator", MyCustomValidatorFunc) + utilityRoutes(app) apiv1 := app.Group("/api/v1") - userRoutes(apiv1, &services.UserService{DB: db}) - categoryRoutes(apiv1, &services.CategoryService{DB: db}) - tagRoutes(apiv1, &services.TagService{DB: db}) + userRoutes(apiv1, &services.UserService{DB: db, Validate: validate}) + categoryRoutes(apiv1, &services.CategoryService{DB: db, Validate: validate}) + tagRoutes(apiv1, &services.TagService{DB: db, Validate: validate}) return app } diff --git a/backend/src/services/category.go b/backend/src/services/category.go index 5e74a8c29..738a49c47 100644 --- a/backend/src/services/category.go +++ b/backend/src/services/category.go @@ -3,7 +3,7 @@ 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/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "golang.org/x/text/cases" @@ -16,11 +16,12 @@ type CategoryServiceInterface interface { } type CategoryService struct { - DB *gorm.DB + DB *gorm.DB + Validate *validator.Validate } func (c *CategoryService) CreateCategory(category models.Category) (*models.Category, error) { - if err := utilities.ValidateData(category); err != nil { + if err := c.Validate.Struct(category); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index dac2203b3..a56f26a36 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -4,6 +4,7 @@ import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" "github.com/GenerateNU/sac/backend/src/utilities" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) @@ -16,7 +17,8 @@ type TagServiceInterface interface { } type TagService struct { - DB *gorm.DB + DB *gorm.DB + Validate *validator.Validate } func (t *TagService) CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) { @@ -25,7 +27,7 @@ func (t *TagService) CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag CategoryID: tagBody.CategoryID, } - if err := utilities.ValidateData(tag); err != nil { + if err := t.Validate.Struct(tag); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") } @@ -54,7 +56,7 @@ func (t *TagService) UpdateTag(id string, tagBody models.UpdateTagRequestBody) ( CategoryID: tagBody.CategoryID, } - if err := utilities.ValidateData(tag); err != nil { + if err := t.Validate.Struct(tag); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") } diff --git a/backend/src/services/user.go b/backend/src/services/user.go index 8e9e60ac1..44830a128 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -4,6 +4,7 @@ import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" "github.com/GenerateNU/sac/backend/src/utilities" + "github.com/go-playground/validator/v10" "gorm.io/gorm" ) @@ -14,7 +15,8 @@ type UserServiceInterface interface { } type UserService struct { - DB *gorm.DB + DB *gorm.DB + Validate *validator.Validate } func (u *UserService) GetAllUsers() ([]models.User, error) { diff --git a/go.work.sum b/go.work.sum index 164d9dd49..7fa9fad8c 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,122 @@ +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= +cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= +github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= +github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk= +github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= +go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= +google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= From de0127b8a6d4b81eeb5a166964679d45334968a8 Mon Sep 17 00:00:00 2001 From: Michael Brennan <76018881+michael-brennan2005@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:02:50 -0500 Subject: [PATCH 12/12] SAC-5 Update User PATCH (#28) Co-authored-by: Melody Yu Co-authored-by: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Co-authored-by: Zackary Lassetter <93090968+zacklassetter@users.noreply.github.com> Co-authored-by: edwinliiiii Co-authored-by: edwinliiiii <91173669+edwinliiiii@users.noreply.github.com> Co-authored-by: garrettladley Co-authored-by: David Oduneye <44040421+DOOduneye@users.noreply.github.com> Co-authored-by: David Oduneye --- backend/go.mod | 13 +-- backend/go.sum | 9 ++ backend/src/controllers/category.go | 17 ++-- backend/src/controllers/tag.go | 27 +++--- backend/src/controllers/user.go | 53 +++++++++--- backend/src/models/category.go | 2 +- backend/src/models/tag.go | 12 +-- backend/src/models/user.go | 10 +++ backend/src/server/server.go | 6 +- backend/src/services/category.go | 19 ++-- backend/src/services/tag.go | 41 ++++----- backend/src/services/user.go | 39 +++++++-- backend/src/transactions/category.go | 10 +-- backend/src/transactions/tag.go | 15 ++-- backend/src/transactions/user.go | 26 +++++- backend/src/utilities/error.go | 10 +++ backend/src/utilities/manipulator.go | 25 ++++++ backend/src/utilities/validator.go | 34 ++++++-- backend/tests/api/user_test.go | 120 ++++++++++++++++++++++++++ backend/tests/utilities_test.go | 38 ++++++++ frontend/node_modules/.yarn-integrity | 10 +++ frontend/yarn.lock | 4 + go.work.sum | 120 -------------------------- 23 files changed, 429 insertions(+), 231 deletions(-) create mode 100644 backend/src/utilities/error.go create mode 100644 backend/src/utilities/manipulator.go create mode 100644 frontend/node_modules/.yarn-integrity create mode 100644 frontend/yarn.lock diff --git a/backend/go.mod b/backend/go.mod index 2730f8f61..309458c49 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -12,7 +12,10 @@ require ( gorm.io/gorm v1.25.5 ) -require github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/mcnijman/go-emailaddress v1.1.1 // indirect +) require ( github.com/KyleBanks/depth v1.2.1 // indirect @@ -61,11 +64,11 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.18.0 // indirect 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 + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 0906300df..5fb160261 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -93,6 +93,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mcnijman/go-emailaddress v1.1.1 h1:AGhgVDG3tCDaL0/Vc6erlPQjDuDN3dAT7rRdgFtetr0= +github.com/mcnijman/go-emailaddress v1.1.1/go.mod h1:5whZrhS8Xp5LxO8zOD35BC+b76kROtsh+dPomeRt/II= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -158,6 +160,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -166,6 +170,7 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= @@ -175,6 +180,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -191,6 +198,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= diff --git a/backend/src/controllers/category.go b/backend/src/controllers/category.go index 02ad115bf..695e5d53e 100644 --- a/backend/src/controllers/category.go +++ b/backend/src/controllers/category.go @@ -1,10 +1,9 @@ package controllers import ( - "fmt" - "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" + "github.com/GenerateNU/sac/backend/src/utilities" "github.com/gofiber/fiber/v2" ) @@ -31,21 +30,15 @@ func NewCategoryController(categoryService services.CategoryServiceInterface) *C // @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 + var categoryBody models.CategoryRequestBody if err := c.BodyParser(&categoryBody); err != nil { - fmt.Print(err) - return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") - } - - category := models.Category{ - Name: categoryBody.Name, + return utilities.Error(c, fiber.StatusBadRequest, "failed to process the request") } - newCategory, err := t.categoryService.CreateCategory(category) - + newCategory, err := t.categoryService.CreateCategory(categoryBody) if err != nil { - return err + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusCreated).JSON(newCategory) diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index fda5186f9..31222e98a 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -3,6 +3,7 @@ package controllers import ( "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" + "github.com/GenerateNU/sac/backend/src/utilities" "github.com/gofiber/fiber/v2" ) @@ -29,16 +30,15 @@ func NewTagController(tagService services.TagServiceInterface) *TagController { // @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 + var tagBody models.TagRequestBody if err := c.BodyParser(&tagBody); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") + return utilities.Error(c, fiber.StatusBadRequest, "failed to process the request") } dbTag, err := t.tagService.CreateTag(tagBody) - if err != nil { - return err + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusCreated).JSON(&dbTag) @@ -59,9 +59,8 @@ func (t *TagController) CreateTag(c *fiber.Ctx) error { // @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 utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusOK).JSON(&tag) @@ -84,16 +83,15 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { // @Failure 500 {string} string "failed to update tag" // @Router /api/v1/tags/{id} [patch] func (t *TagController) UpdateTag(c *fiber.Ctx) error { - var tagBody models.UpdateTagRequestBody + var tagBody models.TagRequestBody if err := c.BodyParser(&tagBody); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") + return utilities.Error(c, fiber.StatusBadRequest, "failed to process the request") } tag, err := t.tagService.UpdateTag(c.Params("id"), tagBody) - if err != nil { - return err + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusOK).JSON(&tag) @@ -106,17 +104,16 @@ func (t *TagController) UpdateTag(c *fiber.Ctx) error { // @ID delete-tag // @Tags tag // @Param id path int true "Tag ID" -// @Success 204 +// @Success 204 {string} string "no content" // @Failure 400 {string} string "failed to validate id" -// @Failure 404 {string} string "failed to find tag" +// @Failure 404 {string} string "tag not found" // @Failure 500 {string} string "failed to delete tag" // @Router /api/v1/tags/{id} [delete] func (t *TagController) DeleteTag(c *fiber.Ctx) error { err := t.tagService.DeleteTag(c.Params("id")) - if err != nil { - return err + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) -} +} \ No newline at end of file diff --git a/backend/src/controllers/user.go b/backend/src/controllers/user.go index 57e6802be..fe4353020 100644 --- a/backend/src/controllers/user.go +++ b/backend/src/controllers/user.go @@ -1,8 +1,9 @@ package controllers import ( + "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" - + "github.com/GenerateNU/sac/backend/src/utilities" "github.com/gofiber/fiber/v2" ) @@ -26,9 +27,8 @@ func NewUserController(userService services.UserServiceInterface) *UserControlle // @Router /api/v1/users/ [get] func (u *UserController) GetAllUsers(c *fiber.Ctx) error { users, err := u.userService.GetAllUsers() - if err != nil { - return err + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusOK).JSON(users) @@ -36,21 +36,52 @@ func (u *UserController) GetAllUsers(c *fiber.Ctx) error { // GetUser godoc // -// @Summary Gets specific user -// @Description Returns specific user -// @ID get-user +// @Summary Gets a user +// @Description Returns a user +// @ID get-user-by-id // @Tags user // @Produce json +// @Param id path string true "User ID" // @Success 200 {object} models.User -// @Failure 400 {string} string "failed to validate id" -// @Failure 404 {string} string "failed to find user" -// @Failure 500 {string} string -// @Router /api/v1/users/ [get] +// @Failure 404 {string} string "user not found" +// @Failure 400 {string} string "failed to validate id" +// @Failure 500 {string} string "failed to get user" +// @Router /api/v1/users/:id [get] func (u *UserController) GetUser(c *fiber.Ctx) error { user, err := u.userService.GetUser(c.Params("id")) if err != nil { - return err + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusOK).JSON(user) } + +// UpdateUser godoc +// +// @Summary Updates a user +// @Description Updates a user +// @ID update-user-by-id +// @Tags user +// @Produce json +// @Success 200 {object} models.User +// @Failure 404 {string} string "user not found" +// @Failure 400 {string} string "invalid request body" +// @Failure 400 {string} string "failed to validate id" +// @Failure 500 {string} string "database error" +// @Failure 500 {string} string "failed to hash password" +// @Router /api/v1/users/:id [patch] +func (u *UserController) UpdateUser(c *fiber.Ctx) error { + var user models.UserRequestBody + + if err := c.BodyParser(&user); err != nil { + return utilities.Error(c, fiber.StatusBadRequest, "invalid request body") + } + + updatedUser, err := u.userService.UpdateUser(c.Params("id"), user) + if err != nil { + return utilities.Error(c, fiber.StatusInternalServerError, err.Error()) + } + + // Return the updated user details + return c.Status(fiber.StatusOK).JSON(updatedUser) +} diff --git a/backend/src/models/category.go b/backend/src/models/category.go index 5705ed9d8..73af78ca1 100644 --- a/backend/src/models/category.go +++ b/backend/src/models/category.go @@ -9,6 +9,6 @@ type Category struct { Tag []Tag `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } -type CreateCategoryRequestBody struct { +type CategoryRequestBody struct { Name string `gorm:"type:varchar(255)" json:"category_name" validate:"required,max=255"` } diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index 767b3d37b..7594c8459 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -16,15 +16,7 @@ type Tag struct { Event []Event `gorm:"many2many:event_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } -type PartialTag struct { +type TagRequestBody struct { Name string `json:"name" validate:"required,max=255"` CategoryID uint `json:"category_id" validate:"required,min=1"` -} - -type CreateTagRequestBody struct { - PartialTag -} - -type UpdateTagRequestBody struct { - PartialTag -} +} \ No newline at end of file diff --git a/backend/src/models/user.go b/backend/src/models/user.go index f5e4433e6..ed6f23952 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -56,3 +56,13 @@ type User struct { RSVP []Event `gorm:"many2many:user_event_rsvps;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` Waitlist []Event `gorm:"many2many:user_event_waitlists;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } + +type UserRequestBody struct { + NUID string `json:"nuid" validate:"required,len=9"` + FirstName string `json:"first_name" validate:"required,max=255"` + LastName string `json:"last_name" validate:"required,max=255"` + Email string `json:"email" validate:"required,email,neu_email,max=255"` + Password string `json:"password" validate:"required,password"` + College College `json:"college" validate:"required,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"` + Year Year `json:"year" validate:"required,min=1,max=6"` +} \ No newline at end of file diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 8388e82a2..b53c7f57e 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -3,6 +3,7 @@ package server import ( "github.com/GenerateNU/sac/backend/src/controllers" "github.com/GenerateNU/sac/backend/src/services" + "github.com/GenerateNU/sac/backend/src/utilities" "github.com/go-playground/validator/v10" "github.com/goccy/go-json" @@ -27,7 +28,7 @@ func Init(db *gorm.DB) *fiber.App { validate := validator.New(validator.WithRequiredStructEnabled()) // MARK: Custom validator tags can be registered here. - // validate.RegisterValidation("my_custom_validator", MyCustomValidatorFunc) + utilities.RegisterCustomValidators(validate) utilityRoutes(app) @@ -69,6 +70,7 @@ func userRoutes(router fiber.Router, userService services.UserServiceInterface) users.Get("/", userController.GetAllUsers) users.Get("/:id", userController.GetUser) + users.Patch("/:id", userController.UpdateUser) } func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) { @@ -84,8 +86,8 @@ func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) { tags := router.Group("/tags") - tags.Post("/", tagController.CreateTag) tags.Get("/:id", tagController.GetTag) + tags.Post("/", tagController.CreateTag) tags.Patch("/:id", tagController.UpdateTag) tags.Delete("/:id", tagController.DeleteTag) } diff --git a/backend/src/services/category.go b/backend/src/services/category.go index 738a49c47..2c9380b91 100644 --- a/backend/src/services/category.go +++ b/backend/src/services/category.go @@ -3,16 +3,18 @@ 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/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" "golang.org/x/text/cases" "golang.org/x/text/language" + + "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) type CategoryServiceInterface interface { - CreateCategory(category models.Category) (*models.Category, error) + CreateCategory(categoryBody models.CategoryRequestBody) (*models.Category, error) } type CategoryService struct { @@ -20,12 +22,17 @@ type CategoryService struct { Validate *validator.Validate } -func (c *CategoryService) CreateCategory(category models.Category) (*models.Category, error) { - if err := c.Validate.Struct(category); err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") +func (c *CategoryService) CreateCategory(categoryBody models.CategoryRequestBody) (*models.Category, error) { + if err := c.Validate.Struct(categoryBody); err != nil { + return nil, fiber.ErrBadRequest + } + + category, err := utilities.MapResponseToModel(categoryBody, &models.Category{}) + if err != nil { + return nil, fiber.ErrInternalServerError } category.Name = cases.Title(language.English).String(category.Name) - return transactions.CreateCategory(c.DB, category) + return transactions.CreateCategory(c.DB, *category) } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index a56f26a36..6304d17ee 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -10,9 +10,9 @@ import ( ) type TagServiceInterface interface { - CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) + CreateTag(tagBody models.TagRequestBody) (*models.Tag, error) GetTag(id string) (*models.Tag, error) - UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, error) + UpdateTag(id string, tagBody models.TagRequestBody) (*models.Tag, error) DeleteTag(id string) error } @@ -21,53 +21,50 @@ type TagService struct { Validate *validator.Validate } -func (t *TagService) CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, error) { - tag := models.Tag{ - Name: tagBody.Name, - CategoryID: tagBody.CategoryID, +func (t *TagService) CreateTag(tagBody models.TagRequestBody) (*models.Tag, error) { + if err := t.Validate.Struct(tagBody); err != nil { + return nil, fiber.ErrBadRequest } - if err := t.Validate.Struct(tag); err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") + tag, err := utilities.MapResponseToModel(tagBody, &models.Tag{}) + if err != nil { + return nil, fiber.ErrInternalServerError } - return transactions.CreateTag(t.DB, tag) + 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 nil, fiber.ErrBadRequest } return transactions.GetTag(t.DB, *idAsUint) } -func (t *TagService) UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, error) { +func (t *TagService) UpdateTag(id string, tagBody models.TagRequestBody) (*models.Tag, error) { idAsUint, err := utilities.ValidateID(id) - if err != nil { - return nil, err + return nil, fiber.ErrBadRequest } - tag := models.Tag{ - Name: tagBody.Name, - CategoryID: tagBody.CategoryID, + if err := t.Validate.Struct(tagBody); err != nil { + return nil, fiber.ErrBadRequest } - if err := t.Validate.Struct(tag); err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "failed to validate the data") + tag, err := utilities.MapResponseToModel(tagBody, &models.Tag{}) + if err != nil { + return nil, fiber.ErrInternalServerError } - return transactions.UpdateTag(t.DB, *idAsUint, tag) + return transactions.UpdateTag(t.DB, *idAsUint, *tag) } func (t *TagService) DeleteTag(id string) error { idAsUint, err := utilities.ValidateID(id) - if err != nil { - return err + return fiber.ErrBadRequest } return transactions.DeleteTag(t.DB, *idAsUint) diff --git a/backend/src/services/user.go b/backend/src/services/user.go index 44830a128..3d0a6a992 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -1,17 +1,20 @@ package services import ( + "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/transactions" "github.com/GenerateNU/sac/backend/src/utilities" "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) type UserServiceInterface interface { GetAllUsers() ([]models.User, error) - GetUser(string) (*models.User, error) + GetUser(id string) (*models.User, error) + UpdateUser(id string, userBody models.UserRequestBody) (*models.User, error) } type UserService struct { @@ -19,16 +22,42 @@ type UserService struct { Validate *validator.Validate } +// Gets all users (including soft deleted users) for testing func (u *UserService) GetAllUsers() ([]models.User, error) { return transactions.GetAllUsers(u.DB) } -func (u *UserService) GetUser(userID string) (*models.User, error) { - idAsUint, err := utilities.ValidateID(userID) - +func (u *UserService) GetUser(id string) (*models.User, error) { + idAsUint, err := utilities.ValidateID(id) if err != nil { - return nil, err + return nil, fiber.ErrBadRequest } return transactions.GetUser(u.DB, *idAsUint) } + +// Updates a user +func (u *UserService) UpdateUser(id string, userBody models.UserRequestBody) (*models.User, error) { + idAsUint, err := utilities.ValidateID(id) + if err != nil { + return nil, fiber.ErrBadRequest + } + + if err := u.Validate.Struct(userBody); err != nil { + return nil, fiber.ErrBadRequest + } + + passwordHash, err := auth.ComputePasswordHash(userBody.Password) + if err != nil { + return nil, fiber.ErrInternalServerError + } + + user, err := utilities.MapResponseToModel(userBody, &models.User{}) + if err != nil { + return nil, fiber.ErrInternalServerError + } + + user.PasswordHash = *passwordHash + + return transactions.UpdateUser(u.DB, *idAsUint, *user) +} diff --git a/backend/src/transactions/category.go b/backend/src/transactions/category.go index 209d941ed..017eb2199 100644 --- a/backend/src/transactions/category.go +++ b/backend/src/transactions/category.go @@ -14,14 +14,14 @@ func CreateCategory(db *gorm.DB, category models.Category) (*models.Category, er 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") + return nil, fiber.ErrInternalServerError } } else { - return nil, fiber.NewError(fiber.StatusBadRequest, "category with that name already exists") + return nil, fiber.ErrBadRequest } if err := db.Create(&category).Error; err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to create category") + return nil, fiber.ErrInternalServerError } return &category, nil @@ -32,9 +32,9 @@ func GetCategory(db *gorm.DB, id uint) (*models.Category, error) { if err := db.First(&category, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "invalid category id") + return nil, fiber.ErrNotFound } else { - return nil, fiber.NewError(fiber.StatusInternalServerError, "unable to retrieve category") + return nil, fiber.ErrInternalServerError } } diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index 8ba91210a..56e2ef248 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -11,8 +11,9 @@ import ( 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 nil, fiber.ErrInternalServerError } + return &tag, nil } @@ -21,9 +22,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.StatusNotFound, "failed to find tag") + return nil, fiber.ErrNotFound } else { - return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to retrieve tag") + return nil, fiber.ErrInternalServerError } } @@ -33,9 +34,9 @@ func GetTag(db *gorm.DB, id uint) (*models.Tag, error) { func UpdateTag(db *gorm.DB, id uint, tag models.Tag) (*models.Tag, error) { if err := db.Model(&models.Tag{}).Where("id = ?", id).Updates(tag).First(&tag, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "failed to find tag") + return nil, fiber.ErrNotFound } else { - return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to update tag") + return nil, fiber.ErrInternalServerError } } @@ -46,9 +47,9 @@ func UpdateTag(db *gorm.DB, id uint, tag models.Tag) (*models.Tag, error) { func DeleteTag(db *gorm.DB, id uint) error { if result := db.Delete(&models.Tag{}, id); result.RowsAffected == 0 { if result.Error != nil { - return fiber.NewError(fiber.StatusInternalServerError, "failed to delete tag") + return fiber.ErrInternalServerError } else { - return fiber.NewError(fiber.StatusNotFound, "failed to find tag") + return fiber.ErrNotFound } } diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 54792b068..10a3bbe72 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -13,7 +13,7 @@ func GetAllUsers(db *gorm.DB) ([]models.User, error) { var users []models.User if err := db.Omit("password_hash").Find(&users).Error; err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to get all users") + return nil, fiber.ErrInternalServerError } return users, nil @@ -24,10 +24,30 @@ func GetUser(db *gorm.DB, id uint) (*models.User, error) { if err := db.Omit("password_hash").First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "failed to find tag") + return nil, fiber.ErrNotFound + } else { + return nil, fiber.ErrInternalServerError } - return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to get user") } return &user, nil } + +func UpdateUser(db *gorm.DB, id uint, user models.User) (*models.User, error) { + var existingUser models.User + + err := db.First(&existingUser, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.ErrNotFound + } else { + return nil, fiber.ErrInternalServerError + } + } + + if err := db.Model(&existingUser).Updates(&user).Error; err != nil { + return nil, fiber.ErrInternalServerError + } + + return &existingUser, nil +} diff --git a/backend/src/utilities/error.go b/backend/src/utilities/error.go new file mode 100644 index 000000000..468f70004 --- /dev/null +++ b/backend/src/utilities/error.go @@ -0,0 +1,10 @@ +package utilities + +import ( + "github.com/gofiber/fiber/v2" +) + +// ErrorResponse sends a standardized error response +func Error(c *fiber.Ctx, statusCode int, message string) error { + return c.Status(statusCode).JSON(fiber.Map{"error": message}) +} diff --git a/backend/src/utilities/manipulator.go b/backend/src/utilities/manipulator.go new file mode 100644 index 000000000..bada3a068 --- /dev/null +++ b/backend/src/utilities/manipulator.go @@ -0,0 +1,25 @@ +package utilities + +import ( + "github.com/mitchellh/mapstructure" +) + +// MapResponseToModel maps response data to a target model using mapstructure +func MapResponseToModel[T any, U any](responseData T, targetModel *U) (*U, error) { + config := &mapstructure.DecoderConfig{ + Result: targetModel, + TagName: "json", + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return nil, err + } + + err = decoder.Decode(responseData) + if err != nil { + return nil, err + } + + return targetModel, nil +} \ No newline at end of file diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 07ef87d87..b032c1780 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -1,20 +1,40 @@ package utilities import ( + "regexp" "strconv" - "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + + "github.com/go-playground/validator/v10" + "github.com/mcnijman/go-emailaddress" ) -// Validate the data sent to the server if the data is invalid, return an error otherwise, return nil -func ValidateData(model interface{}) error { - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.Struct(model); err != nil { - return err +func RegisterCustomValidators(validate *validator.Validate) { + validate.RegisterValidation("neu_email", ValidateEmail) + validate.RegisterValidation("password", ValidatePassword) +} + +func ValidateEmail(fl validator.FieldLevel) bool { + email, err := emailaddress.Parse(fl.Field().String()) + if err != nil { + return false } - return nil + if email.Domain != "northeastern.edu" { + return false + } + + return true +} + +func ValidatePassword(fl validator.FieldLevel) bool { + if len(fl.Field().String()) < 8 { + return false + } + specialCharactersMatch, _ := regexp.MatchString("[@#%&*+]", fl.Field().String()) + numbersMatch, _ := regexp.MatchString("[0-9]", fl.Field().String()) + return specialCharactersMatch && numbersMatch } // Validates that an id follows postgres uint format, returns a uint otherwise returns an error diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index 693d93c8e..acbfeca14 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -1,8 +1,10 @@ package tests import ( + "bytes" "fmt" "net/http" + "net/http/httptest" "testing" "github.com/GenerateNU/sac/backend/src/models" @@ -112,3 +114,121 @@ func TestGetUserFailsNotFound(t *testing.T) { }, ).Close() } + +func TestUpdateUserWorks(t *testing.T) { + // initialize the test + app, assert := InitTest(t) + + user := models.User{ + Role: models.Student, + NUID: "123456789", + FirstName: "Melody", + LastName: "Yu", + Email: "melody.yu@northeastern.edu", + PasswordHash: "rainbows", + College: models.KCCS, + Year: models.Second, + } + + err := app.Conn.Create(&user).Error + assert.NilError(err) + + data := map[string]interface{}{ + "first_name": "Michael", + "last_name": "Brennan", + } + body, err := json.Marshal(data) + assert.NilError(err) + + req := httptest.NewRequest( + "PATCH", + fmt.Sprintf("%s/api/v1/users/%d", app.Address, user.ID), + bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + resp, _ := app.App.Test(req) + + var updatedUser models.User + err = json.NewDecoder(resp.Body).Decode(&updatedUser) + assert.NilError(err) + assert.Equal(resp.StatusCode, 200) + assert.Equal(updatedUser.FirstName, "Michael") + assert.Equal(updatedUser.LastName, "Brennan") +} + +func TestUpdateUserFailsOnInvalidParams(t *testing.T) { + // initialize the test + app, assert := InitTest(t) + + user := models.User{ + Role: models.Student, + NUID: "123456789", + FirstName: "Melody", + LastName: "Yu", + Email: "melody.yu@northeastern.edu", + PasswordHash: "rainbows", + College: models.KCCS, + Year: models.Second, + } + + err := app.Conn.Create(&user).Error + assert.NilError(err) + + // Each entry in invalid_datas represents JSON for a request that should fail (status code 400) + invalidDatas := []map[string]interface{}{ + {"email": "not.northeastern@gmail.com"}, + {"nuid": "1800-123-4567"}, + {"password": "1234"}, + {"year": 1963}, + {"college": "UT-Austin"}, + } + + for i := 0; i < len(invalidDatas); i++ { + body, err := json.Marshal(invalidDatas[i]) + assert.NilError(err) + + req := httptest.NewRequest( + "PATCH", + fmt.Sprintf("%s/api/v1/users/%d", app.Address, user.ID), + bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + resp, err := app.App.Test(req) + assert.NilError(err) + assert.Equal(resp.StatusCode, 400) + } +} + +func TestUpdateUserFailsOnInvalidId(t *testing.T) { + // initialize the test + app, assert := InitTest(t) + + user := models.User{ + Role: models.Student, + NUID: "123456789", + FirstName: "Melody", + LastName: "Yu", + Email: "melody.yu@northeastern.edu", + PasswordHash: "rainbows", + College: models.KCCS, + Year: models.Second, + } + + err := app.Conn.Create(&user).Error + assert.NilError(err) + + // User to update does not exist (should return 400) + data := map[string]interface{}{ + "first_name": "Michael", + "last_name": "Brennan", + } + body, err := json.Marshal(data) + assert.NilError(err) + + req := httptest.NewRequest( + "PATCH", + fmt.Sprintf("%s/api/v1/users/%d", app.Address, 12345678), + bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + resp, err := app.App.Test(req) + assert.NilError(err) + assert.Equal(resp.StatusCode, 404) +} diff --git a/backend/tests/utilities_test.go b/backend/tests/utilities_test.go index d7ed0bf33..0c73650b4 100644 --- a/backend/tests/utilities_test.go +++ b/backend/tests/utilities_test.go @@ -1,6 +1,7 @@ package tests import ( + "github.com/go-playground/validator/v10" "testing" "github.com/GenerateNU/sac/backend/src/utilities" @@ -18,3 +19,40 @@ func TestThatContainsWorks(t *testing.T) { assert.Assert(utilities.Contains(slice, "baz")) assert.Assert(!utilities.Contains(slice, "qux")) } + +func TestPasswordValidationWorks(t *testing.T) { + assert := assert.New(t) + + type Thing struct { + password string `validate:"password"` + } + + validate := validator.New() + validate.RegisterValidation("password", utilities.ValidatePassword) + + assert.NilError(validate.Struct(Thing{password: "password!56"})) + assert.NilError(validate.Struct(Thing{password: "cor+ect-h*rse-batte#ry-stap@le-100"})) + assert.NilError(validate.Struct(Thing{password: "1!gooood"})) + assert.Error(validate.Struct(Thing{password: "1!"})) + assert.Error(validate.Struct(Thing{password: "tooshor"})) + assert.Error(validate.Struct(Thing{password: "NoSpecialsOrNumbers"})) +} + +func TestEmailValidationWorks(t *testing.T) { + assert := assert.New(t) + + type Thing struct { + email string `validate:"neu_email"` + } + + validate := validator.New() + validate.RegisterValidation("neu_email", utilities.ValidateEmail) + + assert.NilError(validate.Struct(Thing{email: "brennan.mic@northeastern.edu"})) + assert.NilError(validate.Struct(Thing{email: "blerner@northeastern.edu"})) + assert.NilError(validate.Struct(Thing{email: "validemail@northeastern.edu"})) + assert.Error(validate.Struct(Thing{email: "notanortheasternemail@gmail.com"})) + assert.Error(validate.Struct(Thing{email: "random123@_#!$string"})) + assert.Error(validate.Struct(Thing{email: "local@mail"})) + +} diff --git a/frontend/node_modules/.yarn-integrity b/frontend/node_modules/.yarn-integrity new file mode 100644 index 000000000..044a5ddd9 --- /dev/null +++ b/frontend/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "darwin-arm64-115", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/go.work.sum b/go.work.sum index 7fa9fad8c..08a09683e 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,122 +1,2 @@ -cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= -cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= -cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= -cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= -cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= -cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= -cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= -cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= -cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= -github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= -github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= -github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= -github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= -github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= -github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= -github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= -github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= -github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk= -github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= -go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= -go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= -go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= -go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= -go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= -go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= -go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= -google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= -google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= -google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=