diff --git a/.github/workflows/auto_assign_author.yml b/.github/workflows/auto_assign_author.yml index 5ac4a7936..00512ecf4 100644 --- a/.github/workflows/auto_assign_author.yml +++ b/.github/workflows/auto_assign_author.yml @@ -2,7 +2,11 @@ name: Auto Assign Author on: pull_request: - types: [opened, ready_for_review, reopened] + types: [opened, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: auto-add-assignee: diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml index 78f14dcf8..797b2771b 100644 --- a/.github/workflows/auto_request_review.yml +++ b/.github/workflows/auto_request_review.yml @@ -2,8 +2,11 @@ name: Auto Request Review on: pull_request: - types: [opened, ready_for_review, reopened] + types: [opened, reopened] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: auto-request-review: runs-on: ubuntu-latest diff --git a/.github/workflows/go.yml b/.github/workflows/backend.yml similarity index 71% rename from .github/workflows/go.yml rename to .github/workflows/backend.yml index e0632ddb0..1fa9b0328 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/backend.yml @@ -1,16 +1,15 @@ -name: Go +name: Backend on: push: - branches: - - main paths: - "backend/**" - - ".github/workflows/go.yml" + - ".github/workflows/backend.yml" pull_request: + types: opened paths: - "backend/**" - - ".github/workflows/go.yml" + - ".github/workflows/backend.yml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -27,9 +26,23 @@ jobs: uses: actions/setup-go@v3 with: go-version: "1.21" - - name: Enforce formatting - run: gofmt -l ./backend/ | grep ".go$" | xargs -r echo "Files not formatted:" - + - name: Cache Go Modules + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Install gofumpt + run: go install mvdan.cc/gofumpt@latest + - name: Check code formatting + run: | + unformatted_files=$(gofumpt -l ./backend/) + if [ -n "$unformatted_files" ]; then + echo "Files not formatted:" + echo "$unformatted_files" + exit 1 + fi lint: name: Lint runs-on: ubuntu-latest @@ -48,7 +61,6 @@ jobs: echo "::error::Linting issues found" exit 1 fi - test: name: Test runs-on: ubuntu-latest @@ -68,6 +80,13 @@ jobs: uses: actions/setup-go@v3 with: go-version: "1.21" + - name: Cache Go Modules + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- - name: Install Dependencies run: cd ./backend/ && go get ./... - name: Increase max_connections in PostgreSQL diff --git a/.github/workflows/backend_codeql.yml b/.github/workflows/backend_codeql.yml new file mode 100644 index 000000000..9e394fc1a --- /dev/null +++ b/.github/workflows/backend_codeql.yml @@ -0,0 +1,44 @@ +name: Backend CodeQL + +on: + push: + paths: + - "backend/**" + - ".github/workflows/backend_codeql.yml" + pull_request: + types: opened + paths: + - "backend/**" + - ".github/workflows/backend_codeql.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.21" + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "go" + queries: security-and-quality + - name: Build + run: | + cd ./backend/ && go build -o backend src/main.go + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:go" diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml new file mode 100644 index 000000000..8abe3bc6d --- /dev/null +++ b/.github/workflows/cli.yml @@ -0,0 +1,63 @@ +name: CLI + +on: + push: + paths: + - "cli/**" + - ".github/workflows/cli.yml" + pull_request: + types: opened + paths: + - "cli/**" + - ".github/workflows/cli.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format: + name: Format + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.21" + - name: Cache Go Modules + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Install gofumpt + run: go install mvdan.cc/gofumpt@latest + - name: Check code formatting + run: | + unformatted_files=$(gofumpt -l ./cli/) + if [ -n "$unformatted_files" ]; then + echo "Files not formatted:" + echo "$unformatted_files" + exit 1 + fi + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.21" + - name: Enforce linting + run: | + cd ./cli/ && lint_output=$(go vet ./...) + if [[ -n "$lint_output" ]]; then + echo "$lint_output" + echo "::error::Linting issues found" + exit 1 + fi diff --git a/.github/workflows/cli_codeql.yml b/.github/workflows/cli_codeql.yml new file mode 100644 index 000000000..529636c27 --- /dev/null +++ b/.github/workflows/cli_codeql.yml @@ -0,0 +1,44 @@ +name: CLI CodeQL + +on: + push: + paths: + - "cli/**" + - ".github/workflows/cli_codeql.yml" + pull_request: + types: opened + paths: + - "cli/**" + - ".github/workflows/cli_codeql.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "1.21" + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "go" + queries: security-and-quality + - name: Build + run: | + cd ./cli/ && go build -o cli main.go + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:go" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 787c4b419..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: CodeQL - -on: - push: - branches: ["main"] - paths: - - "backend/**" - - "cli/**" - - "frontend/**" - - ".github/workflows/codeql.yml" - pull_request: - branches: ["main"] - paths: - - "backend/**" - - "cli/**" - - "frontend/**" - - ".github/workflows/codeql.yml" - schedule: - - cron: "0 0 * * 1" - -jobs: - analyze-js-ts: - name: Analyze JavaScript and TypeScript - runs-on: ubuntu-latest - if: (github.event_name == 'push' && contains(github.event.head_commit.modified, 'frontend/')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.changed_files, 'frontend/')) - permissions: - security-events: write - strategy: - fail-fast: false - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "javascript-typescript" - queries: security-and-quality - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:javascript-typescript" - - analyze-backend-go: - name: Analyze Backend Go - runs-on: ubuntu-latest - if: (github.event_name == 'push' && contains(github.event.head_commit.modified, 'backend/')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.changed_files, 'backend/')) - permissions: - security-events: write - strategy: - fail-fast: false - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: "1.21" - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "go" - queries: security-and-quality - - name: Build - run: | - cd ./backend/ && go build -o backend src/main.go - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:go" - - analyze-cli-go: - name: Analyze CLI Go - runs-on: ubuntu-latest - if: (github.event_name == 'push' && contains(github.event.head_commit.modified, 'cli/')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.changed_files, 'cli/')) - permissions: - security-events: write - strategy: - fail-fast: false - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: "1.21" - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "go" - queries: security-and-quality - - name: Build - run: | - cd ./cli/ && go build -o cli main.go - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:go" diff --git a/.github/workflows/mobile_codeql.yml b/.github/workflows/mobile_codeql.yml new file mode 100644 index 000000000..3b85ea66a --- /dev/null +++ b/.github/workflows/mobile_codeql.yml @@ -0,0 +1,41 @@ +name: Mobile CodeQL + +on: + push: + paths: + - "frontend/sac-mobile/**" + - ".github/workflows/mobile_codeql.yml" + pull_request: + types: opened + paths: + - "frontend/sac-mobile/**" + - ".github/workflows/mobile_codeql.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "javascript-typescript" + queries: security-and-quality + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + with: + working-directory: frontend/sac-mobile + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" diff --git a/.github/workflows/web_codeql.yml b/.github/workflows/web_codeql.yml new file mode 100644 index 000000000..1b6b87bc0 --- /dev/null +++ b/.github/workflows/web_codeql.yml @@ -0,0 +1,41 @@ +name: Web CodeQL + +on: + push: + paths: + - "frontend/sac-web/**" + - ".github/workflows/mweb_codeql.yml" + pull_request: + types: opened + paths: + - "frontend/sac-web/**" + - ".github/workflows/web_codeql.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: "javascript-typescript" + queries: security-and-quality + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + with: + working-directory: frontend/sac-web + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" diff --git a/README.md b/README.md index f9fdebd02..cf5aa1666 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,40 @@
- - Go Workflow Status + + Backend Workflow Status + + + + Backend CodeQL Workflow Status + + +
+ + + CLI Workflow Status + + + + CLI CodeQL Workflow Status + + +
+ + + Mobile CodeQL Workflow Status + + +
+ + + Web CodeQL Workflow Status
diff --git a/backend/src/auth/password.go b/backend/src/auth/password.go index 467a8d1c9..852d5c2ed 100644 --- a/backend/src/auth/password.go +++ b/backend/src/auth/password.go @@ -58,7 +58,6 @@ var ( func ComparePasswordAndHash(password string, encodedHash string) (bool, error) { p, salt, hash, err := decodeHash(encodedHash) - if err != nil { return false, err } @@ -82,7 +81,6 @@ func decodeHash(encodedHash string) (p *params, salt []byte, hash []byte, err er var version int _, err = fmt.Sscanf(vals[2], "v=%d", &version) - if err != nil { return nil, nil, nil, err } @@ -94,13 +92,11 @@ func decodeHash(encodedHash string) (p *params, salt []byte, hash []byte, err er 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 } @@ -108,7 +104,6 @@ func decodeHash(encodedHash string) (p *params, salt []byte, hash []byte, err er p.saltLength = uint32(len(salt)) hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) - if err != nil { return nil, nil, nil, err } diff --git a/backend/src/config/config.go b/backend/src/config/config.go index df5bd70dd..efc436980 100644 --- a/backend/src/config/config.go +++ b/backend/src/config/config.go @@ -77,7 +77,6 @@ const ( ) func GetConfiguration(path string) (Settings, error) { - var environment Environment if env := os.Getenv("APP_ENVIRONMENT"); env != "" { environment = Environment(env) @@ -124,7 +123,6 @@ func GetConfiguration(path string) (Settings, error) { portStr := os.Getenv(fmt.Sprintf("%sPORT", appPrefix)) portInt, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { return Settings{}, fmt.Errorf("failed to parse port: %w", err) } diff --git a/backend/src/controllers/category.go b/backend/src/controllers/category.go index ba7f24dd7..317e312bc 100644 --- a/backend/src/controllers/category.go +++ b/backend/src/controllers/category.go @@ -39,7 +39,6 @@ func (t *CategoryController) CreateCategory(c *fiber.Ctx) error { } newCategory, err := t.categoryService.CreateCategory(categoryBody) - if err != nil { return err.FiberError(c) } @@ -130,7 +129,6 @@ func (t *CategoryController) UpdateCategory(c *fiber.Ctx) error { } updatedCategory, err := t.categoryService.UpdateCategory(c.Params("id"), category) - if err != nil { return err.FiberError(c) } diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index c5acce3e6..78ca83d77 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -36,7 +36,7 @@ func (t *TagController) CreateTag(c *fiber.Ctx) error { return errors.FailedToParseRequestBody.FiberError(c) } - dbTag, err := t.tagService.CreateTag(tagBody) + dbTag, err := t.tagService.CreateTag(c.Params("categoryID"), tagBody) if err != nil { return err.FiberError(c) } @@ -58,8 +58,7 @@ func (t *TagController) CreateTag(c *fiber.Ctx) error { // @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")) - + tag, err := t.tagService.GetTag(c.Params("categoryID"), c.Params("tagID")) if err != nil { return err.FiberError(c) } @@ -90,7 +89,7 @@ func (t *TagController) UpdateTag(c *fiber.Ctx) error { return errors.FailedToParseRequestBody.FiberError(c) } - tag, err := t.tagService.UpdateTag(c.Params("id"), tagBody) + tag, err := t.tagService.UpdateTag(c.Params("categoryID"), c.Params("tagID"), tagBody) if err != nil { return err.FiberError(c) } @@ -111,7 +110,7 @@ func (t *TagController) UpdateTag(c *fiber.Ctx) error { // @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")) + err := t.tagService.DeleteTag(c.Params("categoryID"), c.Params("tagID")) if err != nil { return err.FiberError(c) } diff --git a/backend/src/database/db.go b/backend/src/database/db.go index f29639944..edceaf962 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -16,13 +16,11 @@ func ConfigureDB(settings config.Settings) (*gorm.DB, error) { SkipDefaultTransaction: true, TranslateError: true, }) - if err != nil { return nil, err } err = db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error - if err != nil { return nil, err } @@ -36,7 +34,6 @@ func ConfigureDB(settings config.Settings) (*gorm.DB, error) { func ConnPooling(db *gorm.DB) error { sqlDB, err := db.DB() - if err != nil { return err } @@ -59,7 +56,6 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error { &models.User{}, &models.Membership{}, ) - if err != nil { return err } diff --git a/backend/src/errors/user.go b/backend/src/errors/user.go index dd80dbb98..de07afbb7 100644 --- a/backend/src/errors/user.go +++ b/backend/src/errors/user.go @@ -7,9 +7,9 @@ var ( StatusCode: fiber.StatusBadRequest, Message: "failed to validate user", } - FailedToValidateUserTags = Error { + FailedToValidateUserTags = Error{ StatusCode: fiber.StatusBadRequest, - Message: "failed to validate user tags", + Message: "failed to validate user tags", } FailedToCreateUser = Error{ StatusCode: fiber.StatusInternalServerError, diff --git a/backend/src/models/contact.go b/backend/src/models/contact.go index 527ad678d..8c86ada90 100644 --- a/backend/src/models/contact.go +++ b/backend/src/models/contact.go @@ -2,23 +2,26 @@ package models import "github.com/google/uuid" -type Media string +type ContactType string const ( - Facebook Media = "facebook" - Instagram Media = "instagram" - Twitter Media = "twitter" - LinkedIn Media = "linkedin" - YouTube Media = "youtube" - GitHub Media = "github" - Custom Media = "custom" + Facebook ContactType = "facebook" + Instagram ContactType = "instagram" + Twitter ContactType = "twitter" + LinkedIn ContactType = "linkedin" + YouTube ContactType = "youtube" + GitHub ContactType = "github" + Slack ContactType = "slack" + Discord ContactType = "discord" + Email ContactType = "email" + CustomSite ContactType = "customSite" ) type Contact struct { Model - Type Media `gorm:"type:varchar(255)" json:"type" validate:"required,max=255"` - Content string `gorm:"type:varchar(255)" json:"content" validate:"required,http_url,max=255"` // media URL + Type ContactType `gorm:"type:varchar(255)" json:"type" validate:"required,max=255"` + Content string `gorm:"type:varchar(255)" json:"content" validate:"required,contact_pointer,max=255"` ClubID uuid.UUID `gorm:"foreignKey:ClubID" json:"-" validate:"uuid4"` } diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index 36c71381a..15aac5b04 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -15,6 +15,5 @@ type Tag struct { } type TagRequestBody struct { - Name string `json:"name" validate:"required,max=255"` - CategoryID uuid.UUID `json:"category_id" validate:"required,uuid4"` + Name string `json:"name" validate:"required,max=255"` } diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 5387cf298..63b4cc021 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -116,7 +116,7 @@ func authRoutes(router fiber.Router, authService services.AuthServiceInterface) auth.Get("/me", authController.Me) } -func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) { +func categoryRoutes(router fiber.Router, categoryService services.CategoryServiceInterface) fiber.Router { categoryController := controllers.NewCategoryController(categoryService) categories := router.Group("/categories") @@ -126,15 +126,17 @@ func categoryRoutes(router fiber.Router, categoryService services.CategoryServic categories.Get("/:id", categoryController.GetCategory) categories.Delete("/:id", categoryController.DeleteCategory) categories.Patch("/:id", categoryController.UpdateCategory) + + return categories } func tagRoutes(router fiber.Router, tagService services.TagServiceInterface) { tagController := controllers.NewTagController(tagService) - tags := router.Group("/tags") + tags := router.Group("/:categoryID/tags") - tags.Get("/:id", tagController.GetTag) + tags.Get("/:tagID", tagController.GetTag) tags.Post("/", tagController.CreateTag) - tags.Patch("/:id", tagController.UpdateTag) - tags.Delete("/:id", tagController.DeleteTag) + tags.Patch("/:tagID", tagController.UpdateTag) + tags.Delete("/:tagID", tagController.DeleteTag) } diff --git a/backend/src/services/category.go b/backend/src/services/category.go index 88817febc..ead4df2c0 100644 --- a/backend/src/services/category.go +++ b/backend/src/services/category.go @@ -48,13 +48,11 @@ func (c *CategoryService) CreateCategory(categoryBody models.CategoryRequestBody func (c *CategoryService) GetCategories(limit string, page string) ([]models.Category, *errors.Error) { limitAsInt, err := utilities.ValidateNonNegative(limit) - if err != nil { return nil, &errors.FailedToValidateLimit } pageAsInt, err := utilities.ValidateNonNegative(page) - if err != nil { return nil, &errors.FailedToValidatePage } @@ -66,7 +64,6 @@ func (c *CategoryService) GetCategories(limit string, page string) ([]models.Cat func (c *CategoryService) GetCategory(id string) (*models.Category, *errors.Error) { idAsUUID, err := utilities.ValidateID(id) - if err != nil { return nil, err } diff --git a/backend/src/services/club.go b/backend/src/services/club.go index f6baef065..c05610c1f 100644 --- a/backend/src/services/club.go +++ b/backend/src/services/club.go @@ -29,13 +29,11 @@ func NewClubService(db *gorm.DB, validate *validator.Validate) *ClubService { func (c *ClubService) GetClubs(limit string, page string) ([]models.Club, *errors.Error) { limitAsInt, err := utilities.ValidateNonNegative(limit) - if err != nil { return nil, &errors.FailedToValidateLimit } pageAsInt, err := utilities.ValidateNonNegative(page) - if err != nil { return nil, &errors.FailedToValidatePage } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index bf404ed5e..3c4784cbe 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -10,10 +10,10 @@ import ( ) type TagServiceInterface interface { - CreateTag(tagBody models.TagRequestBody) (*models.Tag, *errors.Error) - GetTag(id string) (*models.Tag, *errors.Error) - UpdateTag(id string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) - DeleteTag(id string) *errors.Error + CreateTag(categoryID string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) + GetTag(categoryID string, tagID string) (*models.Tag, *errors.Error) + UpdateTag(categoryID string, tagID string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) + DeleteTag(categoryID string, tagID string) *errors.Error } type TagService struct { @@ -25,7 +25,12 @@ func NewTagService(db *gorm.DB, validate *validator.Validate) *TagService { return &TagService{DB: db, Validate: validate} } -func (t *TagService) CreateTag(tagBody models.TagRequestBody) (*models.Tag, *errors.Error) { +func (t *TagService) CreateTag(categoryID string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) { + categoryIDAsUUID, idErr := utilities.ValidateID(categoryID) + if idErr != nil { + return nil, idErr + } + if err := t.Validate.Struct(tagBody); err != nil { return nil, &errors.FailedToValidateTag } @@ -35,20 +40,36 @@ func (t *TagService) CreateTag(tagBody models.TagRequestBody) (*models.Tag, *err return nil, &errors.FailedToMapRequestToModel } + tag.CategoryID = *categoryIDAsUUID + return transactions.CreateTag(t.DB, *tag) } -func (t *TagService) GetTag(id string) (*models.Tag, *errors.Error) { - idAsUUID, err := utilities.ValidateID(id) - if err != nil { - return nil, err +func (t *TagService) GetTag(categoryID string, tagID string) (*models.Tag, *errors.Error) { + categoryIDAsUUID, idErr := utilities.ValidateID(categoryID) + + if idErr != nil { + return nil, idErr + } + + tagIDAsUUID, idErr := utilities.ValidateID(tagID) + + if idErr != nil { + return nil, idErr } - return transactions.GetTag(t.DB, *idAsUUID) + return transactions.GetTag(t.DB, *categoryIDAsUUID, *tagIDAsUUID) } -func (t *TagService) UpdateTag(id string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) { - idAsUUID, idErr := utilities.ValidateID(id) +func (t *TagService) UpdateTag(categoryID string, tagID string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) { + categoryIDAsUUID, idErr := utilities.ValidateID(categoryID) + + if idErr != nil { + return nil, idErr + } + + tagIDAsUUID, idErr := utilities.ValidateID(tagID) + if idErr != nil { return nil, idErr } @@ -62,14 +83,23 @@ func (t *TagService) UpdateTag(id string, tagBody models.TagRequestBody) (*model return nil, &errors.FailedToMapRequestToModel } - return transactions.UpdateTag(t.DB, *idAsUUID, *tag) + tag.CategoryID = *categoryIDAsUUID + + return transactions.UpdateTag(t.DB, *tagIDAsUUID, *tag) } -func (t *TagService) DeleteTag(id string) *errors.Error { - idAsUUID, err := utilities.ValidateID(id) - if err != nil { - return &errors.FailedToValidateID +func (t *TagService) DeleteTag(categoryID string, tagID string) *errors.Error { + categoryIDAsUUID, idErr := utilities.ValidateID(categoryID) + + if idErr != nil { + return idErr + } + + tagIDAsUUID, idErr := utilities.ValidateID(tagID) + + if idErr != nil { + return idErr } - return transactions.DeleteTag(t.DB, *idAsUUID) + return transactions.DeleteTag(t.DB, *categoryIDAsUUID, *tagIDAsUUID) } diff --git a/backend/src/services/user.go b/backend/src/services/user.go index f05488f8c..4a985332b 100644 --- a/backend/src/services/user.go +++ b/backend/src/services/user.go @@ -134,7 +134,6 @@ func (u *UserService) CreateUserTags(id string, tagIDs models.CreateUserTagsBody // Retrieve a list of valid tags from the ids: tags, err := transactions.GetTagsByIDs(u.DB, tagIDs.Tags) - if err != nil { return nil, err } diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index 2101ae177..e2ad6c689 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -2,6 +2,7 @@ package transactions import ( stdliberrors "errors" + "github.com/GenerateNU/sac/backend/src/errors" "github.com/google/uuid" @@ -11,17 +12,32 @@ import ( ) func CreateTag(db *gorm.DB, tag models.Tag) (*models.Tag, *errors.Error) { - if err := db.Create(&tag).Error; err != nil { + tx := db.Begin() + + var category models.Category + if err := tx.Where("id = ?", tag.CategoryID).First(&category).Error; err != nil { + if err == gorm.ErrRecordNotFound { + tx.Rollback() + return nil, &errors.CategoryNotFound + } else { + tx.Rollback() + return nil, &errors.InternalServerError + } + } + + if err := tx.Create(&tag).Error; err != nil { + tx.Rollback() return nil, &errors.FailedToCreateTag } + tx.Commit() + return &tag, nil } -func GetTag(db *gorm.DB, id uuid.UUID) (*models.Tag, *errors.Error) { +func GetTag(db *gorm.DB, categoryID uuid.UUID, tagID uuid.UUID) (*models.Tag, *errors.Error) { var tag models.Tag - - if err := db.First(&tag, id).Error; err != nil { + if err := db.Where("category_id = ? AND id = ?", categoryID, tagID).First(&tag).Error; err != nil { if stdliberrors.Is(err, gorm.ErrRecordNotFound) { return nil, &errors.TagNotFound } else { @@ -44,8 +60,8 @@ func UpdateTag(db *gorm.DB, id uuid.UUID, tag models.Tag) (*models.Tag, *errors. return &tag, nil } -func DeleteTag(db *gorm.DB, id uuid.UUID) *errors.Error { - if result := db.Delete(&models.Tag{}, id); result.RowsAffected == 0 { +func DeleteTag(db *gorm.DB, categoryID uuid.UUID, tagID uuid.UUID) *errors.Error { + if result := db.Where("category_id = ? AND id = ?", categoryID, tagID).Delete(&models.Tag{}); result.RowsAffected == 0 { if result.Error == nil { return &errors.TagNotFound } else { @@ -62,7 +78,7 @@ func GetTagsByIDs(db *gorm.DB, selectedTagIDs []uuid.UUID) ([]models.Tag, *error if err := db.Model(models.Tag{}).Where("id IN ?", selectedTagIDs).Find(&tags).Error; err != nil { return nil, &errors.FailedToGetTag } - + return tags, nil } return []models.Tag{}, nil diff --git a/backend/src/transactions/user.go b/backend/src/transactions/user.go index 6ab1f9b0c..73d9f07c2 100644 --- a/backend/src/transactions/user.go +++ b/backend/src/transactions/user.go @@ -94,7 +94,7 @@ func GetUserTags(db *gorm.DB, id uuid.UUID) ([]models.Tag, *errors.Error) { return nil, &errors.UserNotFound } - if err := db.Model(&user).Association("Tag").Find(&tags) ; err != nil { + if err := db.Model(&user).Association("Tag").Find(&tags); err != nil { return nil, &errors.FailedToGetTag } return tags, nil diff --git a/backend/src/utilities/manipulator.go b/backend/src/utilities/manipulator.go index 64486b93e..ff0a1d791 100644 --- a/backend/src/utilities/manipulator.go +++ b/backend/src/utilities/manipulator.go @@ -4,7 +4,6 @@ import ( "github.com/mitchellh/mapstructure" ) - // MapRequestToModel maps request data to a target model using mapstructure func MapRequestToModel[T any, U any](responseData T, targetModel *U) (*U, error) { config := &mapstructure.DecoderConfig{ diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 8d27f037e..dc46d7392 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -5,6 +5,8 @@ import ( "strconv" "github.com/GenerateNU/sac/backend/src/errors" + "github.com/GenerateNU/sac/backend/src/models" + "github.com/google/uuid" "github.com/mcnijman/go-emailaddress" @@ -18,8 +20,11 @@ func RegisterCustomValidators() *validator.Validate { validate.RegisterValidation("password", validatePassword) validate.RegisterValidation("mongo_url", validateMongoURL) validate.RegisterValidation("s3_url", validateS3URL) + validate.RegisterValidation("contact_pointer", func(fl validator.FieldLevel) bool { + return validateContactPointer(validate, fl) + }) - return validate + return validate } func validateEmail(fl validator.FieldLevel) bool { @@ -52,9 +57,23 @@ func validateS3URL(fl validator.FieldLevel) bool { return true } +func validateContactPointer(validate *validator.Validate, fl validator.FieldLevel) bool { + contact, ok := fl.Parent().Interface().(models.Contact) + + if !ok { + return false + } + + switch contact.Type { + case models.Email: + return validate.Var(contact.Content, "email") == nil + default: + return validate.Var(contact.Content, "http_url") == nil + } +} + func ValidateID(id string) (*uuid.UUID, *errors.Error) { idAsUUID, err := uuid.Parse(id) - if err != nil { return nil, &errors.FailedToValidateID } diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index f96a2abe7..61fbe6018 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -165,7 +165,7 @@ func TestCreateCategoryFailsIfNameIsMissing(t *testing.T) { func TestCreateCategoryFailsIfCategoryWithThatNameAlreadyExists(t *testing.T) { existingAppAssert, _ := CreateSampleCategory(t, nil) - var TestNumCategoriesRemainsAt1 = func(app TestApp, assert *assert.A, resp *http.Response) { + TestNumCategoriesRemainsAt1 := func(app TestApp, assert *assert.A, resp *http.Response) { AssertNumCategoriesRemainsAtN(app, assert, resp, 1) } @@ -295,7 +295,7 @@ func TestUpdateCategoryWorks(t *testing.T) { "name": "Arts & Crafts", } - var AssertUpdatedCategoryBodyRespDB = func(app TestApp, assert *assert.A, resp *http.Response) { + AssertUpdatedCategoryBodyRespDB := func(app TestApp, assert *assert.A, resp *http.Response) { AssertUpdatedCategoryBodyRespDB(app, assert, resp, &category) } @@ -317,7 +317,7 @@ func TestUpdateCategoryWorksWithSameDetails(t *testing.T) { category := *SampleCategoryFactory() category["id"] = uuid - var AssertSampleCategoryBodyRespDB = func(app TestApp, assert *assert.A, resp *http.Response) { + AssertSampleCategoryBodyRespDB := func(app TestApp, assert *assert.A, resp *http.Response) { AssertUpdatedCategoryBodyRespDB(app, assert, resp, &category) } diff --git a/backend/tests/api/club_test.go b/backend/tests/api/club_test.go index fb78e4fd2..b24f1ac7d 100644 --- a/backend/tests/api/club_test.go +++ b/backend/tests/api/club_test.go @@ -283,7 +283,6 @@ func TestCreateClubFailsOnInvalidRecruitmentType(t *testing.T) { "https://google.com", }, ) - } func TestCreateClubFailsOnInvalidApplicationLink(t *testing.T) { @@ -294,7 +293,6 @@ func TestCreateClubFailsOnInvalidApplicationLink(t *testing.T) { "@#139081#$Ad_Axf", }, ) - } func TestCreateClubFailsOnInvalidLogo(t *testing.T) { diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 09ee4ff87..dc728ac53 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -44,13 +44,11 @@ type TestApp struct { func spawnApp() (TestApp, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { return TestApp{}, err } configuration, err := config.GetConfiguration(filepath.Join("..", "..", "..", "config")) - if err != nil { return TestApp{}, err } @@ -60,7 +58,6 @@ func spawnApp() (TestApp, error) { configuration.Database.DatabaseName = generateRandomDBName() connectionWithDB, err := configureDatabase(configuration) - if err != nil { return TestApp{}, err } @@ -73,6 +70,7 @@ func spawnApp() (TestApp, error) { InitialDBName: initialDBName, }, nil } + func generateRandomInt(max int64) int64 { randInt, _ := crand.Int(crand.Reader, big.NewInt(max)) return randInt.Int64() @@ -104,18 +102,15 @@ func configureDatabase(config config.Settings) (*gorm.DB, error) { dsnWithDB := config.Database.WithDb() dbWithDB, err := gorm.Open(gormPostgres.Open(dsnWithDB), &gorm.Config{SkipDefaultTransaction: true, TranslateError: true}) - if err != nil { return nil, err } err = dbWithDB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error - if err != nil { return nil, err } err = database.MigrateDB(config, dbWithDB) - if err != nil { return nil, err } @@ -130,13 +125,11 @@ type ExistingAppAssert struct { func (eaa ExistingAppAssert) Close() { db, err := eaa.App.Conn.DB() - if err != nil { panic(err) } err = db.Close() - if err != nil { panic(err) } diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 6c9d07db1..17f4f8b1b 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -14,10 +14,9 @@ import ( "github.com/goccy/go-json" ) -func SampleTagFactory(categoryID uuid.UUID) *map[string]interface{} { +func SampleTagFactory() *map[string]interface{} { return &map[string]interface{}{ - "name": "Generate", - "category_id": categoryID, + "name": "Generate", } } @@ -39,28 +38,29 @@ func AssertTagWithBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, assert.Equal(dbTag.CategoryID, respTag.CategoryID) assert.Equal((*body)["name"].(string), dbTag.Name) - assert.Equal((*body)["category_id"].(uuid.UUID), dbTag.CategoryID) return dbTag.ID } func AssertSampleTagBodyRespDB(t *testing.T, app TestApp, assert *assert.A, resp *http.Response) uuid.UUID { - appAssert, uuid := CreateSampleCategory(t, &ExistingAppAssert{App: app, - Assert: assert}) - return AssertTagWithBodyRespDB(appAssert.App, appAssert.Assert, resp, SampleTagFactory(uuid)) + appAssert, _ := CreateSampleCategory(t, &ExistingAppAssert{ + App: app, + Assert: assert, + }) + return AssertTagWithBodyRespDB(appAssert.App, appAssert.Assert, resp, SampleTagFactory()) } func CreateSampleTag(t *testing.T) (appAssert ExistingAppAssert, categoryUUID uuid.UUID, tagUUID uuid.UUID) { appAssert, categoryUUID = CreateSampleCategory(t, nil) - var AssertSampleTagBodyRespDB = func(app TestApp, assert *assert.A, resp *http.Response) { - tagUUID = AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory(categoryUUID)) + AssertSampleTagBodyRespDB := func(app TestApp, assert *assert.A, resp *http.Response) { + tagUUID = AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory()) } TestRequest{ Method: fiber.MethodPost, - Path: "/api/v1/tags/", - Body: SampleTagFactory(categoryUUID), + Path: fmt.Sprintf("/api/v1/categories/%s/tags", categoryUUID), + Body: SampleTagFactory(), }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ Status: fiber.StatusCreated, @@ -95,54 +95,65 @@ func Assert1Tag(app TestApp, assert *assert.A, resp *http.Response) { } func TestCreateTagFailsBadRequest(t *testing.T) { + appAssert, categoryUUID := CreateSampleCategory(t, nil) + badBodys := []map[string]interface{}{ { - "name": "Generate", - "category_id": "1", - }, - { - "name": 1, - "category_id": 1, + "name": 1, }, } for _, badBody := range badBodys { TestRequest{ Method: fiber.MethodPost, - Path: "/api/v1/tags/", + Path: fmt.Sprintf("/api/v1/categories/%s/tags", categoryUUID), Body: &badBody, - }.TestOnErrorAndDB(t, nil, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.FailedToParseRequestBody, DBTester: AssertNoTags, }, - ).Close() + ) } + + appAssert.Close() +} + +func TestCreateTagFailsCategoryNotFound(t *testing.T) { + uuid := uuid.New() + TestRequest{ + Method: fiber.MethodPost, + Path: fmt.Sprintf("/api/v1/categories/%s/tags", uuid), + Body: SampleTagFactory(), + }.TestOnErrorAndDB(t, nil, + ErrorWithDBTester{ + Error: errors.CategoryNotFound, + DBTester: AssertNoTags, + }, + ).Close() } func TestCreateTagFailsValidation(t *testing.T) { + appAssert, categoryUUID := CreateSampleCategory(t, nil) + badBodys := []map[string]interface{}{ - { - "name": "Generate", - }, - { - "category_id": uuid.New(), - }, {}, } for _, badBody := range badBodys { TestRequest{ Method: fiber.MethodPost, - Path: "/api/v1/tags/", + Path: fmt.Sprintf("/api/v1/categories/%s/tags", categoryUUID), Body: &badBody, - }.TestOnErrorAndDB(t, nil, + }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.FailedToValidateTag, DBTester: AssertNoTags, }, - ).Close() + ) } + + appAssert.Close() } func TestGetTagWorks(t *testing.T) { @@ -150,18 +161,41 @@ func TestGetTagWorks(t *testing.T) { TestRequest{ Method: fiber.MethodGet, - Path: fmt.Sprintf("/api/v1/tags/%s", tagUUID), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID), }.TestOnStatusAndDB(t, &existingAppAssert, DBTesterWithStatus{ Status: fiber.StatusOK, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory(categoryUUID)) + AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory()) }, }, ).Close() } -func TestGetTagFailsBadRequest(t *testing.T) { +func TestGetTagFailsCategoryBadRequest(t *testing.T) { + appAssert, _, tagUUID := CreateSampleTag(t) + + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodGet, + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", badRequest, tagUUID), + }.TestOnError(t, &appAssert, errors.FailedToValidateID) + } + + appAssert.Close() +} + +func TestGetTagFailsTagBadRequest(t *testing.T) { + appAssert, categoryUUID := CreateSampleCategory(t, nil) + badRequests := []string{ "0", "-1", @@ -173,31 +207,44 @@ func TestGetTagFailsBadRequest(t *testing.T) { for _, badRequest := range badRequests { TestRequest{ Method: fiber.MethodGet, - Path: fmt.Sprintf("/api/v1/tags/%s", badRequest), - }.TestOnError(t, nil, errors.FailedToValidateID).Close() + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, badRequest), + }.TestOnError(t, &appAssert, errors.FailedToValidateID) } + + appAssert.Close() } -func TestGetTagFailsNotFound(t *testing.T) { +func TestGetTagFailsCategoryNotFound(t *testing.T) { + appAssert, _, tagUUID := CreateSampleTag(t) + TestRequest{ Method: fiber.MethodGet, - Path: fmt.Sprintf("/api/v1/tags/%s", uuid.New()), - }.TestOnError(t, nil, errors.TagNotFound).Close() + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", uuid.New(), tagUUID), + }.TestOnError(t, &appAssert, errors.TagNotFound).Close() +} + +func TestGetTagFailsTagNotFound(t *testing.T) { + appAssert, categoryUUID := CreateSampleCategory(t, nil) + + TestRequest{ + Method: fiber.MethodGet, + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, uuid.New()), + }.TestOnError(t, &appAssert, errors.TagNotFound).Close() } func TestUpdateTagWorksUpdateName(t *testing.T) { existingAppAssert, categoryUUID, tagUUID := CreateSampleTag(t) - generateNUTag := *SampleTagFactory(categoryUUID) + generateNUTag := *SampleTagFactory() generateNUTag["name"] = "GenerateNU" - var AssertUpdatedTagBodyRespDB = func(app TestApp, assert *assert.A, resp *http.Response) { + AssertUpdatedTagBodyRespDB := func(app TestApp, assert *assert.A, resp *http.Response) { tagUUID = AssertTagWithBodyRespDB(app, assert, resp, &generateNUTag) } TestRequest{ Method: fiber.MethodPatch, - Path: fmt.Sprintf("/api/v1/tags/%s", tagUUID), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID), Body: &generateNUTag, }.TestOnStatusAndDB(t, &existingAppAssert, DBTesterWithStatus{ @@ -208,15 +255,13 @@ func TestUpdateTagWorksUpdateName(t *testing.T) { } func TestUpdateTagWorksUpdateCategory(t *testing.T) { - existingAppAssert, _, tagUUID := CreateSampleTag(t) + existingAppAssert, categoryUUID, tagUUID := CreateSampleTag(t) technologyCategory := *SampleCategoryFactory() technologyCategory["name"] = "Technology" - var technologyCategoryUUID uuid.UUID - - var AssertNewCategoryBodyRespDB = func(app TestApp, assert *assert.A, resp *http.Response) { - technologyCategoryUUID = AssertCategoryWithBodyRespDBMostRecent(app, assert, resp, &technologyCategory) + AssertNewCategoryBodyRespDB := func(app TestApp, assert *assert.A, resp *http.Response) { + AssertCategoryWithBodyRespDBMostRecent(app, assert, resp, &technologyCategory) } TestRequest{ @@ -230,15 +275,15 @@ func TestUpdateTagWorksUpdateCategory(t *testing.T) { }, ) - technologyTag := *SampleTagFactory(technologyCategoryUUID) + technologyTag := *SampleTagFactory() - var AssertUpdatedTagBodyRespDB = func(app TestApp, assert *assert.A, resp *http.Response) { + AssertUpdatedTagBodyRespDB := func(app TestApp, assert *assert.A, resp *http.Response) { AssertTagWithBodyRespDB(app, assert, resp, &technologyTag) } TestRequest{ Method: fiber.MethodPatch, - Path: fmt.Sprintf("/api/v1/tags/%s", tagUUID), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID), Body: &technologyTag, }.TestOnStatusAndDB(t, &existingAppAssert, DBTesterWithStatus{ @@ -253,20 +298,42 @@ func TestUpdateTagWorksWithSameDetails(t *testing.T) { TestRequest{ Method: fiber.MethodPatch, - Path: fmt.Sprintf("/api/v1/tags/%s", tagUUID), - Body: SampleTagFactory(categoryUUID), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID), + Body: SampleTagFactory(), }.TestOnStatusAndDB(t, &existingAppAssert, DBTesterWithStatus{ Status: fiber.StatusOK, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory(categoryUUID)) + AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory()) }, }, ).Close() } -func TestUpdateTagFailsBadRequest(t *testing.T) { - appAssert, uuid := CreateSampleCategory(t, nil) +func TestUpdateTagFailsCategoryBadRequest(t *testing.T) { + appAssert, _, tagUUID := CreateSampleTag(t) + + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodPatch, + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", badRequest, tagUUID), + Body: SampleTagFactory(), + }.TestOnError(t, &appAssert, errors.FailedToValidateID) + } + + appAssert.Close() +} + +func TestUpdateTagFailsTagBadRequest(t *testing.T) { + appAssert, categoryUUID := CreateSampleCategory(t, nil) badRequests := []string{ "0", @@ -279,18 +346,20 @@ func TestUpdateTagFailsBadRequest(t *testing.T) { for _, badRequest := range badRequests { TestRequest{ Method: fiber.MethodPatch, - Path: fmt.Sprintf("/api/v1/tags/%s", badRequest), - Body: SampleTagFactory(uuid), - }.TestOnError(t, &appAssert, errors.FailedToValidateID).Close() + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, badRequest), + Body: SampleTagFactory(), + }.TestOnError(t, &appAssert, errors.FailedToValidateID) } + + appAssert.Close() } func TestDeleteTagWorks(t *testing.T) { - existingAppAssert, _, tagUUID := CreateSampleTag(t) + existingAppAssert, categoryUUID, tagUUID := CreateSampleTag(t) TestRequest{ Method: fiber.MethodDelete, - Path: fmt.Sprintf("/api/v1/tags/%s", tagUUID), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID), }.TestOnStatusAndDB(t, &existingAppAssert, DBTesterWithStatus{ Status: fiber.StatusNoContent, @@ -299,8 +368,8 @@ func TestDeleteTagWorks(t *testing.T) { ).Close() } -func TestDeleteTagFailsBadRequest(t *testing.T) { - appAssert, _, _ := CreateSampleTag(t) +func TestDeleteTagFailsCategoryBadRequest(t *testing.T) { + appAssert, _, tagUUID := CreateSampleTag(t) badRequests := []string{ "0", @@ -313,7 +382,7 @@ func TestDeleteTagFailsBadRequest(t *testing.T) { for _, badRequest := range badRequests { TestRequest{ Method: fiber.MethodDelete, - Path: fmt.Sprintf("/api/v1/tags/%s", badRequest), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", badRequest, tagUUID), }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.FailedToValidateID, @@ -325,12 +394,52 @@ func TestDeleteTagFailsBadRequest(t *testing.T) { appAssert.Close() } -func TestDeleteTagFailsNotFound(t *testing.T) { - appAssert, _, _ := CreateSampleTag(t) +func TestDeleteTagFailsTagBadRequest(t *testing.T) { + appAssert, categoryUUID, _ := CreateSampleTag(t) + + badRequests := []string{ + "0", + "-1", + "1.1", + "foo", + "null", + } + + for _, badRequest := range badRequests { + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, badRequest), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.FailedToValidateID, + DBTester: Assert1Tag, + }, + ) + } + + appAssert.Close() +} + +func TestDeleteTagFailsCategoryNotFound(t *testing.T) { + appAssert, _, tagUUID := CreateSampleTag(t) + + TestRequest{ + Method: fiber.MethodDelete, + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", uuid.New(), tagUUID), + }.TestOnErrorAndDB(t, &appAssert, + ErrorWithDBTester{ + Error: errors.TagNotFound, + DBTester: Assert1Tag, + }, + ).Close() +} + +func TestDeleteTagFailsTagNotFound(t *testing.T) { + appAssert, categoryUUID, _ := CreateSampleTag(t) TestRequest{ Method: fiber.MethodDelete, - Path: fmt.Sprintf("/api/v1/tags/%s", uuid.New()), + Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, uuid.New()), }.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{ Error: errors.TagNotFound, diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index f1cd872d9..acd94abec 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -2,11 +2,11 @@ package tests import ( "fmt" - - stdliberrors "errors" "net/http" "testing" + stdliberrors "errors" + "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/errors" "github.com/GenerateNU/sac/backend/src/models" @@ -125,7 +125,6 @@ func TestGetUserFailsNotExist(t *testing.T) { err := app.Conn.Where("id = ?", uuid).First(&user).Error assert.Assert(stdliberrors.Is(err, gorm.ErrRecordNotFound)) - }, }, ).Close() @@ -300,7 +299,6 @@ func TestDeleteUserBadRequest(t *testing.T) { func SampleUserFactory() *map[string]interface{} { return &map[string]interface{}{ - "first_name": "Jane", "last_name": "Doe", "email": "doe.jane@northeastern.edu", @@ -517,7 +515,6 @@ func TestCreateUserFailsOnInvalidCollege(t *testing.T) { if permutation != khouryAbbreviation { permutationsWithoutKhoury = append(permutationsWithoutKhoury, permutation) } - } AssertCreateBadDataFails(t, @@ -565,27 +562,27 @@ func SampleCategoriesFactory() *[]map[string]interface{} { } } -func SampleTagsFactory(categoryIDs []uuid.UUID) *[]map[string]interface{} { - lenOfIDs := len(categoryIDs) +func SampleTagsFactory(categoryUUIDs []uuid.UUID) *[]map[string]interface{} { + lenOfUUIDs := len(categoryUUIDs) return &[]map[string]interface{}{ { "name": "Computer Science", - "category_id": categoryIDs[1%lenOfIDs], + "category_id": categoryUUIDs[1%lenOfUUIDs], }, { "name": "Mechanical Engineering", - "category_id": categoryIDs[1%lenOfIDs], + "category_id": categoryUUIDs[1%lenOfUUIDs], }, { "name": "Finance", - "category_id": categoryIDs[0%lenOfIDs], + "category_id": categoryUUIDs[0%lenOfUUIDs], }, } } -func SampleTagIDsFactory(tagIDs *[]uuid.UUID) *map[string]interface{} { - tags := tagIDs +func SampleTagUUIDsFactory(tagUUIDs *[]uuid.UUID) *map[string]interface{} { + tags := tagUUIDs if tags == nil { tags = &[]uuid.UUID{uuid.New()} @@ -599,7 +596,7 @@ func SampleTagIDsFactory(tagIDs *[]uuid.UUID) *map[string]interface{} { func CreateSetOfTags(t *testing.T, appAssert ExistingAppAssert) []uuid.UUID { categories := SampleCategoriesFactory() - categoryIDs := []uuid.UUID{} + categoryUUIDs := []uuid.UUID{} for _, category := range *categories { TestRequest{ Method: fiber.MethodPost, @@ -615,38 +612,40 @@ func CreateSetOfTags(t *testing.T, appAssert ExistingAppAssert) []uuid.UUID { assert.NilError(err) - categoryIDs = append(categoryIDs, respCategory.ID) + categoryUUIDs = append(categoryUUIDs, respCategory.ID) }, }, ) } - tags := SampleTagsFactory(categoryIDs) + tags := SampleTagsFactory(categoryUUIDs) + tagUUIDs := []uuid.UUID{} - tagIDs := []uuid.UUID{} - for _, tag := range *tags { - TestRequest{ - Method: fiber.MethodPost, - Path: "/api/v1/tags/", - Body: &tag, - }.TestOnStatusAndDB(t, &appAssert, - DBTesterWithStatus{ - Status: fiber.StatusCreated, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - var respTag models.Tag + for _, categoryUUID := range categoryUUIDs { + for _, tag := range *tags { + TestRequest{ + Method: fiber.MethodPost, + Path: fmt.Sprintf("/api/v1/categories/%s/tags/", categoryUUID), + Body: &tag, + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: fiber.StatusCreated, + DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { + var respTag models.Tag - err := json.NewDecoder(resp.Body).Decode(&respTag) + err := json.NewDecoder(resp.Body).Decode(&respTag) - assert.NilError(err) + assert.NilError(err) - tagIDs = append(tagIDs, respTag.ID) + tagUUIDs = append(tagUUIDs, respTag.ID) + }, }, - }, - ) + ) + } } - return tagIDs + return tagUUIDs } func AssertUserTagsRespDB(app TestApp, assert *assert.A, resp *http.Response, id uuid.UUID) { @@ -691,7 +690,7 @@ func TestCreateUserTagsFailsOnInvalidDataType(t *testing.T) { // Test each of the invalid tags: for _, tag := range invalidTags { - malformedTag := *SampleTagIDsFactory(nil) + malformedTag := *SampleTagUUIDsFactory(nil) malformedTag["tags"] = tag TestRequest{ @@ -715,7 +714,7 @@ func TestCreateUserTagsFailsOnInvalidUserID(t *testing.T) { TestRequest{ Method: fiber.MethodPost, Path: fmt.Sprintf("/api/v1/users/%s/tags", badRequest), - Body: SampleTagIDsFactory(nil), + Body: SampleTagUUIDsFactory(nil), }.TestOnError(t, nil, errors.FailedToValidateID).Close() } } @@ -751,24 +750,22 @@ func TestCreateUserTagsFailsOnNonExistentUser(t *testing.T) { TestRequest{ Method: fiber.MethodPost, Path: fmt.Sprintf("/api/v1/users/%s/tags", uuid.New()), - Body: SampleTagIDsFactory(nil), + Body: SampleTagUUIDsFactory(nil), }.TestOnError(t, nil, errors.UserNotFound).Close() } func TestCreateUserTagsWorks(t *testing.T) { appAssert, uuid := CreateSampleUser(t, nil) - // Create a set of tags: tagUUIDs := CreateSetOfTags(t, appAssert) - // Confirm adding real tags adds them to the user: TestRequest{ Method: fiber.MethodPost, Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid), - Body: SampleTagIDsFactory(&tagUUIDs), + Body: SampleTagUUIDsFactory(&tagUUIDs), }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ - Status: fiber.StatusCreated, + Status: fiber.StatusCreated, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { AssertSampleUserTagsRespDB(app, assert, resp, uuid) }, @@ -780,11 +777,11 @@ func TestCreateUserTagsWorks(t *testing.T) { func TestCreateUserTagsNoneAddedIfInvalid(t *testing.T) { appAssert, uuid := CreateSampleUser(t, nil) - + TestRequest{ Method: fiber.MethodPost, Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid), - Body: SampleTagIDsFactory(nil), + Body: SampleTagUUIDsFactory(nil), }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ Status: fiber.StatusCreated, @@ -837,23 +834,20 @@ func TestGetUserTagsReturnsEmptyListWhenNoneAdded(t *testing.T) { func TestGetUserTagsReturnsCorrectList(t *testing.T) { appAssert, uuid := CreateSampleUser(t, nil) - // Create a set of tags: tagUUIDs := CreateSetOfTags(t, appAssert) - // Add the tags: TestRequest{ Method: fiber.MethodPost, Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid), - Body: SampleTagIDsFactory(&tagUUIDs), + Body: SampleTagUUIDsFactory(&tagUUIDs), }.TestOnStatus(t, &appAssert, fiber.StatusCreated) - // Get the tags: TestRequest{ Method: fiber.MethodGet, Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid), }.TestOnStatusAndDB(t, &appAssert, DBTesterWithStatus{ - Status: fiber.StatusOK, + Status: fiber.StatusOK, DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { AssertSampleUserTagsRespDB(app, assert, resp, uuid) }, diff --git a/cli/commands/be.go b/cli/commands/be.go index 17ad9ef6c..88a4c01b7 100644 --- a/cli/commands/be.go +++ b/cli/commands/be.go @@ -19,7 +19,6 @@ func RunBackendCommand() *cli.Command { } err := RunBE() - if err != nil { return cli.Exit(err.Error(), 1) } @@ -41,7 +40,6 @@ func RunBE() error { fmt.Println("Running backend") err := goCmd.Run() - if err != nil { return fmt.Errorf("error running backend: %w", err) } diff --git a/cli/commands/clean_tests.go b/cli/commands/clean_tests.go index e87a33697..fb2a553a7 100644 --- a/cli/commands/clean_tests.go +++ b/cli/commands/clean_tests.go @@ -2,9 +2,8 @@ package commands import ( "database/sql" - "os/user" - "fmt" + "os/user" "sync" _ "github.com/lib/pq" @@ -22,7 +21,6 @@ func ClearDBCommand() *cli.Command { } err := CleanTestDBs() - if err != nil { return cli.Exit(err.Error(), 1) } @@ -38,7 +36,6 @@ func CleanTestDBs() error { fmt.Println("Cleaning test databases") db, err := sql.Open("postgres", CONFIG.Database.WithDb()) - if err != nil { return err } @@ -46,13 +43,11 @@ func CleanTestDBs() error { defer db.Close() currentUser, err := user.Current() - if err != nil { return fmt.Errorf("failed to get current user: %w", err) } rows, err := db.Query("SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres' AND datname != $1 AND datname != $2", currentUser.Username, CONFIG.Database.DatabaseName) - if err != nil { return err } @@ -71,7 +66,6 @@ func CleanTestDBs() error { wg.Add(1) go func(dbName string) { - defer wg.Done() fmt.Printf("Dropping database %s\n", dbName) @@ -79,7 +73,6 @@ func CleanTestDBs() error { if _, err := db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)); err != nil { fmt.Printf("Failed to drop database %s: %v\n", dbName, err) } - }(dbName) } diff --git a/cli/commands/config.go b/cli/commands/config.go index a06375782..203219218 100644 --- a/cli/commands/config.go +++ b/cli/commands/config.go @@ -7,8 +7,10 @@ import ( "github.com/GenerateNU/sac/cli/utils" ) -var ROOT_DIR, _ = utils.GetRootDir() -var FRONTEND_DIR = filepath.Join(ROOT_DIR, "/frontend") -var BACKEND_DIR = filepath.Join(ROOT_DIR, "/backend/src") -var CONFIG, _ = config.GetConfiguration(filepath.Join(ROOT_DIR, "/config")) -var MIGRATION_FILE = filepath.Join(BACKEND_DIR, "/migrations/data.sql") +var ( + ROOT_DIR, _ = utils.GetRootDir() + FRONTEND_DIR = filepath.Join(ROOT_DIR, "/frontend") + BACKEND_DIR = filepath.Join(ROOT_DIR, "/backend/src") + CONFIG, _ = config.GetConfiguration(filepath.Join(ROOT_DIR, "/config")) + MIGRATION_FILE = filepath.Join(BACKEND_DIR, "/migrations/data.sql") +) diff --git a/cli/commands/drop.go b/cli/commands/drop.go index a79acc79d..5f75e1726 100644 --- a/cli/commands/drop.go +++ b/cli/commands/drop.go @@ -95,7 +95,6 @@ func DropDB() error { defer dbMutex.Unlock() db, err := sql.Open("postgres", CONFIG.Database.WithDb()) - if err != nil { return err } @@ -105,7 +104,6 @@ func DropDB() error { var tableCount int err = db.QueryRow("SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'public'").Scan(&tableCount) - if err != nil { return fmt.Errorf("error checking tables: %w", err) } @@ -118,7 +116,6 @@ func DropDB() error { fmt.Println("Generating DROP TABLE statements...") rows, err := db.Query("SELECT tablename FROM pg_tables WHERE schemaname = 'public'") - if err != nil { return fmt.Errorf("error generating DROP TABLE statements: %w", err) } diff --git a/cli/commands/format.go b/cli/commands/format.go index 9b9f7f1be..c6ea9e9c7 100644 --- a/cli/commands/format.go +++ b/cli/commands/format.go @@ -77,7 +77,7 @@ func Format(folder string, runFrontend bool, runBackend bool) error { func BackendFormat() error { fmt.Println("Formatting backend") - cmd := exec.Command("go", "fmt", "./...") + cmd := exec.Command("gofumpt", "-l", "-w", ".") cmd.Dir = BACKEND_DIR err := cmd.Run() @@ -91,5 +91,5 @@ func BackendFormat() error { func FrontendFormat(folder string) error { fmt.Println("UNIMPLEMENTED") - return nil -} \ No newline at end of file + return nil +} diff --git a/cli/commands/insert.go b/cli/commands/insert.go index 9e961b2ae..542a0dcbe 100644 --- a/cli/commands/insert.go +++ b/cli/commands/insert.go @@ -25,7 +25,6 @@ func InsertDBCommand() *cli.Command { } err := InsertDB() - if err != nil { return cli.Exit(err.Error(), 1) } @@ -39,7 +38,6 @@ func InsertDBCommand() *cli.Command { func InsertDB() error { db, err := sql.Open("postgres", CONFIG.Database.WithDb()) - if err != nil { return err } @@ -49,7 +47,6 @@ func InsertDB() error { var exists bool err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' LIMIT 1);").Scan(&exists) - if err != nil { return err } diff --git a/cli/commands/test.go b/cli/commands/test.go index 9f7e3653f..1bd32eba2 100644 --- a/cli/commands/test.go +++ b/cli/commands/test.go @@ -81,7 +81,6 @@ func BackendTest() error { defer CleanTestDBs() out, err := cmd.CombinedOutput() - if err != nil { fmt.Println(string(out)) return cli.Exit("Failed to run backend tests", 1) diff --git a/cli/utils/path.go b/cli/utils/path.go index d91a6b504..01b9a6a1a 100644 --- a/cli/utils/path.go +++ b/cli/utils/path.go @@ -6,7 +6,6 @@ import ( "path/filepath" ) - func GetRootDir() (string, error) { // Get the current working directory currentDir, err := os.Getwd() @@ -41,4 +40,4 @@ func FindRootDir(dir string) (string, error) { // Recursively search in the parent directory return FindRootDir(parentDir) -} \ No newline at end of file +} diff --git a/frontend/sac-mobile/package.json b/frontend/sac-mobile/package.json index c5f8939e0..deaf689b6 100644 --- a/frontend/sac-mobile/package.json +++ b/frontend/sac-mobile/package.json @@ -38,7 +38,7 @@ "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-web": "~0.19.6", - "semver": "7.5.3", + "semver": "7.5.4", "zod": "^3.22.4", "zustand": "^4.4.7" }, diff --git a/frontend/sac-mobile/yarn.lock b/frontend/sac-mobile/yarn.lock index 3fb63d04b..da5a4231e 100644 --- a/frontend/sac-mobile/yarn.lock +++ b/frontend/sac-mobile/yarn.lock @@ -9087,6 +9087,13 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" +semver@7.5.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@^5.5.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -9097,13 +9104,6 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - send@0.18.0, send@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"