Skip to content

Commit

Permalink
Handle Auth in Tests (#143)
Browse files Browse the repository at this point in the history
Co-authored-by: David Oduneye <[email protected]>
garrettladley and DOOduneye authored Feb 5, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 6e25748 commit 60e360b
Showing 49 changed files with 1,801 additions and 1,193 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
@@ -96,9 +96,7 @@ jobs:
docker exec $CONTAINER_ID cat /var/lib/postgresql/data/postgresql.conf | grep max_connections
- name: Restart PostgreSQL Container
run: docker restart $(docker ps --filter "publish=5432" --format "{{.ID}}")
- name: Migrate DB
run: cd ./backend/src && go run main.go --only-migrate
- name: Run Tests with Coverage
run: cd ./backend/ && go test -benchmem -race -coverprofile=coverage.txt ./...
run: cd ./backend/ && go test -bench=. -benchmem -race -coverprofile=coverage.txt ./...
- name: Print Coverage
run: cd ./backend/ && go tool cover -func=coverage.txt
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.DS_Store
.env
sac-cli
.vscode
.trunk
30 changes: 12 additions & 18 deletions backend/src/auth/tokens.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package auth

import (
"fmt"
"time"

"github.com/GenerateNU/sac/backend/src/config"
@@ -71,7 +70,6 @@ func CreateRefreshToken(id string, refreshExpiresAfter uint, refreshTokenSecret

func SignToken(token *jwt.Token, secret string) (*string, *errors.Error) {
if token == nil || secret == "" {
fmt.Println(token)
return nil, &errors.FailedToSignToken
}

@@ -103,9 +101,9 @@ func ExpireCookie(name string) *fiber.Cookie {
}

// RefreshAccessToken refreshes the access token
func RefreshAccessToken(refreshCookie string, role string, accessExpiresAfter uint, accessTokenSecret string) (*string, *errors.Error) {
func RefreshAccessToken(refreshCookie string, role string, refreshTokenSecret string, accessExpiresAfter uint, accessTokenSecret string) (*string, *errors.Error) {
// Parse the refresh token
refreshToken, err := ParseRefreshToken(refreshCookie)
refreshToken, err := ParseRefreshToken(refreshCookie, refreshTokenSecret)
if err != nil {
return nil, &errors.FailedToParseRefreshToken
}
@@ -126,26 +124,22 @@ func RefreshAccessToken(refreshCookie string, role string, accessExpiresAfter ui
}

// ParseAccessToken parses the access token
func ParseAccessToken(cookie string) (*jwt.Token, error) {
var settings config.Settings

func ParseAccessToken(cookie string, accessTokenSecret string) (*jwt.Token, error) {
return jwt.ParseWithClaims(cookie, &types.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(settings.Auth.AccessToken), nil
return []byte(accessTokenSecret), nil
})
}

// ParseRefreshToken parses the refresh token
func ParseRefreshToken(cookie string) (*jwt.Token, error) {
var settings config.Settings

func ParseRefreshToken(cookie string, refreshTokenSecret string) (*jwt.Token, error) {
return jwt.ParseWithClaims(cookie, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(settings.Auth.RefreshToken), nil
return []byte(refreshTokenSecret), nil
})
}

// GetRoleFromToken gets the role from the custom claims
func GetRoleFromToken(tokenString string) (*string, error) {
token, err := ParseAccessToken(tokenString)
func GetRoleFromToken(tokenString string, accessTokenSecret string) (*string, error) {
token, err := ParseAccessToken(tokenString, accessTokenSecret)
if err != nil {
return nil, err
}
@@ -159,8 +153,8 @@ func GetRoleFromToken(tokenString string) (*string, error) {
}

// ExtractClaims extracts the claims from the token
func ExtractAccessClaims(tokenString string) (*types.CustomClaims, *errors.Error) {
token, err := ParseAccessToken(tokenString)
func ExtractAccessClaims(tokenString string, accessTokenSecret string) (*types.CustomClaims, *errors.Error) {
token, err := ParseAccessToken(tokenString, accessTokenSecret)
if err != nil {
return nil, &errors.FailedToParseAccessToken
}
@@ -174,8 +168,8 @@ func ExtractAccessClaims(tokenString string) (*types.CustomClaims, *errors.Error
}

// ExtractClaims extracts the claims from the token
func ExtractRefreshClaims(tokenString string) (*jwt.StandardClaims, *errors.Error) {
token, err := ParseRefreshToken(tokenString)
func ExtractRefreshClaims(tokenString string, refreshTokenSecret string) (*jwt.StandardClaims, *errors.Error) {
token, err := ParseRefreshToken(tokenString, refreshTokenSecret)
if err != nil {
return nil, &errors.FailedToParseRefreshToken
}
17 changes: 7 additions & 10 deletions backend/src/controllers/auth.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/GenerateNU/sac/backend/src/types"
"github.com/GenerateNU/sac/backend/src/utilities"
"github.com/gofiber/fiber/v2"
)
@@ -33,14 +34,10 @@ func NewAuthController(authService services.AuthServiceInterface, authSettings c
// @Failure 401 {string} string "failed to get current user"
// @Router /api/v1/auth/me [get]
func (a *AuthController) Me(c *fiber.Ctx) error {
// Extract token values from cookies
accessTokenValue := c.Cookies("access_token")

claims, err := auth.ExtractAccessClaims(accessTokenValue)
claims, err := types.From(c)
if err != nil {
return err.FiberError(c)
}

user, err := a.authService.Me(claims.Issuer)
if err != nil {
return err.FiberError(c)
@@ -66,7 +63,7 @@ func (a *AuthController) Login(c *fiber.Ctx) error {
var userBody models.LoginUserResponseBody

if err := c.BodyParser(&userBody); err != nil {
errors.FailedToParseRequestBody.FiberError(c)
return errors.FailedToParseRequestBody.FiberError(c)
}

user, err := a.authService.Login(userBody)
@@ -80,8 +77,8 @@ func (a *AuthController) Login(c *fiber.Ctx) error {
}

// Set the tokens in the response
c.Cookie(auth.CreateCookie("access_token", *accessToken, time.Now().Add(time.Minute*60)))
c.Cookie(auth.CreateCookie("refresh_token", *refreshToken, time.Now().Add(time.Hour*24*30)))
c.Cookie(auth.CreateCookie("access_token", *accessToken, time.Now().Add(time.Minute*time.Duration(a.AuthSettings.AccessTokenExpiry))))
c.Cookie(auth.CreateCookie("refresh_token", *refreshToken, time.Now().Add(time.Hour*time.Duration(a.AuthSettings.RefreshTokenExpiry))))

return utilities.FiberMessage(c, fiber.StatusOK, "success")
}
@@ -102,7 +99,7 @@ func (a *AuthController) Refresh(c *fiber.Ctx) error {
refreshTokenValue := c.Cookies("refresh_token")

// Extract id from refresh token
claims, err := auth.ExtractRefreshClaims(refreshTokenValue)
claims, err := auth.ExtractRefreshClaims(refreshTokenValue, a.AuthSettings.RefreshToken)
if err != nil {
return err.FiberError(c)
}
@@ -112,7 +109,7 @@ func (a *AuthController) Refresh(c *fiber.Ctx) error {
return err.FiberError(c)
}

accessToken, err := auth.RefreshAccessToken(refreshTokenValue, string(*role), a.AuthSettings.AccessTokenExpiry, a.AuthSettings.AccessToken)
accessToken, err := auth.RefreshAccessToken(refreshTokenValue, string(*role), a.AuthSettings.RefreshToken, a.AuthSettings.AccessTokenExpiry, a.AuthSettings.AccessToken)
if err != nil {
return err.FiberError(c)
}
6 changes: 3 additions & 3 deletions backend/src/controllers/category.go
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ func (t *CategoryController) GetCategories(c *fiber.Ctx) error {
// @Failure 500 {string} string "failed to retrieve category"
// @Router /api/v1/category/{id} [get]
func (t *CategoryController) GetCategory(c *fiber.Ctx) error {
category, err := t.categoryService.GetCategory(c.Params("id"))
category, err := t.categoryService.GetCategory(c.Params("categoryID"))
if err != nil {
return err.FiberError(c)
}
@@ -102,7 +102,7 @@ func (t *CategoryController) GetCategory(c *fiber.Ctx) error {
// @Failure 500 {string} string "failed to delete category"
// @Router /api/v1/category/{id} [delete]
func (t *CategoryController) DeleteCategory(c *fiber.Ctx) error {
if err := t.categoryService.DeleteCategory(c.Params("id")); err != nil {
if err := t.categoryService.DeleteCategory(c.Params("categoryID")); err != nil {
return err.FiberError(c)
}

@@ -128,7 +128,7 @@ func (t *CategoryController) UpdateCategory(c *fiber.Ctx) error {
return errors.FailedToValidateCategory.FiberError(c)
}

updatedCategory, err := t.categoryService.UpdateCategory(c.Params("id"), category)
updatedCategory, err := t.categoryService.UpdateCategory(c.Params("categoryID"), category)
if err != nil {
return err.FiberError(c)
}
20 changes: 10 additions & 10 deletions backend/src/controllers/club.go
Original file line number Diff line number Diff line change
@@ -17,58 +17,58 @@ func NewClubController(clubService services.ClubServiceInterface) *ClubControlle
return &ClubController{clubService: clubService}
}

func (l *ClubController) GetAllClubs(c *fiber.Ctx) error {
func (cl *ClubController) GetAllClubs(c *fiber.Ctx) error {
defaultLimit := 10
defaultPage := 1

clubs, err := l.clubService.GetClubs(c.Query("limit", strconv.Itoa(defaultLimit)), c.Query("page", strconv.Itoa(defaultPage)))
clubs, err := cl.clubService.GetClubs(c.Query("limit", strconv.Itoa(defaultLimit)), c.Query("page", strconv.Itoa(defaultPage)))
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusOK).JSON(clubs)
}

func (l *ClubController) CreateClub(c *fiber.Ctx) error {
func (cl *ClubController) CreateClub(c *fiber.Ctx) error {
var clubBody models.CreateClubRequestBody
if err := c.BodyParser(&clubBody); err != nil {
return errors.FailedToParseRequestBody.FiberError(c)
}

club, err := l.clubService.CreateClub(clubBody)
club, err := cl.clubService.CreateClub(clubBody)
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusCreated).JSON(club)
}

func (l *ClubController) GetClub(c *fiber.Ctx) error {
club, err := l.clubService.GetClub(c.Params("id"))
func (cl *ClubController) GetClub(c *fiber.Ctx) error {
club, err := cl.clubService.GetClub(c.Params("clubID"))
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusOK).JSON(club)
}

func (l *ClubController) UpdateClub(c *fiber.Ctx) error {
func (cl *ClubController) UpdateClub(c *fiber.Ctx) error {
var clubBody models.UpdateClubRequestBody

if err := c.BodyParser(&clubBody); err != nil {
return errors.FailedToParseRequestBody.FiberError(c)
}

updatedClub, err := l.clubService.UpdateClub(c.Params("id"), clubBody)
updatedClub, err := cl.clubService.UpdateClub(c.Params("clubID"), clubBody)
if err != nil {
return err.FiberError(c)
}

return c.Status(fiber.StatusOK).JSON(updatedClub)
}

func (l *ClubController) DeleteClub(c *fiber.Ctx) error {
err := l.clubService.DeleteClub(c.Params("id"))
func (cl *ClubController) DeleteClub(c *fiber.Ctx) error {
err := cl.clubService.DeleteClub(c.Params("clubID"))
if err != nil {
return err.FiberError(c)
}
6 changes: 3 additions & 3 deletions backend/src/controllers/user.go
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ func (u *UserController) GetUsers(c *fiber.Ctx) error {
// @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"))
user, err := u.userService.GetUser(c.Params("userID"))
if err != nil {
return err.FiberError(c)
}
@@ -110,7 +110,7 @@ func (u *UserController) UpdateUser(c *fiber.Ctx) error {
return errors.FailedToParseRequestBody.FiberError(c)
}

updatedUser, err := u.userService.UpdateUser(c.Params("id"), user)
updatedUser, err := u.userService.UpdateUser(c.Params("userID"), user)
if err != nil {
return err.FiberError(c)
}
@@ -130,7 +130,7 @@ func (u *UserController) UpdateUser(c *fiber.Ctx) error {
// @Failure 500 {string} string "failed to get all users"
// @Router /api/v1/users/:id [delete]
func (u *UserController) DeleteUser(c *fiber.Ctx) error {
err := u.userService.DeleteUser(c.Params("id"))
err := u.userService.DeleteUser(c.Params("userID"))
if err != nil {
return err.FiberError(c)
}
66 changes: 35 additions & 31 deletions backend/src/database/db.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package database

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

@@ -11,21 +10,44 @@ import (
)

func ConfigureDB(settings config.Settings) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(settings.Database.WithDb()), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
SkipDefaultTransaction: true,
TranslateError: true,
})
db, err := EstablishConn(settings.Database.WithDb(), WithLoggerInfo())
if err != nil {
return nil, err
}

err = db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error
if err := MigrateDB(settings, db); err != nil {
return nil, err
}

return db, nil
}

type OptionalFunc func(gorm.Config) gorm.Config

func WithLoggerInfo() OptionalFunc {
return func(gormConfig gorm.Config) gorm.Config {
gormConfig.Logger = logger.Default.LogMode(logger.Info)
return gormConfig
}
}

func EstablishConn(dsn string, opts ...OptionalFunc) (*gorm.DB, error) {
rootConfig := gorm.Config{
SkipDefaultTransaction: true,
TranslateError: true,
}

for _, opt := range opts {
rootConfig = opt(rootConfig)
}

db, err := gorm.Open(postgres.Open(dsn), &rootConfig)
if err != nil {
return nil, err
}

if err := MigrateDB(settings, db); err != nil {
err = db.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"").Error
if err != nil {
return nil, err
}

@@ -78,22 +100,12 @@ func createSuperUser(settings config.Settings, db *gorm.DB) error {
return err
}

passwordHash, err := auth.ComputePasswordHash(settings.SuperUser.Password)
superUser, err := SuperUser(settings.SuperUser)
if err != nil {
tx.Rollback()
return err
}

superUser := models.User{
Role: models.Super,
NUID: "000000000",
Email: "[email protected]",
PasswordHash: *passwordHash,
FirstName: "SAC",
LastName: "Super",
College: models.KCCS,
Year: models.First,
}

var user models.User

if err := db.Where("nuid = ?", superUser.NUID).First(&user).Error; err != nil {
@@ -108,17 +120,9 @@ func createSuperUser(settings config.Settings, db *gorm.DB) error {
return err
}

superClub := models.Club{
Name: "SAC",
Preview: "SAC",
Description: "SAC",
NumMembers: 0,
IsRecruiting: true,
RecruitmentCycle: models.RecruitmentCycle(models.Always),
RecruitmentType: models.Application,
ApplicationLink: "https://generatenu.com/apply",
Logo: "https://aws.amazon.com/s3",
}
SuperUserUUID = superUser.ID

superClub := SuperClub()

if err := tx.Create(&superClub).Error; err != nil {
tx.Rollback()
43 changes: 43 additions & 0 deletions backend/src/database/super.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package database

import (
"github.com/GenerateNU/sac/backend/src/auth"
"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
"github.com/google/uuid"
)

var SuperUserUUID uuid.UUID

func SuperUser(superUserSettings config.SuperUserSettings) (*models.User, *errors.Error) {
passwordHash, err := auth.ComputePasswordHash(superUserSettings.Password)
if err != nil {
return nil, &errors.FailedToComputePasswordHash
}

return &models.User{
Role: models.Super,
NUID: "000000000",
Email: "generatesac@gmail.com",
PasswordHash: *passwordHash,
FirstName: "SAC",
LastName: "Super",
College: models.KCCS,
Year: models.First,
}, nil
}

func SuperClub() models.Club {
return models.Club{
Name: "SAC",
Preview: "SAC",
Description: "SAC",
NumMembers: 0,
IsRecruiting: true,
RecruitmentCycle: models.RecruitmentCycle(models.Always),
RecruitmentType: models.Application,
ApplicationLink: "https://generatenu.com/apply",
Logo: "https://aws.amazon.com/s3",
}
}
14 changes: 14 additions & 0 deletions backend/src/errors/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package errors

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

var (
PassedAuthenticateMiddlewareButNilClaims = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "passed authenticate middleware but claims is nil",
}
FailedToCastToCustomClaims = Error{
StatusCode: fiber.StatusInternalServerError,
Message: "failed to cast to custom claims",
}
)
4 changes: 0 additions & 4 deletions backend/src/errors/common.go
Original file line number Diff line number Diff line change
@@ -63,8 +63,4 @@ var (
StatusCode: fiber.StatusUnauthorized,
Message: "failed to validate access token",
}
FailedToParseUUID = Error{
StatusCode: fiber.StatusBadRequest,
Message: "failed to parse uuid",
}
)
12 changes: 1 addition & 11 deletions backend/src/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"flag"
"fmt"
"path/filepath"

@@ -19,12 +18,7 @@ import (
// @host 127.0.0.1:8080
// @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", filepath.Join("..", "..", "config"), "Specify the path to the config directory")

flag.Parse()

config, err := config.GetConfiguration(*configPath)
config, err := config.GetConfiguration(filepath.Join("..", "..", "config"))
if err != nil {
panic(fmt.Sprintf("Error getting configuration: %s", err.Error()))
}
@@ -34,10 +28,6 @@ func main() {
panic(fmt.Sprintf("Error configuring database: %s", err.Error()))
}

if *onlyMigrate {
return
}

err = database.ConnPooling(db)
if err != nil {
panic(err)
32 changes: 29 additions & 3 deletions backend/src/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import (
"github.com/GenerateNU/sac/backend/src/types"

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

var paths = []string{
@@ -18,17 +19,31 @@ var paths = []string{
"/api/v1/auth/logout",
}

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)
return false
}
if claims == nil {
return false
}
return claims.Role == string(models.Super)
})
}

func (m *MiddlewareService) Authenticate(c *fiber.Ctx) error {
if slices.Contains(paths, c.Path()) {
return c.Next()
}

token, err := auth.ParseAccessToken(c.Cookies("access_token"))
token, err := auth.ParseAccessToken(c.Cookies("access_token"), m.AuthSettings.AccessToken)
if err != nil {
return errors.FailedToParseAccessToken.FiberError(c)
}

_, ok := token.Claims.(*types.CustomClaims)
claims, ok := token.Claims.(*types.CustomClaims)
if !ok || !token.Valid {
return errors.FailedToValidateAccessToken.FiberError(c)
}
@@ -37,12 +52,23 @@ func (m *MiddlewareService) Authenticate(c *fiber.Ctx) error {
return errors.Unauthorized.FiberError(c)
}

c.Locals("claims", claims)

return c.Next()
}

func (m *MiddlewareService) Authorize(requiredPermissions ...types.Permission) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
role, err := auth.GetRoleFromToken(c.Cookies("access_token"))
claims, fromErr := types.From(c)
if fromErr != nil {
return fromErr.FiberError(c)
}

if claims != nil && claims.Role == string(models.Super) {
return c.Next()
}

role, err := auth.GetRoleFromToken(c.Cookies("access_token"), m.AuthSettings.AccessToken)
if err != nil {
return errors.FailedToParseAccessToken.FiberError(c)
}
6 changes: 3 additions & 3 deletions backend/src/middleware/club.go
Original file line number Diff line number Diff line change
@@ -12,12 +12,12 @@ import (
)

func (m *MiddlewareService) ClubAuthorizeById(c *fiber.Ctx) error {
clubUUID, err := utilities.ValidateID(c.Params("id"))
clubUUID, err := utilities.ValidateID(c.Params("clubID"))
if err != nil {
return errors.FailedToParseUUID.FiberError(c)
return errors.FailedToValidateID.FiberError(c)
}

token, tokenErr := auth.ParseAccessToken(c.Cookies("access_token"))
token, tokenErr := auth.ParseAccessToken(c.Cookies("access_token"), m.AuthSettings.AccessToken)
if tokenErr != nil {
return errors.FailedToParseAccessToken.FiberError(c)
}
13 changes: 8 additions & 5 deletions backend/src/middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package middleware

import (
"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/types"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -15,13 +16,15 @@ type MiddlewareInterface interface {
}

type MiddlewareService struct {
DB *gorm.DB
Validate *validator.Validate
DB *gorm.DB
Validate *validator.Validate
AuthSettings config.AuthSettings
}

func NewMiddlewareService(db *gorm.DB, validate *validator.Validate) *MiddlewareService {
func NewMiddlewareService(db *gorm.DB, validate *validator.Validate, authSettings config.AuthSettings) *MiddlewareService {
return &MiddlewareService{
DB: db,
Validate: validate,
DB: db,
Validate: validate,
AuthSettings: authSettings,
}
}
8 changes: 4 additions & 4 deletions backend/src/middleware/user.go
Original file line number Diff line number Diff line change
@@ -9,12 +9,12 @@ import (
)

func (m *MiddlewareService) UserAuthorizeById(c *fiber.Ctx) error {
idAsUUID, err := utilities.ValidateID(c.Params("id"))
idAsUUID, err := utilities.ValidateID(c.Params("userID"))
if err != nil {
return errors.FailedToParseUUID.FiberError(c)
return errors.FailedToValidateID.FiberError(c)
}

token, tokenErr := auth.ParseAccessToken(c.Cookies("access_token"))
token, tokenErr := auth.ParseAccessToken(c.Cookies("access_token"), m.AuthSettings.AccessToken)
if tokenErr != nil {
return err
}
@@ -26,7 +26,7 @@ func (m *MiddlewareService) UserAuthorizeById(c *fiber.Ctx) error {

issuerIDAsUUID, err := utilities.ValidateID(claims.Issuer)
if err != nil {
return errors.FailedToParseUUID.FiberError(c)
return errors.FailedToValidateID.FiberError(c)
}

if issuerIDAsUUID.String() == idAsUUID.String() {
3 changes: 1 addition & 2 deletions backend/src/models/club.go
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ type Club struct {
Parent *uuid.UUID `gorm:"foreignKey:Parent" json:"-" validate:"uuid4"`
Tag []Tag `gorm:"many2many:club_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
// User
Admin []User `gorm:"many2many:user_club_admins;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"required"`
Member []User `gorm:"many2many:user_club_members;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"required"`
Follower []User `gorm:"many2many:user_club_followers;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
IntendedApplicant []User `gorm:"many2many:user_club_intended_applicants;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
@@ -56,7 +57,6 @@ type CreateClubRequestBody struct {
Name string `json:"name" validate:"required,max=255"`
Preview string `json:"preview" validate:"required,max=255"`
Description string `json:"description" validate:"required,http_url,mongo_url,max=255"` // MongoDB URL
NumMembers int `json:"num_members" validate:"required,min=1"`
IsRecruiting bool `json:"is_recruiting" validate:"required"`
RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
@@ -68,7 +68,6 @@ type UpdateClubRequestBody struct {
Name string `json:"name" validate:"omitempty,max=255"`
Preview string `json:"preview" validate:"omitempty,max=255"`
Description string `json:"description" validate:"omitempty,http_url,mongo_url,max=255"` // MongoDB URL
NumMembers int `json:"num_members" validate:"omitempty,min=1"`
IsRecruiting bool `json:"is_recruiting" validate:"omitempty"`
RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
4 changes: 2 additions & 2 deletions backend/src/models/user.go
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import "github.com/google/uuid"

type UserRole string

const (
var (
Super UserRole = "super"
Student UserRole = "student"
)
@@ -47,6 +47,7 @@ type User struct {
Year Year `gorm:"type:smallint" json:"year" validate:"required,min=1,max=6"`

Tag []Tag `gorm:"many2many:user_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Admin []Club `gorm:"many2many:user_club_admins;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Member []Club `gorm:"many2many:user_club_members;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Follower []Club `gorm:"many2many:user_club_followers;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
IntendedApplicant []Club `gorm:"many2many:user_club_intended_applicants;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
@@ -71,7 +72,6 @@ type UpdateUserRequestBody struct {
FirstName string `json:"first_name" validate:"omitempty,max=255"`
LastName string `json:"last_name" validate:"omitempty,max=255"`
Email string `json:"email" validate:"omitempty,email,neu_email,max=255"`
Password string `json:"password" validate:"omitempty,password"`
College College `json:"college" validate:"omitempty,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"`
Year Year `json:"year" validate:"omitempty,min=1,max=6"`
}
20 changes: 20 additions & 0 deletions backend/src/server/routes/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/gofiber/fiber/v2"
)

func Auth(router fiber.Router, authService services.AuthServiceInterface, authSettings config.AuthSettings) {
authController := controllers.NewAuthController(authService, authSettings)

// api/v1/auth/*
auth := router.Group("/auth")

auth.Post("/login", authController.Login)
auth.Get("/logout", authController.Logout)
auth.Get("/refresh", authController.Refresh)
auth.Get("/me", authController.Me)
}
21 changes: 21 additions & 0 deletions backend/src/server/routes/category.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/gofiber/fiber/v2"
)

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

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

categories.Post("/", categoryController.CreateCategory)
categories.Get("/", categoryController.GetCategories)
categories.Get("/:categoryID", categoryController.GetCategory)
categories.Delete("/:categoryID", categoryController.DeleteCategory)
categories.Patch("/:categoryID", categoryController.UpdateCategory)

return categories
}
16 changes: 16 additions & 0 deletions backend/src/server/routes/category_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/gofiber/fiber/v2"
)

func CategoryTag(router fiber.Router, categoryTagService services.CategoryTagServiceInterface) {
categoryTagController := controllers.NewCategoryTagController(categoryTagService)

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

categoryTags.Get("/", categoryTagController.GetTagsByCategory)
categoryTags.Get("/:tagID", categoryTagController.GetTagByCategory)
}
26 changes: 26 additions & 0 deletions backend/src/server/routes/club.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/middleware"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/GenerateNU/sac/backend/src/types"
"github.com/gofiber/fiber/v2"
)

func Club(router fiber.Router, clubService services.ClubServiceInterface, middlewareService middleware.MiddlewareInterface) {
clubController := controllers.NewClubController(clubService)

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

clubs.Get("/", middlewareService.Authorize(types.ClubReadAll), clubController.GetAllClubs)
clubs.Post("/", clubController.CreateClub)

// api/v1/clubs/:clubID/*
clubsID := clubs.Group("/:clubID")
clubsID.Use(middleware.SuperSkipper(middlewareService.UserAuthorizeById))

clubsID.Get("/", clubController.GetClub)
clubsID.Patch("/", middlewareService.Authorize(types.ClubWrite), clubController.UpdateClub)
clubsID.Delete("/", middleware.SuperSkipper(middlewareService.Authorize(types.ClubDelete)), clubController.DeleteClub)
}
18 changes: 18 additions & 0 deletions backend/src/server/routes/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/gofiber/fiber/v2"
)

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

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

tags.Get("/:tagID", tagController.GetTag)
tags.Post("/", tagController.CreateTag)
tags.Patch("/:tagID", tagController.UpdateTag)
tags.Delete("/:tagID", tagController.DeleteTag)
}
28 changes: 28 additions & 0 deletions backend/src/server/routes/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/middleware"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/GenerateNU/sac/backend/src/types"
"github.com/gofiber/fiber/v2"
)

func User(router fiber.Router, userService services.UserServiceInterface, middlewareService middleware.MiddlewareInterface) fiber.Router {
userController := controllers.NewUserController(userService)

// api/v1/users/*
users := router.Group("/users")
users.Post("/", userController.CreateUser)
users.Get("/", middleware.SuperSkipper(middlewareService.Authorize(types.UserReadAll)), userController.GetUsers)

// api/v1/users/:userID/*
usersID := users.Group("/:userID")
usersID.Use(middleware.SuperSkipper(middlewareService.UserAuthorizeById))

usersID.Get("/", userController.GetUser)
usersID.Patch("/", userController.UpdateUser)
usersID.Delete("/", userController.DeleteUser)

return users
}
16 changes: 16 additions & 0 deletions backend/src/server/routes/user_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package routes

import (
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/gofiber/fiber/v2"
)

func UserTag(router fiber.Router, userTagService services.UserTagServiceInterface) {
userTagController := controllers.NewUserTagController(userTagService)

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

userTags.Post("/", userTagController.CreateUserTags)
userTags.Get("/", userTagController.GetUserTags)
}
13 changes: 13 additions & 0 deletions backend/src/server/routes/utility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package routes

import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/swagger"
)

func Utility(router fiber.Router) {
router.Get("/swagger/*", swagger.HandlerDefault)
router.Get("/health", func(c *fiber.Ctx) error {
return c.SendStatus(200)
})
}
129 changes: 15 additions & 114 deletions backend/src/server/server.go
Original file line number Diff line number Diff line change
@@ -2,18 +2,16 @@ package server

import (
"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/controllers"
"github.com/GenerateNU/sac/backend/src/middleware"
"github.com/GenerateNU/sac/backend/src/server/routes"
"github.com/GenerateNU/sac/backend/src/services"
"github.com/GenerateNU/sac/backend/src/types"
"github.com/GenerateNU/sac/backend/src/utilities"
"github.com/goccy/go-json"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/gofiber/swagger"
"gorm.io/gorm"
)

@@ -29,19 +27,24 @@ func Init(db *gorm.DB, settings config.Settings) *fiber.App {
app := newFiberApp()

validate := utilities.RegisterCustomValidators()
middlewareService := middleware.NewMiddlewareService(db, validate)
middlewareService := middleware.NewMiddlewareService(db, validate, settings.Auth)

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

utilityRoutes(app)
authRoutes(apiv1, services.NewAuthService(db, validate), settings.Auth)
userRouter := userRoutes(apiv1, services.NewUserService(db, validate), middlewareService)
userTagRouter(userRouter, services.NewUserTagService(db, validate))
clubRoutes(apiv1, services.NewClubService(db, validate), middlewareService)
categoryRouter := categoryRoutes(apiv1, services.NewCategoryService(db, validate))
tagRoutes(apiv1, services.NewTagService(db, validate))
categoryTagRoutes(categoryRouter, services.NewCategoryTagService(db, validate))
routes.Utility(app)

routes.Auth(apiv1, services.NewAuthService(db, validate), settings.Auth)

userRouter := routes.User(apiv1, services.NewUserService(db, validate), middlewareService)
routes.UserTag(userRouter, services.NewUserTagService(db, validate))

routes.Club(apiv1, services.NewClubService(db, validate), middlewareService)

routes.Tag(apiv1, services.NewTagService(db, validate))

categoryRouter := routes.Category(apiv1, services.NewCategoryService(db, validate))
routes.CategoryTag(categoryRouter, services.NewCategoryTagService(db, validate))

return app
}
@@ -63,105 +66,3 @@ func newFiberApp() *fiber.App {

return app
}

func utilityRoutes(router fiber.Router) {
router.Get("/swagger/*", swagger.HandlerDefault)
router.Get("/health", func(c *fiber.Ctx) error {
return c.SendStatus(200)
})
}

func userRoutes(router fiber.Router, userService services.UserServiceInterface, middlewareService middleware.MiddlewareInterface) fiber.Router {
userController := controllers.NewUserController(userService)

// api/v1/users/*
users := router.Group("/users")
users.Post("/", userController.CreateUser)
users.Get("/", middlewareService.Authorize(types.UserReadAll), userController.GetUsers)

// api/v1/users/:id/*
usersID := users.Group("/:id")
usersID.Use(middlewareService.UserAuthorizeById)

usersID.Get("/", middlewareService.Authorize(types.UserRead), userController.GetUser)
usersID.Patch("/", middlewareService.Authorize(types.UserWrite), userController.UpdateUser)
usersID.Delete("/", middlewareService.Authorize(types.UserDelete), userController.DeleteUser)

users.Get("/", userController.GetUsers)
users.Get("/:id", userController.GetUser)
users.Patch("/:id", userController.UpdateUser)
users.Delete("/:id", userController.DeleteUser)

return users
}

func userTagRouter(router fiber.Router, userTagService services.UserTagServiceInterface) {
userTagController := controllers.NewUserTagController(userTagService)

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

userTags.Post("/", userTagController.CreateUserTags)
userTags.Get("/", userTagController.GetUserTags)
}

func clubRoutes(router fiber.Router, clubService services.ClubServiceInterface, middlewareService middleware.MiddlewareInterface) {
clubController := controllers.NewClubController(clubService)

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

clubs.Get("/", middlewareService.Authorize(types.ClubReadAll), clubController.GetAllClubs)
clubs.Post("/", clubController.CreateClub)

// api/v1/clubs/:id/*
clubsID := clubs.Group("/:id")
clubsID.Use(middlewareService.ClubAuthorizeById)

clubsID.Get("/", clubController.GetClub)
clubsID.Patch("/", middlewareService.Authorize(types.ClubWrite), clubController.UpdateClub)
clubsID.Delete("/", middlewareService.Authorize(types.ClubDelete), clubController.DeleteClub)
}

func authRoutes(router fiber.Router, authService services.AuthServiceInterface, authSettings config.AuthSettings) {
authController := controllers.NewAuthController(authService, authSettings)

// api/v1/auth/*
auth := router.Group("/auth")
auth.Post("/login", authController.Login)
auth.Get("/logout", authController.Logout)
auth.Get("/refresh", authController.Refresh)
auth.Get("/me", authController.Me)
}

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

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

categories.Post("/", categoryController.CreateCategory)
categories.Get("/", categoryController.GetCategories)
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.Get("/:tagID", tagController.GetTag)
tags.Post("/", tagController.CreateTag)
tags.Patch("/:tagID", tagController.UpdateTag)
tags.Delete("/:tagID", tagController.DeleteTag)
}

func categoryTagRoutes(router fiber.Router, categoryTagService services.CategoryTagServiceInterface) {
categoryTagController := controllers.NewCategoryTagController(categoryTagService)

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

categoryTags.Get("/", categoryTagController.GetTagsByCategory)
categoryTags.Get("/:tagID", categoryTagController.GetTagByCategory)
}
7 changes: 0 additions & 7 deletions backend/src/services/user.go
Original file line number Diff line number Diff line change
@@ -86,18 +86,11 @@ func (u *UserService) UpdateUser(id string, userBody models.UpdateUserRequestBod
return nil, &errors.FailedToValidateUser
}

passwordHash, err := auth.ComputePasswordHash(userBody.Password)
if err != nil {
return nil, &errors.FailedToComputePasswordHash
}

user, err := utilities.MapRequestToModel(userBody, &models.User{})
if err != nil {
return nil, &errors.FailedToMapRequestToModel
}

user.PasswordHash = *passwordHash

return transactions.UpdateUser(u.DB, *idAsUUID, *user)
}

9 changes: 9 additions & 0 deletions backend/src/transactions/category_tag.go
Original file line number Diff line number Diff line change
@@ -12,6 +12,15 @@ import (
)

func GetTagsByCategory(db *gorm.DB, categoryID uuid.UUID, limit int, offset int) ([]models.Tag, *errors.Error) {
var category models.Category

if err := db.Where("id = ?", categoryID).First(&category).Error; err != nil {
if stdliberrors.Is(err, gorm.ErrRecordNotFound) {
return nil, &errors.CategoryNotFound
}
return nil, &errors.FailedToGetCategory
}

var tags []models.Tag
if err := db.Where("category_id = ?", categoryID).Limit(limit).Offset(offset).Find(&tags).Error; err != nil {
return nil, &errors.FailedToGetTags
20 changes: 19 additions & 1 deletion backend/src/types/custom_claims.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
package types

import "github.com/golang-jwt/jwt"
import (
"github.com/GenerateNU/sac/backend/src/errors"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt"
)

type CustomClaims struct {
jwt.StandardClaims
Role string `json:"role"`
}

func From(c *fiber.Ctx) (*CustomClaims, *errors.Error) {
rawClaims := c.Locals("claims")
if rawClaims == nil {
return nil, nil
}

claims, ok := rawClaims.(*CustomClaims)
if !ok {
return nil, &errors.FailedToCastToCustomClaims
}

return claims, nil
}
2 changes: 1 addition & 1 deletion backend/src/types/permissions.go
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ const (

var rolePermissions = map[models.UserRole][]Permission{
models.Super: {
UserRead, UserWrite, UserDelete,
UserRead, UserReadAll, UserWrite, UserDelete,
TagRead, TagCreate, TagWrite, TagDelete,
ClubRead, ClubCreate, ClubWrite, ClubDelete,
PointOfContactRead, PointOfContactCreate, PointOfContactWrite, PointOfContactDelete,
4 changes: 2 additions & 2 deletions backend/tests/README.md
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ TestRequest{
Path: "/api/v1/categories/",
Body: SampleCategoryFactory(),
}.TestOnStatusAndDB(t, nil,
DBTesterWithStatus{
TesterWithStatus{
Status: fiber.StatusCreated,
DBTester: AssertSampleCategoryBodyRespDB,
},
@@ -62,7 +62,7 @@ TestRequest{

### 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.
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 `TesterWithStatus` struct.

```go
func AssertSampleCategoryBodyRespDB(app TestApp, assert *assert.A, resp *http.Response) {
134 changes: 78 additions & 56 deletions backend/tests/api/category_tag_test.go
Original file line number Diff line number Diff line change
@@ -7,13 +7,15 @@ import (

"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
h "github.com/GenerateNU/sac/backend/tests/api/helpers"

"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/huandu/go-assert"
)

func AssertTagsWithBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, body *[]map[string]interface{}) []uuid.UUID {
func AssertTagsWithBodyRespDB(app h.TestApp, assert *assert.A, resp *http.Response, body *[]map[string]interface{}) []uuid.UUID {
var respTags []models.Tag

err := json.NewDecoder(resp.Body).Decode(&respTags)
@@ -43,26 +45,28 @@ func AssertTagsWithBodyRespDB(app TestApp, assert *assert.A, resp *http.Response
}

func TestGetCategoryTagsWorks(t *testing.T) {
appAssert, categoryUUID, tagID := CreateSampleTag(t)
appAssert, categoryUUID, tagID := CreateSampleTag(h.InitTest(t))

body := SampleTagFactory(categoryUUID)
(*body)["id"] = tagID

TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags", categoryUUID),
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
appAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags", categoryUUID),
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusOK,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
AssertTagsWithBodyRespDB(app, assert, resp, &[]map[string]interface{}{*body})
},
},
).Close()
}

func TestGetCategoryTagsFailsCategoryBadRequest(t *testing.T) {
appAssert, _ := CreateSampleCategory(t, nil)
appAssert, _ := CreateSampleCategory(h.InitTest(t))

badRequests := []string{
"0",
@@ -73,56 +77,60 @@ func TestGetCategoryTagsFailsCategoryBadRequest(t *testing.T) {
}

for _, badRequest := range badRequests {
TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags", badRequest),
}.TestOnError(t, &appAssert, errors.FailedToValidateID)
appAssert.TestOnError(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags", badRequest),
Role: &models.Super,
},
errors.FailedToValidateID,
)
}

appAssert.Close()
}

func TestGetCategoryTagsFailsCategoryNotFound(t *testing.T) {
appAssert, _ := CreateSampleCategory(t, nil)
appAssert, _ := CreateSampleCategory(h.InitTest(t))

uuid := uuid.New()

TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags", uuid),
}.TestOnErrorAndDB(t, &appAssert, ErrorWithDBTester{
Error: errors.CategoryNotFound,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
var category models.Category
err := app.Conn.Where("id = ?", uuid).First(&category).Error
assert.Error(err)

var respBody []map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&respBody)
assert.NilError(err)
assert.Equal(0, len(respBody))
appAssert.TestOnErrorAndDB(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags", uuid),
Role: &models.Super,
}, h.ErrorWithTester{
Error: errors.CategoryNotFound,
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var category models.Category
err := app.Conn.Where("id = ?", uuid).First(&category).Error
assert.Assert(err != nil)
},
},
}).Close()
).Close()
}

func TestGetCategoryTagWorks(t *testing.T) {
existingAppAssert, categoryUUID, tagUUID := CreateSampleTag(t)
existingAppAssert, categoryUUID, tagUUID := CreateSampleTag(h.InitTest(t))

TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID),
}.TestOnStatusAndDB(t, &existingAppAssert,
DBTesterWithStatus{
existingAppAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, tagUUID),
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusOK,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
AssertTagWithBodyRespDB(app, assert, resp, SampleTagFactory(categoryUUID))
},
},
).Close()
}

func TestGetCategoryTagFailsCategoryBadRequest(t *testing.T) {
appAssert, _, tagUUID := CreateSampleTag(t)
appAssert, _, tagUUID := CreateSampleTag(h.InitTest(t))

badRequests := []string{
"0",
@@ -133,17 +141,20 @@ func TestGetCategoryTagFailsCategoryBadRequest(t *testing.T) {
}

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.TestOnError(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", badRequest, tagUUID),
Role: &models.Super,
}, errors.FailedToValidateID,
)
}

appAssert.Close()
}

func TestGetCategoryTagFailsTagBadRequest(t *testing.T) {
appAssert, categoryUUID := CreateSampleCategory(t, nil)
appAssert, categoryUUID := CreateSampleCategory(h.InitTest(t))

badRequests := []string{
"0",
@@ -154,29 +165,40 @@ func TestGetCategoryTagFailsTagBadRequest(t *testing.T) {
}

for _, badRequest := range badRequests {
TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, badRequest),
}.TestOnError(t, &appAssert, errors.FailedToValidateID)
appAssert.TestOnError(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, badRequest),
Role: &models.Super,
},
errors.FailedToValidateID)
}

appAssert.Close()
}

func TestGetCategoryTagFailsCategoryNotFound(t *testing.T) {
appAssert, _, tagUUID := CreateSampleTag(t)
appAssert, _, tagUUID := CreateSampleTag(h.InitTest(t))

TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", uuid.New(), tagUUID),
}.TestOnError(t, &appAssert, errors.TagNotFound).Close()
appAssert.TestOnError(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", uuid.New(), tagUUID),
Role: &models.Super,
},
errors.TagNotFound,
).Close()
}

func TestGetCategoryTagFailsTagNotFound(t *testing.T) {
appAssert, categoryUUID := CreateSampleCategory(t, nil)
appAssert, categoryUUID := CreateSampleCategory(h.InitTest(t))

TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, uuid.New()),
}.TestOnError(t, &appAssert, errors.TagNotFound).Close()
appAssert.TestOnError(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/categories/%s/tags/%s", categoryUUID, uuid.New()),
Role: &models.Super,
},
errors.TagNotFound,
).Close()
}
302 changes: 171 additions & 131 deletions backend/tests/api/category_test.go

Large diffs are not rendered by default.

215 changes: 121 additions & 94 deletions backend/tests/api/club_test.go
Original file line number Diff line number Diff line change
@@ -8,20 +8,20 @@ import (

"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
h "github.com/GenerateNU/sac/backend/tests/api/helpers"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/huandu/go-assert"
"gorm.io/gorm"
)

func SampleClubFactory(userID uuid.UUID) *map[string]interface{} {
func SampleClubFactory(userID *uuid.UUID) *map[string]interface{} {
return &map[string]interface{}{
"user_id": userID,
"name": "Generate",
"preview": "Generate is Northeastern's premier student-led product development studio.",
"description": "https://mongodb.com",
"num_members": 1,
"is_recruiting": true,
"recruitment_cycle": "always",
"recruitment_type": "application",
@@ -30,7 +30,7 @@ func SampleClubFactory(userID uuid.UUID) *map[string]interface{} {
}
}

func AssertClubBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID {
func AssertClubBodyRespDB(app h.TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID {
var respClub models.Club

err := json.NewDecoder(resp.Body).Decode(&respClub)
@@ -66,11 +66,10 @@ func AssertClubBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, bo

assert.Equal(1, len(dbAdmins))

assert.Equal((*body)["user_id"].(uuid.UUID), dbAdmins[0].ID)
assert.Equal(*(*body)["user_id"].(*uuid.UUID), dbAdmins[0].ID)
assert.Equal((*body)["name"].(string), dbClub.Name)
assert.Equal((*body)["preview"].(string), dbClub.Preview)
assert.Equal((*body)["description"].(string), dbClub.Description)
assert.Equal((*body)["num_members"].(int), dbClub.NumMembers)
assert.Equal((*body)["is_recruiting"].(bool), dbClub.IsRecruiting)
assert.Equal(models.RecruitmentCycle((*body)["recruitment_cycle"].(string)), dbClub.RecruitmentCycle)
assert.Equal(models.RecruitmentType((*body)["recruitment_type"].(string)), dbClub.RecruitmentType)
@@ -80,7 +79,7 @@ func AssertClubBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, bo
return dbClub.ID
}

func AssertClubWithBodyRespDBMostRecent(app TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID {
func AssertClubWithBodyRespDBMostRecent(app h.TestApp, assert *assert.A, resp *http.Response, body *map[string]interface{}) uuid.UUID {
var respClub models.Club

err := json.NewDecoder(resp.Body).Decode(&respClub)
@@ -128,48 +127,49 @@ func AssertClubWithBodyRespDBMostRecent(app TestApp, assert *assert.A, resp *htt
return dbClub.ID
}

func AssertSampleClubBodyRespDB(app TestApp, assert *assert.A, resp *http.Response, userID uuid.UUID) uuid.UUID {
return AssertClubBodyRespDB(app, assert, resp, SampleClubFactory(userID))
}
func AssertSampleClubBodyRespDB(app h.TestApp, assert *assert.A, resp *http.Response, userID uuid.UUID) uuid.UUID {
sampleClub := SampleClubFactory(&userID)
(*sampleClub)["num_members"] = 1

func CreateSampleClub(t *testing.T, existingAppAssert *ExistingAppAssert) (eaa ExistingAppAssert, userUUID uuid.UUID, clubUUID uuid.UUID) {
appAssert, userID := CreateSampleUser(t, existingAppAssert)
return AssertClubBodyRespDB(app, assert, resp, sampleClub)
}

func CreateSampleClub(existingAppAssert h.ExistingAppAssert) (eaa h.ExistingAppAssert, studentUUID uuid.UUID, clubUUID uuid.UUID) {
var sampleClubUUID uuid.UUID

newAppAssert := TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/clubs/",
Body: SampleClubFactory(userID),
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
newAppAssert := existingAppAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/clubs/",
Body: SampleClubFactory(nil),
Role: &models.Super,
TestUserIDReplaces: h.StringToPointer("user_id"),
},
h.TesterWithStatus{
Status: fiber.StatusCreated,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
sampleClubUUID = AssertSampleClubBodyRespDB(app, assert, resp, userID)
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
sampleClubUUID = AssertSampleClubBodyRespDB(app, assert, resp, app.TestUser.UUID)
},
},
)

if existingAppAssert == nil {
return newAppAssert, userID, sampleClubUUID
} else {
return *existingAppAssert, userID, sampleClubUUID
}
return existingAppAssert, newAppAssert.App.TestUser.UUID, sampleClubUUID
}

func TestCreateClubWorks(t *testing.T) {
existingAppAssert, _, _ := CreateSampleClub(t, nil)
existingAppAssert, _, _ := CreateSampleClub(h.InitTest(t))
existingAppAssert.Close()
}

func TestGetClubsWorks(t *testing.T) {
TestRequest{
h.InitTest(t).TestOnStatusAndDB(h.TestRequest{
Method: fiber.MethodGet,
Path: "/api/v1/clubs/",
}.TestOnStatusAndDB(t, nil,
DBTesterWithStatus{
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusOK,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var respClubs []models.Club

err := json.NewDecoder(resp.Body).Decode(&respClubs)
@@ -215,7 +215,7 @@ func TestGetClubsWorks(t *testing.T) {
).Close()
}

func AssertNumClubsRemainsAtN(app TestApp, assert *assert.A, resp *http.Response, n int) {
func AssertNumClubsRemainsAtN(app h.TestApp, assert *assert.A, resp *http.Response, n int) {
var dbClubs []models.Club

err := app.Conn.Order("created_at desc").Find(&dbClubs).Error
@@ -225,25 +225,27 @@ func AssertNumClubsRemainsAtN(app TestApp, assert *assert.A, resp *http.Response
assert.Equal(n, len(dbClubs))
}

var TestNumClubsRemainsAt1 = func(app TestApp, assert *assert.A, resp *http.Response) {
var TestNumClubsRemainsAt1 = func(app h.TestApp, assert *assert.A, resp *http.Response) {
AssertNumClubsRemainsAtN(app, assert, resp, 1)
}

func AssertCreateBadClubDataFails(t *testing.T, jsonKey string, badValues []interface{}) {
appAssert, uuid := CreateSampleUser(t, nil)
appAssert, uuid, _ := CreateSampleStudent(t, nil)

for _, badValue := range badValues {
sampleClubPermutation := *SampleClubFactory(uuid)
sampleClubPermutation := *SampleClubFactory(&uuid)
sampleClubPermutation[jsonKey] = badValue

TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/clubs/",
Body: &sampleClubPermutation,
}.TestOnErrorAndDB(t, &appAssert,
ErrorWithDBTester{
Error: errors.FailedToValidateClub,
DBTester: TestNumClubsRemainsAt1,
appAssert.TestOnErrorAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/clubs/",
Body: &sampleClubPermutation,
Role: &models.Super,
},
h.ErrorWithTester{
Error: errors.FailedToValidateClub,
Tester: TestNumClubsRemainsAt1,
},
)
}
@@ -307,30 +309,32 @@ func TestCreateClubFailsOnInvalidLogo(t *testing.T) {
}

func TestUpdateClubWorks(t *testing.T) {
appAssert, userUUID, clubUUID := CreateSampleClub(t, nil)
appAssert, studentUUID, clubUUID := CreateSampleClub(h.InitTest(t))

updatedClub := SampleClubFactory(userUUID)
updatedClub := SampleClubFactory(&studentUUID)
(*updatedClub)["name"] = "Updated Name"
(*updatedClub)["preview"] = "Updated Preview"

TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID),
Body: updatedClub,
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
appAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID),
Body: updatedClub,
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusOK,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
AssertClubBodyRespDB(app, assert, resp, updatedClub)
},
},
).Close()
}

func TestUpdateClubFailsOnInvalidBody(t *testing.T) {
appAssert, userUUID, clubUUID := CreateSampleClub(t, nil)
appAssert, studentUUID, clubUUID := CreateSampleClub(h.InitTest(t))

body := SampleClubFactory(userUUID)
body := SampleClubFactory(&studentUUID)

for _, invalidData := range []map[string]interface{}{
{"description": "Not a URL"},
@@ -339,14 +343,16 @@ func TestUpdateClubFailsOnInvalidBody(t *testing.T) {
{"application_link": "Not an URL"},
{"logo": "@12394X_2"},
} {
TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID),
Body: &invalidData,
}.TestOnErrorAndDB(t, &appAssert,
ErrorWithDBTester{
appAssert.TestOnErrorAndDB(
h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID),
Body: &invalidData,
Role: &models.Super,
},
h.ErrorWithTester{
Error: errors.FailedToValidateClub,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var dbClubs []models.Club

err := app.Conn.Order("created_at desc").Find(&dbClubs).Error
@@ -365,11 +371,10 @@ func TestUpdateClubFailsOnInvalidBody(t *testing.T) {

assert.Equal(1, len(dbAdmins))

assert.Equal((*body)["user_id"].(uuid.UUID), dbAdmins[0].ID)
assert.Equal(*(*body)["user_id"].(*uuid.UUID), dbAdmins[0].ID)
assert.Equal((*body)["name"].(string), dbClub.Name)
assert.Equal((*body)["preview"].(string), dbClub.Preview)
assert.Equal((*body)["description"].(string), dbClub.Description)
assert.Equal((*body)["num_members"].(int), dbClub.NumMembers)
assert.Equal((*body)["is_recruiting"].(bool), dbClub.IsRecruiting)
assert.Equal(models.RecruitmentCycle((*body)["recruitment_cycle"].(string)), dbClub.RecruitmentCycle)
assert.Equal(models.RecruitmentType((*body)["recruitment_type"].(string)), dbClub.RecruitmentType)
@@ -383,6 +388,8 @@ func TestUpdateClubFailsOnInvalidBody(t *testing.T) {
}

func TestUpdateClubFailsBadRequest(t *testing.T) {
appAssert := h.InitTest(t)

badRequests := []string{
"0",
"-1",
@@ -391,28 +398,36 @@ func TestUpdateClubFailsBadRequest(t *testing.T) {
"null",
}

sampleStudent, rawPassword := h.SampleStudentFactory()

for _, badRequest := range badRequests {
TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", badRequest),
Body: SampleUserFactory(),
}.TestOnError(t, nil, errors.FailedToValidateID).Close()
appAssert.TestOnError(
h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", badRequest),
Body: h.SampleStudentJSONFactory(sampleStudent, rawPassword),
Role: &models.Super,
},
errors.FailedToValidateID,
)
}

appAssert.Close()
}

func TestUpdateClubFailsOnClubIdNotExist(t *testing.T) {
appAssert, userUUID := CreateSampleUser(t, nil)

uuid := uuid.New()

TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", uuid),
Body: SampleClubFactory(userUUID),
}.TestOnErrorAndDB(t, &appAssert,
ErrorWithDBTester{
h.InitTest(t).TestOnErrorAndDB(h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/clubs/%s", uuid),
Body: SampleClubFactory(nil),
Role: &models.Super,
TestUserIDReplaces: h.StringToPointer("user_id"),
},
h.ErrorWithTester{
Error: errors.ClubNotFound,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var club models.Club

err := app.Conn.Where("id = ?", uuid).First(&club).Error
@@ -424,28 +439,32 @@ func TestUpdateClubFailsOnClubIdNotExist(t *testing.T) {
}

func TestDeleteClubWorks(t *testing.T) {
appAssert, _, clubUUID := CreateSampleClub(t, nil)

TestRequest{
Method: fiber.MethodDelete,
Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID),
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
Status: fiber.StatusNoContent,
DBTester: TestNumClubsRemainsAt1,
appAssert, _, clubUUID := CreateSampleClub(h.InitTest(t))

appAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodDelete,
Path: fmt.Sprintf("/api/v1/clubs/%s", clubUUID),
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusNoContent,
Tester: TestNumClubsRemainsAt1,
},
).Close()
}

func TestDeleteClubNotExist(t *testing.T) {
uuid := uuid.New()
TestRequest{
Method: fiber.MethodDelete,
Path: fmt.Sprintf("/api/v1/clubs/%s", uuid),
}.TestOnErrorAndDB(t, nil,
ErrorWithDBTester{
h.InitTest(t).TestOnErrorAndDB(
h.TestRequest{
Method: fiber.MethodDelete,
Path: fmt.Sprintf("/api/v1/clubs/%s", uuid),
Role: &models.Super,
},
h.ErrorWithTester{
Error: errors.ClubNotFound,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var club models.Club

err := app.Conn.Where("id = ?", uuid).First(&club).Error
@@ -459,6 +478,8 @@ func TestDeleteClubNotExist(t *testing.T) {
}

func TestDeleteClubBadRequest(t *testing.T) {
appAssert := h.InitTest(t)

badRequests := []string{
"0",
"-1",
@@ -468,9 +489,15 @@ func TestDeleteClubBadRequest(t *testing.T) {
}

for _, badRequest := range badRequests {
TestRequest{
Method: fiber.MethodDelete,
Path: fmt.Sprintf("/api/v1/clubs/%s", badRequest),
}.TestOnError(t, nil, errors.FailedToValidateID).Close()
appAssert.TestOnError(
h.TestRequest{
Method: fiber.MethodDelete,
Path: fmt.Sprintf("/api/v1/clubs/%s", badRequest),
Role: &models.Super,
},
errors.FailedToValidateID,
)
}

appAssert.Close()
}
10 changes: 6 additions & 4 deletions backend/tests/api/health_test.go
Original file line number Diff line number Diff line change
@@ -3,14 +3,16 @@ package tests
import (
"testing"

h "github.com/GenerateNU/sac/backend/tests/api/helpers"
"github.com/gofiber/fiber/v2"
)

func TestHealthWorks(t *testing.T) {
TestRequest{
Method: fiber.MethodGet,
Path: "/health",
}.TestOnStatus(t, nil,
h.InitTest(t).TestOnStatus(
h.TestRequest{
Method: fiber.MethodGet,
Path: "/health",
},
fiber.StatusOK,
).Close()
}
262 changes: 0 additions & 262 deletions backend/tests/api/helpers.go

This file was deleted.

64 changes: 64 additions & 0 deletions backend/tests/api/helpers/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package helpers

import (
"fmt"
"net"
"path/filepath"

"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/server"
"github.com/gofiber/fiber/v2"
"github.com/huandu/go-assert"
"gorm.io/gorm"
)

type TestApp struct {
App *fiber.App
Address string
Conn *gorm.DB
Settings config.Settings
TestUser *TestUser
}

func spawnApp() (*TestApp, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}

configuration, err := config.GetConfiguration(filepath.Join("..", "..", "..", "config"))
if err != nil {
return nil, err
}

configuration.Database.DatabaseName = generateRandomDBName()

connectionWithDB, err := configureDatabase(configuration)
if err != nil {
return nil, err
}

return &TestApp{
App: server.Init(connectionWithDB, configuration),
Address: fmt.Sprintf("http://%s", listener.Addr().String()),
Conn: connectionWithDB,
Settings: configuration,
}, nil
}

type ExistingAppAssert struct {
App TestApp
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)
}
}
166 changes: 166 additions & 0 deletions backend/tests/api/helpers/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package helpers

import (
"github.com/GenerateNU/sac/backend/src/auth"
"github.com/GenerateNU/sac/backend/src/database"
"github.com/GenerateNU/sac/backend/src/models"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)

type TestUser struct {
UUID uuid.UUID
Email string
Password string
AccessToken string
RefreshToken string
}

func (app *TestApp) Auth(role models.UserRole) {
if role == models.Super {
app.authSuper()
} else if role == models.Student {
app.authStudent()
}
// unauthed -> do nothing
}

func (app *TestApp) authSuper() {
superUser, superUserErr := database.SuperUser(app.Settings.SuperUser)
if superUserErr != nil {
panic(superUserErr)
}

email := superUser.Email
password := app.Settings.SuperUser.Password

resp, err := app.Send(TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/auth/login",
Body: &map[string]interface{}{
"email": email,
"password": password,
},
})
if err != nil {
panic(err)
}

var accessToken string
var refreshToken string

for _, cookie := range resp.Cookies() {
if cookie.Name == "access_token" {
accessToken = cookie.Value
} else if cookie.Name == "refresh_token" {
refreshToken = cookie.Value
}
}

if accessToken == "" || refreshToken == "" {
panic("Failed to authenticate super user")
}

app.TestUser = &TestUser{
UUID: database.SuperUserUUID,
Email: email,
Password: password,
AccessToken: accessToken,
RefreshToken: refreshToken,
}
}

func (app *TestApp) authStudent() {
studentUser, rawPassword := SampleStudentFactory()

resp, err := app.Send(TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/",
Body: SampleStudentJSONFactory(studentUser, rawPassword),
})
if err != nil {
panic(err)
}
var respBody map[string]interface{}

err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
panic(err)
}

rawStudentUserUUID := respBody["id"].(string)
studentUserUUID, err := uuid.Parse(rawStudentUserUUID)
if err != nil {
panic(err)
}

resp, err = app.Send(TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/auth/login",
Body: &map[string]interface{}{
"email": studentUser.Email,
"password": rawPassword,
},
})
if err != nil {
panic(err)
}

var accessToken string
var refreshToken string

for _, cookie := range resp.Cookies() {
if cookie.Name == "access_token" {
accessToken = cookie.Value
} else if cookie.Name == "refresh_token" {
refreshToken = cookie.Value
}
}

if accessToken == "" || refreshToken == "" {
panic("Failed to authenticate sample student user")
}

app.TestUser = &TestUser{
UUID: studentUserUUID,
Email: studentUser.Email,
Password: rawPassword,
AccessToken: accessToken,
RefreshToken: refreshToken,
}
}

func SampleStudentFactory() (models.User, string) {
password := "1234567890&"
hashedPassword, err := auth.ComputePasswordHash(password)
if err != nil {
panic(err)
}

return models.User{
Role: models.Student,
FirstName: "Jane",
LastName: "Doe",
Email: "doe.jane@northeastern.edu",
PasswordHash: *hashedPassword,
NUID: "001234567",
College: models.KCCS,
Year: models.Third,
}, password
}

func SampleStudentJSONFactory(sampleStudent models.User, rawPassword string) *map[string]interface{} {
if sampleStudent.Role != models.Student {
panic("User is not a student")
}
return &map[string]interface{}{
"first_name": sampleStudent.FirstName,
"last_name": sampleStudent.LastName,
"email": sampleStudent.Email,
"password": rawPassword,
"nuid": sampleStudent.NUID,
"college": string(sampleStudent.College),
"year": int(sampleStudent.Year),
}
}
46 changes: 46 additions & 0 deletions backend/tests/api/helpers/database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package helpers

import (
"fmt"
"sync"

"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/database"
"gorm.io/gorm"
)

var (
rootConn *gorm.DB
once sync.Once
)

func RootConn(dbSettings config.DatabaseSettings) {
once.Do(func() {
var err error
rootConn, err = database.EstablishConn(dbSettings.WithoutDb())
if err != nil {
panic(err)
}
})
}

func configureDatabase(settings config.Settings) (*gorm.DB, error) {
RootConn(settings.Database)

err := rootConn.Exec(fmt.Sprintf("CREATE DATABASE %s", settings.Database.DatabaseName)).Error
if err != nil {
return nil, err
}

dbWithDB, err := database.EstablishConn(settings.Database.WithDb())
if err != nil {
return nil, err
}

err = database.MigrateDB(settings, dbWithDB)
if err != nil {
return nil, err
}

return dbWithDB, nil
}
164 changes: 164 additions & 0 deletions backend/tests/api/helpers/requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package helpers

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"

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

"github.com/goccy/go-json"

"github.com/huandu/go-assert"
)

type TestRequest struct {
Method string
Path string
Body *map[string]interface{}
Headers *map[string]string
Role *models.UserRole
TestUserIDReplaces *string
}

func (app TestApp) Send(request TestRequest) (*http.Response, error) {
address := fmt.Sprintf("%s%s", app.Address, request.Path)

var req *http.Request

if request.TestUserIDReplaces != nil {
if strings.Contains(request.Path, *request.TestUserIDReplaces) {
request.Path = strings.Replace(request.Path, *request.TestUserIDReplaces, app.TestUser.UUID.String(), 1)
address = fmt.Sprintf("%s%s", app.Address, request.Path)
}
if request.Body != nil {
if _, ok := (*request.Body)[*request.TestUserIDReplaces]; ok {
(*request.Body)[*request.TestUserIDReplaces] = app.TestUser.UUID.String()
}
}
}

if request.Body == nil {
req = httptest.NewRequest(request.Method, address, nil)
} else {
bodyBytes, err := json.Marshal(request.Body)
if err != nil {
return nil, err
}

req = httptest.NewRequest(request.Method, address, bytes.NewBuffer(bodyBytes))

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)
}
}

if app.TestUser != nil {
req.AddCookie(&http.Cookie{
Name: "access_token",
Value: app.TestUser.AccessToken,
})
}

resp, err := app.App.Test(req)
if err != nil {
return nil, err
}

return resp, nil
}

func (request TestRequest) test(existingAppAssert ExistingAppAssert) (ExistingAppAssert, *http.Response) {
if existingAppAssert.App.TestUser == nil && request.Role != nil {
existingAppAssert.App.Auth(*request.Role)
}

resp, err := existingAppAssert.App.Send(request)

existingAppAssert.Assert.NilError(err)

return existingAppAssert, resp
}

func (existingAppAssert ExistingAppAssert) TestOnStatus(request TestRequest, status int) ExistingAppAssert {
appAssert, resp := request.test(existingAppAssert)

_, assert := appAssert.App, appAssert.Assert

assert.Equal(status, resp.StatusCode)

return appAssert
}

func (request *TestRequest) testOn(existingAppAssert ExistingAppAssert, status int, key string, value string) (ExistingAppAssert, *http.Response) {
appAssert, resp := request.test(existingAppAssert)
assert := appAssert.Assert

var respBody map[string]interface{}

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

assert.NilError(err)
assert.Equal(value, respBody[key].(string))

assert.Equal(status, resp.StatusCode)
return appAssert, resp
}

func (existingAppAssert ExistingAppAssert) TestOnError(request TestRequest, expectedError errors.Error) ExistingAppAssert {
appAssert, _ := request.testOn(existingAppAssert, expectedError.StatusCode, "error", expectedError.Message)
return appAssert
}

type ErrorWithTester struct {
Error errors.Error
Tester Tester
}

func (existingAppAssert ExistingAppAssert) TestOnErrorAndDB(request TestRequest, errorWithDBTester ErrorWithTester) ExistingAppAssert {
appAssert, resp := request.testOn(existingAppAssert, errorWithDBTester.Error.StatusCode, "error", errorWithDBTester.Error.Message)
errorWithDBTester.Tester(appAssert.App, appAssert.Assert, resp)
return appAssert
}

func (existingAppAssert ExistingAppAssert) TestOnMessage(request TestRequest, status int, message string) ExistingAppAssert {
request.testOn(existingAppAssert, status, "message", message)
return existingAppAssert
}

func (existingAppAssert ExistingAppAssert) TestOnMessageAndDB(request TestRequest, status int, message string, dbTester Tester) ExistingAppAssert {
appAssert, resp := request.testOn(existingAppAssert, status, "message", message)
dbTester(appAssert.App, appAssert.Assert, resp)
return appAssert
}

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

type TesterWithStatus struct {
Status int
Tester
}

func (existingAppAssert ExistingAppAssert) TestOnStatusAndDB(request TestRequest, testerStatus TesterWithStatus) ExistingAppAssert {
appAssert, resp := request.test(existingAppAssert)
app, assert := appAssert.App, appAssert.Assert

assert.Equal(testerStatus.Status, resp.StatusCode)

testerStatus.Tester(app, assert, resp)

return appAssert
}
19 changes: 19 additions & 0 deletions backend/tests/api/helpers/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package helpers

import (
"testing"

"github.com/huandu/go-assert"
)

func InitTest(t *testing.T) ExistingAppAssert {
assert := assert.New(t)
app, err := spawnApp()

assert.NilError(err)

return ExistingAppAssert{
App: *app,
Assert: assert,
}
}
45 changes: 45 additions & 0 deletions backend/tests/api/helpers/utilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package helpers

import (
crand "crypto/rand"
"fmt"
"math/big"
"strings"
)

func generateRandomInt(max int64) int64 {
randInt, _ := crand.Int(crand.Reader, big.NewInt(max))
return randInt.Int64()
}

func generateRandomDBName() string {
prefix := "sac_test_"
letterBytes := "abcdefghijklmnopqrstuvwxyz"
length := len(prefix) + 36
result := make([]byte, length)
for i := 0; i < length; i++ {
result[i] = letterBytes[generateRandomInt(int64(len(letterBytes)))]
}

return fmt.Sprintf("%s%s", prefix, string(result))
}

func generateCasingPermutations(word string, currentPermutation string, index int, results *[]string) {
if index == len(word) {
*results = append(*results, currentPermutation)
return
}

generateCasingPermutations(word, fmt.Sprintf("%s%s", currentPermutation, strings.ToLower(string(word[index]))), index+1, results)
generateCasingPermutations(word, fmt.Sprintf("%s%s", currentPermutation, strings.ToUpper(string(word[index]))), index+1, results)
}

func AllCasingPermutations(word string) []string {
results := make([]string, 0)
generateCasingPermutations(word, "", 0, &results)
return results
}

func StringToPointer(s string) *string {
return &s
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tests
package helpers

import (
"slices"
298 changes: 174 additions & 124 deletions backend/tests/api/tag_test.go

Large diffs are not rendered by default.

243 changes: 140 additions & 103 deletions backend/tests/api/user_tag_test.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (

"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
h "github.com/GenerateNU/sac/backend/tests/api/helpers"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
@@ -55,19 +56,26 @@ func SampleTagIDsFactory(tagIDs *[]uuid.UUID) *map[string]interface{} {
}
}

func CreateSetOfTags(t *testing.T, appAssert ExistingAppAssert) []uuid.UUID {
func CreateSetOfTags(t *testing.T, appAssert *h.ExistingAppAssert) ([]uuid.UUID, *h.ExistingAppAssert) {
if appAssert == nil {
newAppAssert := h.InitTest(t)
appAssert = &newAppAssert
}

categories := SampleCategoriesFactory()

categoryIDs := []uuid.UUID{}
for _, category := range *categories {
TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/categories/",
Body: &category,
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
appAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/categories/",
Body: &category,
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusCreated,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var respCategory models.Category

err := json.NewDecoder(resp.Body).Decode(&respCategory)
@@ -84,14 +92,16 @@ func CreateSetOfTags(t *testing.T, appAssert ExistingAppAssert) []uuid.UUID {

tagIDs := []uuid.UUID{}
for _, tag := range *tags {
TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/tags/",
Body: &tag,
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
appAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/tags/",
Body: &tag,
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusCreated,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var respTag models.Tag

err := json.NewDecoder(resp.Body).Decode(&respTag)
@@ -104,10 +114,10 @@ func CreateSetOfTags(t *testing.T, appAssert ExistingAppAssert) []uuid.UUID {
)
}

return tagIDs
return tagIDs, appAssert
}

func AssertUserTagsRespDB(app TestApp, assert *assert.A, resp *http.Response, id uuid.UUID) {
func AssertUserTagsRespDB(app h.TestApp, assert *assert.A, resp *http.Response, id uuid.UUID) {
var respTags []models.Tag

// Retrieve the tags from the response:
@@ -135,7 +145,7 @@ func AssertUserTagsRespDB(app TestApp, assert *assert.A, resp *http.Response, id
}
}

func AssertSampleUserTagsRespDB(app TestApp, assert *assert.A, resp *http.Response, uuid uuid.UUID) {
func AssertSampleUserTagsRespDB(app h.TestApp, assert *assert.A, resp *http.Response, uuid uuid.UUID) {
AssertUserTagsRespDB(app, assert, resp, uuid)
}

@@ -152,11 +162,16 @@ func TestCreateUserTagsFailsOnInvalidDataType(t *testing.T) {
malformedTag := *SampleTagIDsFactory(nil)
malformedTag["tags"] = tag

TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/1/tags/",
Body: &malformedTag,
}.TestOnError(t, nil, errors.FailedToParseRequestBody).Close()
h.InitTest(t).TestOnError(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/:userID/tags/",
Body: &malformedTag,
Role: &models.Student,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
errors.FailedToParseRequestBody,
).Close()
}
}

@@ -170,11 +185,15 @@ func TestCreateUserTagsFailsOnInvalidUserID(t *testing.T) {
}

for _, badRequest := range badRequests {
TestRequest{
Method: fiber.MethodPost,
Path: fmt.Sprintf("/api/v1/users/%s/tags", badRequest),
Body: SampleTagIDsFactory(nil),
}.TestOnError(t, nil, errors.FailedToValidateID).Close()
h.InitTest(t).TestOnError(
h.TestRequest{
Method: fiber.MethodPost,
Path: fmt.Sprintf("/api/v1/users/%s/tags", badRequest),
Body: SampleTagIDsFactory(nil),
Role: &models.Student,
},
errors.FailedToValidateID,
).Close()
}
}

@@ -183,8 +202,6 @@ type UUIDSlice []uuid.UUID
var testUUID = uuid.New()

func TestCreateUserTagsFailsOnInvalidKey(t *testing.T) {
appAssert, uuid := CreateSampleUser(t, nil)

invalidBody := []map[string]interface{}{
{
"tag": UUIDSlice{testUUID, testUUID},
@@ -195,40 +212,58 @@ func TestCreateUserTagsFailsOnInvalidKey(t *testing.T) {
}

for _, body := range invalidBody {
TestRequest{
Method: fiber.MethodPost,
Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid),
Body: &body,
}.TestOnError(t, &appAssert, errors.FailedToValidateUserTags)
h.InitTest(t).TestOnError(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/:userID/tags/",
Body: &body,
Role: &models.Student,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
errors.FailedToValidateUserTags,
).Close()
}

appAssert.Close()
}

func TestCreateUserTagsFailsOnNonExistentUser(t *testing.T) {
TestRequest{
Method: fiber.MethodPost,
Path: fmt.Sprintf("/api/v1/users/%s/tags", uuid.New()),
Body: SampleTagIDsFactory(nil),
}.TestOnError(t, nil, errors.UserNotFound).Close()
uuid := uuid.New()

h.InitTest(t).TestOnErrorAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid),
Body: SampleTagIDsFactory(nil),
Role: &models.Super,
},
h.ErrorWithTester{
Error: errors.UserNotFound,
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var dbUser models.User
err := app.Conn.First(&dbUser, uuid).Error

assert.Assert(err != nil)
},
},
).Close()
}

func TestCreateUserTagsWorks(t *testing.T) {
appAssert, uuid := CreateSampleUser(t, nil)

// Create a set of tags:
tagUUIDs := CreateSetOfTags(t, appAssert)
tagUUIDs, appAssert := CreateSetOfTags(t, nil)

// 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),
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
appAssert.TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/:userID/tags/",
Body: SampleTagIDsFactory(&tagUUIDs),
Role: &models.Super,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
h.TesterWithStatus{
Status: fiber.StatusCreated,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
AssertSampleUserTagsRespDB(app, assert, resp, uuid)
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
AssertSampleUserTagsRespDB(app, assert, resp, app.TestUser.UUID)
},
},
)
@@ -237,16 +272,17 @@ 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),
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
h.InitTest(t).TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/:userID/tags/",
Body: SampleTagIDsFactory(nil),
Role: &models.Super,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
h.TesterWithStatus{
Status: fiber.StatusCreated,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var respTags []models.Tag

err := json.NewDecoder(resp.Body).Decode(&respTags)
@@ -256,28 +292,30 @@ func TestCreateUserTagsNoneAddedIfInvalid(t *testing.T) {
assert.Equal(len(respTags), 0)
},
},
)

appAssert.Close()
).Close()
}

func TestGetUserTagsFailsOnNonExistentUser(t *testing.T) {
TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid.New()),
}.TestOnError(t, nil, errors.UserNotFound).Close()
h.InitTest(t).TestOnError(
h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid.New()),
Role: &models.Super,
}, errors.UserNotFound,
).Close()
}

func TestGetUserTagsReturnsEmptyListWhenNoneAdded(t *testing.T) {
appAssert, uuid := CreateSampleUser(t, nil)

TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/users/%s/tags/", uuid),
}.TestOnStatusAndDB(t, &appAssert,
DBTesterWithStatus{
Status: 200,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
h.InitTest(t).TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodGet,
Path: "/api/v1/users/:userID/tags/",
Role: &models.Student,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
h.TesterWithStatus{
Status: fiber.StatusOK,
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
var respTags []models.Tag

err := json.NewDecoder(resp.Body).Decode(&respTags)
@@ -287,36 +325,35 @@ func TestGetUserTagsReturnsEmptyListWhenNoneAdded(t *testing.T) {
assert.Equal(len(respTags), 0)
},
},
)

appAssert.Close()
).Close()
}

func TestGetUserTagsReturnsCorrectList(t *testing.T) {
appAssert, uuid := CreateSampleUser(t, nil)
tagUUIDs, appAssert := CreateSetOfTags(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),
}.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{
newAppAssert := *appAssert

newAppAssert.TestOnStatus(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/users/:userID/tags/",
Body: SampleTagIDsFactory(&tagUUIDs),
Role: &models.Student,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
fiber.StatusCreated,
).TestOnStatusAndDB(
h.TestRequest{
Method: fiber.MethodGet,
Path: "/api/v1/users/:userID/tags/",
Role: &models.Student,
TestUserIDReplaces: h.StringToPointer(":userID"),
},
h.TesterWithStatus{
Status: fiber.StatusOK,
DBTester: func(app TestApp, assert *assert.A, resp *http.Response) {
AssertSampleUserTagsRespDB(app, assert, resp, uuid)
Tester: func(app h.TestApp, assert *assert.A, resp *http.Response) {
AssertSampleUserTagsRespDB(app, assert, resp, app.TestUser.UUID)
},
},
)

appAssert.Close()
).Close()
}
391 changes: 224 additions & 167 deletions backend/tests/api/user_test.go

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions frontend/node_modules/.yarn-integrity

This file was deleted.

4 changes: 0 additions & 4 deletions frontend/yarn.lock

This file was deleted.

0 comments on commit 60e360b

Please sign in to comment.