diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 26801fd58..4310d47fe 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -46,21 +46,21 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + checks: write steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: "1.21" - - name: Enforce linting - run: | - cd ./backend/ && lint_output=$(go vet ./...) - if [[ -n "$lint_output" ]]; then - echo "$lint_output" - echo "::error::Linting issues found" - exit 1 - fi + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + working-directory: ./backend/ test: name: Test runs-on: ubuntu-latest diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 8abe3bc6d..6d7da6357 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -46,18 +46,18 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + checks: write steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 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 + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + working-directory: ./cli/ diff --git a/README.md b/README.md index af4932d48..fb5635004 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@
- + + Backend Go Report + Backend Workflow Status @@ -16,6 +19,10 @@
+
+ CLI Go Report + CLI Workflow Status diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 000000000..0eab32c33 --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,23 @@ +linters: + enable: + - cyclop + - exportloopref + - gocritic + - gosec + - ineffassign + - misspell + - prealloc + - unconvert + - unparam + - goimports + - whitespace + +linters-settings: + whitespace: + multi-func: true + +issues: + exclude-rules: + - path: tests/api/helpers/ + linters: + - cyclop diff --git a/backend/src/database/db.go b/backend/src/database/db.go index 35ad1d868..b45406eff 100644 --- a/backend/src/database/db.go +++ b/backend/src/database/db.go @@ -141,7 +141,6 @@ func createSuperUser(settings config.Settings, db *gorm.DB) error { } return tx.Commit().Error - } return nil } diff --git a/backend/src/database/super.go b/backend/src/database/super.go index 6ad3f930f..22700693e 100644 --- a/backend/src/database/super.go +++ b/backend/src/database/super.go @@ -35,7 +35,7 @@ func SuperClub() models.Club { Description: "SAC", NumMembers: 0, IsRecruiting: true, - RecruitmentCycle: models.RecruitmentCycle(models.Always), + RecruitmentCycle: models.Always, RecruitmentType: models.Application, ApplicationLink: "https://generatenu.com/apply", Logo: "https://aws.amazon.com/s3", diff --git a/backend/src/main.go b/backend/src/main.go index 9ace36db9..2c2f73672 100644 --- a/backend/src/main.go +++ b/backend/src/main.go @@ -35,5 +35,8 @@ func main() { app := server.Init(db, config) - app.Listen(fmt.Sprintf("%s:%d", config.Application.Host, config.Application.Port)) + err = app.Listen(fmt.Sprintf("%s:%d", config.Application.Host, config.Application.Port)) + if err != nil { + panic(err) + } } diff --git a/backend/src/middleware/auth.go b/backend/src/middleware/auth.go index 07fd76a61..3bba7c497 100644 --- a/backend/src/middleware/auth.go +++ b/backend/src/middleware/auth.go @@ -23,7 +23,7 @@ func SuperSkipper(h fiber.Handler) fiber.Handler { return skip.New(h, func(c *fiber.Ctx) bool { claims, err := types.From(c) if err != nil { - err.FiberError(c) + _ = err.FiberError(c) return false } if claims == nil { diff --git a/backend/src/server/server.go b/backend/src/server/server.go index f65b70c23..5d249a0a3 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -26,7 +26,11 @@ import ( func Init(db *gorm.DB, settings config.Settings) *fiber.App { app := newFiberApp() - validate := utilities.RegisterCustomValidators() + validate, err := utilities.RegisterCustomValidators() + if err != nil { + panic(err) + } + middlewareService := middleware.NewMiddlewareService(db, validate, settings.Auth) apiv1 := app.Group("/api/v1") diff --git a/backend/src/services/auth.go b/backend/src/services/auth.go index 4c87dd7e4..4e0d4de72 100644 --- a/backend/src/services/auth.go +++ b/backend/src/services/auth.go @@ -75,7 +75,7 @@ func (a *AuthService) GetRole(id string) (*models.UserRole, *errors.Error) { return nil, &errors.UserNotFound } - role := models.UserRole(user.Role) + role := user.Role return &role, nil } diff --git a/backend/src/transactions/club.go b/backend/src/transactions/club.go index a7fe4495c..016e2d0ef 100644 --- a/backend/src/transactions/club.go +++ b/backend/src/transactions/club.go @@ -17,7 +17,7 @@ func GetAdminIDs(db *gorm.DB, clubID uuid.UUID) ([]uuid.UUID, *errors.Error) { return nil, &errors.FailedtoGetAdminIDs } - var adminUUIDs []uuid.UUID + adminUUIDs := make([]uuid.UUID, 0) for _, adminID := range adminIDs { adminUUIDs = append(adminUUIDs, adminID.ClubID) } diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go index 78c2e53ef..701f3c595 100644 --- a/backend/src/utilities/validator.go +++ b/backend/src/utilities/validator.go @@ -13,18 +13,32 @@ import ( "github.com/go-playground/validator/v10" ) -func RegisterCustomValidators() *validator.Validate { +func RegisterCustomValidators() (*validator.Validate, error) { validate := validator.New(validator.WithRequiredStructEnabled()) - validate.RegisterValidation("neu_email", validateEmail) - validate.RegisterValidation("password", validatePassword) - validate.RegisterValidation("mongo_url", validateMongoURL) - validate.RegisterValidation("s3_url", validateS3URL) - validate.RegisterValidation("contact_pointer", func(fl validator.FieldLevel) bool { + if err := validate.RegisterValidation("neu_email", validateEmail); err != nil { + return nil, err + } + + if err := validate.RegisterValidation("password", validatePassword); err != nil { + return nil, err + } + + if err := validate.RegisterValidation("mongo_url", validateMongoURL); err != nil { + return nil, err + } + + if err := validate.RegisterValidation("s3_url", validateS3URL); err != nil { + return nil, err + } + + if err := validate.RegisterValidation("contact_pointer", func(fl validator.FieldLevel) bool { return validateContactPointer(validate, fl) - }) + }); err != nil { + return nil, err + } - return validate + return validate, nil } func validateEmail(fl validator.FieldLevel) bool { diff --git a/backend/tests/api/club_test.go b/backend/tests/api/club_test.go index bdce0892e..4a632dd42 100644 --- a/backend/tests/api/club_test.go +++ b/backend/tests/api/club_test.go @@ -206,8 +206,8 @@ func TestGetClubsWorks(t *testing.T) { assert.Equal("SAC", dbClub.Description) assert.Equal(1, dbClub.NumMembers) assert.Equal(true, dbClub.IsRecruiting) - assert.Equal(models.RecruitmentCycle(models.Always), dbClub.RecruitmentCycle) - assert.Equal(models.RecruitmentType(models.Application), dbClub.RecruitmentType) + assert.Equal(models.Always, dbClub.RecruitmentCycle) + assert.Equal(models.Application, dbClub.RecruitmentType) assert.Equal("https://generatenu.com/apply", dbClub.ApplicationLink) assert.Equal("https://aws.amazon.com/s3", dbClub.Logo) }, @@ -303,7 +303,7 @@ func TestCreateClubFailsOnInvalidLogo(t *testing.T) { []interface{}{ "Not an URL", "@#139081#$Ad_Axf", - //"https://google.com", <-- TODO uncomment once we figure out s3 url validation + // "https://google.com", <-- TODO uncomment once we figure out s3 url validation }, ) } @@ -343,6 +343,7 @@ func TestUpdateClubFailsOnInvalidBody(t *testing.T) { {"application_link": "Not an URL"}, {"logo": "@12394X_2"}, } { + invalidData := invalidData appAssert.TestOnErrorAndDB( h.TestRequest{ Method: fiber.MethodPatch, diff --git a/backend/tests/api/helpers/requests.go b/backend/tests/api/helpers/requests.go index 85bf4fd44..3e76dc5bb 100644 --- a/backend/tests/api/helpers/requests.go +++ b/backend/tests/api/helpers/requests.go @@ -24,6 +24,7 @@ type TestRequest struct { TestUserIDReplaces *string } +//gocyclo:ignore func (app TestApp) Send(request TestRequest) (*http.Response, error) { address := fmt.Sprintf("%s%s", app.Address, request.Path) diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 531993fcd..f61cbe7d0 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -115,6 +115,7 @@ func TestCreateTagFailsBadRequest(t *testing.T) { } for _, badBody := range badBodys { + badBody := badBody appAssert.TestOnErrorAndDB( h.TestRequest{ Method: fiber.MethodPost, @@ -146,6 +147,7 @@ func TestCreateTagFailsValidation(t *testing.T) { } for _, badBody := range badBodys { + badBody := badBody appAssert.TestOnErrorAndDB( h.TestRequest{ Method: fiber.MethodPost, diff --git a/backend/tests/api/user_tag_test.go b/backend/tests/api/user_tag_test.go index 013b5ddb9..9d8a2ecc4 100644 --- a/backend/tests/api/user_tag_test.go +++ b/backend/tests/api/user_tag_test.go @@ -66,6 +66,7 @@ func CreateSetOfTags(t *testing.T, appAssert *h.ExistingAppAssert) ([]uuid.UUID, categoryIDs := []uuid.UUID{} for _, category := range *categories { + category := category appAssert.TestOnStatusAndDB( h.TestRequest{ Method: fiber.MethodPost, @@ -92,6 +93,7 @@ func CreateSetOfTags(t *testing.T, appAssert *h.ExistingAppAssert) ([]uuid.UUID, tagIDs := []uuid.UUID{} for _, tag := range *tags { + tag := tag appAssert.TestOnStatusAndDB( h.TestRequest{ Method: fiber.MethodPost, @@ -212,6 +214,7 @@ func TestCreateUserTagsFailsOnInvalidKey(t *testing.T) { } for _, body := range invalidBody { + body := body h.InitTest(t).TestOnError( h.TestRequest{ Method: fiber.MethodPost, diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go index 5ba723114..66bd629c4 100644 --- a/backend/tests/api/user_test.go +++ b/backend/tests/api/user_test.go @@ -216,6 +216,7 @@ func TestUpdateUserFailsOnInvalidBody(t *testing.T) { {"year": 1963}, {"college": "UT-Austin"}, } { + invalidData := invalidData h.InitTest(t).TestOnErrorAndDB( h.TestRequest{ Method: fiber.MethodPatch, diff --git a/cli/.golangci.yml b/cli/.golangci.yml new file mode 100644 index 000000000..31fd57a91 --- /dev/null +++ b/cli/.golangci.yml @@ -0,0 +1,17 @@ +linters: + enable: + - cyclop + - exportloopref + - gocritic + - gosec + - ineffassign + - misspell + - prealloc + - unconvert + - unparam + - goimports + - whitespace + +linters-settings: + whitespace: + multi-func: true diff --git a/cli/commands/drop.go b/cli/commands/drop.go index 5f75e1726..3a34f1ae1 100644 --- a/cli/commands/drop.go +++ b/cli/commands/drop.go @@ -70,8 +70,7 @@ func DropData() error { return fmt.Errorf("error scanning table name: %w", err) } - deleteStmt := fmt.Sprintf("DELETE FROM \"%s\"", tablename) - _, err := db.Exec(deleteStmt) + _, err := db.Exec("DELETE FROM $1", tablename) if err != nil { return fmt.Errorf("error deleting rows from table %s: %w", tablename, err) } @@ -82,7 +81,9 @@ func DropData() error { return fmt.Errorf("error in rows handling: %w", err) } - Migrate() + if Migrate() != nil { + return fmt.Errorf("error migrating database: %w", err) + } fmt.Println("All rows removed successfully.") return nil diff --git a/cli/commands/format.go b/cli/commands/format.go index 9b60c9fc2..cfaa9ddea 100644 --- a/cli/commands/format.go +++ b/cli/commands/format.go @@ -39,7 +39,10 @@ func FormatCommand() *cli.Command { runFrontend := folder != "" runBackend := c.Bool("backend") - Format(folder, runFrontend, runBackend) + err := Format(folder, runFrontend, runBackend) + if err != nil { + return cli.Exit(err.Error(), 1) + } return nil }, @@ -56,7 +59,10 @@ func Format(folder string, runFrontend bool, runBackend bool) error { wg.Add(1) go func() { defer wg.Done() - BackendFormat() + err := BackendFormat() + if err != nil { + fmt.Println(err) + } }() } @@ -65,7 +71,10 @@ func Format(folder string, runFrontend bool, runBackend bool) error { wg.Add(1) go func() { defer wg.Done() - FrontendFormat(folder) + err := FrontendFormat(folder) + if err != nil { + fmt.Println(err) + } }() } diff --git a/cli/commands/insert.go b/cli/commands/insert.go index 376468ad3..9b8a55754 100644 --- a/cli/commands/insert.go +++ b/cli/commands/insert.go @@ -1,15 +1,12 @@ package commands import ( - "bytes" "database/sql" - "errors" "fmt" + "os" "os/exec" - "strconv" - "strings" - _ "github.com/lib/pq" + "github.com/lib/pq" "github.com/urfave/cli/v2" ) @@ -65,18 +62,20 @@ func InsertDB() error { fmt.Println("Database exists with tables.") } - insertCmd := exec.Command("psql", "-h", CONFIG.Database.Host, "-p", strconv.Itoa(int(CONFIG.Database.Port)), "-U", CONFIG.Database.Username, "-d", CONFIG.Database.DatabaseName, "-a", "-f", MIGRATION_FILE) - - var output bytes.Buffer - insertCmd.Stdout = &output - insertCmd.Stderr = &output - - if err := insertCmd.Run(); err != nil { - return fmt.Errorf("error inserting data: %w", err) + migrationSQL, err := os.ReadFile(MIGRATION_FILE) + if err != nil { + return fmt.Errorf("error reading migration file: %w", err) } - if strings.Contains(output.String(), "ROLLBACK") { - return errors.New("insertion failed, rolling back") + _, err = db.Exec(string(migrationSQL)) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + fmt.Println("PostgreSQL Error:") + fmt.Println("Code:", pqErr.Code) + fmt.Println("Message:", pqErr.Message) + } else { + return fmt.Errorf("error executing migration: %w", err) + } } fmt.Println("Data inserted successfully.") diff --git a/cli/commands/lint.go b/cli/commands/lint.go index 3351ab554..e6836ce55 100644 --- a/cli/commands/lint.go +++ b/cli/commands/lint.go @@ -39,7 +39,10 @@ func LintCommand() *cli.Command { runFrontend := folder != "" runBackend := c.Bool("backend") - Lint(folder, runFrontend, runBackend) + err := Lint(folder, runFrontend, runBackend) + if err != nil { + return cli.Exit(err.Error(), 1) + } return nil }, @@ -50,26 +53,45 @@ func LintCommand() *cli.Command { func Lint(folder string, runFrontend bool, runBackend bool) error { var wg sync.WaitGroup + errChan := make(chan error, 1) + var errOccurred bool // Flag to indicate whether an error has occurred // Start the backend if specified - if runBackend { + if runBackend && !errOccurred { wg.Add(1) go func() { defer wg.Done() - BackendLint() + err := BackendLint() + if err != nil { + errChan <- err + errOccurred = true + } }() } // Start the frontend if specified - if runFrontend { + if runFrontend && !errOccurred { wg.Add(1) go func() { defer wg.Done() - FrontendLint(folder) + err := FrontendLint(folder) + if err != nil { + errChan <- err + errOccurred = true + } }() } - wg.Wait() + go func() { + wg.Wait() + close(errChan) + }() + + for err := range errChan { + if err != nil { + return err + } + } return nil } @@ -77,7 +99,7 @@ func Lint(folder string, runFrontend bool, runBackend bool) error { func BackendLint() error { fmt.Println("Linting backend") - cmd := exec.Command("go", "vet", "./...") + cmd := exec.Command("golangci-lint", "run") cmd.Dir = BACKEND_DIR err := cmd.Run() diff --git a/cli/commands/migrate.go b/cli/commands/migrate.go index a2189e805..7aa7d8aa4 100644 --- a/cli/commands/migrate.go +++ b/cli/commands/migrate.go @@ -17,7 +17,10 @@ func MigrateCommand() *cli.Command { return cli.Exit("Invalid arguments", 1) } - Migrate() + err := Migrate() + if err != nil { + return cli.Exit(err.Error(), 1) + } return nil }, } diff --git a/cli/commands/reset.go b/cli/commands/reset.go index 07929c1fb..4e9cf4324 100644 --- a/cli/commands/reset.go +++ b/cli/commands/reset.go @@ -46,17 +46,23 @@ func ResetCommand() *cli.Command { func ResetData() error { fmt.Println("Clearing database") - DropData() + err := DropData() + if err != nil { + return cli.Exit(err.Error(), 1) + } cmd := exec.Command("sleep", "1") cmd.Dir = BACKEND_DIR - err := cmd.Run() + err = cmd.Run() if err != nil { return cli.Exit("Error running sleep", 1) } - Migrate() + err = Migrate() + if err != nil { + return cli.Exit(err.Error(), 1) + } cmd = exec.Command("sleep", "1") cmd.Dir = BACKEND_DIR @@ -66,7 +72,10 @@ func ResetData() error { return cli.Exit("Error running sleep", 1) } - InsertDB() + err = InsertDB() + if err != nil { + return cli.Exit(err.Error(), 1) + } fmt.Println("Data reset successfully") @@ -76,17 +85,23 @@ func ResetData() error { func ResetMigration() error { fmt.Println("Resetting migration") - DropDB() + err := DropDB() + if err != nil { + return cli.Exit(err.Error(), 1) + } cmd := exec.Command("sleep", "1") cmd.Dir = BACKEND_DIR - err := cmd.Run() + err = cmd.Run() if err != nil { return cli.Exit("Error running sleep", 1) } - Migrate() + err = Migrate() + if err != nil { + return cli.Exit(err.Error(), 1) + } fmt.Println("Migration reset successfully") diff --git a/cli/commands/swagger.go b/cli/commands/swagger.go index bac4191d3..3881d93cf 100644 --- a/cli/commands/swagger.go +++ b/cli/commands/swagger.go @@ -17,7 +17,10 @@ func SwaggerCommand() *cli.Command { return cli.Exit("Invalid arguments", 1) } - Swagger() + err := Swagger() + if err != nil { + return cli.Exit(err.Error(), 1) + } return nil }, } diff --git a/cli/commands/test.go b/cli/commands/test.go index 0fa9d6a27..be12543c4 100644 --- a/cli/commands/test.go +++ b/cli/commands/test.go @@ -38,7 +38,10 @@ func TestCommand() *cli.Command { folder := c.String("frontend") runFrontend := folder != "" runBackend := c.Bool("backend") - Test(folder, runFrontend, runBackend) + err := Test(folder, runFrontend, runBackend) + if err != nil { + return cli.Exit(err.Error(), 1) + } return nil }, } @@ -47,26 +50,46 @@ func TestCommand() *cli.Command { func Test(folder string, runFrontend bool, runBackend bool) error { var wg sync.WaitGroup + errChan := make(chan error, 1) + var errOccurred bool // Flag to indicate whether an error has occurred // Start the backend if specified - if runBackend { + if runBackend && !errOccurred { wg.Add(1) go func() { defer wg.Done() - BackendTest() + err := BackendTest() + if err != nil { + errChan <- err + errOccurred = true + } }() } // Start the frontend if specified - if runFrontend { + if runFrontend && !errOccurred { wg.Add(1) go func() { defer wg.Done() - FrontendTest(folder) + err := FrontendTest(folder) + if err != nil { + errChan <- err + errOccurred = true + } }() } - wg.Wait() + go func() { + wg.Wait() + close(errChan) + }() + + for err := range errChan { + if err != nil { + return err + } + } + return nil } @@ -74,8 +97,6 @@ func BackendTest() error { cmd := exec.Command("go", "test", "./...") cmd.Dir = fmt.Sprintf("%s/..", BACKEND_DIR) - defer CleanTestDBs() - out, err := cmd.CombinedOutput() if err != nil { fmt.Println(string(out)) @@ -83,6 +104,12 @@ func BackendTest() error { } fmt.Println(string(out)) + + err = CleanTestDBs() + if err != nil { + return cli.Exit(err.Error(), 1) + } + return nil } diff --git a/go.work.sum b/go.work.sum index 9dfa3c6d2..a9dfabbc1 100644 --- a/go.work.sum +++ b/go.work.sum @@ -10,7 +10,12 @@ 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/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=