diff --git a/server/src/.mockery.yaml b/server/src/.mockery.yaml index a468e5a628..43581f5227 100644 --- a/server/src/.mockery.yaml +++ b/server/src/.mockery.yaml @@ -41,6 +41,7 @@ packages: interfaces: SessionService: SessionDatabase: + SessionApi: scrumlr.io/server/sessionrequests: interfaces: @@ -52,6 +53,7 @@ packages: interfaces: UserService: UserDatabase: + UsersApi: scrumlr.io/server/notes: interfaces: diff --git a/server/src/api/board_sessions.go b/server/src/api/board_sessions.go deleted file mode 100644 index bafa67f733..0000000000 --- a/server/src/api/board_sessions.go +++ /dev/null @@ -1,139 +0,0 @@ -package api - -import ( - "net/http" - - "go.opentelemetry.io/otel/codes" - "scrumlr.io/server/identifiers" - "scrumlr.io/server/logger" - "scrumlr.io/server/sessions" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/google/uuid" - "scrumlr.io/server/common" -) - -//var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/api") - -// getBoardSessions get participants -func (s *Server) getBoardSessions(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.get.all") - defer span.End() - - board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - filter := s.sessions.BoardSessionFilterTypeFromQueryString(r.URL.Query()) - sessions, err := s.sessions.GetAll(ctx, board, filter) - if err != nil { - span.SetStatus(codes.Error, "failed to get sessions") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, sessions) -} - -// getBoardSession get a participant -func (s *Server) getBoardSession(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.get") - defer span.End() - log := logger.FromContext(ctx) - - board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - userParam := chi.URLParam(r, "session") - userId, err := uuid.Parse(userParam) - if err != nil { - span.SetStatus(codes.Error, "failed to parse user id") - span.RecordError(err) - log.Errorw("Invalid user id", "err", err) - common.Throw(w, r, err) - return - } - - session, err := s.sessions.Get(ctx, board, userId) - if err != nil { - span.SetStatus(codes.Error, "failed to get session") - span.RecordError(err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, session) -} - -// updateBoardSession updates a participant -func (s *Server) updateBoardSession(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.update") - defer span.End() - log := logger.FromContext(ctx) - - board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - caller := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - userParam := chi.URLParam(r, "session") - userId, err := uuid.Parse(userParam) - if err != nil { - span.SetStatus(codes.Error, "failed to parse user id") - span.RecordError(err) - log.Errorw("Invalid user session id", "err", err) - http.Error(w, "invalid user session id", http.StatusBadRequest) - return - } - - var body sessions.BoardSessionUpdateRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "unable to decode body") - span.RecordError(err) - log.Errorw("Unable to decode body", "err", err) - http.Error(w, "unable to parse request body", http.StatusBadRequest) - return - } - - body.Board = board - body.Caller = caller - body.User = userId - - session, err := s.sessions.Update(ctx, body) - if err != nil { - span.SetStatus(codes.Error, "failed to update session") - span.RecordError(err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, session) -} - -// updateBoardSessions updates all participants -func (s *Server) updateBoardSessions(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.update.all") - defer span.End() - log := logger.FromContext(ctx) - - board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - var body sessions.BoardSessionsUpdateRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "unable to decode body") - span.RecordError(err) - log.Errorw("Unable to decode body", "err", err) - http.Error(w, "unable to parse request body", http.StatusBadRequest) - return - } - - body.Board = board - updatedSessions, err := s.sessions.UpdateAll(ctx, body) - if err != nil { - span.SetStatus(codes.Error, "failed to update all sessions") - span.RecordError(err) - http.Error(w, "unable to update board sessions", http.StatusInternalServerError) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, updatedSessions) -} diff --git a/server/src/api/board_templates_test.go b/server/src/api/board_templates_test.go index fbb77a35d7..aadb8ecf21 100644 --- a/server/src/api/board_templates_test.go +++ b/server/src/api/board_templates_test.go @@ -19,6 +19,8 @@ import ( "scrumlr.io/server/columntemplates" "scrumlr.io/server/common" "scrumlr.io/server/identifiers" + "scrumlr.io/server/serviceinitialize" + "scrumlr.io/server/sessions" "scrumlr.io/server/users" ) @@ -388,11 +390,28 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { mockBoardTemplates.EXPECT().Update(mock.Anything, mock.Anything).Return(mockTemplate, nil).Maybe() mockBoardTemplates.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil).Maybe() + sessionApiMock := sessions.NewMockSessionApi(t) + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + sessionApiMock.EXPECT().BoardParticipantContext(mock.Anything).Return(next) + sessionApiMock.EXPECT().BoardModeratorContext(mock.Anything).Return(next) + sessionServiceMock := sessions.NewMockSessionService(t) + + apiInitializer := serviceinitialize.NewApiInitializer("/") + userApi := apiInitializer.InitializeUserApi(mockUsers, sessionServiceMock, false, false) + routesInitializer := serviceinitialize.NewRoutesInitializer() + userRoutes := routesInitializer.InitializeUserRoutes(userApi, sessionApiMock) + sessionRoutes := routesInitializer.InitializeSessionRoutes(sessionApiMock) + // Use the actual router from router.go with minimal mocked dependencies - r := New( - "/", // basePath - nil, // realtime (not needed for templates) - mockAuth, // auth + s := New( + "/", // basePath + nil, // realtime (not needed for templates) + mockAuth, // auth + userRoutes, + sessionRoutes, nil, // boards nil, // columns nil, // votings @@ -439,7 +458,7 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { rr := httptest.NewRecorder() // Execute request - r.ServeHTTP(rr, req) + s.ServeHTTP(rr, req) // Verify status assert.Equal(t, tt.expectedStatus, rr.Code, "Expected status %d for %s %s, got %d", tt.expectedStatus, tt.method, tt.path, rr.Code) diff --git a/server/src/api/boards.go b/server/src/api/boards.go index 534db45d0a..3db510195d 100644 --- a/server/src/api/boards.go +++ b/server/src/api/boards.go @@ -1,659 +1,659 @@ package api import ( - "database/sql" - "encoding/csv" - "errors" - "fmt" - "net/http" - "strconv" - - "go.opentelemetry.io/otel/codes" - "scrumlr.io/server/hash" - "scrumlr.io/server/sessions" - - "scrumlr.io/server/boards" - "scrumlr.io/server/votings" - - "scrumlr.io/server/columns" - "scrumlr.io/server/notes" - - "scrumlr.io/server/identifiers" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/google/uuid" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" + "database/sql" + "encoding/csv" + "errors" + "fmt" + "net/http" + "strconv" + + "go.opentelemetry.io/otel/codes" + "scrumlr.io/server/columns" + "scrumlr.io/server/hash" + "scrumlr.io/server/sessions" + + "scrumlr.io/server/boards" + "scrumlr.io/server/votings" + + "scrumlr.io/server/notes" + + "scrumlr.io/server/identifiers" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/google/uuid" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" ) //var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/api") // createBoard creates a new board func (s *Server) createBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.create") - defer span.End() - log := logger.FromContext(ctx) - - owner := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - // parse request - var body boards.CreateBoardRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "failed to decode body") - span.RecordError(err) - log.Errorw("Unable to decode body", "err", err) - common.Throw(w, r, common.BadRequestError(err)) - return - } - - body.Owner = owner - - b, err := s.boards.Create(ctx, body) - if err != nil { - span.SetStatus(codes.Error, "failed to create board") - span.RecordError(err) - log.Errorw("failed to create board", "err", err) - common.Throw(w, r, err) - return - } - - // build the response - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s", common.GetProtocol(r), r.Host, b.ID)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s", common.GetProtocol(r), r.Host, s.basePath, b.ID)) - } - render.Status(r, http.StatusCreated) - render.Respond(w, r, b) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.create") + defer span.End() + log := logger.FromContext(ctx) + + owner := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + // parse request + var body boards.CreateBoardRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "failed to decode body") + span.RecordError(err) + log.Errorw("Unable to decode body", "err", err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + + body.Owner = owner + + b, err := s.boards.Create(ctx, body) + if err != nil { + span.SetStatus(codes.Error, "failed to create board") + span.RecordError(err) + log.Errorw("failed to create board", "err", err) + common.Throw(w, r, err) + return + } + + // build the response + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s", common.GetProtocol(r), r.Host, b.ID)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s", common.GetProtocol(r), r.Host, s.basePath, b.ID)) + } + render.Status(r, http.StatusCreated) + render.Respond(w, r, b) } // deleteBoard deletes a board func (s *Server) deleteBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.delete") - defer span.End() - log := logger.FromContext(ctx) - - board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - err := s.boards.Delete(ctx, board) - if err != nil { - span.SetStatus(codes.Error, "failed to create board") - span.RecordError(err) - log.Errorw("failed to delete board", "err", err) - http.Error(w, "failed to delete board", http.StatusInternalServerError) - return - } - - render.Status(r, http.StatusNoContent) - render.Respond(w, r, nil) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.delete") + defer span.End() + log := logger.FromContext(ctx) + + board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + err := s.boards.Delete(ctx, board) + if err != nil { + span.SetStatus(codes.Error, "failed to create board") + span.RecordError(err) + log.Errorw("failed to delete board", "err", err) + http.Error(w, "failed to delete board", http.StatusInternalServerError) + return + } + + render.Status(r, http.StatusNoContent) + render.Respond(w, r, nil) } func (s *Server) getBoards(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.get.all") - defer span.End() - log := logger.FromContext(ctx) - - user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - - boardIDs, err := s.boards.GetBoards(ctx, user) - if err != nil { - span.SetStatus(codes.Error, "failed to get boards") - span.RecordError(err) - log.Errorw("failed to get boards", "err", err) - common.Throw(w, r, common.InternalServerError) - return - } - - OverviewBoards, err := s.boards.BoardOverview(ctx, boardIDs, user) - if err != nil { - span.SetStatus(codes.Error, "failed to get board overview") - span.RecordError(err) - log.Errorw("failed to get board overview", "err", err) - common.Throw(w, r, common.InternalServerError) - return - } - render.Status(r, http.StatusOK) - render.Respond(w, r, OverviewBoards) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.get.all") + defer span.End() + log := logger.FromContext(ctx) + + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + boardIDs, err := s.boards.GetBoards(ctx, user) + if err != nil { + span.SetStatus(codes.Error, "failed to get boards") + span.RecordError(err) + log.Errorw("failed to get boards", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + OverviewBoards, err := s.boards.BoardOverview(ctx, boardIDs, user) + if err != nil { + span.SetStatus(codes.Error, "failed to get board overview") + span.RecordError(err) + log.Errorw("failed to get board overview", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + render.Status(r, http.StatusOK) + render.Respond(w, r, OverviewBoards) } // getBoard get a board func (s *Server) getBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.get") - defer span.End() - log := logger.FromContext(ctx) - - boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - if len(r.Header["Upgrade"]) > 0 && r.Header["Upgrade"][0] == "websocket" { - s.openBoardSocket(w, r) - return - } - - board, err := s.boards.Get(ctx, boardId) - if err != nil { - if err == sql.ErrNoRows { - span.SetStatus(codes.Error, "no board found") - span.RecordError(err) - common.Throw(w, r, common.NotFoundError) - return - } - - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - log.Errorw("unable to access board", "err", err) - common.Throw(w, r, common.InternalServerError) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, board) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.get") + defer span.End() + log := logger.FromContext(ctx) + + boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + if len(r.Header["Upgrade"]) > 0 && r.Header["Upgrade"][0] == "websocket" { + s.openBoardSocket(w, r) + return + } + + board, err := s.boards.Get(ctx, boardId) + if err != nil { + if err == sql.ErrNoRows { + span.SetStatus(codes.Error, "no board found") + span.RecordError(err) + common.Throw(w, r, common.NotFoundError) + return + } + + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + log.Errorw("unable to access board", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, board) } // JoinBoardRequest represents the request to create a new participant of a board. type JoinBoardRequest struct { - // The passphrase challenge if the access policy is 'BY_PASSPHRASE'. - Passphrase string `json:"passphrase"` + // The passphrase challenge if the access policy is 'BY_PASSPHRASE'. + Passphrase string `json:"passphrase"` } // joinBoard create a new participant func (s *Server) joinBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.join") - defer span.End() - log := logger.FromContext(ctx) - - boardParam := chi.URLParam(r, "id") - board, err := uuid.Parse(boardParam) - if err != nil { - span.SetStatus(codes.Error, "failed to parse board id") - span.RecordError(err) - log.Errorw("Wrong board id", "err", err) - common.Throw(w, r, common.BadRequestError(err)) - return - } - user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - - exists, err := s.sessions.Exists(ctx, board, user) - if err != nil { - span.SetStatus(codes.Error, "failed to check session") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - if exists { - banned, err := s.sessions.IsParticipantBanned(ctx, board, user) - if err != nil { - span.SetStatus(codes.Error, "failed to check if participant is banned") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - if banned { - err := errors.New("participant is currently banned from this session") - span.SetStatus(codes.Error, "participant is banned") - span.RecordError(err) - common.Throw(w, r, common.ForbiddenError(err)) - return - } - - if s.basePath == "/" { - http.Redirect(w, r, fmt.Sprintf("%s://%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, board, user), http.StatusSeeOther) - } else { - http.Redirect(w, r, fmt.Sprintf("%s://%s%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, s.basePath, board, user), http.StatusSeeOther) - } - return - } - - b, err := s.boards.Get(ctx, board) - - if err != nil { - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - common.Throw(w, r, common.NotFoundError) - return - } - - if b.AccessPolicy == boards.Public { - _, err := s.sessions.Create(ctx, sessions.BoardSessionCreateRequest{Board: board, User: user, Role: common.ParticipantRole}) - if err != nil { - span.SetStatus(codes.Error, "failed to create session") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, board, user)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) - } - w.WriteHeader(http.StatusCreated) - return - } - - if b.AccessPolicy == boards.ByPassphrase { - var body JoinBoardRequest - err := render.Decode(r, &body) - if err != nil { - span.SetStatus(codes.Error, "failed to decode body") - span.RecordError(err) - log.Errorw("Unable to decode body", "err", err) - common.Throw(w, r, common.BadRequestError(errors.New("unable to parse request body"))) - return - } - if body.Passphrase == "" { - err := errors.New("missing passphrase") - span.SetStatus(codes.Error, "no passphrase provided") - span.RecordError(err) - common.Throw(w, r, common.BadRequestError(err)) - return - } - encodedPassphrase := hash.NewHashSha512().HashBySalt(body.Passphrase, *b.Salt) - if encodedPassphrase == *b.Passphrase { - _, err := s.sessions.Create(ctx, sessions.BoardSessionCreateRequest{Board: board, User: user, Role: common.ParticipantRole}) - if err != nil { - span.SetStatus(codes.Error, "failed to create session") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, board, user)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) - } - w.WriteHeader(http.StatusCreated) - return - } else { - err := errors.New("wrong passphrase") - span.SetStatus(codes.Error, "wrong passphrase provided") - span.RecordError(err) - common.Throw(w, r, common.BadRequestError(err)) - return - } - } - - if b.AccessPolicy == boards.ByInvite { - sessionExists, err := s.sessionRequests.Exists(ctx, board, user) - if err != nil { - span.SetStatus(codes.Error, "failed to check session requests") - span.RecordError(err) - http.Error(w, "failed to check for existing board session request", http.StatusInternalServerError) - return - } - - if sessionExists { - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, board, user)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) - } - w.WriteHeader(http.StatusSeeOther) - return - } - - _, err = s.sessionRequests.Create(ctx, board, user) - if err != nil { - span.SetStatus(codes.Error, "failed to create session request") - span.RecordError(err) - http.Error(w, "failed to create board session request", http.StatusInternalServerError) - return - } - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, board, user)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) - } - w.WriteHeader(http.StatusSeeOther) - return - } - - w.WriteHeader(http.StatusBadRequest) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.join") + defer span.End() + log := logger.FromContext(ctx) + + boardParam := chi.URLParam(r, "id") + board, err := uuid.Parse(boardParam) + if err != nil { + span.SetStatus(codes.Error, "failed to parse board id") + span.RecordError(err) + log.Errorw("Wrong board id", "err", err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + exists, err := s.sessions.Exists(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "failed to check session") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + if exists { + banned, err := s.sessions.IsParticipantBanned(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "failed to check if participant is banned") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + if banned { + err := errors.New("participant is currently banned from this session") + span.SetStatus(codes.Error, "participant is banned") + span.RecordError(err) + common.Throw(w, r, common.ForbiddenError(err)) + return + } + + if s.basePath == "/" { + http.Redirect(w, r, fmt.Sprintf("%s://%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, board, user), http.StatusSeeOther) + } else { + http.Redirect(w, r, fmt.Sprintf("%s://%s%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, s.basePath, board, user), http.StatusSeeOther) + } + return + } + + b, err := s.boards.Get(ctx, board) + + if err != nil { + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + common.Throw(w, r, common.NotFoundError) + return + } + + if b.AccessPolicy == boards.Public { + _, err := s.sessions.Create(ctx, sessions.BoardSessionCreateRequest{Board: board, User: user, Role: common.ParticipantRole}) + if err != nil { + span.SetStatus(codes.Error, "failed to create session") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, board, user)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) + } + w.WriteHeader(http.StatusCreated) + return + } + + if b.AccessPolicy == boards.ByPassphrase { + var body JoinBoardRequest + err := render.Decode(r, &body) + if err != nil { + span.SetStatus(codes.Error, "failed to decode body") + span.RecordError(err) + log.Errorw("Unable to decode body", "err", err) + common.Throw(w, r, common.BadRequestError(errors.New("unable to parse request body"))) + return + } + if body.Passphrase == "" { + err := errors.New("missing passphrase") + span.SetStatus(codes.Error, "no passphrase provided") + span.RecordError(err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + encodedPassphrase := hash.NewHashSha512().HashBySalt(body.Passphrase, *b.Salt) + if encodedPassphrase == *b.Passphrase { + _, err := s.sessions.Create(ctx, sessions.BoardSessionCreateRequest{Board: board, User: user, Role: common.ParticipantRole}) + if err != nil { + span.SetStatus(codes.Error, "failed to create session") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, board, user)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/participants/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) + } + w.WriteHeader(http.StatusCreated) + return + } else { + err := errors.New("wrong passphrase") + span.SetStatus(codes.Error, "wrong passphrase provided") + span.RecordError(err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + } + + if b.AccessPolicy == boards.ByInvite { + sessionExists, err := s.sessionRequests.Exists(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "failed to check session requests") + span.RecordError(err) + http.Error(w, "failed to check for existing board session request", http.StatusInternalServerError) + return + } + + if sessionExists { + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, board, user)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) + } + w.WriteHeader(http.StatusSeeOther) + return + } + + _, err = s.sessionRequests.Create(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "failed to create session request") + span.RecordError(err) + http.Error(w, "failed to create board session request", http.StatusInternalServerError) + return + } + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, board, user)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/boards/%s/requests/%s", common.GetProtocol(r), r.Host, s.basePath, board, user)) + } + w.WriteHeader(http.StatusSeeOther) + return + } + + w.WriteHeader(http.StatusBadRequest) } // updateBoard updates a board func (s *Server) updateBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.get.all") - defer span.End() - log := logger.FromContext(ctx) - - boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - var body boards.BoardUpdateRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "failed to decode body") - span.RecordError(err) - log.Errorw("Unable to decode body", "err", err) - http.Error(w, "unable to parse request body", http.StatusBadRequest) - return - } - - body.ID = boardId - board, err := s.boards.Update(ctx, body) - if err != nil { - span.SetStatus(codes.Error, "failed to update board") - span.RecordError(err) - log.Errorw("Unable to update board", "err", err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, board) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.get.all") + defer span.End() + log := logger.FromContext(ctx) + + boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + var body boards.BoardUpdateRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "failed to decode body") + span.RecordError(err) + log.Errorw("Unable to decode body", "err", err) + http.Error(w, "unable to parse request body", http.StatusBadRequest) + return + } + + body.ID = boardId + board, err := s.boards.Update(ctx, body) + if err != nil { + span.SetStatus(codes.Error, "failed to update board") + span.RecordError(err) + log.Errorw("Unable to update board", "err", err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, board) } func (s *Server) setTimer(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.timer.set") - defer span.End() - log := logger.FromContext(ctx) - - boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - var body boards.SetTimerRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "failed to decode body") - span.RecordError(err) - log.Errorw("Unable to decode body", "err", err) - common.Throw(w, r, err) - return - } - - board, err := s.boards.SetTimer(ctx, boardId, body.Minutes) - if err != nil { - span.SetStatus(codes.Error, "failed to set board timer") - span.RecordError(err) - log.Errorw("Unable to set board timer", "err", err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, board) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.timer.set") + defer span.End() + log := logger.FromContext(ctx) + + boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + var body boards.SetTimerRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "failed to decode body") + span.RecordError(err) + log.Errorw("Unable to decode body", "err", err) + common.Throw(w, r, err) + return + } + + board, err := s.boards.SetTimer(ctx, boardId, body.Minutes) + if err != nil { + span.SetStatus(codes.Error, "failed to set board timer") + span.RecordError(err) + log.Errorw("Unable to set board timer", "err", err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, board) } func (s *Server) deleteTimer(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.timer.delete") - defer span.End() - log := logger.FromContext(ctx) - - boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - board, err := s.boards.DeleteTimer(ctx, boardId) - if err != nil { - span.SetStatus(codes.Error, "failed to delete board timer") - span.RecordError(err) - log.Errorw("Unable to delete board timer", "err", err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, board) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.timer.delete") + defer span.End() + log := logger.FromContext(ctx) + + boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + board, err := s.boards.DeleteTimer(ctx, boardId) + if err != nil { + span.SetStatus(codes.Error, "failed to delete board timer") + span.RecordError(err) + log.Errorw("Unable to delete board timer", "err", err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, board) } func (s *Server) incrementTimer(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.timer.increment") - defer span.End() - log := logger.FromContext(ctx) - - boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - board, err := s.boards.IncrementTimer(ctx, boardId) - if err != nil { - span.SetStatus(codes.Error, "failed to increment board timer") - span.RecordError(err) - log.Errorw("Unable to increment board timer", "err", err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, board) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.timer.increment") + defer span.End() + log := logger.FromContext(ctx) + + boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + board, err := s.boards.IncrementTimer(ctx, boardId) + if err != nil { + span.SetStatus(codes.Error, "failed to increment board timer") + span.RecordError(err) + log.Errorw("Unable to increment board timer", "err", err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, board) } func (s *Server) exportBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.export") - defer span.End() - log := logger.FromContext(ctx) - - boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - fullBoard, err := s.boards.FullBoard(ctx, boardId) - if err != nil { - span.SetStatus(codes.Error, "failed to get full board") - span.RecordError(err) - common.Throw(w, r, err) - return - } - - visibleColumns := make([]*columns.Column, 0, len(fullBoard.Columns)) - for _, column := range fullBoard.Columns { - if column.Visible { - visibleColumns = append(visibleColumns, column) - } - } - - visibleNotes := make([]*notes.Note, 0, len(fullBoard.Notes)) - for _, note := range fullBoard.Notes { - for _, column := range visibleColumns { - if note.Position.Column == column.ID { - visibleNotes = append(visibleNotes, note) - } - } - } - - if r.Header.Get("Accept") == "" || r.Header.Get("Accept") == "*/*" || r.Header.Get("Accept") == "application/json" { - render.Status(r, http.StatusOK) - render.Respond(w, r, struct { - Board *boards.Board `json:"board"` - Participants []*sessions.BoardSession `json:"participants"` - Columns []*columns.Column `json:"columns"` - Notes []*notes.Note `json:"notes"` - Votings []*votings.Voting `json:"votings"` - }{ - Board: fullBoard.Board, - Participants: fullBoard.BoardSessions, - Columns: visibleColumns, - Notes: visibleNotes, - Votings: fullBoard.Votings, - }) - return - } else if r.Header.Get("Accept") == "text/csv" { - header := []string{"note_id", "author_id", "author", "text", "column_id", "column", "rank", "stack"} - for index, closedVoting := range fullBoard.Votings { - if closedVoting.Status == votings.Closed { - header = append(header, fmt.Sprintf("voting_%d", index)) - } - } - records := [][]string{header} - - for _, note := range visibleNotes { - stack := "null" - if note.Position.Stack.Valid { - stack = note.Position.Stack.UUID.String() - } - - author := note.Author.String() - for _, session := range fullBoard.BoardSessions { - if session.UserID == note.Author { - user, _ := s.users.Get(ctx, session.UserID) // TODO handle error - author = user.Name - } - } - - column := note.Position.Column.String() - for _, c := range visibleColumns { - if c.ID == note.Position.Column { - column = c.Name - } - } - - resultOnNote := []string{ - note.ID.String(), - note.Author.String(), - author, - note.Text, - note.Position.Column.String(), - column, - strconv.Itoa(note.Position.Rank), - stack, - } - - for _, closedVoting := range fullBoard.Votings { - if closedVoting.Status == votings.Closed { - if closedVoting.VotingResults != nil { - resultOnNote = append(resultOnNote, strconv.Itoa(closedVoting.VotingResults.Votes[note.ID].Total)) - } else { - resultOnNote = append(resultOnNote, "0") - } - } - } - - records = append(records, resultOnNote) - } - - render.Status(r, http.StatusOK) - csvWriter := csv.NewWriter(w) - err := csvWriter.WriteAll(records) - if err != nil { - span.SetStatus(codes.Error, "failed to respond with csv") - span.RecordError(err) - log.Errorw("failed to respond with csv", "err", err) - common.Throw(w, r, common.InternalServerError) - return - } - return - } - - render.Status(r, http.StatusNotAcceptable) - render.Respond(w, r, nil) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.export") + defer span.End() + log := logger.FromContext(ctx) + + boardId := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + fullBoard, err := s.boards.FullBoard(ctx, boardId) + if err != nil { + span.SetStatus(codes.Error, "failed to get full board") + span.RecordError(err) + common.Throw(w, r, err) + return + } + + visibleColumns := make([]*columns.Column, 0, len(fullBoard.Columns)) + for _, column := range fullBoard.Columns { + if column.Visible { + visibleColumns = append(visibleColumns, column) + } + } + + visibleNotes := make([]*notes.Note, 0, len(fullBoard.Notes)) + for _, note := range fullBoard.Notes { + for _, column := range visibleColumns { + if note.Position.Column == column.ID { + visibleNotes = append(visibleNotes, note) + } + } + } + + if r.Header.Get("Accept") == "" || r.Header.Get("Accept") == "*/*" || r.Header.Get("Accept") == "application/json" { + render.Status(r, http.StatusOK) + render.Respond(w, r, struct { + Board *boards.Board `json:"board"` + Participants []*sessions.BoardSession `json:"participants"` + Columns []*columns.Column `json:"columns"` + Notes []*notes.Note `json:"notes"` + Votings []*votings.Voting `json:"votings"` + }{ + Board: fullBoard.Board, + Participants: fullBoard.BoardSessions, + Columns: visibleColumns, + Notes: visibleNotes, + Votings: fullBoard.Votings, + }) + return + } else if r.Header.Get("Accept") == "text/csv" { + header := []string{"note_id", "author_id", "author", "text", "column_id", "column", "rank", "stack"} + for index, closedVoting := range fullBoard.Votings { + if closedVoting.Status == votings.Closed { + header = append(header, fmt.Sprintf("voting_%d", index)) + } + } + records := [][]string{header} + + for _, note := range visibleNotes { + stack := "null" + if note.Position.Stack.Valid { + stack = note.Position.Stack.UUID.String() + } + + author := note.Author.String() + for _, session := range fullBoard.BoardSessions { + if session.UserID == note.Author { + user, _ := s.users.Get(ctx, session.UserID) // TODO handle error + author = user.Name + } + } + + column := note.Position.Column.String() + for _, c := range visibleColumns { + if c.ID == note.Position.Column { + column = c.Name + } + } + + resultOnNote := []string{ + note.ID.String(), + note.Author.String(), + author, + note.Text, + note.Position.Column.String(), + column, + strconv.Itoa(note.Position.Rank), + stack, + } + + for _, closedVoting := range fullBoard.Votings { + if closedVoting.Status == votings.Closed { + if closedVoting.VotingResults != nil { + resultOnNote = append(resultOnNote, strconv.Itoa(closedVoting.VotingResults.Votes[note.ID].Total)) + } else { + resultOnNote = append(resultOnNote, "0") + } + } + } + + records = append(records, resultOnNote) + } + + render.Status(r, http.StatusOK) + csvWriter := csv.NewWriter(w) + err := csvWriter.WriteAll(records) + if err != nil { + span.SetStatus(codes.Error, "failed to respond with csv") + span.RecordError(err) + log.Errorw("failed to respond with csv", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + return + } + + render.Status(r, http.StatusNotAcceptable) + render.Respond(w, r, nil) } func (s *Server) importBoard(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.import") - defer span.End() - log := logger.FromContext(ctx) - - owner := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - - var body boards.ImportBoardRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "failed to decode body") - span.RecordError(err) - log.Errorw("Could not read body", "err", err) - common.Throw(w, r, common.BadRequestError(err)) - return - } - - body.Board.Owner = owner - importColumns := make([]columns.ColumnRequest, 0, len(body.Columns)) - - for _, column := range body.Columns { - importColumns = append(importColumns, columns.ColumnRequest{ - Name: column.Name, - Color: column.Color, - Visible: &column.Visible, - Index: &column.Index, - }) - } - b, err := s.boards.Create(ctx, boards.CreateBoardRequest{ - Name: body.Board.Name, - Description: body.Board.Description, - AccessPolicy: body.Board.AccessPolicy, - Passphrase: body.Board.Passphrase, - Columns: importColumns, - Owner: owner, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to import board") - span.RecordError(err) - log.Errorw("Could not import board", "err", err) - common.Throw(w, r, err) - return - } - - cols, err := s.columns.GetAll(ctx, b.ID) - if err != nil { - span.SetStatus(codes.Error, "failed to get columns from imported board") - span.RecordError(err) - _ = s.boards.Delete(ctx, b.ID) - } - - type ParentChildNotes struct { - Parent notes.Note - Children []notes.Note - } - parentNotes := make(map[uuid.UUID]notes.Note) - childNotes := make(map[uuid.UUID][]notes.Note) - - for _, note := range body.Notes { - if !note.Position.Stack.Valid { - parentNotes[note.ID] = note - } else { - childNotes[note.Position.Stack.UUID] = append(childNotes[note.Position.Stack.UUID], note) - } - } - - var organizedNotes []ParentChildNotes - for parentID, parentNote := range parentNotes { - for i, column := range body.Columns { - if parentNote.Position.Column == column.ID { - - note, err := s.notes.Import(ctx, notes.NoteImportRequest{ - Text: parentNote.Text, - Position: notes.NotePosition{ - Column: cols[i].ID, - Stack: uuid.NullUUID{}, - Rank: 0, - }, - Board: b.ID, - User: parentNote.Author, - }) - if err != nil { - span.SetStatus(codes.Error, "failed to import notes") - span.RecordError(err) - _ = s.boards.Delete(ctx, b.ID) - common.Throw(w, r, err) - return - } - parentNote = *note - } - } - organizedNotes = append(organizedNotes, ParentChildNotes{ - Parent: parentNote, - Children: childNotes[parentID], - }) - } - - for _, node := range organizedNotes { - for _, note := range node.Children { - _, err := s.notes.Import(ctx, notes.NoteImportRequest{ - Text: note.Text, - Board: b.ID, - User: note.Author, - Position: notes.NotePosition{ - Column: node.Parent.Position.Column, - Rank: note.Position.Rank, - Stack: uuid.NullUUID{ - UUID: node.Parent.ID, - Valid: true, - }, - }, - }) - if err != nil { - span.SetStatus(codes.Error, "failed to import note") - span.RecordError(err) - _ = s.boards.Delete(ctx, b.ID) - common.Throw(w, r, err) - return - } - } - } - - render.Status(r, http.StatusCreated) - render.Respond(w, r, b) + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.api.import") + defer span.End() + log := logger.FromContext(ctx) + + owner := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + var body boards.ImportBoardRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "failed to decode body") + span.RecordError(err) + log.Errorw("Could not read body", "err", err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + + body.Board.Owner = owner + importColumns := make([]columns.ColumnRequest, 0, len(body.Columns)) + + for _, column := range body.Columns { + importColumns = append(importColumns, columns.ColumnRequest{ + Name: column.Name, + Color: column.Color, + Visible: &column.Visible, + Index: &column.Index, + }) + } + b, err := s.boards.Create(ctx, boards.CreateBoardRequest{ + Name: body.Board.Name, + Description: body.Board.Description, + AccessPolicy: body.Board.AccessPolicy, + Passphrase: body.Board.Passphrase, + Columns: importColumns, + Owner: owner, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to import board") + span.RecordError(err) + log.Errorw("Could not import board", "err", err) + common.Throw(w, r, err) + return + } + + cols, err := s.columns.GetAll(ctx, b.ID) + if err != nil { + span.SetStatus(codes.Error, "failed to get columns from imported board") + span.RecordError(err) + _ = s.boards.Delete(ctx, b.ID) + } + + type ParentChildNotes struct { + Parent notes.Note + Children []notes.Note + } + parentNotes := make(map[uuid.UUID]notes.Note) + childNotes := make(map[uuid.UUID][]notes.Note) + + for _, note := range body.Notes { + if !note.Position.Stack.Valid { + parentNotes[note.ID] = note + } else { + childNotes[note.Position.Stack.UUID] = append(childNotes[note.Position.Stack.UUID], note) + } + } + + var organizedNotes []ParentChildNotes + for parentID, parentNote := range parentNotes { + for i, column := range body.Columns { + if parentNote.Position.Column == column.ID { + + note, err := s.notes.Import(ctx, notes.NoteImportRequest{ + Text: parentNote.Text, + Position: notes.NotePosition{ + Column: cols[i].ID, + Stack: uuid.NullUUID{}, + Rank: 0, + }, + Board: b.ID, + User: parentNote.Author, + }) + if err != nil { + span.SetStatus(codes.Error, "failed to import notes") + span.RecordError(err) + _ = s.boards.Delete(ctx, b.ID) + common.Throw(w, r, err) + return + } + parentNote = *note + } + } + organizedNotes = append(organizedNotes, ParentChildNotes{ + Parent: parentNote, + Children: childNotes[parentID], + }) + } + + for _, node := range organizedNotes { + for _, note := range node.Children { + _, err := s.notes.Import(ctx, notes.NoteImportRequest{ + Text: note.Text, + Board: b.ID, + User: note.Author, + Position: notes.NotePosition{ + Column: node.Parent.Position.Column, + Rank: note.Position.Rank, + Stack: uuid.NullUUID{ + UUID: node.Parent.ID, + Valid: true, + }, + }, + }) + if err != nil { + span.SetStatus(codes.Error, "failed to import note") + span.RecordError(err) + _ = s.boards.Delete(ctx, b.ID) + common.Throw(w, r, err) + return + } + } + } + + render.Status(r, http.StatusCreated) + render.Respond(w, r, b) } diff --git a/server/src/api/boards_test.go b/server/src/api/boards_test.go index d26e45532b..4fafeed234 100644 --- a/server/src/api/boards_test.go +++ b/server/src/api/boards_test.go @@ -11,6 +11,7 @@ import ( "scrumlr.io/server/hash" "scrumlr.io/server/sessions" + "scrumlr.io/server/technical_helper" "scrumlr.io/server/boards" @@ -75,7 +76,7 @@ func (suite *BoardTestSuite) TestCreateBoard() { color := columns.Color("backlog-blue") ownerID := uuid.New() - req := NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf(` { + req := technical_helper.NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf(` { "accessPolicy": "%s", "columns": [ { @@ -133,7 +134,7 @@ func (suite *BoardTestSuite) TestDeleteBoard() { s.boards = boardMock boardID := uuid.New() - req := NewTestRequestBuilder("POST", "/", nil). + req := technical_helper.NewTestRequestBuilder("POST", "/", nil). AddToContext(identifiers.BoardIdentifier, boardID) boardMock.EXPECT().Delete(mock.Anything, boardID).Return(te.err) @@ -174,7 +175,7 @@ func (suite *BoardTestSuite) TestGetBoards() { secondBoard := suite.createBoard(&boardName, &boardDescription, boards.Public, nil, nil) boardIDs := []uuid.UUID{firstBoard.ID, secondBoard.ID} - req := NewTestRequestBuilder("POST", "/", nil). + req := technical_helper.NewTestRequestBuilder("POST", "/", nil). AddToContext(identifiers.UserIdentifier, userID) boardMock.EXPECT().GetBoards(mock.Anything, userID).Return(boardIDs, te.err) @@ -226,7 +227,7 @@ func (suite *BoardTestSuite) TestGetBoard() { boardDescription := "Test Description" board := suite.createBoard(&boardName, &boardDescription, "", nil, nil) - req := NewTestRequestBuilder("POST", "/", nil). + req := technical_helper.NewTestRequestBuilder("POST", "/", nil). AddToContext(identifiers.BoardIdentifier, boardID) boardMock.EXPECT().Get(mock.Anything, boardID).Return(board, te.err) @@ -273,7 +274,7 @@ func (suite *BoardTestSuite) TestJoinBoard() { boardID := uuid.New() userID := uuid.New() - req := NewTestRequestBuilder("POST", fmt.Sprintf("/%s", boardID), strings.NewReader(`{"passphrase": "123"}`)). + req := technical_helper.NewTestRequestBuilder("POST", fmt.Sprintf("/%s", boardID), strings.NewReader(`{"passphrase": "123"}`)). AddToContext(identifiers.UserIdentifier, userID) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", boardID.String()) @@ -339,7 +340,7 @@ func (suite *BoardTestSuite) TestUpdateBoards() { ID: boardID, } - req := NewTestRequestBuilder("PUT", fmt.Sprintf("/%s", boardID), strings.NewReader(fmt.Sprintf(`{ + req := technical_helper.NewTestRequestBuilder("PUT", fmt.Sprintf("/%s", boardID), strings.NewReader(fmt.Sprintf(`{ "id": "%s", "name": "%s", "description": "%s", @@ -379,7 +380,7 @@ func (suite *BoardTestSuite) TestSetTimer() { minutes := uint8(4) - req := NewTestRequestBuilder("PUT", "/timer", strings.NewReader(fmt.Sprintf(`{"minutes": %d}`, minutes))). + req := technical_helper.NewTestRequestBuilder("PUT", "/timer", strings.NewReader(fmt.Sprintf(`{"minutes": %d}`, minutes))). AddToContext(identifiers.BoardIdentifier, boardID) boardMock.EXPECT().SetTimer(mock.Anything, boardID, minutes).Return(new(boards.Board), te.err) @@ -412,7 +413,7 @@ func (suite *BoardTestSuite) TestDeleteTimer() { s.boards = boardMock boardID := uuid.New() - req := NewTestRequestBuilder("DEL", "/timer", nil). + req := technical_helper.NewTestRequestBuilder("DEL", "/timer", nil). AddToContext(identifiers.BoardIdentifier, boardID) boardMock.EXPECT().DeleteTimer(mock.Anything, boardID).Return(new(boards.Board), tt.err) @@ -445,7 +446,7 @@ func (suite *BoardTestSuite) TestIncrementTimer() { s.boards = boardMock boardID := uuid.New() - req := NewTestRequestBuilder("POST", "/timer/increment", nil). + req := technical_helper.NewTestRequestBuilder("POST", "/timer/increment", nil). AddToContext(identifiers.BoardIdentifier, boardID) boardMock.EXPECT().IncrementTimer(mock.Anything, boardID).Return(new(boards.Board), tt.err) diff --git a/server/src/api/columns_test.go b/server/src/api/columns_test.go index 55a464a187..55a0609fe9 100644 --- a/server/src/api/columns_test.go +++ b/server/src/api/columns_test.go @@ -14,6 +14,7 @@ import ( "scrumlr.io/server/columns" "scrumlr.io/server/common" "scrumlr.io/server/identifiers" + "scrumlr.io/server/technical_helper" ) type ColumnTestSuite struct { @@ -46,7 +47,7 @@ func (suite *ColumnTestSuite) TestCreateColumn() { boardID, _ := uuid.NewRandom() userID, _ := uuid.NewRandom() - req := NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf( + req := technical_helper.NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf( `{"name": "%s", "color": "%s", "visible": %t, "index": %d}`, name, color, visible, index, ))).AddToContext(identifiers.BoardIdentifier, boardID). AddToContext(identifiers.UserIdentifier, userID) @@ -95,7 +96,7 @@ func (suite *ColumnTestSuite) TestDeleteColumn() { columnID, _ := uuid.NewRandom() userID, _ := uuid.NewRandom() - req := NewTestRequestBuilder("DEL", "/", nil). + req := technical_helper.NewTestRequestBuilder("DEL", "/", nil). AddToContext(identifiers.BoardIdentifier, boardID). AddToContext(identifiers.UserIdentifier, userID). AddToContext(identifiers.ColumnIdentifier, columnID) @@ -135,7 +136,7 @@ func (suite *ColumnTestSuite) TestUpdateColumn() { visible := false index := 0 - req := NewTestRequestBuilder("PUT", "/", strings.NewReader( + req := technical_helper.NewTestRequestBuilder("PUT", "/", strings.NewReader( fmt.Sprintf(`{"name": "%s", "color": "%s", "visible": %v, "index": %d }`, colName, color, visible, index))). AddToContext(identifiers.BoardIdentifier, boardID). AddToContext(identifiers.ColumnIdentifier, columnID) @@ -197,7 +198,7 @@ func (suite *ColumnTestSuite) TestGetColumn() { Index: index, } - req := NewTestRequestBuilder("GET", "/", nil). + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). AddToContext(identifiers.BoardIdentifier, boardID). AddToContext(identifiers.ColumnIdentifier, columnID) rr := httptest.NewRecorder() @@ -245,7 +246,7 @@ func (suite *ColumnTestSuite) TestGetColumns() { Index: index, } - req := NewTestRequestBuilder("GET", "/", nil). + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). AddToContext(identifiers.BoardIdentifier, boardID) rr := httptest.NewRecorder() columnsMock.EXPECT().GetAll(mock.Anything, boardID).Return([]*columns.Column{column}, tt.err) diff --git a/server/src/api/json_parse_test.go b/server/src/api/json_parse_test.go index 889d10daf5..f439ae0eb1 100644 --- a/server/src/api/json_parse_test.go +++ b/server/src/api/json_parse_test.go @@ -8,6 +8,7 @@ import ( "scrumlr.io/server/identifiers" "scrumlr.io/server/logger" + "scrumlr.io/server/technical_helper" "github.com/google/uuid" "github.com/stretchr/testify/suite" @@ -63,10 +64,6 @@ func (suite *JSONErrTestSuite) TestJSONErrs() { name: "boards.updateBoard", handler: func(s *Server) func(w http.ResponseWriter, r *http.Request) { return s.updateBoard }, }, - { - name: "board_sessions.updateBoardSessions", - handler: func(s *Server) func(w http.ResponseWriter, r *http.Request) { return s.updateBoardSessions }, - }, { name: "board_session_request.updateBoardSessionRequest", handler: func(s *Server) func(w http.ResponseWriter, r *http.Request) { return s.updateBoardSessionRequest }, @@ -78,11 +75,11 @@ func (suite *JSONErrTestSuite) TestJSONErrs() { s := new(Server) mockUUID := uuid.New() - req := NewTestRequestBuilder("POST", "/", strings.NewReader(`{ + req := technical_helper.NewTestRequestBuilder("POST", "/", strings.NewReader(`{ "id": %s }`)) - req.req = logger.InitTestLoggerRequest(req.Request()) + req.Req = logger.InitTestLoggerRequest(req.Request()) req.AddToContext(identifiers.BoardIdentifier, mockUUID). AddToContext(identifiers.UserIdentifier, mockUUID). AddToContext(identifiers.NoteIdentifier, mockUUID). diff --git a/server/src/api/notes_test.go b/server/src/api/notes_test.go index f55eed2c1c..cf0837e180 100644 --- a/server/src/api/notes_test.go +++ b/server/src/api/notes_test.go @@ -11,6 +11,7 @@ import ( "testing" "scrumlr.io/server/sessions" + "scrumlr.io/server/technical_helper" "scrumlr.io/server/boards" @@ -57,11 +58,11 @@ func (suite *NotesTestSuite) TestCreateNote() { s.notes = noteMock - req := NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf(`{ + req := technical_helper.NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf(`{ "column": "%s", "text" : "%s" }`, colId.String(), testText))) - req.req = logger.InitTestLoggerRequest(req.Request()) + req.Req = logger.InitTestLoggerRequest(req.Request()) req.AddToContext(identifiers.BoardIdentifier, boardId). AddToContext(identifiers.UserIdentifier, userId) @@ -103,7 +104,7 @@ func (suite *NotesTestSuite) TestGetNote() { noteID, _ := uuid.NewRandom() - req := NewTestRequestBuilder("GET", "/", nil). + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). AddToContext(identifiers.NoteIdentifier, noteID) noteMock.EXPECT().Get(mock.Anything, noteID).Return(¬es.Note{ @@ -163,8 +164,8 @@ func (suite *NotesTestSuite) TestDeleteNote() { r := chi.NewRouter() s.initNoteResources(r) - req := NewTestRequestBuilder("DELETE", fmt.Sprintf("/notes/%s", noteID.String()), strings.NewReader(`{"deleteStack": false}`)) - req.req = logger.InitTestLoggerRequest(req.Request()) + req := technical_helper.NewTestRequestBuilder("DELETE", fmt.Sprintf("/notes/%s", noteID.String()), strings.NewReader(`{"deleteStack": false}`)) + req.Req = logger.InitTestLoggerRequest(req.Request()) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", boardID.String()) req.AddToContext(chi.RouteCtxKey, rctx) @@ -225,9 +226,9 @@ func (suite *NotesTestSuite) TestEditNote() { s.notes = noteMock - req := NewTestRequestBuilder("PUT", fmt.Sprintf("/notes/%s", noteId.String()), strings.NewReader(fmt.Sprintf(`{ + req := technical_helper.NewTestRequestBuilder("PUT", fmt.Sprintf("/notes/%s", noteId.String()), strings.NewReader(fmt.Sprintf(`{ "text": "%s"}`, updatedText))) - req.req = logger.InitTestLoggerRequest(req.Request()) + req.Req = logger.InitTestLoggerRequest(req.Request()) req.AddToContext(identifiers.BoardIdentifier, boardId). AddToContext(identifiers.NoteIdentifier, noteId). AddToContext(identifiers.UserIdentifier, userId) diff --git a/server/src/api/router.go b/server/src/api/router.go index 30369236ce..df41540e16 100644 --- a/server/src/api/router.go +++ b/server/src/api/router.go @@ -46,6 +46,9 @@ type Server struct { realtime *realtime.Broker auth auth.Auth + userRoutes chi.Router + sessionRoutes chi.Router + boards boards.BoardService columns columns.ColumnService votings votings.VotingService @@ -75,9 +78,13 @@ type Server struct { func New( basePath string, + rt *realtime.Broker, auth auth.Auth, + userRoutes chi.Router, + sessionRoutes chi.Router, + boards boards.BoardService, columns columns.ColumnService, votings votings.VotingService, @@ -126,6 +133,8 @@ func New( s := Server{ basePath: basePath, realtime: rt, + userRoutes: userRoutes, + sessionRoutes: sessionRoutes, boardSubscriptions: make(map[uuid.UUID]*BoardSubscription), boardSessionRequestSubscriptions: make(map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription), auth: auth, @@ -258,12 +267,7 @@ func (s *Server) protectedRoutes(r chi.Router) { s.initBoardReactionResources(r) }) - r.Route("/users", func(r chi.Router) { - r.Get("/", s.getUser) - r.Get("/{user}", s.getUserByID) - r.Put("/", s.updateUser) - r.With(s.BoardParticipantContext).Get("/board/{id}", s.getUsers) - }) + r.Mount("/", s.userRoutes) }) } @@ -313,16 +317,9 @@ func (s *Server) initBoardSessionResources(r chi.Router) { }), )) - r.Post("/", s.joinBoard) - }) - r.With(s.BoardParticipantContext).Get("/", s.getBoardSessions) - r.With(s.BoardModeratorContext).Put("/", s.updateBoardSessions) - - r.Route("/{session}", func(r chi.Router) { - r.Use(s.BoardParticipantContext) - r.Get("/", s.getBoardSession) - r.Put("/", s.updateBoardSession) + r.Post("/", s.joinBoard) //board }) + r.Mount("/", s.sessionRoutes) }) } diff --git a/server/src/api/users.go b/server/src/api/users.go deleted file mode 100644 index c5b50cc2b4..0000000000 --- a/server/src/api/users.go +++ /dev/null @@ -1,123 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/render" - "github.com/google/uuid" - "go.opentelemetry.io/otel/codes" - "scrumlr.io/server/common" - "scrumlr.io/server/identifiers" - "scrumlr.io/server/logger" - "scrumlr.io/server/sessions" - "scrumlr.io/server/users" -) - -//var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/api") - -// getUser get a user -func (s *Server) getUser(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.get") - defer span.End() - - userId := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - - user, err := s.users.Get(ctx, userId) - if err != nil { - span.SetStatus(codes.Error, "failed to get user") - span.RecordError(err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, user) -} - -func (s *Server) getUserByID(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.get") - defer span.End() - log := logger.FromContext(ctx) - - userParam := chi.URLParam(r, "user") - requestedUserId, err := uuid.Parse(userParam) - if err != nil { - span.SetStatus(codes.Error, "unable to parse uuid") - span.RecordError(err) - log.Errorw("unable to parse uuid", "err", err) - common.Throw(w, r, err) - return - } - user, err := s.users.Get(ctx, requestedUserId) - if err != nil { - span.SetStatus(codes.Error, "failed to get user by id") - span.RecordError(err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, user) -} - -func (s *Server) getUsers(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.getAll") - defer span.End() - - boardID := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) - - users, err := s.users.GetBoardUsers(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get users") - span.RecordError(err) - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, users) -} - -func (s *Server) updateUser(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.update") - defer span.End() - log := logger.FromContext(ctx) - - user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - - var body users.UserUpdateRequest - if err := render.Decode(r, &body); err != nil { - span.SetStatus(codes.Error, "unable to decode body") - span.RecordError(err) - log.Errorw("unable to decode body", "err", err) - common.Throw(w, r, common.BadRequestError(err)) - return - } - - body.ID = user - - updatedUser, err := s.users.Update(ctx, body) - if err != nil { - span.SetStatus(codes.Error, "failed to update user") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - // because of a import cycle the boards are updated through the session service - // after a user update. - updateBoards := sessions.BoardSessionUpdateRequest{ - User: user, - } - - _, err = s.sessions.UpdateUserBoards(ctx, updateBoards) - if err != nil { - span.SetStatus(codes.Error, "failed to update user board") - span.RecordError(err) - log.Errorw("Unable to update user boards") - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, updatedUser) -} diff --git a/server/src/api/votes_test.go b/server/src/api/votes_test.go index 5eaebbd650..a26d83a7ca 100644 --- a/server/src/api/votes_test.go +++ b/server/src/api/votes_test.go @@ -11,6 +11,7 @@ import ( "scrumlr.io/server/common" "scrumlr.io/server/identifiers" "scrumlr.io/server/logger" + "scrumlr.io/server/technical_helper" "scrumlr.io/server/votings" "github.com/google/uuid" @@ -50,10 +51,10 @@ func (suite *VoteTestSuite) TestAddVote() { s.votings = votingMock - req := NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf(`{ + req := technical_helper.NewTestRequestBuilder("POST", "/", strings.NewReader(fmt.Sprintf(`{ "note": "%s" }`, noteId.String()))) - req.req = logger.InitTestLoggerRequest(req.Request()) + req.Req = logger.InitTestLoggerRequest(req.Request()) req.AddToContext(identifiers.BoardIdentifier, boardId). AddToContext(identifiers.UserIdentifier, userId) diff --git a/server/src/api/votings_test.go b/server/src/api/votings_test.go index 6181788fb9..3a0ba429c5 100644 --- a/server/src/api/votings_test.go +++ b/server/src/api/votings_test.go @@ -8,6 +8,7 @@ import ( "testing" "scrumlr.io/server/notes" + "scrumlr.io/server/technical_helper" "scrumlr.io/server/common" "scrumlr.io/server/identifiers" @@ -43,11 +44,11 @@ func (suite *VotingTestSuite) TestCreateVoting() { boardId, _ := uuid.NewRandom() s.votings = votingMock - req := NewTestRequestBuilder("POST", "/", strings.NewReader(`{ + req := technical_helper.NewTestRequestBuilder("POST", "/", strings.NewReader(`{ "voteLimit": 4, "allowMultipleVotes": false }`)) - req.req = logger.InitTestLoggerRequest(req.Request()) + req.Req = logger.InitTestLoggerRequest(req.Request()) req.AddToContext(identifiers.BoardIdentifier, boardId) votingMock.EXPECT().Create(mock.Anything, votings.VotingCreateRequest{ @@ -87,8 +88,8 @@ func (suite *VotingTestSuite) TestCloseVoting() { s.votings = votingMock s.notes = notesMock - req := NewTestRequestBuilder("PUT", "/", nil) - req.req = logger.InitTestLoggerRequest(req.Request()) + req := technical_helper.NewTestRequestBuilder("PUT", "/", nil) + req.Req = logger.InitTestLoggerRequest(req.Request()) req.AddToContext(identifiers.BoardIdentifier, boardId). AddToContext(identifiers.VotingIdentifier, votingId) rr := httptest.NewRecorder() @@ -114,7 +115,7 @@ func (suite *VotingTestSuite) TestGetVoting() { boardId, _ := uuid.NewRandom() votingId, _ := uuid.NewRandom() - req := NewTestRequestBuilder("GET", "/", nil). + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). AddToContext(identifiers.BoardIdentifier, boardId). AddToContext(identifiers.VotingIdentifier, votingId) rr := httptest.NewRecorder() diff --git a/server/src/boards/api.go b/server/src/boards/api.go index 511d62e08e..9603a451c2 100644 --- a/server/src/boards/api.go +++ b/server/src/boards/api.go @@ -2,6 +2,7 @@ package boards import ( "context" + "net/http" "github.com/google/uuid" ) @@ -19,4 +20,6 @@ type BoardService interface { FullBoard(ctx context.Context, boardID uuid.UUID) (*FullBoard, error) BoardOverview(ctx context.Context, boardIDs []uuid.UUID, user uuid.UUID) ([]*BoardOverview, error) GetBoards(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) + + BoardEditableContext(next http.Handler) http.Handler } diff --git a/server/src/boards/mock_BoardService.go b/server/src/boards/mock_BoardService.go index f078c5b76c..30b4efa2cf 100644 --- a/server/src/boards/mock_BoardService.go +++ b/server/src/boards/mock_BoardService.go @@ -6,6 +6,7 @@ package boards import ( "context" + "net/http" "github.com/google/uuid" mock "github.com/stretchr/testify/mock" @@ -38,6 +39,59 @@ func (_m *MockBoardService) EXPECT() *MockBoardService_Expecter { return &MockBoardService_Expecter{mock: &_m.Mock} } +// BoardEditableContext provides a mock function for the type MockBoardService +func (_mock *MockBoardService) BoardEditableContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for BoardEditableContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockBoardService_BoardEditableContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BoardEditableContext' +type MockBoardService_BoardEditableContext_Call struct { + *mock.Call +} + +// BoardEditableContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockBoardService_Expecter) BoardEditableContext(next interface{}) *MockBoardService_BoardEditableContext_Call { + return &MockBoardService_BoardEditableContext_Call{Call: _e.mock.On("BoardEditableContext", next)} +} + +func (_c *MockBoardService_BoardEditableContext_Call) Run(run func(next http.Handler)) *MockBoardService_BoardEditableContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockBoardService_BoardEditableContext_Call) Return(handler http.Handler) *MockBoardService_BoardEditableContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockBoardService_BoardEditableContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockBoardService_BoardEditableContext_Call { + _c.Call.Return(run) + return _c +} + // BoardOverview provides a mock function for the type MockBoardService func (_mock *MockBoardService) BoardOverview(ctx context.Context, boardIDs []uuid.UUID, user uuid.UUID) ([]*BoardOverview, error) { ret := _mock.Called(ctx, boardIDs, user) diff --git a/server/src/boards/service.go b/server/src/boards/service.go index 5e318a5be0..26c0b44b58 100644 --- a/server/src/boards/service.go +++ b/server/src/boards/service.go @@ -1,612 +1,659 @@ package boards import ( - "context" - "errors" - "fmt" - "time" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "scrumlr.io/server/sessions" - - "github.com/google/uuid" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/hash" - "scrumlr.io/server/logger" - "scrumlr.io/server/notes" - "scrumlr.io/server/reactions" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessionrequests" - "scrumlr.io/server/timeprovider" - "scrumlr.io/server/votings" + "context" + "errors" + "fmt" + "net/http" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "scrumlr.io/server/identifiers" + "scrumlr.io/server/sessions" + + "github.com/google/uuid" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/hash" + "scrumlr.io/server/logger" + "scrumlr.io/server/notes" + "scrumlr.io/server/reactions" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessionrequests" + "scrumlr.io/server/timeprovider" + "scrumlr.io/server/votings" ) var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/boards") var meter metric.Meter = otel.Meter("scrumlr.io/server/boards") type Service struct { - clock timeprovider.TimeProvider - hash hash.Hash - database BoardDatabase - realtime *realtime.Broker - - columnService columns.ColumnService - notesService notes.NotesService - sessionService sessions.SessionService - sessionRequestService sessionrequests.SessionRequestService - reactionService reactions.ReactionService - votingService votings.VotingService + clock timeprovider.TimeProvider + hash hash.Hash + database BoardDatabase + realtime *realtime.Broker + + columnService columns.ColumnService + notesService notes.NotesService + sessionService sessions.SessionService + sessionRequestService sessionrequests.SessionRequestService + reactionService reactions.ReactionService + votingService votings.VotingService } type BoardDatabase interface { - CreateBoard(ctx context.Context, board DatabaseBoardInsert) (DatabaseBoard, error) - UpdateBoardTimer(ctx context.Context, update DatabaseBoardTimerUpdate) (DatabaseBoard, error) - UpdateBoard(ctx context.Context, update DatabaseBoardUpdate) (DatabaseBoard, error) - GetBoard(ctx context.Context, id uuid.UUID) (DatabaseBoard, error) - DeleteBoard(ctx context.Context, id uuid.UUID) error - GetBoards(ctx context.Context, userID uuid.UUID) ([]DatabaseBoard, error) + CreateBoard(ctx context.Context, board DatabaseBoardInsert) (DatabaseBoard, error) + UpdateBoardTimer(ctx context.Context, update DatabaseBoardTimerUpdate) (DatabaseBoard, error) + UpdateBoard(ctx context.Context, update DatabaseBoardUpdate) (DatabaseBoard, error) + GetBoard(ctx context.Context, id uuid.UUID) (DatabaseBoard, error) + DeleteBoard(ctx context.Context, id uuid.UUID) error + GetBoards(ctx context.Context, userID uuid.UUID) ([]DatabaseBoard, error) } func NewBoardService(db BoardDatabase, rt *realtime.Broker, sessionRequestService sessionrequests.SessionRequestService, sessionService sessions.SessionService, columnService columns.ColumnService, noteService notes.NotesService, reactionService reactions.ReactionService, votingService votings.VotingService, clock timeprovider.TimeProvider, hash hash.Hash) BoardService { - b := new(Service) - b.clock = clock - b.hash = hash - b.database = db - b.realtime = rt - b.sessionService = sessionService - b.sessionRequestService = sessionRequestService - b.columnService = columnService - b.notesService = noteService - b.reactionService = reactionService - b.votingService = votingService - - return b + b := new(Service) + b.clock = clock + b.hash = hash + b.database = db + b.realtime = rt + b.sessionService = sessionService + b.sessionRequestService = sessionRequestService + b.columnService = columnService + b.notesService = noteService + b.reactionService = reactionService + b.votingService = votingService + + return b } func (service *Service) Get(ctx context.Context, id uuid.UUID) (*Board, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.get") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.get.board", id.String()), - ) - - board, err := service.database.GetBoard(ctx, id) - if err != nil { - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - log.Errorw("unable to get board", "boardID", id, "err", err) - return nil, err - } - - return new(Board).From(board), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.get") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.get.board", id.String()), + ) + + board, err := service.database.GetBoard(ctx, id) + if err != nil { + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + log.Errorw("unable to get board", "boardID", id, "err", err) + return nil, err + } + + return new(Board).From(board), err } // GetBoards get all associated boards of a given user func (service *Service) GetBoards(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.get.all") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.get.all.user", userID.String()), - ) - - boards, err := service.database.GetBoards(ctx, userID) - if err != nil { - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - log.Errorw("unable to get boards of user", "userID", userID, "err", err) - return nil, common.InternalServerError - } - - result := make([]uuid.UUID, 0, len(boards)) - for _, board := range boards { - result = append(result, board.ID) - } - - return result, nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.get.all") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.get.all.user", userID.String()), + ) + + boards, err := service.database.GetBoards(ctx, userID) + if err != nil { + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + log.Errorw("unable to get boards of user", "userID", userID, "err", err) + return nil, common.InternalServerError + } + + result := make([]uuid.UUID, 0, len(boards)) + for _, board := range boards { + result = append(result, board.ID) + } + + return result, nil } func (service *Service) Create(ctx context.Context, body CreateBoardRequest) (*Board, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.create") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.create.user", body.Owner.String()), - attribute.String("scrumlr.boards.service.create.access_policy", string(body.AccessPolicy)), - attribute.Int("scrumlr.boards.service.crete.columns.count", len(body.Columns)), - ) - - // map request on board object to insert into database - var board DatabaseBoardInsert - switch body.AccessPolicy { - case Public, ByInvite: - if body.Passphrase != nil { - err := errors.New("passphrase should not be set for policies except 'BY_PASSPHRASE'") - span.SetStatus(codes.Error, "passphrase should not be set for policies except 'BY_PASSPHRASE'") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - board = DatabaseBoardInsert{Name: body.Name, Description: body.Description, AccessPolicy: body.AccessPolicy} - - case ByPassphrase: - if body.Passphrase == nil || len(*body.Passphrase) == 0 { - err := errors.New("passphrase must be set on access policy 'BY_PASSPHRASE'") - span.SetStatus(codes.Error, "passphrase not set") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - encodedPassphrase, salt, _ := service.hash.HashWithSalt(*body.Passphrase) - board = DatabaseBoardInsert{ - Name: body.Name, - Description: body.Description, - AccessPolicy: body.AccessPolicy, - Passphrase: encodedPassphrase, - Salt: salt, - } - } - - // create the board - b, err := service.database.CreateBoard(ctx, board) - if err != nil { - span.SetStatus(codes.Error, "failed to create board") - span.RecordError(err) - log.Errorw("unable to create board", "owner", body.Owner, "policy", body.AccessPolicy, "error", err) - return nil, common.InternalServerError - } - - // create the columns - for index, value := range body.Columns { - column := columns.ColumnRequest{Board: b.ID, User: body.Owner, Name: value.Name, Description: value.Description, Color: value.Color, Visible: value.Visible, Index: &index} - _, err = service.columnService.Create(ctx, column) - if err != nil { - span.SetStatus(codes.Error, "failed to create column") - span.RecordError(err) - return nil, err - } - } - - // create the owner session - sessionRequest := sessions.BoardSessionCreateRequest{Board: b.ID, User: body.Owner, Role: common.OwnerRole} - _, err = service.sessionService.Create(ctx, sessionRequest) - if err != nil { - span.SetStatus(codes.Error, "failed to create session") - span.RecordError(err) - return nil, err - } - - boardCreatedCounter.Add(ctx, 1) - return new(Board).From(b), nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.create") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.create.user", body.Owner.String()), + attribute.String("scrumlr.boards.service.create.access_policy", string(body.AccessPolicy)), + attribute.Int("scrumlr.boards.service.crete.columns.count", len(body.Columns)), + ) + + // map request on board object to insert into database + var board DatabaseBoardInsert + switch body.AccessPolicy { + case Public, ByInvite: + if body.Passphrase != nil { + err := errors.New("passphrase should not be set for policies except 'BY_PASSPHRASE'") + span.SetStatus(codes.Error, "passphrase should not be set for policies except 'BY_PASSPHRASE'") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + board = DatabaseBoardInsert{Name: body.Name, Description: body.Description, AccessPolicy: body.AccessPolicy} + + case ByPassphrase: + if body.Passphrase == nil || len(*body.Passphrase) == 0 { + err := errors.New("passphrase must be set on access policy 'BY_PASSPHRASE'") + span.SetStatus(codes.Error, "passphrase not set") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + encodedPassphrase, salt, _ := service.hash.HashWithSalt(*body.Passphrase) + board = DatabaseBoardInsert{ + Name: body.Name, + Description: body.Description, + AccessPolicy: body.AccessPolicy, + Passphrase: encodedPassphrase, + Salt: salt, + } + } + + // create the board + b, err := service.database.CreateBoard(ctx, board) + if err != nil { + span.SetStatus(codes.Error, "failed to create board") + span.RecordError(err) + log.Errorw("unable to create board", "owner", body.Owner, "policy", body.AccessPolicy, "error", err) + return nil, common.InternalServerError + } + + // create the columns + for index, value := range body.Columns { + column := columns.ColumnRequest{Board: b.ID, User: body.Owner, Name: value.Name, Description: value.Description, Color: value.Color, Visible: value.Visible, Index: &index} + _, err = service.columnService.Create(ctx, column) + if err != nil { + span.SetStatus(codes.Error, "failed to create column") + span.RecordError(err) + return nil, err + } + } + + // create the owner session + sessionRequest := sessions.BoardSessionCreateRequest{Board: b.ID, User: body.Owner, Role: common.OwnerRole} + _, err = service.sessionService.Create(ctx, sessionRequest) + if err != nil { + span.SetStatus(codes.Error, "failed to create session") + span.RecordError(err) + return nil, err + } + + boardCreatedCounter.Add(ctx, 1) + return new(Board).From(b), nil } func (service *Service) FullBoard(ctx context.Context, boardID uuid.UUID) (*FullBoard, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.get.full") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.get.full.board", boardID.String()), - ) - - board, err := service.Get(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardRequests, err := service.sessionRequestService.GetAll(ctx, boardID, string(sessionrequests.RequestAccepted)) - if err != nil { - span.SetStatus(codes.Error, "failed to get session requests") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardSessions, err := service.sessionService.GetAll(ctx, boardID, sessions.BoardSessionFilter{}) - if err != nil { - span.SetStatus(codes.Error, "failed to get sessions") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardColumns, err := service.columnService.GetAll(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get columns") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardNotes, err := service.notesService.GetAll(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get notes") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardReactions, err := service.reactionService.GetAll(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get reactions") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardVotings, err := service.votingService.GetAll(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get votings") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - boardVotes, err := service.votingService.GetVotes(ctx, boardID, votings.VoteFilter{}) - if err != nil { - span.SetStatus(codes.Error, "failed to get votes") - span.RecordError(err) - log.Errorw("unable to get full board", "boardID", boardID, "err", err) - return nil, err - } - - return &FullBoard{ - Board: board, - BoardSessionRequests: boardRequests, - BoardSessions: boardSessions, - Columns: boardColumns, - Notes: boardNotes, - Reactions: boardReactions, - Votings: boardVotings, - Votes: boardVotes, - }, err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.get.full") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.get.full.board", boardID.String()), + ) + + board, err := service.Get(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardRequests, err := service.sessionRequestService.GetAll(ctx, boardID, string(sessionrequests.RequestAccepted)) + if err != nil { + span.SetStatus(codes.Error, "failed to get session requests") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardSessions, err := service.sessionService.GetAll(ctx, boardID, sessions.BoardSessionFilter{}) + if err != nil { + span.SetStatus(codes.Error, "failed to get sessions") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardColumns, err := service.columnService.GetAll(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get columns") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardNotes, err := service.notesService.GetAll(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get notes") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardReactions, err := service.reactionService.GetAll(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get reactions") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardVotings, err := service.votingService.GetAll(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get votings") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + boardVotes, err := service.votingService.GetVotes(ctx, boardID, votings.VoteFilter{}) + if err != nil { + span.SetStatus(codes.Error, "failed to get votes") + span.RecordError(err) + log.Errorw("unable to get full board", "boardID", boardID, "err", err) + return nil, err + } + + return &FullBoard{ + Board: board, + BoardSessionRequests: boardRequests, + BoardSessions: boardSessions, + Columns: boardColumns, + Notes: boardNotes, + Reactions: boardReactions, + Votings: boardVotings, + Votes: boardVotes, + }, err } func (service *Service) BoardOverview(ctx context.Context, boardIDs []uuid.UUID, user uuid.UUID) ([]*BoardOverview, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.get.overview") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.get.overview.user", user.String()), - ) - - overviewBoards := make([]*BoardOverview, 0, len(boardIDs)) - for _, id := range boardIDs { - board, err := service.Get(ctx, id) - if err != nil { - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - log.Errorw("unable to get board overview", "board", id, "err", err) - return nil, err - } - - boardSessions, err := service.sessionService.GetAll(ctx, id, sessions.BoardSessionFilter{}) - if err != nil { - span.SetStatus(codes.Error, "failed to get sessions") - span.RecordError(err) - log.Errorw("unable to get board overview", "board", id, "err", err) - return nil, err - } - - numColumns, err := service.columnService.GetCount(ctx, id) - if err != nil { - span.SetStatus(codes.Error, "failed to get columns") - span.RecordError(err) - log.Errorw("unable to get board overview", "board", id, "err", err) - return nil, err - } - - participantNum := len(boardSessions) - for _, session := range boardSessions { - if session.UserID == user { - sessionCreated := session.CreatedAt - overviewBoards = append(overviewBoards, &BoardOverview{ - Board: board, - Participants: participantNum, - CreatedAt: sessionCreated, - Columns: numColumns, - }) - } - } - } - return overviewBoards, nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.get.overview") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.get.overview.user", user.String()), + ) + + overviewBoards := make([]*BoardOverview, 0, len(boardIDs)) + for _, id := range boardIDs { + board, err := service.Get(ctx, id) + if err != nil { + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + log.Errorw("unable to get board overview", "board", id, "err", err) + return nil, err + } + + boardSessions, err := service.sessionService.GetAll(ctx, id, sessions.BoardSessionFilter{}) + if err != nil { + span.SetStatus(codes.Error, "failed to get sessions") + span.RecordError(err) + log.Errorw("unable to get board overview", "board", id, "err", err) + return nil, err + } + + numColumns, err := service.columnService.GetCount(ctx, id) + if err != nil { + span.SetStatus(codes.Error, "failed to get columns") + span.RecordError(err) + log.Errorw("unable to get board overview", "board", id, "err", err) + return nil, err + } + + participantNum := len(boardSessions) + for _, session := range boardSessions { + if session.UserID == user { + sessionCreated := session.CreatedAt + overviewBoards = append(overviewBoards, &BoardOverview{ + Board: board, + Participants: participantNum, + CreatedAt: sessionCreated, + Columns: numColumns, + }) + } + } + } + return overviewBoards, nil } func (service *Service) Delete(ctx context.Context, id uuid.UUID) error { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.delete") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.delete.board", id.String()), - ) - - err := service.database.DeleteBoard(ctx, id) - if err != nil { - span.SetStatus(codes.Error, "failed to delete board") - span.RecordError(err) - log.Errorw("unable to delete board", "err", err) - return err - } - - service.DeletedBoard(ctx, id) - boardDeletedCounter.Add(ctx, 1) - - return nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.delete") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.delete.board", id.String()), + ) + + err := service.database.DeleteBoard(ctx, id) + if err != nil { + span.SetStatus(codes.Error, "failed to delete board") + span.RecordError(err) + log.Errorw("unable to delete board", "err", err) + return err + } + + service.DeletedBoard(ctx, id) + boardDeletedCounter.Add(ctx, 1) + + return nil } func (service *Service) Update(ctx context.Context, body BoardUpdateRequest) (*Board, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.update") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.update.board", body.ID.String()), - ) - - if body.Name != nil { - if len(*body.Name) == 0 { - err := errors.New("name cannot be empty") - span.SetStatus(codes.Error, "name cannot be empty") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - } - - update := DatabaseBoardUpdate{ - ID: body.ID, - Name: body.Name, - Description: body.Description, - ShowAuthors: body.ShowAuthors, - ShowNotesOfOtherUsers: body.ShowNotesOfOtherUsers, - ShowNoteReactions: body.ShowNoteReactions, - AllowStacking: body.AllowStacking, - IsLocked: body.IsLocked, - TimerStart: body.TimerStart, - TimerEnd: body.TimerEnd, - SharedNote: body.SharedNote, - } - - if body.AccessPolicy != nil { - update.AccessPolicy = body.AccessPolicy - switch *body.AccessPolicy { - case ByInvite, Public: - if body.Passphrase != nil { - err := errors.New("passphrase should not be set for policies except 'BY_PASSPHRASE'") - span.SetStatus(codes.Error, "passphrase should not be set for policies except 'BY_PASSPHRASE'") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - case ByPassphrase: - if body.Passphrase == nil || len(*body.Passphrase) == 0 { - err := errors.New("passphrase must be set if policy 'BY_PASSPHRASE' is selected") - span.SetStatus(codes.Error, "no passphrase provided") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - passphrase, salt, err := service.hash.HashWithSalt(*body.Passphrase) - if err != nil { - span.SetStatus(codes.Error, "failed to encode passphrase") - span.RecordError(err) - log.Error("failed to encode passphrase") - return nil, fmt.Errorf("failed to encode passphrase: %w", err) - } - - update.Passphrase = passphrase - update.Salt = salt - } - } - - board, err := service.database.UpdateBoard(ctx, update) - if err != nil { - span.SetStatus(codes.Error, "failed to update board") - span.RecordError(err) - log.Errorw("unable to update board", "err", err) - return nil, err - } - - service.UpdatedBoard(ctx, board) - - return new(Board).From(board), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.update") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.update.board", body.ID.String()), + ) + + if body.Name != nil { + if len(*body.Name) == 0 { + err := errors.New("name cannot be empty") + span.SetStatus(codes.Error, "name cannot be empty") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + } + + update := DatabaseBoardUpdate{ + ID: body.ID, + Name: body.Name, + Description: body.Description, + ShowAuthors: body.ShowAuthors, + ShowNotesOfOtherUsers: body.ShowNotesOfOtherUsers, + ShowNoteReactions: body.ShowNoteReactions, + AllowStacking: body.AllowStacking, + IsLocked: body.IsLocked, + TimerStart: body.TimerStart, + TimerEnd: body.TimerEnd, + SharedNote: body.SharedNote, + } + + if body.AccessPolicy != nil { + update.AccessPolicy = body.AccessPolicy + switch *body.AccessPolicy { + case ByInvite, Public: + if body.Passphrase != nil { + err := errors.New("passphrase should not be set for policies except 'BY_PASSPHRASE'") + span.SetStatus(codes.Error, "passphrase should not be set for policies except 'BY_PASSPHRASE'") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + case ByPassphrase: + if body.Passphrase == nil || len(*body.Passphrase) == 0 { + err := errors.New("passphrase must be set if policy 'BY_PASSPHRASE' is selected") + span.SetStatus(codes.Error, "no passphrase provided") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + passphrase, salt, err := service.hash.HashWithSalt(*body.Passphrase) + if err != nil { + span.SetStatus(codes.Error, "failed to encode passphrase") + span.RecordError(err) + log.Error("failed to encode passphrase") + return nil, fmt.Errorf("failed to encode passphrase: %w", err) + } + + update.Passphrase = passphrase + update.Salt = salt + } + } + + board, err := service.database.UpdateBoard(ctx, update) + if err != nil { + span.SetStatus(codes.Error, "failed to update board") + span.RecordError(err) + log.Errorw("unable to update board", "err", err) + return nil, err + } + + service.UpdatedBoard(ctx, board) + + return new(Board).From(board), err } func (service *Service) SetTimer(ctx context.Context, id uuid.UUID, minutes uint8) (*Board, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.timer.set") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.timer.set.board", id.String()), - attribute.Int("scrumlr.boards.service.board.timer.set.minutes", int(minutes)), - ) - - timerStart := service.clock.Now().Local() - timerEnd := timerStart.Add(time.Minute * time.Duration(minutes)) - update := DatabaseBoardTimerUpdate{ - ID: id, - TimerStart: &timerStart, - TimerEnd: &timerEnd, - } - - board, err := service.database.UpdateBoardTimer(ctx, update) - if err != nil { - span.SetStatus(codes.Error, "failed to update board timer") - span.RecordError(err) - log.Errorw("unable to update board timer", "err", err) - return nil, err - } - - service.UpdatedBoardTimer(ctx, board) - - boardTimerSetCounter.Add(ctx, 1) - return new(Board).From(board), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.timer.set") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.timer.set.board", id.String()), + attribute.Int("scrumlr.boards.service.board.timer.set.minutes", int(minutes)), + ) + + timerStart := service.clock.Now().Local() + timerEnd := timerStart.Add(time.Minute * time.Duration(minutes)) + update := DatabaseBoardTimerUpdate{ + ID: id, + TimerStart: &timerStart, + TimerEnd: &timerEnd, + } + + board, err := service.database.UpdateBoardTimer(ctx, update) + if err != nil { + span.SetStatus(codes.Error, "failed to update board timer") + span.RecordError(err) + log.Errorw("unable to update board timer", "err", err) + return nil, err + } + + service.UpdatedBoardTimer(ctx, board) + + boardTimerSetCounter.Add(ctx, 1) + return new(Board).From(board), err } func (service *Service) DeleteTimer(ctx context.Context, id uuid.UUID) (*Board, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.timer.delete") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.timer.delete.board", id.String()), - ) - - update := DatabaseBoardTimerUpdate{ - ID: id, - TimerStart: nil, - TimerEnd: nil, - } - - board, err := service.database.UpdateBoardTimer(ctx, update) - if err != nil { - span.SetStatus(codes.Error, "failed to delete board timer") - span.RecordError(err) - log.Errorw("unable to update board timer", "err", err) - return nil, err - } - - service.UpdatedBoardTimer(ctx, board) - - boardTimerDeletedCounter.Add(ctx, 1) - return new(Board).From(board), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.timer.delete") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.timer.delete.board", id.String()), + ) + + update := DatabaseBoardTimerUpdate{ + ID: id, + TimerStart: nil, + TimerEnd: nil, + } + + board, err := service.database.UpdateBoardTimer(ctx, update) + if err != nil { + span.SetStatus(codes.Error, "failed to delete board timer") + span.RecordError(err) + log.Errorw("unable to update board timer", "err", err) + return nil, err + } + + service.UpdatedBoardTimer(ctx, board) + + boardTimerDeletedCounter.Add(ctx, 1) + return new(Board).From(board), err } func (service *Service) IncrementTimer(ctx context.Context, id uuid.UUID) (*Board, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.timer.increment") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.timer.increment.board", id.String()), - ) - - board, err := service.database.GetBoard(ctx, id) - if err != nil { - span.SetStatus(codes.Error, "failed to get board") - span.RecordError(err) - log.Errorw("unable to get board", "boardID", id, "err", err) - return nil, err - } - - var timerStart time.Time - var timerEnd time.Time - - currentTime := service.clock.Now().Local() - - if board.TimerEnd.After(currentTime) { - timerStart = *board.TimerStart - timerEnd = board.TimerEnd.Add(time.Minute * time.Duration(1)) - } else { - timerStart = currentTime - timerEnd = currentTime.Add(time.Minute * time.Duration(1)) - } - - update := DatabaseBoardTimerUpdate{ - ID: board.ID, - TimerStart: &timerStart, - TimerEnd: &timerEnd, - } - - board, err = service.database.UpdateBoardTimer(ctx, update) - if err != nil { - span.SetStatus(codes.Error, "failed to update board timer") - span.RecordError(err) - log.Errorw("unable to update board timer", "err", err) - return nil, err - } - - service.UpdatedBoardTimer(ctx, board) - - return new(Board).From(board), nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.timer.increment") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.timer.increment.board", id.String()), + ) + + board, err := service.database.GetBoard(ctx, id) + if err != nil { + span.SetStatus(codes.Error, "failed to get board") + span.RecordError(err) + log.Errorw("unable to get board", "boardID", id, "err", err) + return nil, err + } + + var timerStart time.Time + var timerEnd time.Time + + currentTime := service.clock.Now().Local() + + if board.TimerEnd.After(currentTime) { + timerStart = *board.TimerStart + timerEnd = board.TimerEnd.Add(time.Minute * time.Duration(1)) + } else { + timerStart = currentTime + timerEnd = currentTime.Add(time.Minute * time.Duration(1)) + } + + update := DatabaseBoardTimerUpdate{ + ID: board.ID, + TimerStart: &timerStart, + TimerEnd: &timerEnd, + } + + board, err = service.database.UpdateBoardTimer(ctx, update) + if err != nil { + span.SetStatus(codes.Error, "failed to update board timer") + span.RecordError(err) + log.Errorw("unable to update board timer", "err", err) + return nil, err + } + + service.UpdatedBoardTimer(ctx, board) + + return new(Board).From(board), nil } func (service *Service) UpdatedBoardTimer(ctx context.Context, board DatabaseBoard) { - _ = service.realtime.BroadcastToBoard(ctx, board.ID, realtime.BoardEvent{ - Type: realtime.BoardEventBoardTimerUpdated, - Data: new(Board).From(board), - }) + _ = service.realtime.BroadcastToBoard(ctx, board.ID, realtime.BoardEvent{ + Type: realtime.BoardEventBoardTimerUpdated, + Data: new(Board).From(board), + }) } func (service *Service) UpdatedBoard(ctx context.Context, board DatabaseBoard) { - _ = service.realtime.BroadcastToBoard(ctx, board.ID, realtime.BoardEvent{ - Type: realtime.BoardEventBoardUpdated, - Data: new(Board).From(board), - }) - - err_msg, err := service.SyncBoardSettingChange(ctx, board.ID) - if err != nil { - logger.Get().Errorw(err_msg, "err", err) - } + _ = service.realtime.BroadcastToBoard(ctx, board.ID, realtime.BoardEvent{ + Type: realtime.BoardEventBoardUpdated, + Data: new(Board).From(board), + }) + + err_msg, err := service.SyncBoardSettingChange(ctx, board.ID) + if err != nil { + logger.Get().Errorw(err_msg, "err", err) + } } func (service *Service) SyncBoardSettingChange(ctx context.Context, boardID uuid.UUID) (string, error) { - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.sync") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.sync.board", boardID.String()), - ) - - var err_msg string - columnsOnBoard, err := service.columnService.GetAll(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get columns") - span.RecordError(err) - err_msg = "unable to retrieve columns, following a updated board call" - return err_msg, err - } - - var columnsID []uuid.UUID - for _, column := range columnsOnBoard { - columnsID = append(columnsID, column.ID) - } - - notesOnBoard, err := service.notesService.GetAll(ctx, boardID, columnsID...) - if err != nil { - span.SetStatus(codes.Error, "failed to get notes") - span.RecordError(err) - err_msg = "unable to retrieve notes, following a updated board call" - return err_msg, err - } - - err = service.realtime.BroadcastToBoard(ctx, boardID, realtime.BoardEvent{ - Type: realtime.BoardEventNotesSync, - Data: notesOnBoard, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to broadcast notes") - span.RecordError(err) - err_msg = "unable to broadcast notes, following a updated board call" - return err_msg, err - } - - return "", err + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.sync") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.sync.board", boardID.String()), + ) + + var err_msg string + columnsOnBoard, err := service.columnService.GetAll(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get columns") + span.RecordError(err) + err_msg = "unable to retrieve columns, following a updated board call" + return err_msg, err + } + + var columnsID []uuid.UUID + for _, column := range columnsOnBoard { + columnsID = append(columnsID, column.ID) + } + + notesOnBoard, err := service.notesService.GetAll(ctx, boardID, columnsID...) + if err != nil { + span.SetStatus(codes.Error, "failed to get notes") + span.RecordError(err) + err_msg = "unable to retrieve notes, following a updated board call" + return err_msg, err + } + + err = service.realtime.BroadcastToBoard(ctx, boardID, realtime.BoardEvent{ + Type: realtime.BoardEventNotesSync, + Data: notesOnBoard, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to broadcast notes") + span.RecordError(err) + err_msg = "unable to broadcast notes, following a updated board call" + return err_msg, err + } + + return "", err } func (service *Service) DeletedBoard(ctx context.Context, board uuid.UUID) { - ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.delete") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.boards.service.board.delete") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.boards.service.board.delete.board", board.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.boards.service.board.delete.board", board.String()), + ) - _ = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventBoardDeleted, - }) + _ = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventBoardDeleted, + }) +} + +func (service *Service) BoardEditableContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.boards.service.board.editable") + defer span.End() + log := logger.FromContext(ctx) + + board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.context.editable.board", board.String()), + attribute.String("scrumlr.sessions.service.context.editable.user", user.String()), + ) + + isMod, err := service.sessionService.ModeratorSessionExists(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "failed to check session") + span.RecordError(err) + log.Errorw("unable to verify board session", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + settings, err := service.Get(ctx, board) + if err != nil { + span.SetStatus(codes.Error, "failed to get board settings") + span.RecordError(err) + log.Errorw("unable to verify board settings", "err", err) + common.Throw(w, r, common.BadRequestError(errors.New("unable to verify board settings"))) + return + } + + if !isMod && settings.IsLocked { + span.SetStatus(codes.Error, "not allowed to edit board") + span.RecordError(err) + log.Errorw("not allowed to edit board", "err", err) + common.Throw(w, r, common.ForbiddenError(errors.New("not authorized to change board"))) + return + } + + boardEditable := context.WithValue(ctx, identifiers.BoardEditableIdentifier, settings.IsLocked) + next.ServeHTTP(w, r.WithContext(boardEditable)) + }) } diff --git a/server/src/main.go b/server/src/main.go index 1db8e66320..5c7f39a432 100644 --- a/server/src/main.go +++ b/server/src/main.go @@ -428,11 +428,20 @@ func run(c *cli.Context) error { boardService := initializer.InitializeBoardService(sessionRequestService, sessionService, columnService, noteService, reactionService, votingService) + apiInitializer := serviceinitialize.NewApiInitializer(basePath) + sessionApi := apiInitializer.InitializeSessionApi(sessionService) + userApi := apiInitializer.InitializeUserApi(userService, sessionService, c.Bool("allow-anonymous-board-creation"), c.Bool("allow-anonymous-custom-templates")) + + routesInitializer := serviceinitialize.NewRoutesInitializer() + userRoutes := routesInitializer.InitializeUserRoutes(userApi, sessionApi) + sessionRoutes := routesInitializer.InitializeSessionRoutes(sessionApi) s := api.New( basePath, rt, authConfig, + userRoutes, + sessionRoutes, boardService, columnService, votingService, diff --git a/server/src/serviceinitialize/api.go b/server/src/serviceinitialize/api.go new file mode 100644 index 0000000000..a2b40282c7 --- /dev/null +++ b/server/src/serviceinitialize/api.go @@ -0,0 +1,76 @@ +package serviceinitialize + +import ( + "scrumlr.io/server/sessions" + "scrumlr.io/server/users" +) + +type ApiInitializer struct { + basePath string +} + +func NewApiInitializer(basePath string) ApiInitializer { + initializer := new(ApiInitializer) + initializer.basePath = basePath + return *initializer +} + +func (init *ApiInitializer) InitializeBoardApi() { + panic("Not implemented") +} + +func (init *ApiInitializer) InitializeColumnApi() { + panic("Not implemented") + +} + +func (init *ApiInitializer) InitializeBoardReactionApi() { + panic("Not implemented") + +} + +func (init *ApiInitializer) InitializeBoardTemplateApi() { + panic("Not implemented") + +} + +func (init *ApiInitializer) InitializeColumnTemplateApi() { + +} + +func (init *ApiInitializer) InitializeFeedbackApi() { + +} + +func (init *ApiInitializer) InitializeHealthApi() { + panic("Not implemented") + +} + +func (init *ApiInitializer) InitializeReactionApi() { + panic("Not implemented") + +} + +func (init *ApiInitializer) InitializeSessionApi(sessionService sessions.SessionService) sessions.SessionApi { + sessionApi := sessions.NewSessionApi(sessionService) + return sessionApi +} + +func (init *ApiInitializer) InitializeSessionRequestApi() { + panic("Not implemented") + +} + +func (init *ApiInitializer) InitializeUserApi(userService users.UserService, sessionService sessions.SessionService, allowAnonymousBoardCreation, allowAnonymousCustomTemplates bool) users.UsersApi { + usersApi := users.NewUserApi(userService, sessionService, allowAnonymousBoardCreation, allowAnonymousCustomTemplates) + return usersApi +} + +func (init *ApiInitializer) InitializeNotesApi() { + panic("Not implemented") +} + +func (init *ApiInitializer) InitializeVotingApi() { + panic("Not implemented") +} diff --git a/server/src/serviceinitialize/routes.go b/server/src/serviceinitialize/routes.go new file mode 100644 index 0000000000..37f3e47571 --- /dev/null +++ b/server/src/serviceinitialize/routes.go @@ -0,0 +1,76 @@ +package serviceinitialize + +import ( + "github.com/go-chi/chi/v5" + "scrumlr.io/server/sessions" + "scrumlr.io/server/users" +) + +type RoutesInitializer struct { +} + +func NewRoutesInitializer() RoutesInitializer { + initializer := new(RoutesInitializer) + + return *initializer +} + +func (init *RoutesInitializer) InitializeBoardRoutes() { + panic("Not implemented") +} + +func (init *RoutesInitializer) InitializeColumnRoutes() { + panic("Not implemented") + +} + +func (init *RoutesInitializer) InitializeBoardReactionRoutes() { + panic("Not implemented") + +} + +func (init *RoutesInitializer) InitializeBoardTemplateRoutes() { + panic("Not implemented") + +} + +func (init *RoutesInitializer) InitializeColumnTemplateRoutes() { + +} + +func (init *RoutesInitializer) InitializeFeedbackRoutes() { + +} + +func (init *RoutesInitializer) InitializeHealthRoutes() { + panic("Not implemented") + +} + +func (init *RoutesInitializer) InitializeReactionRoutes() { + panic("Not implemented") + +} + +func (init *RoutesInitializer) InitializeSessionRoutes(sessionApi sessions.SessionApi) chi.Router { + router := sessions.NewSessionRouter(sessionApi).RegisterRoutes() + return router +} + +func (init *RoutesInitializer) InitializeSessionRequestRoutes() { + panic("Not implemented") + +} + +func (init *RoutesInitializer) InitializeUserRoutes(userApi users.UsersApi, sessionApi sessions.SessionApi) chi.Router { + router := users.NewUsersRouter(userApi, sessionApi).RegisterRoutes() + return router +} + +func (init *RoutesInitializer) InitializeNotesRoutes() { + panic("Not implemented") +} + +func (init *RoutesInitializer) InitializeVotingRoutes() { + panic("Not implemented") +} diff --git a/server/src/serviceinitialize/service.go b/server/src/serviceinitialize/service.go index 0fcf1a2b0c..6aa5be06fb 100644 --- a/server/src/serviceinitialize/service.go +++ b/server/src/serviceinitialize/service.go @@ -128,7 +128,6 @@ func (init *ServiceInitializer) InitializeWebsocket() sessionrequests.Websocket func (init *ServiceInitializer) InitializeUserService(sessionService sessions.SessionService) users.UserService { userDb := users.NewUserDatabase(init.db) userService := users.NewUserService(userDb, init.rt, sessionService) - return userService } diff --git a/server/src/sessionrequests/api.go b/server/src/sessionrequests/api.go index 3378201016..eb5b030437 100644 --- a/server/src/sessionrequests/api.go +++ b/server/src/sessionrequests/api.go @@ -14,4 +14,6 @@ type SessionRequestService interface { GetAll(ctx context.Context, boardID uuid.UUID, statusQuery string) ([]*BoardSessionRequest, error) Exists(ctx context.Context, boardID, userID uuid.UUID) (bool, error) OpenSocket(ctx context.Context, w http.ResponseWriter, r *http.Request) + + BoardCandidateContext(next http.Handler) http.Handler } diff --git a/server/src/sessionrequests/mock_SessionRequestService.go b/server/src/sessionrequests/mock_SessionRequestService.go index b0fde41f9a..681d1fc8fa 100644 --- a/server/src/sessionrequests/mock_SessionRequestService.go +++ b/server/src/sessionrequests/mock_SessionRequestService.go @@ -39,6 +39,59 @@ func (_m *MockSessionRequestService) EXPECT() *MockSessionRequestService_Expecte return &MockSessionRequestService_Expecter{mock: &_m.Mock} } +// BoardCandidateContext provides a mock function for the type MockSessionRequestService +func (_mock *MockSessionRequestService) BoardCandidateContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for BoardCandidateContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockSessionRequestService_BoardCandidateContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BoardCandidateContext' +type MockSessionRequestService_BoardCandidateContext_Call struct { + *mock.Call +} + +// BoardCandidateContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockSessionRequestService_Expecter) BoardCandidateContext(next interface{}) *MockSessionRequestService_BoardCandidateContext_Call { + return &MockSessionRequestService_BoardCandidateContext_Call{Call: _e.mock.On("BoardCandidateContext", next)} +} + +func (_c *MockSessionRequestService_BoardCandidateContext_Call) Run(run func(next http.Handler)) *MockSessionRequestService_BoardCandidateContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockSessionRequestService_BoardCandidateContext_Call) Return(handler http.Handler) *MockSessionRequestService_BoardCandidateContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockSessionRequestService_BoardCandidateContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockSessionRequestService_BoardCandidateContext_Call { + _c.Call.Return(run) + return _c +} + // Create provides a mock function for the type MockSessionRequestService func (_mock *MockSessionRequestService) Create(ctx context.Context, boardID uuid.UUID, userID uuid.UUID) (*BoardSessionRequest, error) { ret := _mock.Called(ctx, boardID, userID) diff --git a/server/src/sessionrequests/service.go b/server/src/sessionrequests/service.go index 0acfe6da59..cdc2ca9cbb 100644 --- a/server/src/sessionrequests/service.go +++ b/server/src/sessionrequests/service.go @@ -1,228 +1,270 @@ package sessionrequests import ( - "context" - "database/sql" - "errors" - "fmt" - "net/http" - - "github.com/google/uuid" - "github.com/gorilla/websocket" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessions" + "context" + "database/sql" + "errors" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "scrumlr.io/server/common" + "scrumlr.io/server/identifiers" + "scrumlr.io/server/logger" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessions" ) var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/sessionrequests") var meter metric.Meter = otel.Meter("scrumlr.io/server/sessionrequests") type SessionRequestDatabase interface { - Create(ctx context.Context, request DatabaseBoardSessionRequestInsert) (DatabaseBoardSessionRequest, error) - Update(ctx context.Context, update DatabaseBoardSessionRequestUpdate) (DatabaseBoardSessionRequest, error) - Get(ctx context.Context, board, user uuid.UUID) (DatabaseBoardSessionRequest, error) - GetAll(ctx context.Context, board uuid.UUID, status ...RequestStatus) ([]DatabaseBoardSessionRequest, error) - Exists(ctx context.Context, board, user uuid.UUID) (bool, error) + Create(ctx context.Context, request DatabaseBoardSessionRequestInsert) (DatabaseBoardSessionRequest, error) + Update(ctx context.Context, update DatabaseBoardSessionRequestUpdate) (DatabaseBoardSessionRequest, error) + Get(ctx context.Context, board, user uuid.UUID) (DatabaseBoardSessionRequest, error) + GetAll(ctx context.Context, board uuid.UUID, status ...RequestStatus) ([]DatabaseBoardSessionRequest, error) + Exists(ctx context.Context, board, user uuid.UUID) (bool, error) } type Websocket interface { - OpenSocket(w http.ResponseWriter, r *http.Request) - listenOnBoardSessionRequest(boardID, userID uuid.UUID, conn *websocket.Conn) - closeSocket(conn *websocket.Conn) + OpenSocket(w http.ResponseWriter, r *http.Request) + listenOnBoardSessionRequest(boardID, userID uuid.UUID, conn *websocket.Conn) + closeSocket(conn *websocket.Conn) } type BoardSessionRequestService struct { - database SessionRequestDatabase - realtime *realtime.Broker - websocket Websocket - sessionService sessions.SessionService + database SessionRequestDatabase + realtime *realtime.Broker + websocket Websocket + sessionService sessions.SessionService } func NewSessionRequestService(db SessionRequestDatabase, rt *realtime.Broker, websocket Websocket, sessionService sessions.SessionService) SessionRequestService { - service := new(BoardSessionRequestService) - service.database = db - service.realtime = rt - service.websocket = websocket - service.sessionService = sessionService + service := new(BoardSessionRequestService) + service.database = db + service.realtime = rt + service.websocket = websocket + service.sessionService = sessionService - return service + return service } func (service *BoardSessionRequestService) Create(ctx context.Context, boardID, userID uuid.UUID) (*BoardSessionRequest, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.create") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.session_requests.service.create.board", boardID.String()), - attribute.String("scrumlr.session_requests.service.create.user", userID.String()), - ) - - request, err := service.database.Create(ctx, DatabaseBoardSessionRequestInsert{ - Board: boardID, - User: userID, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to create board session request") - span.RecordError(err) - log.Errorw("unable to create BoardSessionRequest", "board", boardID, "user", userID, "error", err) - return nil, err - } - - service.createdSessionRequest(ctx, boardID, request) - - sessionRequestsCreatedCounter.Add(ctx, 1) - return new(BoardSessionRequest).From(request), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.create") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.session_requests.service.create.board", boardID.String()), + attribute.String("scrumlr.session_requests.service.create.user", userID.String()), + ) + + request, err := service.database.Create(ctx, DatabaseBoardSessionRequestInsert{ + Board: boardID, + User: userID, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to create board session request") + span.RecordError(err) + log.Errorw("unable to create BoardSessionRequest", "board", boardID, "user", userID, "error", err) + return nil, err + } + + service.createdSessionRequest(ctx, boardID, request) + + sessionRequestsCreatedCounter.Add(ctx, 1) + return new(BoardSessionRequest).From(request), err } func (service *BoardSessionRequestService) Update(ctx context.Context, body BoardSessionRequestUpdate) (*BoardSessionRequest, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.update") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.session_requests.service.update.board", body.Board.String()), - attribute.String("scrumlr.session_requests.service.update.user", body.User.String()), - attribute.String("scrumlr.session_requests.service.update.status", string(body.Status)), - ) - - request, err := service.database.Update(ctx, DatabaseBoardSessionRequestUpdate{ - Board: body.Board, - User: body.User, - Status: body.Status, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to update board session request") - span.RecordError(err) - log.Errorw("unable to update BoardSessionRequest", "board", body.Board, "user", body.User, "error", err) - return nil, err - } - - if request.Status == RequestAccepted { - _, err := service.sessionService.Create(ctx, sessions.BoardSessionCreateRequest{Board: request.Board, User: request.User, Role: common.ParticipantRole}) - if err != nil { - span.SetStatus(codes.Error, "failed to create board session") - span.RecordError(err) - return nil, err - } - } - - service.updatedSessionRequest(ctx, body.Board, request) - - return new(BoardSessionRequest).From(request), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.update") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.session_requests.service.update.board", body.Board.String()), + attribute.String("scrumlr.session_requests.service.update.user", body.User.String()), + attribute.String("scrumlr.session_requests.service.update.status", string(body.Status)), + ) + + request, err := service.database.Update(ctx, DatabaseBoardSessionRequestUpdate{ + Board: body.Board, + User: body.User, + Status: body.Status, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to update board session request") + span.RecordError(err) + log.Errorw("unable to update BoardSessionRequest", "board", body.Board, "user", body.User, "error", err) + return nil, err + } + + if request.Status == RequestAccepted { + _, err := service.sessionService.Create(ctx, sessions.BoardSessionCreateRequest{Board: request.Board, User: request.User, Role: common.ParticipantRole}) + if err != nil { + span.SetStatus(codes.Error, "failed to create board session") + span.RecordError(err) + return nil, err + } + } + + service.updatedSessionRequest(ctx, body.Board, request) + + return new(BoardSessionRequest).From(request), err } func (service *BoardSessionRequestService) Get(ctx context.Context, boardID, userID uuid.UUID) (*BoardSessionRequest, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.get") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.session_requests.service.get.board", boardID.String()), - attribute.String("scrumlr.session_requests.service.get.user", userID.String()), - ) - - request, err := service.database.Get(ctx, boardID, userID) - if err != nil { - if err == sql.ErrNoRows { - span.SetStatus(codes.Error, "board session request not found") - span.RecordError(err) - return nil, common.NotFoundError - } - - span.SetStatus(codes.Error, "failed to get board session request") - span.RecordError(err) - log.Errorw("failed to load board session request", "board", boardID, "user", userID, "err", err) - return nil, fmt.Errorf("failed to load board session request: %w", err) - } - - return new(BoardSessionRequest).From(request), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.get") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.session_requests.service.get.board", boardID.String()), + attribute.String("scrumlr.session_requests.service.get.user", userID.String()), + ) + + request, err := service.database.Get(ctx, boardID, userID) + if err != nil { + if err == sql.ErrNoRows { + span.SetStatus(codes.Error, "board session request not found") + span.RecordError(err) + return nil, common.NotFoundError + } + + span.SetStatus(codes.Error, "failed to get board session request") + span.RecordError(err) + log.Errorw("failed to load board session request", "board", boardID, "user", userID, "err", err) + return nil, fmt.Errorf("failed to load board session request: %w", err) + } + + return new(BoardSessionRequest).From(request), err } func (service *BoardSessionRequestService) GetAll(ctx context.Context, boardID uuid.UUID, statusQuery string) ([]*BoardSessionRequest, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.get.all") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.session_requests.service.get.all.board", boardID.String()), - attribute.String("scrumlr.session_requests.service.get.all.status.query", statusQuery), - ) - - var filters []RequestStatus - if statusQuery != "" { - if statusQuery == (string)(RequestPending) || statusQuery == (string)(RequestAccepted) || statusQuery == (string)(RequestRejected) { - f := (RequestStatus)(statusQuery) - filters = append(filters, f) - } else { - err := common.BadRequestError(errors.New("invalid status filter")) - span.SetStatus(codes.Error, "invalide status filter") - span.RecordError(err) - return nil, err - } - } - - requests, err := service.database.GetAll(ctx, boardID, filters...) - if err != nil { - span.SetStatus(codes.Error, "failed to get board session requests") - span.RecordError(err) - log.Errorw("failed to load board session requests", "board", boardID, "err", err) - return nil, fmt.Errorf("failed to load board session requests: %w", err) - } - - return BoardSessionRequests(requests), nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.get.all") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.session_requests.service.get.all.board", boardID.String()), + attribute.String("scrumlr.session_requests.service.get.all.status.query", statusQuery), + ) + + var filters []RequestStatus + if statusQuery != "" { + if statusQuery == (string)(RequestPending) || statusQuery == (string)(RequestAccepted) || statusQuery == (string)(RequestRejected) { + f := (RequestStatus)(statusQuery) + filters = append(filters, f) + } else { + err := common.BadRequestError(errors.New("invalid status filter")) + span.SetStatus(codes.Error, "invalide status filter") + span.RecordError(err) + return nil, err + } + } + + requests, err := service.database.GetAll(ctx, boardID, filters...) + if err != nil { + span.SetStatus(codes.Error, "failed to get board session requests") + span.RecordError(err) + log.Errorw("failed to load board session requests", "board", boardID, "err", err) + return nil, fmt.Errorf("failed to load board session requests: %w", err) + } + + return BoardSessionRequests(requests), nil } func (service *BoardSessionRequestService) Exists(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { - ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.exists") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.session_requests.service.exists") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.session_requests.service.exists.board", boardID.String()), - attribute.String("scrumlr.session_requests.service.exists.user", userID.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.session_requests.service.exists.board", boardID.String()), + attribute.String("scrumlr.session_requests.service.exists.user", userID.String()), + ) - return service.database.Exists(ctx, boardID, userID) + return service.database.Exists(ctx, boardID, userID) } func (service *BoardSessionRequestService) OpenSocket(ctx context.Context, w http.ResponseWriter, r *http.Request) { - _, span := tracer.Start(ctx, "scrumlr.session_requests.service.open_socket") - defer span.End() + _, span := tracer.Start(ctx, "scrumlr.session_requests.service.open_socket") + defer span.End() - service.websocket.OpenSocket(w, r) + service.websocket.OpenSocket(w, r) } func (service *BoardSessionRequestService) createdSessionRequest(ctx context.Context, board uuid.UUID, request DatabaseBoardSessionRequest) { - _ = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventSessionRequestCreated, - Data: new(BoardSessionRequest).From(request), - }) + _ = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventSessionRequestCreated, + Data: new(BoardSessionRequest).From(request), + }) } func (service *BoardSessionRequestService) updatedSessionRequest(ctx context.Context, board uuid.UUID, request DatabaseBoardSessionRequest) { - var status realtime.BoardSessionRequestEventType - switch request.Status { - case RequestAccepted: - status = realtime.RequestAccepted - case RequestRejected: - status = realtime.RequestRejected - } - - if status != "" { - _ = service.realtime.BroadcastUpdateOnBoardSessionRequest(ctx, board, request.User, status) - } - - _ = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventSessionRequestUpdated, - Data: new(BoardSessionRequest).From(request), - }) + var status realtime.BoardSessionRequestEventType + switch request.Status { + case RequestAccepted: + status = realtime.RequestAccepted + case RequestRejected: + status = realtime.RequestRejected + } + + if status != "" { + _ = service.realtime.BroadcastUpdateOnBoardSessionRequest(ctx, board, request.User, status) + } + + _ = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventSessionRequestUpdated, + Data: new(BoardSessionRequest).From(request), + }) } + +func (service *BoardSessionRequestService) BoardCandidateContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessionrequest.service.context.boardCandidate") + defer span.End() + + log := logger.FromContext(ctx) + boardParam := chi.URLParam(r, "id") + board, err := uuid.Parse(boardParam) + if err != nil { + span.SetStatus(codes.Error, "unable to parse uuid") + span.RecordError(err) + common.Throw(w, r, common.BadRequestError(errors.New("invalid board id"))) + return + } + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + span.SetAttributes( + attribute.String("scrumlr.sessionrequest.service.context.boardCandidate.board", board.String()), + attribute.String("scrumlr.sessionrequest.service.context.boardCandidate.user", user.String()), + ) + exists, err := service.Exists(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "unable to check board session") + span.RecordError(err) + log.Errorw("unable to check board session", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + if !exists { + span.SetStatus(codes.Error, "board session request not found") + span.RecordError(err) + common.Throw(w, r, common.NotFoundError) + return + } + + boardContext := context.WithValue(ctx, identifiers.BoardIdentifier, board) + next.ServeHTTP(w, r.WithContext(boardContext)) + }) +} diff --git a/server/src/sessions/api.go b/server/src/sessions/api.go index 305b2d1af5..3ea0a659b9 100644 --- a/server/src/sessions/api.go +++ b/server/src/sessions/api.go @@ -2,16 +2,24 @@ package sessions import ( "context" + "errors" + "net/http" "net/url" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "scrumlr.io/server/common" + "scrumlr.io/server/identifiers" + "scrumlr.io/server/logger" ) type SessionService interface { Create(ctx context.Context, body BoardSessionCreateRequest) (*BoardSession, error) Update(ctx context.Context, body BoardSessionUpdateRequest) (*BoardSession, error) UpdateAll(ctx context.Context, body BoardSessionsUpdateRequest) ([]*BoardSession, error) - UpdateUserBoards(ctx context.Context, body BoardSessionUpdateRequest) ([]*BoardSession, error) Get(ctx context.Context, boardID, userID uuid.UUID) (*BoardSession, error) GetAll(ctx context.Context, boardID uuid.UUID, filter BoardSessionFilter) ([]*BoardSession, error) GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]*BoardSession, error) @@ -25,3 +33,231 @@ type SessionService interface { BoardSessionFilterTypeFromQueryString(query url.Values) BoardSessionFilter } + +type API struct { + service SessionService +} + +func (api *API) GetBoardSessions(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.get.all") + defer span.End() + + board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + filter := api.service.BoardSessionFilterTypeFromQueryString(r.URL.Query()) + sessions, err := api.service.GetAll(ctx, board, filter) + if err != nil { + span.SetStatus(codes.Error, "failed to get sessions") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, sessions) +} + +func (api *API) GetBoardSession(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.get") + defer span.End() + log := logger.FromContext(ctx) + + board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + userParam := chi.URLParam(r, "session") + userId, err := uuid.Parse(userParam) + if err != nil { + span.SetStatus(codes.Error, "failed to parse user id") + span.RecordError(err) + log.Errorw("Invalid user id", "err", err) + common.Throw(w, r, err) + return + } + + session, err := api.service.Get(ctx, board, userId) + if err != nil { + span.SetStatus(codes.Error, "failed to get session") + span.RecordError(err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, session) +} + +func (api *API) UpdateBoardSession(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.update") + defer span.End() + log := logger.FromContext(ctx) + + board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + caller := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + userParam := chi.URLParam(r, "session") + userId, err := uuid.Parse(userParam) + if err != nil { + span.SetStatus(codes.Error, "failed to parse user id") + span.RecordError(err) + log.Errorw("Invalid user session id", "err", err) + http.Error(w, "invalid user session id", http.StatusBadRequest) + return + } + + var body BoardSessionUpdateRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "unable to decode body") + span.RecordError(err) + log.Errorw("Unable to decode body", "err", err) + http.Error(w, "unable to parse request body", http.StatusBadRequest) + return + } + + body.Board = board + body.Caller = caller + body.User = userId + + session, err := api.service.Update(ctx, body) + if err != nil { + span.SetStatus(codes.Error, "failed to update session") + span.RecordError(err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, session) +} + +func (api *API) UpdateBoardSessions(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.update.all") + defer span.End() + log := logger.FromContext(ctx) + + board := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + var body BoardSessionsUpdateRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "unable to decode body") + span.RecordError(err) + log.Errorw("Unable to decode body", "err", err) + http.Error(w, "unable to parse request body", http.StatusBadRequest) + return + } + + body.Board = board + updatedSessions, err := api.service.UpdateAll(ctx, body) + if err != nil { + span.SetStatus(codes.Error, "failed to update all sessions") + span.RecordError(err) + http.Error(w, "unable to update board sessions", http.StatusInternalServerError) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, updatedSessions) +} + +func (api *API) BoardParticipantContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.context.participant") + defer span.End() + log := logger.FromContext(ctx) + + boardParam := chi.URLParam(r, "id") + board, err := uuid.Parse(boardParam) + if err != nil { + span.SetStatus(codes.Error, "failed to parse board id") + span.RecordError(err) + common.Throw(w, r, common.BadRequestError(errors.New("invalid board id"))) + return + } + + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + span.SetAttributes( + attribute.String("scrumlr.sessions.api.context.participant.board", board.String()), + attribute.String("scrumlr.sessions.api.context.participant.user", user.String()), + ) + + exists, err := api.service.Exists(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "unable to check board session") + span.RecordError(err) + log.Errorw("unable to check board session", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + if !exists { + span.SetStatus(codes.Error, "user board session not found") + span.RecordError(err) + common.Throw(w, r, common.ForbiddenError(errors.New("user board session not found"))) + return + } + + banned, err := api.service.IsParticipantBanned(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "unable to check if participant is banned") + span.RecordError(err) + log.Errorw("unable to check if participant is banned", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + if banned { + span.SetStatus(codes.Error, "participant is currently banned from this session") + span.RecordError(err) + common.Throw(w, r, common.ForbiddenError(errors.New("participant is currently banned from this session"))) + return + } + + boardContext := context.WithValue(ctx, identifiers.BoardIdentifier, board) + next.ServeHTTP(w, r.WithContext(boardContext)) + }) +} + +func (api *API) BoardModeratorContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.sessions.api.context.moderator") + defer span.End() + log := logger.FromContext(ctx) + + boardParam := chi.URLParam(r, "id") + board, err := uuid.Parse(boardParam) + if err != nil { + span.SetStatus(codes.Error, "unable to parse board id") + span.RecordError(err) + common.Throw(w, r, common.BadRequestError(errors.New("invalid board id"))) + return + } + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + span.SetAttributes( + attribute.String("scrumlr.sessions.api.context.moderator.board", board.String()), + attribute.String("scrumlr.sessions.api.context.moderator.user", user.String()), + ) + + exists, err := api.service.ModeratorSessionExists(ctx, board, user) + if err != nil { + span.SetStatus(codes.Error, "unable to check board session") + span.RecordError(err) + log.Errorw("unable to verify board session", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + if !exists { + span.SetStatus(codes.Error, "moderator session does not exist") + span.RecordError(err) + common.Throw(w, r, common.NotFoundError) + return + } + + boardContext := context.WithValue(ctx, identifiers.BoardIdentifier, board) + next.ServeHTTP(w, r.WithContext(boardContext)) + }) +} + +func NewSessionApi(service SessionService) SessionApi { + api := new(API) + api.service = service + return api +} diff --git a/server/src/sessions/api_test.go b/server/src/sessions/api_test.go new file mode 100644 index 0000000000..79fa2cca73 --- /dev/null +++ b/server/src/sessions/api_test.go @@ -0,0 +1,400 @@ +package sessions + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "scrumlr.io/server/common" + "scrumlr.io/server/identifiers" + "scrumlr.io/server/technical_helper" +) + +func Test_GetBoardSessions_api(t *testing.T) { + boardID := uuid.New() + mockFilter := BoardSessionFilter{Ready: nil} + mockSessions := []*BoardSession{ + {UserID: uuid.New(), Board: boardID, Role: "PARTICIPANT"}, + } + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().BoardSessionFilterTypeFromQueryString(mock.Anything).Return(mockFilter) + mockService.EXPECT().GetAll(mock.Anything, boardID, mockFilter).Return(mockSessions, nil) + + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil).AddToContext(identifiers.BoardIdentifier, boardID) + + api.GetBoardSessions(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var sessions []*BoardSession + err := json.Unmarshal(rr.Body.Bytes(), &sessions) + assert.NoError(t, err) + assert.Len(t, sessions, 1) +} + +func Test_GetBoardSessions_ServiceError(t *testing.T) { + boardID := uuid.New() + mockFilter := BoardSessionFilter{} + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().BoardSessionFilterTypeFromQueryString(mock.Anything).Return(mockFilter) + mockService.EXPECT().GetAll(mock.Anything, boardID, mockFilter).Return(nil, errors.New("db error")) + + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil).AddToContext(identifiers.BoardIdentifier, boardID) + + api.GetBoardSessions(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) + +} + +func Test_GetBoardSession_api(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + mockSession := &BoardSession{Board: uuid.New(), UserID: userID, Role: "PARTICIPANT"} + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().Get(mock.Anything, boardID, userID).Return(mockSession, nil) + + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/boards/"+boardID.String()+"/participants/"+userID.String(), nil).AddToContext(identifiers.BoardIdentifier, boardID) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("session", userID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + api.GetBoardSession(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var session BoardSession + err := json.Unmarshal(rr.Body.Bytes(), &session) + assert.NoError(t, err) + assert.Equal(t, userID, session.UserID) +} + +func Test_GetBoardSession_ServiceError(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().Get(mock.Anything, boardID, userID).Return(nil, errors.New("db error")) + + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/", nil).AddToContext(identifiers.BoardIdentifier, boardID) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("session", userID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + api.GetBoardSession(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_GetBoardSession_InvalidUUID(t *testing.T) { + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + // Keine Mocks, da der Aufruf fehlschlagen sollte, bevor der Service erreicht wird + + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/sessions/not-a-uuid", nil).AddToContext(identifiers.BoardIdentifier, uuid.Nil) + + api.GetBoardSession(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) + +} + +func Test_UpdateBoardSession_api(t *testing.T) { + boardID := uuid.New() + callerID := uuid.New() + targetUserID := uuid.New() + + ready := true + + body := BoardSessionUpdateRequest{Ready: &ready} + + serviceArg := BoardSessionUpdateRequest{ + Board: boardID, + Caller: callerID, + User: targetUserID, + Ready: &ready, + } + mockResponse := &BoardSession{Board: boardID, UserID: targetUserID, Role: common.ParticipantRole, Ready: ready} + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().Update(mock.Anything, serviceArg).Return(mockResponse, nil) + + rr := httptest.NewRecorder() + bodyBytes, _ := json.Marshal(body) + req := technical_helper.NewTestRequestBuilder("PUT", "/sessions/"+targetUserID.String(), bytes.NewReader(bodyBytes)).AddToContext(identifiers.BoardIdentifier, boardID).AddToContext(identifiers.UserIdentifier, callerID) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("session", targetUserID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + api.UpdateBoardSession(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var session BoardSession + err := json.Unmarshal(rr.Body.Bytes(), &session) + assert.NoError(t, err) + assert.Equal(t, targetUserID, session.UserID) + assert.Equal(t, ready, session.Ready) +} + +func Test_UpdateBoardSession_NoUUID(t *testing.T) { + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("PUT", "/sessions/"+uuid.NewString(), strings.NewReader("{invalid")).AddToContext(identifiers.BoardIdentifier, uuid.New()).AddToContext(identifiers.UserIdentifier, uuid.New()) + + api.UpdateBoardSession(rr, req.Request()) + assert.Equal(t, http.StatusBadRequest, rr.Result().StatusCode) +} + +func Test_UpdateBoardSession_BadBody(t *testing.T) { + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("PUT", "/sessions/"+uuid.NewString(), strings.NewReader("{invalid")).AddToContext(identifiers.BoardIdentifier, uuid.New()).AddToContext(identifiers.UserIdentifier, uuid.New()) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("session", uuid.NewString()) + req.AddToContext(chi.RouteCtxKey, rctx) + + api.UpdateBoardSession(rr, req.Request()) + assert.Equal(t, http.StatusBadRequest, rr.Result().StatusCode) +} + +func Test_UpdateBoardSession_ServiceError(t *testing.T) { + boardID := uuid.New() + callerID := uuid.New() + targetUserID := uuid.New() + + ready := true + + body := BoardSessionUpdateRequest{Ready: &ready} + + serviceArg := BoardSessionUpdateRequest{ + Board: boardID, + Caller: callerID, + User: targetUserID, + Ready: &ready, + } + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().Update(mock.Anything, serviceArg).Return(nil, errors.New("db error")) + + rr := httptest.NewRecorder() + bodyBytes, _ := json.Marshal(body) + req := technical_helper.NewTestRequestBuilder("PUT", "/", bytes.NewReader(bodyBytes)).AddToContext(identifiers.BoardIdentifier, boardID).AddToContext(identifiers.UserIdentifier, callerID) + rctx := chi.NewRouteContext() + rctx.URLParams.Add("session", targetUserID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + api.UpdateBoardSession(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_UpdateBoardSessions_api(t *testing.T) { + boardID := uuid.New() + body := BoardSessionsUpdateRequest{ + Board: boardID, + } + serviceArg := BoardSessionsUpdateRequest{Board: boardID} + mockResponse := []*BoardSession{ + {UserID: uuid.New(), Role: common.ParticipantRole}, + } + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().UpdateAll(mock.Anything, serviceArg).Return(mockResponse, nil) + + rr := httptest.NewRecorder() + bodyBytes, _ := json.Marshal(body) + req := technical_helper.NewTestRequestBuilder("PUT", "/", bytes.NewReader(bodyBytes)).AddToContext(identifiers.BoardIdentifier, boardID) + + api.UpdateBoardSessions(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var sessions []*BoardSession + err := json.Unmarshal(rr.Body.Bytes(), &sessions) + assert.NoError(t, err) + assert.Len(t, sessions, 1) +} + +func Test_UpdateBoardSessions_ServiceError(t *testing.T) { + boardID := uuid.New() + body := BoardSessionsUpdateRequest{ + Board: boardID, + } + serviceArg := BoardSessionsUpdateRequest{Board: boardID} + + mockService := NewMockSessionService(t) + api := NewSessionApi(mockService) + + mockService.EXPECT().UpdateAll(mock.Anything, serviceArg).Return(nil, errors.New("db error")) + + rr := httptest.NewRecorder() + bodyBytes, _ := json.Marshal(body) + req := technical_helper.NewTestRequestBuilder("PUT", "/", bytes.NewReader(bodyBytes)).AddToContext(identifiers.BoardIdentifier, boardID) + + api.UpdateBoardSessions(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_BoardParticipantContext(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + + sessionServiceMock := NewMockSessionService(t) + sessionApi := NewSessionApi(sessionServiceMock) + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + rr := httptest.NewRecorder() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + sessionServiceMock.EXPECT().Exists(mock.Anything, boardID, userID).Return(true, nil) + sessionServiceMock.EXPECT().IsParticipantBanned(mock.Anything, boardID, userID).Return(false, nil) + + sessionApi.BoardParticipantContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) +} + +func Test_BoardParticipantContext_NoParticipant(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + + sessionServiceMock := NewMockSessionService(t) + sessionApi := NewSessionApi(sessionServiceMock) + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + rr := httptest.NewRecorder() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + sessionServiceMock.EXPECT().Exists(mock.Anything, boardID, userID).Return(false, nil) + + sessionApi.BoardParticipantContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusForbidden, rr.Result().StatusCode) +} + +func Test_BoardParticipantContext_ParticipantBanned(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + + sessionServiceMock := NewMockSessionService(t) + sessionApi := NewSessionApi(sessionServiceMock) + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + rr := httptest.NewRecorder() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + sessionServiceMock.EXPECT().Exists(mock.Anything, boardID, userID).Return(true, nil) + sessionServiceMock.EXPECT().IsParticipantBanned(mock.Anything, boardID, userID).Return(true, nil) + + sessionApi.BoardParticipantContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusForbidden, rr.Result().StatusCode) + assert.Error(t, common.ForbiddenError(errors.New("participant is currently banned from this session"))) +} + +func Test_BoardModeratorContext_Exists(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + sessionServiceMock := NewMockSessionService(t) + sessionApi := NewSessionApi(sessionServiceMock) + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + rr := httptest.NewRecorder() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + sessionServiceMock.EXPECT().ModeratorSessionExists(mock.Anything, boardID, userID).Return(true, nil) + + sessionApi.BoardModeratorContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) +} + +func Test_BoardModeratorContext_DoesNotExists(t *testing.T) { + boardID := uuid.New() + userID := uuid.New() + sessionServiceMock := NewMockSessionService(t) + sessionApi := NewSessionApi(sessionServiceMock) + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + rr := httptest.NewRecorder() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + sessionServiceMock.EXPECT().ModeratorSessionExists(mock.Anything, boardID, userID).Return(false, nil) + + sessionApi.BoardModeratorContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusNotFound, rr.Result().StatusCode) +} diff --git a/server/src/sessions/mock_SessionApi.go b/server/src/sessions/mock_SessionApi.go new file mode 100644 index 0000000000..09d5a432c9 --- /dev/null +++ b/server/src/sessions/mock_SessionApi.go @@ -0,0 +1,328 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package sessions + +import ( + "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// NewMockSessionApi creates a new instance of MockSessionApi. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSessionApi(t interface { + mock.TestingT + Cleanup(func()) +}) *MockSessionApi { + mock := &MockSessionApi{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockSessionApi is an autogenerated mock type for the SessionApi type +type MockSessionApi struct { + mock.Mock +} + +type MockSessionApi_Expecter struct { + mock *mock.Mock +} + +func (_m *MockSessionApi) EXPECT() *MockSessionApi_Expecter { + return &MockSessionApi_Expecter{mock: &_m.Mock} +} + +// BoardModeratorContext provides a mock function for the type MockSessionApi +func (_mock *MockSessionApi) BoardModeratorContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for BoardModeratorContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockSessionApi_BoardModeratorContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BoardModeratorContext' +type MockSessionApi_BoardModeratorContext_Call struct { + *mock.Call +} + +// BoardModeratorContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockSessionApi_Expecter) BoardModeratorContext(next interface{}) *MockSessionApi_BoardModeratorContext_Call { + return &MockSessionApi_BoardModeratorContext_Call{Call: _e.mock.On("BoardModeratorContext", next)} +} + +func (_c *MockSessionApi_BoardModeratorContext_Call) Run(run func(next http.Handler)) *MockSessionApi_BoardModeratorContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockSessionApi_BoardModeratorContext_Call) Return(handler http.Handler) *MockSessionApi_BoardModeratorContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockSessionApi_BoardModeratorContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockSessionApi_BoardModeratorContext_Call { + _c.Call.Return(run) + return _c +} + +// BoardParticipantContext provides a mock function for the type MockSessionApi +func (_mock *MockSessionApi) BoardParticipantContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for BoardParticipantContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockSessionApi_BoardParticipantContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BoardParticipantContext' +type MockSessionApi_BoardParticipantContext_Call struct { + *mock.Call +} + +// BoardParticipantContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockSessionApi_Expecter) BoardParticipantContext(next interface{}) *MockSessionApi_BoardParticipantContext_Call { + return &MockSessionApi_BoardParticipantContext_Call{Call: _e.mock.On("BoardParticipantContext", next)} +} + +func (_c *MockSessionApi_BoardParticipantContext_Call) Run(run func(next http.Handler)) *MockSessionApi_BoardParticipantContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockSessionApi_BoardParticipantContext_Call) Return(handler http.Handler) *MockSessionApi_BoardParticipantContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockSessionApi_BoardParticipantContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockSessionApi_BoardParticipantContext_Call { + _c.Call.Return(run) + return _c +} + +// GetBoardSession provides a mock function for the type MockSessionApi +func (_mock *MockSessionApi) GetBoardSession(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockSessionApi_GetBoardSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBoardSession' +type MockSessionApi_GetBoardSession_Call struct { + *mock.Call +} + +// GetBoardSession is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockSessionApi_Expecter) GetBoardSession(w interface{}, r interface{}) *MockSessionApi_GetBoardSession_Call { + return &MockSessionApi_GetBoardSession_Call{Call: _e.mock.On("GetBoardSession", w, r)} +} + +func (_c *MockSessionApi_GetBoardSession_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_GetBoardSession_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockSessionApi_GetBoardSession_Call) Return() *MockSessionApi_GetBoardSession_Call { + _c.Call.Return() + return _c +} + +func (_c *MockSessionApi_GetBoardSession_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_GetBoardSession_Call { + _c.Run(run) + return _c +} + +// GetBoardSessions provides a mock function for the type MockSessionApi +func (_mock *MockSessionApi) GetBoardSessions(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockSessionApi_GetBoardSessions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBoardSessions' +type MockSessionApi_GetBoardSessions_Call struct { + *mock.Call +} + +// GetBoardSessions is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockSessionApi_Expecter) GetBoardSessions(w interface{}, r interface{}) *MockSessionApi_GetBoardSessions_Call { + return &MockSessionApi_GetBoardSessions_Call{Call: _e.mock.On("GetBoardSessions", w, r)} +} + +func (_c *MockSessionApi_GetBoardSessions_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_GetBoardSessions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockSessionApi_GetBoardSessions_Call) Return() *MockSessionApi_GetBoardSessions_Call { + _c.Call.Return() + return _c +} + +func (_c *MockSessionApi_GetBoardSessions_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_GetBoardSessions_Call { + _c.Run(run) + return _c +} + +// UpdateBoardSession provides a mock function for the type MockSessionApi +func (_mock *MockSessionApi) UpdateBoardSession(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockSessionApi_UpdateBoardSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBoardSession' +type MockSessionApi_UpdateBoardSession_Call struct { + *mock.Call +} + +// UpdateBoardSession is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockSessionApi_Expecter) UpdateBoardSession(w interface{}, r interface{}) *MockSessionApi_UpdateBoardSession_Call { + return &MockSessionApi_UpdateBoardSession_Call{Call: _e.mock.On("UpdateBoardSession", w, r)} +} + +func (_c *MockSessionApi_UpdateBoardSession_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_UpdateBoardSession_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockSessionApi_UpdateBoardSession_Call) Return() *MockSessionApi_UpdateBoardSession_Call { + _c.Call.Return() + return _c +} + +func (_c *MockSessionApi_UpdateBoardSession_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_UpdateBoardSession_Call { + _c.Run(run) + return _c +} + +// UpdateBoardSessions provides a mock function for the type MockSessionApi +func (_mock *MockSessionApi) UpdateBoardSessions(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockSessionApi_UpdateBoardSessions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBoardSessions' +type MockSessionApi_UpdateBoardSessions_Call struct { + *mock.Call +} + +// UpdateBoardSessions is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockSessionApi_Expecter) UpdateBoardSessions(w interface{}, r interface{}) *MockSessionApi_UpdateBoardSessions_Call { + return &MockSessionApi_UpdateBoardSessions_Call{Call: _e.mock.On("UpdateBoardSessions", w, r)} +} + +func (_c *MockSessionApi_UpdateBoardSessions_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_UpdateBoardSessions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockSessionApi_UpdateBoardSessions_Call) Return() *MockSessionApi_UpdateBoardSessions_Call { + _c.Call.Return() + return _c +} + +func (_c *MockSessionApi_UpdateBoardSessions_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockSessionApi_UpdateBoardSessions_Call { + _c.Run(run) + return _c +} diff --git a/server/src/sessions/mock_SessionService.go b/server/src/sessions/mock_SessionService.go index d255ed62da..98c87dde5f 100644 --- a/server/src/sessions/mock_SessionService.go +++ b/server/src/sessions/mock_SessionService.go @@ -851,71 +851,3 @@ func (_c *MockSessionService_UpdateAll_Call) RunAndReturn(run func(ctx context.C _c.Call.Return(run) return _c } - -// UpdateUserBoards provides a mock function for the type MockSessionService -func (_mock *MockSessionService) UpdateUserBoards(ctx context.Context, body BoardSessionUpdateRequest) ([]*BoardSession, error) { - ret := _mock.Called(ctx, body) - - if len(ret) == 0 { - panic("no return value specified for UpdateUserBoards") - } - - var r0 []*BoardSession - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, BoardSessionUpdateRequest) ([]*BoardSession, error)); ok { - return returnFunc(ctx, body) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, BoardSessionUpdateRequest) []*BoardSession); ok { - r0 = returnFunc(ctx, body) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*BoardSession) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, BoardSessionUpdateRequest) error); ok { - r1 = returnFunc(ctx, body) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockSessionService_UpdateUserBoards_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserBoards' -type MockSessionService_UpdateUserBoards_Call struct { - *mock.Call -} - -// UpdateUserBoards is a helper method to define mock.On call -// - ctx context.Context -// - body BoardSessionUpdateRequest -func (_e *MockSessionService_Expecter) UpdateUserBoards(ctx interface{}, body interface{}) *MockSessionService_UpdateUserBoards_Call { - return &MockSessionService_UpdateUserBoards_Call{Call: _e.mock.On("UpdateUserBoards", ctx, body)} -} - -func (_c *MockSessionService_UpdateUserBoards_Call) Run(run func(ctx context.Context, body BoardSessionUpdateRequest)) *MockSessionService_UpdateUserBoards_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 BoardSessionUpdateRequest - if args[1] != nil { - arg1 = args[1].(BoardSessionUpdateRequest) - } - run( - arg0, - arg1, - ) - }) - return _c -} - -func (_c *MockSessionService_UpdateUserBoards_Call) Return(boardSessions []*BoardSession, err error) *MockSessionService_UpdateUserBoards_Call { - _c.Call.Return(boardSessions, err) - return _c -} - -func (_c *MockSessionService_UpdateUserBoards_Call) RunAndReturn(run func(ctx context.Context, body BoardSessionUpdateRequest) ([]*BoardSession, error)) *MockSessionService_UpdateUserBoards_Call { - _c.Call.Return(run) - return _c -} diff --git a/server/src/sessions/router.go b/server/src/sessions/router.go new file mode 100644 index 0000000000..e9b93fa20a --- /dev/null +++ b/server/src/sessions/router.go @@ -0,0 +1,38 @@ +package sessions + +import ( + "net/http" + + "github.com/go-chi/chi/v5" +) + +type SessionApi interface { + GetBoardSessions(w http.ResponseWriter, r *http.Request) + GetBoardSession(w http.ResponseWriter, r *http.Request) + UpdateBoardSession(w http.ResponseWriter, r *http.Request) + UpdateBoardSessions(w http.ResponseWriter, r *http.Request) + BoardParticipantContext(next http.Handler) http.Handler + BoardModeratorContext(next http.Handler) http.Handler +} +type Router struct { + sessionAPI SessionApi +} + +func (r *Router) RegisterRoutes() chi.Router { + router := chi.NewRouter() + router.With(r.sessionAPI.BoardParticipantContext).Get("/", r.sessionAPI.GetBoardSessions) + router.With(r.sessionAPI.BoardModeratorContext).Put("/", r.sessionAPI.UpdateBoardSessions) + + router.Route("/{session}", func(router chi.Router) { + router.Use(r.sessionAPI.BoardParticipantContext) + router.Get("/", r.sessionAPI.GetBoardSession) + router.Put("/", r.sessionAPI.UpdateBoardSession) + }) + return router +} + +func NewSessionRouter(sessionApi SessionApi) *Router { + r := new(Router) + r.sessionAPI = sessionApi + return r +} diff --git a/server/src/sessions/service.go b/server/src/sessions/service.go index 3cbf6d3c32..ca6a4b23eb 100644 --- a/server/src/sessions/service.go +++ b/server/src/sessions/service.go @@ -1,539 +1,515 @@ package sessions import ( - "context" - "database/sql" - "errors" - "fmt" - "net/url" - "slices" - "strconv" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "scrumlr.io/server/columns" - "scrumlr.io/server/notes" - - "github.com/google/uuid" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" - "scrumlr.io/server/realtime" + "context" + "database/sql" + "errors" + "fmt" + "net/url" + "slices" + "strconv" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "scrumlr.io/server/columns" + "scrumlr.io/server/notes" + + "github.com/google/uuid" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" + "scrumlr.io/server/realtime" ) var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/sessions") var meter metric.Meter = otel.Meter("scrumlr.io/server/sessions") type SessionDatabase interface { - Create(ctx context.Context, boardSession DatabaseBoardSessionInsert) (DatabaseBoardSession, error) - Update(ctx context.Context, update DatabaseBoardSessionUpdate) (DatabaseBoardSession, error) - UpdateAll(ctx context.Context, update DatabaseBoardSessionUpdate) ([]DatabaseBoardSession, error) - Exists(ctx context.Context, board, user uuid.UUID) (bool, error) - ModeratorExists(ctx context.Context, board, user uuid.UUID) (bool, error) - IsParticipantBanned(ctx context.Context, board, user uuid.UUID) (bool, error) - Get(ctx context.Context, board, user uuid.UUID) (DatabaseBoardSession, error) - GetAll(ctx context.Context, board uuid.UUID, filter ...BoardSessionFilter) ([]DatabaseBoardSession, error) - GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]DatabaseBoardSession, error) + Create(ctx context.Context, boardSession DatabaseBoardSessionInsert) (DatabaseBoardSession, error) + Update(ctx context.Context, update DatabaseBoardSessionUpdate) (DatabaseBoardSession, error) + UpdateAll(ctx context.Context, update DatabaseBoardSessionUpdate) ([]DatabaseBoardSession, error) + Exists(ctx context.Context, board, user uuid.UUID) (bool, error) + ModeratorExists(ctx context.Context, board, user uuid.UUID) (bool, error) + IsParticipantBanned(ctx context.Context, board, user uuid.UUID) (bool, error) + Get(ctx context.Context, board, user uuid.UUID) (DatabaseBoardSession, error) + GetAll(ctx context.Context, board uuid.UUID, filter ...BoardSessionFilter) ([]DatabaseBoardSession, error) + GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]DatabaseBoardSession, error) } type BoardSessionService struct { - database SessionDatabase - realtime *realtime.Broker - columnService columns.ColumnService - noteService notes.NotesService + database SessionDatabase + realtime *realtime.Broker + columnService columns.ColumnService + noteService notes.NotesService } func NewSessionService(db SessionDatabase, rt *realtime.Broker, columnService columns.ColumnService, noteService notes.NotesService) SessionService { - service := new(BoardSessionService) - service.database = db - service.realtime = rt - service.columnService = columnService - service.noteService = noteService + service := new(BoardSessionService) + service.database = db + service.realtime = rt + service.columnService = columnService + service.noteService = noteService - return service + return service } func (service *BoardSessionService) Create(ctx context.Context, body BoardSessionCreateRequest) (*BoardSession, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.create") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.create.board", body.Board.String()), - attribute.String("scrumlr.sessions.service.create.user", body.User.String()), - attribute.String("scrumlr.sessions.service.create.role", string(body.Role)), - ) - - session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ - Board: body.Board, - User: body.User, - Role: body.Role, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to create board session") - span.RecordError(err) - log.Errorw("unable to create board session", "board", body.Board, "user", body.User, "error", err) - return nil, err - } - - service.createdSession(ctx, body.Board, session) - - sessionCreatedCounter.Add(ctx, 1) - return new(BoardSession).From(session), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.create") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.create.board", body.Board.String()), + attribute.String("scrumlr.sessions.service.create.user", body.User.String()), + attribute.String("scrumlr.sessions.service.create.role", string(body.Role)), + ) + + session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ + Board: body.Board, + User: body.User, + Role: body.Role, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to create board session") + span.RecordError(err) + log.Errorw("unable to create board session", "board", body.Board, "user", body.User, "error", err) + return nil, err + } + + service.createdSession(ctx, body.Board, session) + + sessionCreatedCounter.Add(ctx, 1) + return new(BoardSession).From(session), err } func (service *BoardSessionService) Update(ctx context.Context, body BoardSessionUpdateRequest) (*BoardSession, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.update.board", body.Board.String()), - attribute.String("scrumlr.sessions.service.update.user", body.User.String()), - attribute.String("scrumlr.sessions.service.update.caller", body.Caller.String()), - ) - - sessionOfCaller, err := service.database.Get(ctx, body.Board, body.Caller) - if err != nil { - span.SetStatus(codes.Error, "failed to get board session") - span.RecordError(err) - log.Errorw("unable to get board session", "board", body.Board, "calling user", body.Caller, "error", err) - return nil, fmt.Errorf("unable to get session for board: %w", err) - } - - if sessionOfCaller.Role == common.ParticipantRole && body.User != body.Caller { - span.SetStatus(codes.Error, "not allowed to change user session") - span.RecordError(err) - return nil, common.ForbiddenError(errors.New("not allowed to change other users session")) - } - - sessionOfUserToModify, err := service.database.Get(ctx, body.Board, body.User) - if err != nil { - span.SetStatus(codes.Error, "failed to get session") - span.RecordError(err) - log.Errorw("unable to get board session", "board", body.Board, "target user", body.User, "error", err) - return nil, fmt.Errorf("unable to get session for board: %w", err) - } - - if body.Role != nil { - if sessionOfCaller.Role == common.ParticipantRole && *body.Role != common.ParticipantRole { - err := common.ForbiddenError(errors.New("cannot promote role")) - span.SetStatus(codes.Error, "cannot promote role") - span.RecordError(err) - return nil, err - } else if sessionOfUserToModify.Role == common.OwnerRole && *body.Role != common.OwnerRole { - err := common.ForbiddenError(errors.New("not allowed to change owner role")) - span.SetStatus(codes.Error, "not allowed to change owner role") - span.RecordError(err) - return nil, err - } else if sessionOfUserToModify.Role != common.OwnerRole && *body.Role == common.OwnerRole { - err := common.ForbiddenError(errors.New("not allowed to promote to owner role")) - span.SetStatus(codes.Error, "not allowed to promote to owner role") - span.RecordError(err) - return nil, err - } - } - - session, err := service.database.Update(ctx, DatabaseBoardSessionUpdate{ - Board: body.Board, - User: body.User, - Ready: body.Ready, - RaisedHand: body.RaisedHand, - ShowHiddenColumns: body.ShowHiddenColumns, - Role: body.Role, - Banned: body.Banned, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to update board session") - span.RecordError(err) - log.Errorw("unable to update board session", "board", body.Board, "error", err) - return nil, err - } - - service.updatedSession(ctx, body.Board, session) - - if body.Banned != nil { - if *body.Banned { - bannedSessionsCounter.Add(ctx, 1) - } - } - return new(BoardSession).From(session), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.update.board", body.Board.String()), + attribute.String("scrumlr.sessions.service.update.user", body.User.String()), + attribute.String("scrumlr.sessions.service.update.caller", body.Caller.String()), + ) + + sessionOfCaller, err := service.database.Get(ctx, body.Board, body.Caller) + if err != nil { + span.SetStatus(codes.Error, "failed to get board session") + span.RecordError(err) + log.Errorw("unable to get board session", "board", body.Board, "calling user", body.Caller, "error", err) + return nil, fmt.Errorf("unable to get session for board: %w", err) + } + + if sessionOfCaller.Role == common.ParticipantRole && body.User != body.Caller { + span.SetStatus(codes.Error, "not allowed to change user session") + span.RecordError(err) + return nil, common.ForbiddenError(errors.New("not allowed to change other users session")) + } + + sessionOfUserToModify, err := service.database.Get(ctx, body.Board, body.User) + if err != nil { + span.SetStatus(codes.Error, "failed to get session") + span.RecordError(err) + log.Errorw("unable to get board session", "board", body.Board, "target user", body.User, "error", err) + return nil, fmt.Errorf("unable to get session for board: %w", err) + } + + if body.Role != nil { + if sessionOfCaller.Role == common.ParticipantRole && *body.Role != common.ParticipantRole { + err := common.ForbiddenError(errors.New("cannot promote role")) + span.SetStatus(codes.Error, "cannot promote role") + span.RecordError(err) + return nil, err + } else if sessionOfUserToModify.Role == common.OwnerRole && *body.Role != common.OwnerRole { + err := common.ForbiddenError(errors.New("not allowed to change owner role")) + span.SetStatus(codes.Error, "not allowed to change owner role") + span.RecordError(err) + return nil, err + } else if sessionOfUserToModify.Role != common.OwnerRole && *body.Role == common.OwnerRole { + err := common.ForbiddenError(errors.New("not allowed to promote to owner role")) + span.SetStatus(codes.Error, "not allowed to promote to owner role") + span.RecordError(err) + return nil, err + } + } + + session, err := service.database.Update(ctx, DatabaseBoardSessionUpdate{ + Board: body.Board, + User: body.User, + Ready: body.Ready, + RaisedHand: body.RaisedHand, + ShowHiddenColumns: body.ShowHiddenColumns, + Role: body.Role, + Banned: body.Banned, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to update board session") + span.RecordError(err) + log.Errorw("unable to update board session", "board", body.Board, "error", err) + return nil, err + } + + service.updatedSession(ctx, body.Board, body.User) + + if body.Banned != nil { + if *body.Banned { + bannedSessionsCounter.Add(ctx, 1) + } + } + return new(BoardSession).From(session), err } func (service *BoardSessionService) UpdateAll(ctx context.Context, body BoardSessionsUpdateRequest) ([]*BoardSession, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update.all") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.update.all.board", body.Board.String()), - ) - sessions, err := service.database.UpdateAll(ctx, DatabaseBoardSessionUpdate{ - Board: body.Board, - Ready: body.Ready, - RaisedHand: body.RaisedHand, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to update all sessions") - span.RecordError(err) - log.Errorw("unable to update all sessions for a board", "board", body.Board, "error", err) - return nil, err - } - - service.updatedSessions(ctx, body.Board, sessions) - - return BoardSessions(sessions), err -} - -func (service *BoardSessionService) UpdateUserBoards(ctx context.Context, body BoardSessionUpdateRequest) ([]*BoardSession, error) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update.user.boards") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.update.user.boards.board", body.Board.String()), - attribute.String("scrumlr.sessions.service.update.user.boards.user", body.User.String()), - attribute.String("scrumlr.sessions.service.update.user.boards.caller", body.Caller.String()), - ) - - connectedBoards, err := service.database.GetUserConnectedBoards(ctx, body.User) - if err != nil { - span.SetStatus(codes.Error, "failed to get connected boards") - span.RecordError(err) - return nil, err - } - - for _, session := range connectedBoards { - service.updatedSession(ctx, session.Board, session) - } - - return BoardSessions(connectedBoards), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update.all") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.update.all.board", body.Board.String()), + ) + sessions, err := service.database.UpdateAll(ctx, DatabaseBoardSessionUpdate{ + Board: body.Board, + Ready: body.Ready, + RaisedHand: body.RaisedHand, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to update all sessions") + span.RecordError(err) + log.Errorw("unable to update all sessions for a board", "board", body.Board, "error", err) + return nil, err + } + + service.updatedSessions(ctx, body.Board, sessions) + + return BoardSessions(sessions), err } func (service *BoardSessionService) Get(ctx context.Context, boardID, userID uuid.UUID) (*BoardSession, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.get") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.get.board", boardID.String()), - attribute.String("scrumlr.sessions.service.get.user", userID.String()), - ) - - session, err := service.database.Get(ctx, boardID, userID) - if err != nil { - if err == sql.ErrNoRows { - span.SetStatus(codes.Error, "session not found") - span.RecordError(err) - return nil, common.NotFoundError - } - - span.SetStatus(codes.Error, "failed to get session") - span.RecordError(err) - log.Errorw("unable to get session for board", "board", boardID, "session", userID, "error", err) - return nil, fmt.Errorf("unable to get session for board: %w", err) - } - - return new(BoardSession).From(session), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.get") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.get.board", boardID.String()), + attribute.String("scrumlr.sessions.service.get.user", userID.String()), + ) + + session, err := service.database.Get(ctx, boardID, userID) + if err != nil { + if err == sql.ErrNoRows { + span.SetStatus(codes.Error, "session not found") + span.RecordError(err) + return nil, common.NotFoundError + } + + span.SetStatus(codes.Error, "failed to get session") + span.RecordError(err) + log.Errorw("unable to get session for board", "board", boardID, "session", userID, "error", err) + return nil, fmt.Errorf("unable to get session for board: %w", err) + } + + return new(BoardSession).From(session), err } func (service *BoardSessionService) GetAll(ctx context.Context, boardID uuid.UUID, filter BoardSessionFilter) ([]*BoardSession, error) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.get.all") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.get.all") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.sessions.service.get.all.board", boardID.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.sessions.service.get.all.board", boardID.String()), + ) - sessions, err := service.database.GetAll(ctx, boardID, filter) - if err != nil { - span.SetStatus(codes.Error, "failed to get all session") - span.RecordError(err) - return nil, err - } + sessions, err := service.database.GetAll(ctx, boardID, filter) + if err != nil { + span.SetStatus(codes.Error, "failed to get all session") + span.RecordError(err) + return nil, err + } - return BoardSessions(sessions), err + return BoardSessions(sessions), err } func (service *BoardSessionService) GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]*BoardSession, error) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.get.user_connected_boards") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.get.user_connected_boards") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.sessions.service.get.user_connected_boards.user", user.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.sessions.service.get.user_connected_boards.user", user.String()), + ) - sessions, err := service.database.GetUserConnectedBoards(ctx, user) - if err != nil { - span.SetStatus(codes.Error, "failed to get user connected boards") - span.RecordError(err) - return nil, err - } + sessions, err := service.database.GetUserConnectedBoards(ctx, user) + if err != nil { + span.SetStatus(codes.Error, "failed to get user connected boards") + span.RecordError(err) + return nil, err + } - return BoardSessions(sessions), err + return BoardSessions(sessions), err } func (service *BoardSessionService) Connect(ctx context.Context, boardID, userID uuid.UUID) error { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.connect") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.connect.board", boardID.String()), - attribute.String("scrumlr.sessions.service.connect.user", userID.String()), - ) - - var connected = true - updatedSession, err := service.database.Update(ctx, DatabaseBoardSessionUpdate{ - Board: boardID, - User: userID, - Connected: &connected, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to connect to board session") - span.RecordError(err) - log.Errorw("unable to connect to board session", "board", boardID, "user", userID, "error", err) - return err - } - - service.updatedSession(ctx, boardID, updatedSession) - - connectedSessions.Add(ctx, 1) - return err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.connect") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.connect.board", boardID.String()), + attribute.String("scrumlr.sessions.service.connect.user", userID.String()), + ) + + var connected = true + _, err := service.database.Update(ctx, DatabaseBoardSessionUpdate{ + Board: boardID, + User: userID, + Connected: &connected, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to connect to board session") + span.RecordError(err) + log.Errorw("unable to connect to board session", "board", boardID, "user", userID, "error", err) + return err + } + + service.updatedSession(ctx, boardID, userID) + + connectedSessions.Add(ctx, 1) + return err } func (service *BoardSessionService) Disconnect(ctx context.Context, boardID, userID uuid.UUID) error { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.disconnect") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.disconnect.board", boardID.String()), - attribute.String("scrumlr.sessions.service.disconnect.user", userID.String()), - ) - - var connected = false - updatedSession, err := service.database.Update(ctx, DatabaseBoardSessionUpdate{ - Board: boardID, - User: userID, - Connected: &connected, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to disconnect from board session") - span.RecordError(err) - log.Errorw("unable to disconnect from board session", "board", boardID, "user", userID, "error", err) - return err - } - - service.updatedSession(ctx, boardID, updatedSession) - - connectedSessions.Add(ctx, -1) - return err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.disconnect") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.disconnect.board", boardID.String()), + attribute.String("scrumlr.sessions.service.disconnect.user", userID.String()), + ) + + var connected = false + _, err := service.database.Update(ctx, DatabaseBoardSessionUpdate{ + Board: boardID, + User: userID, + Connected: &connected, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to disconnect from board session") + span.RecordError(err) + log.Errorw("unable to disconnect from board session", "board", boardID, "user", userID, "error", err) + return err + } + + service.updatedSession(ctx, boardID, userID) + + connectedSessions.Add(ctx, -1) + return err } func (service *BoardSessionService) Exists(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.exists") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.exists") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.sessions.service.exists.baord", boardID.String()), - attribute.String("scrumlr.sessions.service.exists.user", userID.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.sessions.service.exists.baord", boardID.String()), + attribute.String("scrumlr.sessions.service.exists.user", userID.String()), + ) - return service.database.Exists(ctx, boardID, userID) + return service.database.Exists(ctx, boardID, userID) } func (service *BoardSessionService) ModeratorSessionExists(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.exists.moderator") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.exists.moderator") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.sessions.service.exists.moderator.baord", boardID.String()), - attribute.String("scrumlr.sessions.service.exists.moderator.user", userID.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.sessions.service.exists.moderator.baord", boardID.String()), + attribute.String("scrumlr.sessions.service.exists.moderator.user", userID.String()), + ) - return service.database.ModeratorExists(ctx, boardID, userID) + return service.database.ModeratorExists(ctx, boardID, userID) } func (service *BoardSessionService) IsParticipantBanned(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.is_banned") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.is_banned") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.sessions.service.is_banned.baord", boardID.String()), - attribute.String("scrumlr.sessions.service.is_banned.user", userID.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.sessions.service.is_banned.baord", boardID.String()), + attribute.String("scrumlr.sessions.service.is_banned.user", userID.String()), + ) - return service.database.IsParticipantBanned(ctx, boardID, userID) + return service.database.IsParticipantBanned(ctx, boardID, userID) } func (service *BoardSessionService) BoardSessionFilterTypeFromQueryString(query url.Values) BoardSessionFilter { - filter := BoardSessionFilter{} - connectedFilter := query.Get("connected") - if connectedFilter != "" { - value, _ := strconv.ParseBool(connectedFilter) - filter.Connected = &value - } - - readyFilter := query.Get("ready") - if readyFilter != "" { - value, _ := strconv.ParseBool(readyFilter) - filter.Ready = &value - } - - raisedHandFilter := query.Get("raisedHand") - if raisedHandFilter != "" { - value, _ := strconv.ParseBool(raisedHandFilter) - filter.RaisedHand = &value - } - - roleFilter := query.Get("role") - if roleFilter != "" { - filter.Role = (*common.SessionRole)(&roleFilter) - } - - return filter + filter := BoardSessionFilter{} + connectedFilter := query.Get("connected") + if connectedFilter != "" { + value, _ := strconv.ParseBool(connectedFilter) + filter.Connected = &value + } + + readyFilter := query.Get("ready") + if readyFilter != "" { + value, _ := strconv.ParseBool(readyFilter) + filter.Ready = &value + } + + raisedHandFilter := query.Get("raisedHand") + if raisedHandFilter != "" { + value, _ := strconv.ParseBool(raisedHandFilter) + filter.RaisedHand = &value + } + + roleFilter := query.Get("role") + if roleFilter != "" { + filter.Role = (*common.SessionRole)(&roleFilter) + } + + return filter } func (service *BoardSessionService) createdSession(ctx context.Context, board uuid.UUID, session DatabaseBoardSession) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.create") - defer span.End() - log := logger.FromContext(ctx) - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.create.board", board.String()), - attribute.String("scrumlr.sessions.service.create.user", session.User.String()), - ) - - err := service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventParticipantCreated, - Data: new(BoardSession).From(session), - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to send participant update") - span.RecordError(err) - log.Errorw("unable to send participant update", "session", session, "error", err) - } + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.create") + defer span.End() + log := logger.FromContext(ctx) + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.create.board", board.String()), + attribute.String("scrumlr.sessions.service.create.user", session.User.String()), + ) + + err := service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventParticipantCreated, + Data: new(BoardSession).From(session), + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to send participant update") + span.RecordError(err) + log.Errorw("unable to send participant update", "session", session, "error", err) + } } -func (service *BoardSessionService) updatedSession(ctx context.Context, board uuid.UUID, session DatabaseBoardSession) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update") - defer span.End() - log := logger.FromContext(ctx) - - span.SetAttributes( - attribute.String("scrumlr.sessions.service.update.board", board.String()), - attribute.String("scrumlr.sessions.service.update.user", session.User.String()), - ) - - connectedBoards, err := service.database.GetUserConnectedBoards(ctx, session.User) - if err != nil { - span.SetStatus(codes.Error, "failed to get user connections") - span.RecordError(err) - log.Errorw("unable to get user connections", "session", session, "error", err) - return - } - - for _, s := range connectedBoards { - userSession, err := service.database.Get(ctx, s.Board, s.User) - if err != nil { - span.SetStatus(codes.Error, "failed to get board sessions of user") - span.RecordError(err) - log.Errorw("unable to get board session of user", "board", s.Board, "user", s.User, "err", err) - return - } - - err = service.realtime.BroadcastToBoard(ctx, s.Board, realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: new(BoardSession).From(userSession), - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to send participant update") - span.RecordError(err) - log.Errorw("unable to send participant update", "board", session.Board, "user", session.User, "err", err) - } - } - - // Sync columns - columns, err := service.columnService.GetAll(ctx, board) - if err != nil { - span.SetStatus(codes.Error, "failed to get columns") - span.RecordError(err) - log.Errorw("unable to get columns", "boardID", board, "err", err) - } - - err = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: columns, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to send columns update") - span.RecordError(err) - log.Errorw("unable to send columns update", "board", session.Board, "user", session.User, "err", err) - } - - columnIds := make([]uuid.UUID, 0, len(columns)) - for _, column := range columns { - columnIds = append(columnIds, column.ID) - } - // Sync notes - notes, err := service.noteService.GetAll(ctx, board, columnIds...) - if err != nil { - span.SetStatus(codes.Error, "failed to get notes") - span.RecordError(err) - log.Errorw("unable to get notes on a updatedsession call", "err", err) - } - - err = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventNotesSync, - Data: notes, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to send note sync") - span.RecordError(err) - log.Errorw("unable to send note sync", "board", session.Board, "user", session.User, "err", err) - } +func (service *BoardSessionService) updatedSession(ctx context.Context, board uuid.UUID, userId uuid.UUID) { + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update") + defer span.End() + log := logger.FromContext(ctx) + + span.SetAttributes( + attribute.String("scrumlr.sessions.service.update.board", board.String()), + attribute.String("scrumlr.sessions.service.update.user", userId.String()), + ) + + connectedBoards, err := service.database.GetUserConnectedBoards(ctx, userId) + if err != nil { + span.SetStatus(codes.Error, "failed to get user connections") + span.RecordError(err) + log.Errorw("unable to get user connections", "userId", userId, "error", err) + return + } + + for _, s := range connectedBoards { + userSession, err := service.database.Get(ctx, s.Board, s.User) + if err != nil { + span.SetStatus(codes.Error, "failed to get board sessions of user") + span.RecordError(err) + log.Errorw("unable to get board session of user", "board", s.Board, "user", s.User, "err", err) + continue + } + + err = service.realtime.BroadcastToBoard(ctx, s.Board, realtime.BoardEvent{ + Type: realtime.BoardEventParticipantUpdated, + Data: new(BoardSession).From(userSession), + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to send participant update") + span.RecordError(err) + log.Errorw("unable to send participant update", "board", board, "user", userId, "err", err) + } + } + + // Sync columns + columns, err := service.columnService.GetAll(ctx, board) + if err != nil { + span.SetStatus(codes.Error, "failed to get columns") + span.RecordError(err) + log.Errorw("unable to get columns", "boardID", board, "err", err) + } + + err = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: columns, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to send columns update") + span.RecordError(err) + log.Errorw("unable to send columns update", "board", board, "user", userId, "err", err) + } + + columnIds := make([]uuid.UUID, 0, len(columns)) + for _, column := range columns { + columnIds = append(columnIds, column.ID) + } + // Sync notes + notes, err := service.noteService.GetAll(ctx, board, columnIds...) + if err != nil { + span.SetStatus(codes.Error, "failed to get notes") + span.RecordError(err) + log.Errorw("unable to get notes on a updatedsession call", "err", err) + } + + err = service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventNotesSync, + Data: notes, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to send note sync") + span.RecordError(err) + log.Errorw("unable to send note sync", "board", board, "user", userId, "err", err) + } } func (service *BoardSessionService) updatedSessions(ctx context.Context, board uuid.UUID, sessions []DatabaseBoardSession) { - ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update") - defer span.End() - log := logger.FromContext(ctx) - - eventSessions := make([]BoardSession, 0, len(sessions)) - for _, session := range sessions { - eventSessions = append(eventSessions, *new(BoardSession).From(session)) - } - - err := service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ - Type: realtime.BoardEventParticipantsUpdated, - Data: eventSessions, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to send participant update") - span.RecordError(err) - log.Errorw("unable to send participant update", "board", board, "err", err) - } + ctx, span := tracer.Start(ctx, "scrumlr.sessions.service.update") + defer span.End() + log := logger.FromContext(ctx) + + eventSessions := make([]BoardSession, 0, len(sessions)) + for _, session := range sessions { + eventSessions = append(eventSessions, *new(BoardSession).From(session)) + } + + err := service.realtime.BroadcastToBoard(ctx, board, realtime.BoardEvent{ + Type: realtime.BoardEventParticipantsUpdated, + Data: eventSessions, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to send participant update") + span.RecordError(err) + log.Errorw("unable to send participant update", "board", board, "err", err) + } } func CheckSessionRole(clientID uuid.UUID, sessions []*BoardSession, sessionsRoles []common.SessionRole) bool { - for _, session := range sessions { - if clientID == session.UserID { - if slices.Contains(sessionsRoles, session.Role) { - return true - } - } - } - return false + for _, session := range sessions { + if clientID == session.UserID { + if slices.Contains(sessionsRoles, session.Role) { + return true + } + } + } + return false } diff --git a/server/src/sessions/service_test.go b/server/src/sessions/service_test.go index fab47ec31a..982045cdde 100644 --- a/server/src/sessions/service_test.go +++ b/server/src/sessions/service_test.go @@ -1,1294 +1,1213 @@ package sessions import ( - "context" - "database/sql" - "errors" - "fmt" - "net/url" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - mock "github.com/stretchr/testify/mock" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" + "context" + "database/sql" + "errors" + "fmt" + "net/url" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" ) func TestGetSession(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSession{Board: boardId, User: userId}, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSession{Board: boardId, User: userId}, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Get(context.Background(), boardId, userId) + session, err := sessionService.Get(context.Background(), boardId, userId) - assert.Nil(t, err) - assert.NotNil(t, session) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) + assert.Nil(t, err) + assert.NotNil(t, session) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) } func TestGetSession_NotFound(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSession{}, sql.ErrNoRows) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSession{}, sql.ErrNoRows) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Get(context.Background(), boardId, userId) + session, err := sessionService.Get(context.Background(), boardId, userId) - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, common.NotFoundError, err) + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, common.NotFoundError, err) } func TestGetSession_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "unable to execute" + boardId := uuid.New() + userId := uuid.New() + dbError := "unable to execute" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Get(context.Background(), boardId, userId) + session, err := sessionService.Get(context.Background(), boardId, userId) - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, fmt.Errorf("unable to get session for board: %w", errors.New(dbError)), err) + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, fmt.Errorf("unable to get session for board: %w", errors.New(dbError)), err) } func TestGetSessions(t *testing.T) { - boardId := uuid.New() - firstUserId := uuid.New() - secondUserId := uuid.New() - filter := BoardSessionFilter{} + boardId := uuid.New() + firstUserId := uuid.New() + secondUserId := uuid.New() + filter := BoardSessionFilter{} - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). - Return([]DatabaseBoardSession{ - {Board: boardId, User: firstUserId}, - {Board: boardId, User: secondUserId}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). + Return([]DatabaseBoardSession{ + {Board: boardId, User: firstUserId}, + {Board: boardId, User: secondUserId}, + }, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) + boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) - assert.Nil(t, err) - assert.NotNil(t, boardSessions) - assert.Len(t, boardSessions, 2) + assert.Nil(t, err) + assert.NotNil(t, boardSessions) + assert.Len(t, boardSessions, 2) - assert.Equal(t, firstUserId, boardSessions[0].UserID) - assert.Equal(t, boardId, boardSessions[0].Board) + assert.Equal(t, firstUserId, boardSessions[0].UserID) + assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, secondUserId, boardSessions[1].UserID) - assert.Equal(t, boardId, boardSessions[1].Board) + assert.Equal(t, secondUserId, boardSessions[1].UserID) + assert.Equal(t, boardId, boardSessions[1].Board) } func TestGetUserConnectedBoards(t *testing.T) { - userId := uuid.New() - firstBoard := uuid.New() - secondBoard := uuid.New() + userId := uuid.New() + firstBoard := uuid.New() + secondBoard := uuid.New() - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{ - {User: userId, Board: firstBoard}, - {User: userId, Board: secondBoard}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]DatabaseBoardSession{ + {User: userId, Board: firstBoard}, + {User: userId, Board: secondBoard}, + }, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - sessions, err := sessionService.GetUserConnectedBoards(context.Background(), userId) + sessions, err := sessionService.GetUserConnectedBoards(context.Background(), userId) - assert.Nil(t, err) - assert.Len(t, sessions, 2) + assert.Nil(t, err) + assert.Len(t, sessions, 2) - assert.Equal(t, userId, sessions[0].UserID) - assert.Equal(t, firstBoard, sessions[0].Board) - assert.Equal(t, userId, sessions[1].UserID) - assert.Equal(t, secondBoard, sessions[1].Board) + assert.Equal(t, userId, sessions[0].UserID) + assert.Equal(t, firstBoard, sessions[0].Board) + assert.Equal(t, userId, sessions[1].UserID) + assert.Equal(t, secondBoard, sessions[1].Board) } func TestGetUserConnectedBoards_DatabaseError(t *testing.T) { - userId := uuid.New() - dbError := "database error" + userId := uuid.New() + dbError := "database error" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - sessions, err := sessionService.GetUserConnectedBoards(context.Background(), userId) + sessions, err := sessionService.GetUserConnectedBoards(context.Background(), userId) - assert.Nil(t, sessions) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.Nil(t, sessions) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestListSessions_WithFilterConnected(t *testing.T) { - boardId := uuid.New() - connected := true - filter := BoardSessionFilter{Connected: &connected} + boardId := uuid.New() + connected := true + filter := BoardSessionFilter{Connected: &connected} - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). - Return([]DatabaseBoardSession{ - {Board: boardId, Connected: connected}, - {Board: boardId, Connected: connected}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). + Return([]DatabaseBoardSession{ + {Board: boardId, Connected: connected}, + {Board: boardId, Connected: connected}, + }, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) + boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) - assert.Nil(t, err) - assert.NotNil(t, boardSessions) - assert.Len(t, boardSessions, 2) + assert.Nil(t, err) + assert.NotNil(t, boardSessions) + assert.Len(t, boardSessions, 2) - assert.Equal(t, connected, boardSessions[0].Connected) - assert.Equal(t, boardId, boardSessions[0].Board) + assert.Equal(t, connected, boardSessions[0].Connected) + assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, connected, boardSessions[1].Connected) - assert.Equal(t, boardId, boardSessions[1].Board) + assert.Equal(t, connected, boardSessions[1].Connected) + assert.Equal(t, boardId, boardSessions[1].Board) } func TestListSessions_WithFilterReady(t *testing.T) { - boardId := uuid.New() - ready := true - filter := BoardSessionFilter{Ready: &ready} + boardId := uuid.New() + ready := true + filter := BoardSessionFilter{Ready: &ready} - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). - Return([]DatabaseBoardSession{ - {Board: boardId, Ready: ready}, - {Board: boardId, Ready: ready}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). + Return([]DatabaseBoardSession{ + {Board: boardId, Ready: ready}, + {Board: boardId, Ready: ready}, + }, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) + boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) - assert.Nil(t, err) - assert.NotNil(t, boardSessions) - assert.Len(t, boardSessions, 2) + assert.Nil(t, err) + assert.NotNil(t, boardSessions) + assert.Len(t, boardSessions, 2) - assert.Equal(t, ready, boardSessions[0].Ready) - assert.Equal(t, boardId, boardSessions[0].Board) + assert.Equal(t, ready, boardSessions[0].Ready) + assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, ready, boardSessions[1].Ready) - assert.Equal(t, boardId, boardSessions[1].Board) + assert.Equal(t, ready, boardSessions[1].Ready) + assert.Equal(t, boardId, boardSessions[1].Board) } func TestListSessions_WithFilterRaisedHand(t *testing.T) { - boardId := uuid.New() - raisedHand := true - filter := BoardSessionFilter{RaisedHand: &raisedHand} + boardId := uuid.New() + raisedHand := true + filter := BoardSessionFilter{RaisedHand: &raisedHand} - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). - Return([]DatabaseBoardSession{ - {Board: boardId, RaisedHand: raisedHand}, - {Board: boardId, RaisedHand: raisedHand}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). + Return([]DatabaseBoardSession{ + {Board: boardId, RaisedHand: raisedHand}, + {Board: boardId, RaisedHand: raisedHand}, + }, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) + boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) - assert.Nil(t, err) - assert.NotNil(t, boardSessions) - assert.Len(t, boardSessions, 2) + assert.Nil(t, err) + assert.NotNil(t, boardSessions) + assert.Len(t, boardSessions, 2) - assert.Equal(t, raisedHand, boardSessions[0].RaisedHand) - assert.Equal(t, boardId, boardSessions[0].Board) + assert.Equal(t, raisedHand, boardSessions[0].RaisedHand) + assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, raisedHand, boardSessions[1].RaisedHand) - assert.Equal(t, boardId, boardSessions[1].Board) + assert.Equal(t, raisedHand, boardSessions[1].RaisedHand) + assert.Equal(t, boardId, boardSessions[1].Board) } func TestListSessions_WithFilterRole(t *testing.T) { - boardId := uuid.New() - moderatorRole := common.ModeratorRole - filter := BoardSessionFilter{Role: &moderatorRole} + boardId := uuid.New() + moderatorRole := common.ModeratorRole + filter := BoardSessionFilter{Role: &moderatorRole} - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). - Return([]DatabaseBoardSession{ - {Board: boardId, Role: common.ModeratorRole}, - {Board: boardId, Role: common.ModeratorRole}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). + Return([]DatabaseBoardSession{ + {Board: boardId, Role: common.ModeratorRole}, + {Board: boardId, Role: common.ModeratorRole}, + }, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) + boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) - assert.Nil(t, err) - assert.NotNil(t, boardSessions) - assert.Len(t, boardSessions, 2) + assert.Nil(t, err) + assert.NotNil(t, boardSessions) + assert.Len(t, boardSessions, 2) - assert.Equal(t, moderatorRole, boardSessions[0].Role) - assert.Equal(t, boardId, boardSessions[0].Board) + assert.Equal(t, moderatorRole, boardSessions[0].Role) + assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, moderatorRole, boardSessions[1].Role) - assert.Equal(t, boardId, boardSessions[1].Board) + assert.Equal(t, moderatorRole, boardSessions[1].Role) + assert.Equal(t, boardId, boardSessions[1].Board) } func TestListSessions_DatabaseError(t *testing.T) { - boardId := uuid.New() - dbError := "unable to execute" - filter := BoardSessionFilter{} + boardId := uuid.New() + dbError := "unable to execute" + filter := BoardSessionFilter{} - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). - Return([]DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().GetAll(mock.Anything, boardId, []BoardSessionFilter{filter}). + Return([]DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) + boardSessions, err := sessionService.GetAll(context.Background(), boardId, filter) - assert.Nil(t, boardSessions) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.Nil(t, boardSessions) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestCreateSession(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - role := common.ParticipantRole + boardId := uuid.New() + userId := uuid.New() + role := common.ParticipantRole - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Create(mock.Anything, DatabaseBoardSessionInsert{Board: boardId, User: userId, Role: role}). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: role}, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Create(mock.Anything, DatabaseBoardSessionInsert{Board: boardId, User: userId, Role: role}). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: role}, nil) - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Create(context.Background(), BoardSessionCreateRequest{Board: boardId, User: userId, Role: role}) + session, err := sessionService.Create(context.Background(), BoardSessionCreateRequest{Board: boardId, User: userId, Role: role}) - assert.Nil(t, err) - assert.NotNil(t, session) + assert.Nil(t, err) + assert.NotNil(t, session) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) - assert.Equal(t, role, session.Role) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, role, session.Role) } func TestCreateSession_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - role := common.ParticipantRole - dbError := "unable to create" + boardId := uuid.New() + userId := uuid.New() + role := common.ParticipantRole + dbError := "unable to create" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Create(mock.Anything, DatabaseBoardSessionInsert{Board: boardId, User: userId, Role: role}). - Return(DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Create(mock.Anything, DatabaseBoardSessionInsert{Board: boardId, User: userId, Role: role}). + Return(DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Create(context.Background(), BoardSessionCreateRequest{Board: boardId, User: userId, Role: role}) + session, err := sessionService.Create(context.Background(), BoardSessionCreateRequest{Board: boardId, User: userId, Role: role}) - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestUpdateSession_Role(t *testing.T) { - boardId := uuid.New() - moderatorId := uuid.New() - userId := uuid.New() - firstColumnId := uuid.New() - secondColumnId := uuid.New() - moderatorRole := common.ModeratorRole - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). - Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ModeratorRole}, nil) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Role: &moderatorRole}). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ModeratorRole}, nil) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{{Board: boardId, User: userId}}, nil) - - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockColumnService.EXPECT().GetAll(mock.Anything, boardId). - Return([]*columns.Column{ - {ID: firstColumnId}, - {ID: secondColumnId}, - }, nil) - - mockNoteService := notes.NewMockNotesService(t) - mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). - Return([]*notes.Note{ - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, - }, nil) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: moderatorId, - User: userId, - Role: &moderatorRole, - }) - - assert.Nil(t, err) - assert.NotNil(t, session) - - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) - assert.Equal(t, common.ModeratorRole, session.Role) + boardId := uuid.New() + moderatorId := uuid.New() + userId := uuid.New() + firstColumnId := uuid.New() + secondColumnId := uuid.New() + moderatorRole := common.ModeratorRole + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). + Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ModeratorRole}, nil) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Role: &moderatorRole}). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ModeratorRole}, nil) + mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]DatabaseBoardSession{{Board: boardId, User: userId}}, nil) + + mockBroker := realtime.NewMockClient(t) + mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockColumnService.EXPECT().GetAll(mock.Anything, boardId). + Return([]*columns.Column{ + {ID: firstColumnId}, + {ID: secondColumnId}, + }, nil) + + mockNoteService := notes.NewMockNotesService(t) + mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). + Return([]*notes.Note{ + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, + }, nil) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: moderatorId, + User: userId, + Role: &moderatorRole, + }) + + assert.Nil(t, err) + assert.NotNil(t, session) + + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ModeratorRole, session.Role) } func TestUpdateSession_RaiseHand(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - firstColumnId := uuid.New() - secondColumnId := uuid.New() - raisedHand := true - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, RaisedHand: &raisedHand}). - Return(DatabaseBoardSession{Board: boardId, User: userId, RaisedHand: raisedHand}, nil) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{{Board: boardId, User: userId}}, nil) - - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockColumnService.EXPECT().GetAll(mock.Anything, boardId). - Return([]*columns.Column{ - {ID: firstColumnId}, - {ID: secondColumnId}, - }, nil) - - mockNoteService := notes.NewMockNotesService(t) - mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). - Return([]*notes.Note{ - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, - }, nil) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: userId, - User: userId, - RaisedHand: &raisedHand, - }) - - assert.Nil(t, err) - assert.NotNil(t, session) - - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) - assert.Equal(t, raisedHand, session.RaisedHand) + boardId := uuid.New() + userId := uuid.New() + firstColumnId := uuid.New() + secondColumnId := uuid.New() + raisedHand := true + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, RaisedHand: &raisedHand}). + Return(DatabaseBoardSession{Board: boardId, User: userId, RaisedHand: raisedHand}, nil) + mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]DatabaseBoardSession{{Board: boardId, User: userId}}, nil) + + mockBroker := realtime.NewMockClient(t) + mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockColumnService.EXPECT().GetAll(mock.Anything, boardId). + Return([]*columns.Column{ + {ID: firstColumnId}, + {ID: secondColumnId}, + }, nil) + + mockNoteService := notes.NewMockNotesService(t) + mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). + Return([]*notes.Note{ + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, + }, nil) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: userId, + User: userId, + RaisedHand: &raisedHand, + }) + + assert.Nil(t, err) + assert.NotNil(t, session) + + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, raisedHand, session.RaisedHand) } func TestUpdateSession_DatbaseErrorGetModerator(t *testing.T) { - boardId := uuid.New() - moderatorId := uuid.New() - userId := uuid.New() - moderatorRole := common.ModeratorRole - dbError := "unable to execute" - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId).Return(DatabaseBoardSession{}, errors.New(dbError)) - - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: moderatorId, - User: userId, - Role: &moderatorRole, - }) - - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, fmt.Errorf("unable to get session for board: %w", errors.New(dbError)), err) + boardId := uuid.New() + moderatorId := uuid.New() + userId := uuid.New() + moderatorRole := common.ModeratorRole + dbError := "unable to execute" + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId).Return(DatabaseBoardSession{}, errors.New(dbError)) + + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: moderatorId, + User: userId, + Role: &moderatorRole, + }) + + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, fmt.Errorf("unable to get session for board: %w", errors.New(dbError)), err) } func TestUpdateSession_DatbaseErrorGetUserToPromote(t *testing.T) { - boardId := uuid.New() - moderatorId := uuid.New() - userId := uuid.New() - moderatorRole := common.ModeratorRole - dbError := "unable to execute" - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). - Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ModeratorRole}, nil) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{}, errors.New(dbError)) - - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: moderatorId, - User: userId, - Role: &moderatorRole, - }) - - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, fmt.Errorf("unable to get session for board: %w", errors.New(dbError)), err) + boardId := uuid.New() + moderatorId := uuid.New() + userId := uuid.New() + moderatorRole := common.ModeratorRole + dbError := "unable to execute" + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). + Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ModeratorRole}, nil) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{}, errors.New(dbError)) + + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: moderatorId, + User: userId, + Role: &moderatorRole, + }) + + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, fmt.Errorf("unable to get session for board: %w", errors.New(dbError)), err) } func TestUpdateSession_DatabaseError(t *testing.T) { - boardId := uuid.New() - moderatorId := uuid.New() - userId := uuid.New() - moderatorRole := common.ModeratorRole - dbError := "unable to execute" - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). - Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ModeratorRole}, nil) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Role: &moderatorRole}). - Return(DatabaseBoardSession{}, errors.New(dbError)) - - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: moderatorId, - User: userId, - Role: &moderatorRole, - }) - - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + boardId := uuid.New() + moderatorId := uuid.New() + userId := uuid.New() + moderatorRole := common.ModeratorRole + dbError := "unable to execute" + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). + Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ModeratorRole}, nil) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Role: &moderatorRole}). + Return(DatabaseBoardSession{}, errors.New(dbError)) + + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: moderatorId, + User: userId, + Role: &moderatorRole, + }) + + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestUpdateSession_ErrorPromotingUserPermission(t *testing.T) { - boardId := uuid.New() - moderatorId := uuid.New() - userId := uuid.New() - moderatorRole := common.ModeratorRole - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). - Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ParticipantRole}, nil) - - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: moderatorId, - User: userId, - Role: &moderatorRole, - }) - - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, common.ForbiddenError(errors.New("not allowed to change other users session")), err) + boardId := uuid.New() + moderatorId := uuid.New() + userId := uuid.New() + moderatorRole := common.ModeratorRole + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, moderatorId). + Return(DatabaseBoardSession{Board: boardId, User: moderatorId, Role: common.ParticipantRole}, nil) + + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: moderatorId, + User: userId, + Role: &moderatorRole, + }) + + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, common.ForbiddenError(errors.New("not allowed to change other users session")), err) } func TestUpdateSession_ErrorPromoting(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - moderatorRole := common.ModeratorRole + boardId := uuid.New() + userId := uuid.New() + moderatorRole := common.ModeratorRole - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ParticipantRole}, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: userId, - User: userId, - Role: &moderatorRole, - }) + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: userId, + User: userId, + Role: &moderatorRole, + }) - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, common.ForbiddenError(errors.New("cannot promote role")), err) + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, common.ForbiddenError(errors.New("cannot promote role")), err) } func TestUpdateSession_ErrorChangingOwner(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - moderatorRole := common.ModeratorRole + boardId := uuid.New() + userId := uuid.New() + moderatorRole := common.ModeratorRole - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.OwnerRole}, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.OwnerRole}, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: userId, - User: userId, - Role: &moderatorRole, - }) + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: userId, + User: userId, + Role: &moderatorRole, + }) - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, common.ForbiddenError(errors.New("not allowed to change owner role")), err) + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, common.ForbiddenError(errors.New("not allowed to change owner role")), err) } func TestUpdateSession_ErrorPromotingToOwner(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - ownerRole := common.OwnerRole + boardId := uuid.New() + userId := uuid.New() + ownerRole := common.OwnerRole - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ModeratorRole}, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId, Role: common.ModeratorRole}, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ - Board: boardId, - Caller: userId, - User: userId, - Role: &ownerRole, - }) + session, err := sessionService.Update(context.Background(), BoardSessionUpdateRequest{ + Board: boardId, + Caller: userId, + User: userId, + Role: &ownerRole, + }) - assert.Nil(t, session) - assert.NotNil(t, err) - assert.Equal(t, common.ForbiddenError(errors.New("not allowed to promote to owner role")), err) + assert.Nil(t, session) + assert.NotNil(t, err) + assert.Equal(t, common.ForbiddenError(errors.New("not allowed to promote to owner role")), err) } func TestUpdateAllSessions(t *testing.T) { - boardId := uuid.New() - firstUserId := uuid.New() - secondUserId := uuid.New() - ready := true + boardId := uuid.New() + firstUserId := uuid.New() + secondUserId := uuid.New() + ready := true - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().UpdateAll(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, Ready: &ready}). - Return([]DatabaseBoardSession{ - {Board: boardId, User: firstUserId, Ready: ready}, - {Board: boardId, User: secondUserId, Ready: ready}, - }, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().UpdateAll(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, Ready: &ready}). + Return([]DatabaseBoardSession{ + {Board: boardId, User: firstUserId, Ready: ready}, + {Board: boardId, User: secondUserId, Ready: ready}, + }, nil) - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.UpdateAll(context.Background(), BoardSessionsUpdateRequest{Board: boardId, Ready: &ready}) + boardSessions, err := sessionService.UpdateAll(context.Background(), BoardSessionsUpdateRequest{Board: boardId, Ready: &ready}) - assert.Nil(t, err) - assert.NotNil(t, boardSessions) - assert.Len(t, boardSessions, 2) + assert.Nil(t, err) + assert.NotNil(t, boardSessions) + assert.Len(t, boardSessions, 2) - assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, firstUserId, boardSessions[0].UserID) - assert.Equal(t, ready, boardSessions[0].Ready) + assert.Equal(t, boardId, boardSessions[0].Board) + assert.Equal(t, firstUserId, boardSessions[0].UserID) + assert.Equal(t, ready, boardSessions[0].Ready) - assert.Equal(t, boardId, boardSessions[1].Board) - assert.Equal(t, secondUserId, boardSessions[1].UserID) - assert.Equal(t, ready, boardSessions[1].Ready) + assert.Equal(t, boardId, boardSessions[1].Board) + assert.Equal(t, secondUserId, boardSessions[1].UserID) + assert.Equal(t, ready, boardSessions[1].Ready) } func TestUpdateAllSessions_DatabaseError(t *testing.T) { - boardId := uuid.New() - ready := true - dbError := "unable to execute" + boardId := uuid.New() + ready := true + dbError := "unable to execute" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().UpdateAll(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, Ready: &ready}). - Return([]DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().UpdateAll(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, Ready: &ready}). + Return([]DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - boardSessions, err := sessionService.UpdateAll(context.Background(), BoardSessionsUpdateRequest{Board: boardId, Ready: &ready}) + boardSessions, err := sessionService.UpdateAll(context.Background(), BoardSessionsUpdateRequest{Board: boardId, Ready: &ready}) - assert.Nil(t, boardSessions) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) -} - -func TestUpdateUserBoard(t *testing.T) { - userId := uuid.New() - firstBoard := uuid.New() - secondBoard := uuid.New() - firstColumnId := uuid.New() - secondColumnId := uuid.New() - thirdColumnId := uuid.New() - fourthColumnId := uuid.New() - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{ - {Board: firstBoard, User: userId}, - {Board: secondBoard, User: userId}, - }, nil) - mockSessiondb.EXPECT().Get(mock.Anything, firstBoard, userId). - Return(DatabaseBoardSession{Board: firstBoard, User: userId}, nil) - mockSessiondb.EXPECT().Get(mock.Anything, secondBoard, userId). - Return(DatabaseBoardSession{Board: secondBoard, User: userId}, nil) - - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockColumnService.EXPECT().GetAll(mock.Anything, firstBoard). - Return([]*columns.Column{ - {ID: firstColumnId, Name: "First Column"}, - {ID: secondColumnId, Name: "Second column"}, - }, nil) - mockColumnService.EXPECT().GetAll(mock.Anything, secondBoard). - Return([]*columns.Column{ - {ID: thirdColumnId, Name: "First Column"}, - {ID: fourthColumnId, Name: "Second column"}, - }, nil) - - mockNoteService := notes.NewMockNotesService(t) - mockNoteService.EXPECT().GetAll(mock.Anything, firstBoard, []uuid.UUID{firstColumnId, secondColumnId}). - Return([]*notes.Note{ - {ID: uuid.New(), Text: "This is a note"}, - {ID: uuid.New(), Text: "This is another note"}, - }, nil) - mockNoteService.EXPECT().GetAll(mock.Anything, secondBoard, []uuid.UUID{thirdColumnId, fourthColumnId}). - Return([]*notes.Note{ - {ID: uuid.New(), Text: "Also a note"}, - {ID: uuid.New(), Text: "You know it"}, - }, nil) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - sessions, err := sessionService.UpdateUserBoards(context.Background(), BoardSessionUpdateRequest{User: userId}) - - assert.Nil(t, err) - assert.Len(t, sessions, 2) -} - -func TestUpdateUserBoard_DatabaseError(t *testing.T) { - userId := uuid.New() - dbError := "database error" - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{}, errors.New(dbError)) - - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - sessions, err := sessionService.UpdateUserBoards(context.Background(), BoardSessionUpdateRequest{User: userId}) - - assert.Nil(t, sessions) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.Nil(t, boardSessions) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestConnectSession(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - firstColumnId := uuid.New() - secondColumnId := uuid.New() - connected := true - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). - Return(DatabaseBoardSession{Board: boardId, User: userId, Connected: connected}, nil) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{{User: userId, Board: boardId}}, nil) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId}, nil) - - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockColumnService.EXPECT().GetAll(mock.Anything, boardId). - Return([]*columns.Column{ - {ID: firstColumnId}, - {ID: secondColumnId}, - }, nil) - - mockNoteService := notes.NewMockNotesService(t) - mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). - Return([]*notes.Note{ - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, - }, nil) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - err := sessionService.Connect(context.Background(), boardId, userId) - - assert.Nil(t, err) + boardId := uuid.New() + userId := uuid.New() + firstColumnId := uuid.New() + secondColumnId := uuid.New() + connected := true + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). + Return(DatabaseBoardSession{Board: boardId, User: userId, Connected: connected}, nil) + mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]DatabaseBoardSession{{User: userId, Board: boardId}}, nil) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId}, nil) + + mockBroker := realtime.NewMockClient(t) + mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockColumnService.EXPECT().GetAll(mock.Anything, boardId). + Return([]*columns.Column{ + {ID: firstColumnId}, + {ID: secondColumnId}, + }, nil) + + mockNoteService := notes.NewMockNotesService(t) + mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). + Return([]*notes.Note{ + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, + }, nil) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + err := sessionService.Connect(context.Background(), boardId, userId) + + assert.Nil(t, err) } func TestConnectSession_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "unable to execute" - connected := true + boardId := uuid.New() + userId := uuid.New() + dbError := "unable to execute" + connected := true - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). - Return(DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). + Return(DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - err := sessionService.Connect(context.Background(), boardId, userId) + err := sessionService.Connect(context.Background(), boardId, userId) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestDisconnectSession(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - firstColumnId := uuid.New() - secondColumnId := uuid.New() - connected := false - - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). - Return(DatabaseBoardSession{Board: boardId, User: userId, Connected: connected}, nil) - mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]DatabaseBoardSession{{User: userId, Board: boardId}}, nil) - mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). - Return(DatabaseBoardSession{Board: boardId, User: userId}, nil) - - mockBroker := realtime.NewMockClient(t) - mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) - broker := new(realtime.Broker) - broker.Con = mockBroker - - mockColumnService := columns.NewMockColumnService(t) - mockColumnService.EXPECT().GetAll(mock.Anything, boardId). - Return([]*columns.Column{ - {ID: firstColumnId}, - {ID: secondColumnId}, - }, nil) - - mockNoteService := notes.NewMockNotesService(t) - mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). - Return([]*notes.Note{ - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, - {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, - }, nil) - - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - - err := sessionService.Disconnect(context.Background(), boardId, userId) - - assert.Nil(t, err) + boardId := uuid.New() + userId := uuid.New() + firstColumnId := uuid.New() + secondColumnId := uuid.New() + connected := false + + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). + Return(DatabaseBoardSession{Board: boardId, User: userId, Connected: connected}, nil) + mockSessiondb.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]DatabaseBoardSession{{User: userId, Board: boardId}}, nil) + mockSessiondb.EXPECT().Get(mock.Anything, boardId, userId). + Return(DatabaseBoardSession{Board: boardId, User: userId}, nil) + + mockBroker := realtime.NewMockClient(t) + mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) + broker := new(realtime.Broker) + broker.Con = mockBroker + + mockColumnService := columns.NewMockColumnService(t) + mockColumnService.EXPECT().GetAll(mock.Anything, boardId). + Return([]*columns.Column{ + {ID: firstColumnId}, + {ID: secondColumnId}, + }, nil) + + mockNoteService := notes.NewMockNotesService(t) + mockNoteService.EXPECT().GetAll(mock.Anything, boardId, []uuid.UUID{firstColumnId, secondColumnId}). + Return([]*notes.Note{ + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: firstColumnId, Rank: 2}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 1}}, + {ID: uuid.New(), Position: notes.NotePosition{Column: secondColumnId, Rank: 2}}, + }, nil) + + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + + err := sessionService.Disconnect(context.Background(), boardId, userId) + + assert.Nil(t, err) } func TestDisconnectSession_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "unable to execute" - connected := false + boardId := uuid.New() + userId := uuid.New() + dbError := "unable to execute" + connected := false - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). - Return(DatabaseBoardSession{}, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Update(mock.Anything, DatabaseBoardSessionUpdate{Board: boardId, User: userId, Connected: &connected}). + Return(DatabaseBoardSession{}, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - err := sessionService.Disconnect(context.Background(), boardId, userId) + err := sessionService.Disconnect(context.Background(), boardId, userId) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestSessionExists(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Exists(mock.Anything, boardId, userId).Return(true, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Exists(mock.Anything, boardId, userId).Return(true, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - exists, err := sessionService.Exists(context.Background(), boardId, userId) + exists, err := sessionService.Exists(context.Background(), boardId, userId) - assert.Nil(t, err) - assert.True(t, exists) + assert.Nil(t, err) + assert.True(t, exists) } func TestSessionExists_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "unable to execute" + boardId := uuid.New() + userId := uuid.New() + dbError := "unable to execute" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().Exists(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().Exists(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - exists, err := sessionService.Exists(context.Background(), boardId, userId) + exists, err := sessionService.Exists(context.Background(), boardId, userId) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) - assert.False(t, exists) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) + assert.False(t, exists) } func TestModeratorSessionExists(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().ModeratorExists(mock.Anything, boardId, userId).Return(true, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().ModeratorExists(mock.Anything, boardId, userId).Return(true, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - exists, err := sessionService.ModeratorSessionExists(context.Background(), boardId, userId) + exists, err := sessionService.ModeratorSessionExists(context.Background(), boardId, userId) - assert.Nil(t, err) - assert.True(t, exists) + assert.Nil(t, err) + assert.True(t, exists) } func TestModeratorSessionExists_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "unable to execute" + boardId := uuid.New() + userId := uuid.New() + dbError := "unable to execute" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().ModeratorExists(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().ModeratorExists(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - exists, err := sessionService.ModeratorSessionExists(context.Background(), boardId, userId) + exists, err := sessionService.ModeratorSessionExists(context.Background(), boardId, userId) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) - assert.False(t, exists) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) + assert.False(t, exists) } func TestIsParticipantBanned(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().IsParticipantBanned(mock.Anything, boardId, userId).Return(true, nil) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().IsParticipantBanned(mock.Anything, boardId, userId).Return(true, nil) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - banned, err := sessionService.IsParticipantBanned(context.Background(), boardId, userId) + banned, err := sessionService.IsParticipantBanned(context.Background(), boardId, userId) - assert.Nil(t, err) - assert.True(t, banned) + assert.Nil(t, err) + assert.True(t, banned) } func TestIsParticipantBanned_DatabaseError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "unable to execute" + boardId := uuid.New() + userId := uuid.New() + dbError := "unable to execute" - mockSessiondb := NewMockSessionDatabase(t) - mockSessiondb.EXPECT().IsParticipantBanned(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) + mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb.EXPECT().IsParticipantBanned(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - banned, err := sessionService.IsParticipantBanned(context.Background(), boardId, userId) + banned, err := sessionService.IsParticipantBanned(context.Background(), boardId, userId) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) - assert.False(t, banned) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) + assert.False(t, banned) } func TestFilterfromQueryString_EmptyQuery(t *testing.T) { - mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb := NewMockSessionDatabase(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - query := url.Values{} - filter := sessionService.BoardSessionFilterTypeFromQueryString(query) + query := url.Values{} + filter := sessionService.BoardSessionFilterTypeFromQueryString(query) - assert.Equal(t, BoardSessionFilter{}, filter) + assert.Equal(t, BoardSessionFilter{}, filter) } func TestFilterfromQueryString_Connected(t *testing.T) { - mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb := NewMockSessionDatabase(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - connected := true - query := url.Values{} - query.Add("connected", "true") - filter := sessionService.BoardSessionFilterTypeFromQueryString(query) + connected := true + query := url.Values{} + query.Add("connected", "true") + filter := sessionService.BoardSessionFilterTypeFromQueryString(query) - assert.Equal(t, BoardSessionFilter{Connected: &connected}, filter) + assert.Equal(t, BoardSessionFilter{Connected: &connected}, filter) } func TestFilterfromQueryString_Ready(t *testing.T) { - mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb := NewMockSessionDatabase(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - ready := true - query := url.Values{} - query.Add("ready", "true") - filter := sessionService.BoardSessionFilterTypeFromQueryString(query) + ready := true + query := url.Values{} + query.Add("ready", "true") + filter := sessionService.BoardSessionFilterTypeFromQueryString(query) - assert.Equal(t, BoardSessionFilter{Ready: &ready}, filter) + assert.Equal(t, BoardSessionFilter{Ready: &ready}, filter) } func TestFilterfromQueryString_Raisedhand(t *testing.T) { - mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb := NewMockSessionDatabase(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - raisedHand := true - query := url.Values{} - query.Add("raisedHand", "true") - filter := sessionService.BoardSessionFilterTypeFromQueryString(query) + raisedHand := true + query := url.Values{} + query.Add("raisedHand", "true") + filter := sessionService.BoardSessionFilterTypeFromQueryString(query) - assert.Equal(t, BoardSessionFilter{RaisedHand: &raisedHand}, filter) + assert.Equal(t, BoardSessionFilter{RaisedHand: &raisedHand}, filter) } func TestFilterfromQueryString_Role(t *testing.T) { - mockSessiondb := NewMockSessionDatabase(t) + mockSessiondb := NewMockSessionDatabase(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockColumnService := columns.NewMockColumnService(t) - mockNoteService := notes.NewMockNotesService(t) + mockColumnService := columns.NewMockColumnService(t) + mockNoteService := notes.NewMockNotesService(t) - sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) + sessionService := NewSessionService(mockSessiondb, broker, mockColumnService, mockNoteService) - role := common.OwnerRole - query := url.Values{} - query.Add("role", "OWNER") - filter := sessionService.BoardSessionFilterTypeFromQueryString(query) + role := common.OwnerRole + query := url.Values{} + query.Add("role", "OWNER") + filter := sessionService.BoardSessionFilterTypeFromQueryString(query) - assert.Equal(t, BoardSessionFilter{Role: &role}, filter) + assert.Equal(t, BoardSessionFilter{Role: &role}, filter) } func TestCheckSessionRole(t *testing.T) { - userId := uuid.New() - sessions := []*BoardSession{ - {UserID: uuid.New(), Role: common.ParticipantRole}, - {UserID: userId, Role: common.ModeratorRole}, - {UserID: uuid.New(), Role: common.OwnerRole}, - } - roles := []common.SessionRole{ - common.ModeratorRole, - common.OwnerRole, - } - - check := CheckSessionRole(userId, sessions, roles) - - assert.True(t, check) + userId := uuid.New() + sessions := []*BoardSession{ + {UserID: uuid.New(), Role: common.ParticipantRole}, + {UserID: userId, Role: common.ModeratorRole}, + {UserID: uuid.New(), Role: common.OwnerRole}, + } + roles := []common.SessionRole{ + common.ModeratorRole, + common.OwnerRole, + } + + check := CheckSessionRole(userId, sessions, roles) + + assert.True(t, check) } func TestCheckSessionRole_NoRole(t *testing.T) { - userId := uuid.New() - sessions := []*BoardSession{ - {UserID: uuid.New(), Role: common.ParticipantRole}, - {UserID: userId, Role: common.ModeratorRole}, - {UserID: uuid.New(), Role: common.OwnerRole}, - } - roles := []common.SessionRole{ - common.ParticipantRole, - common.OwnerRole, - } - - check := CheckSessionRole(userId, sessions, roles) - - assert.False(t, check) + userId := uuid.New() + sessions := []*BoardSession{ + {UserID: uuid.New(), Role: common.ParticipantRole}, + {UserID: userId, Role: common.ModeratorRole}, + {UserID: uuid.New(), Role: common.OwnerRole}, + } + roles := []common.SessionRole{ + common.ParticipantRole, + common.OwnerRole, + } + + check := CheckSessionRole(userId, sessions, roles) + + assert.False(t, check) } func TestCheckSessionRole_NoUser(t *testing.T) { - userId := uuid.New() - sessions := []*BoardSession{ - {UserID: uuid.New(), Role: common.ParticipantRole}, - {UserID: uuid.New(), Role: common.ModeratorRole}, - {UserID: uuid.New(), Role: common.OwnerRole}, - } - roles := []common.SessionRole{ - common.ParticipantRole, - common.OwnerRole, - } - - check := CheckSessionRole(userId, sessions, roles) - - assert.False(t, check) + userId := uuid.New() + sessions := []*BoardSession{ + {UserID: uuid.New(), Role: common.ParticipantRole}, + {UserID: uuid.New(), Role: common.ModeratorRole}, + {UserID: uuid.New(), Role: common.OwnerRole}, + } + roles := []common.SessionRole{ + common.ParticipantRole, + common.OwnerRole, + } + + check := CheckSessionRole(userId, sessions, roles) + + assert.False(t, check) } diff --git a/server/src/api/request_builder.go b/server/src/technical_helper/test_request_builder.go similarity index 55% rename from server/src/api/request_builder.go rename to server/src/technical_helper/test_request_builder.go index 9772fbb5cd..0b54005061 100644 --- a/server/src/api/request_builder.go +++ b/server/src/technical_helper/test_request_builder.go @@ -1,4 +1,4 @@ -package api +package technical_helper import ( "context" @@ -8,23 +8,23 @@ import ( ) type TestRequestBuilder struct { - req *http.Request + Req *http.Request } func NewTestRequestBuilder(method string, target string, body io.Reader) *TestRequestBuilder { r := new(TestRequestBuilder) - r.req = httptest.NewRequest(method, target, body) - r.req.Header.Set("Accept", "application/json") - r.req.Header.Set("Content-Type", "application/json") + r.Req = httptest.NewRequest(method, target, body) + r.Req.Header.Set("Accept", "application/json") + r.Req.Header.Set("Content-Type", "application/json") return r } func (b *TestRequestBuilder) AddToContext(key, val interface{}) *TestRequestBuilder { - ctx := context.WithValue(b.req.Context(), key, val) - b.req = b.req.WithContext(ctx) + ctx := context.WithValue(b.Req.Context(), key, val) + b.Req = b.Req.WithContext(ctx) return b } func (b *TestRequestBuilder) Request() *http.Request { - return b.req.Clone(b.req.Context()) + return b.Req.Clone(b.Req.Context()) } diff --git a/server/src/users/api.go b/server/src/users/api.go index 64b6dd88fa..13fc864212 100644 --- a/server/src/users/api.go +++ b/server/src/users/api.go @@ -1,23 +1,278 @@ package users import ( - "context" + "context" + "errors" + "net/http" - "github.com/google/uuid" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "scrumlr.io/server/common" + "scrumlr.io/server/identifiers" + "scrumlr.io/server/logger" + "scrumlr.io/server/sessions" ) type UserService interface { - CreateAnonymous(ctx context.Context, name string) (*User, error) - CreateAppleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) - CreateAzureAdUser(ctx context.Context, id, name, avatarUrl string) (*User, error) - CreateGitHubUser(ctx context.Context, id, name, avatarUrl string) (*User, error) - CreateGoogleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) - CreateMicrosoftUser(ctx context.Context, id, name, avatarUrl string) (*User, error) - CreateOIDCUser(ctx context.Context, id, name, avatarUrl string) (*User, error) - Update(ctx context.Context, body UserUpdateRequest) (*User, error) - Get(ctx context.Context, id uuid.UUID) (*User, error) - GetBoardUsers(ctx context.Context, boardID uuid.UUID) ([]*User, error) - - IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) - SetKeyMigration(ctx context.Context, id uuid.UUID) (*User, error) + CreateAnonymous(ctx context.Context, name string) (*User, error) + CreateAppleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) + CreateAzureAdUser(ctx context.Context, id, name, avatarUrl string) (*User, error) + CreateGitHubUser(ctx context.Context, id, name, avatarUrl string) (*User, error) + CreateGoogleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) + CreateMicrosoftUser(ctx context.Context, id, name, avatarUrl string) (*User, error) + CreateOIDCUser(ctx context.Context, id, name, avatarUrl string) (*User, error) + Update(ctx context.Context, body UserUpdateRequest) (*User, error) + Get(ctx context.Context, id uuid.UUID) (*User, error) + GetBoardUsers(ctx context.Context, boardID uuid.UUID) ([]*User, error) + + IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) + SetKeyMigration(ctx context.Context, id uuid.UUID) (*User, error) +} +type API struct { + service UserService + sessions sessions.SessionService + allowAnonymousBoardCreation bool + allowAnonymousCustomTemplates bool +} + +func (api *API) GetUser(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.get") + defer span.End() + + userId := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + user, err := api.service.Get(ctx, userId) + if err != nil { + span.SetStatus(codes.Error, "failed to get user") + span.RecordError(err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, user) +} + +func (api *API) GetUserByID(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.get") + defer span.End() + log := logger.FromContext(ctx) + + userParam := chi.URLParam(r, "user") + requestedUserId, err := uuid.Parse(userParam) + if err != nil { + span.SetStatus(codes.Error, "unable to parse uuid") + span.RecordError(err) + log.Errorw("unable to parse uuid", "err", err) + common.Throw(w, r, err) + return + } + user, err := api.service.Get(ctx, requestedUserId) + if err != nil { + span.SetStatus(codes.Error, "failed to get user by id") + span.RecordError(err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, user) +} + +func (api *API) GetUsersFromBoard(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.getAll") + defer span.End() + + boardID := ctx.Value(identifiers.BoardIdentifier).(uuid.UUID) + + users, err := api.service.GetBoardUsers(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get users") + span.RecordError(err) + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, users) +} + +func (api *API) Update(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.update") + defer span.End() + log := logger.FromContext(ctx) + + user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + + var body UserUpdateRequest + if err := render.Decode(r, &body); err != nil { + span.SetStatus(codes.Error, "unable to decode body") + span.RecordError(err) + log.Errorw("unable to decode body", "err", err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + + body.ID = user + + updatedUser, err := api.service.Update(ctx, body) + if err != nil { + span.SetStatus(codes.Error, "failed to update user") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, updatedUser) +} + +func (api *API) Delete(w http.ResponseWriter, r *http.Request) { + //TODO implement me + panic("implement me") +} + +func (api *API) BoardAuthenticatedContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.user.api.context.authenticated") + defer span.End() + log := logger.FromContext(ctx) + + boardParam := chi.URLParam(r, "id") + board, err := uuid.Parse(boardParam) + if err != nil { + span.SetStatus(codes.Error, "unable to parse uuid") + span.RecordError(err) + common.Throw(w, r, common.BadRequestError(errors.New("invalid board id"))) + return + } + + userIDValue := ctx.Value(identifiers.UserIdentifier) + userID, ok := userIDValue.(uuid.UUID) + span.SetAttributes( + attribute.String("scrumlr.user.api.context.authenticated.board", board.String()), + attribute.String("scrumlr.user.api.context.authenticated.user", userID.String()), + ) + if !ok { + span.SetStatus(codes.Error, "unable to authenticate user") + err = errors.New("invalid user id") + span.RecordError(err) + log.Errorw("Invalid user id", "error", err) + common.Throw(w, r, common.BadRequestError(err)) + return + } + + user, err := api.service.Get(ctx, userID) + + if err != nil { + span.SetStatus(codes.Error, "could not fetch user") + span.RecordError(err) + log.Errorw("Could not fetch user", "error", err) + common.Throw(w, r, errors.New("could not fetch user")) + return + } + + if user.AccountType == common.Anonymous { + span.SetStatus(codes.Error, "not authorized to perform this action") + err = errors.New("not authorized") + span.RecordError(err) + log.Errorw("Not authorized to perform this action", "accountType", user.AccountType) + common.Throw(w, r, common.ForbiddenError(err)) + return + } + + boardContext := context.WithValue(ctx, identifiers.BoardIdentifier, board) + next.ServeHTTP(w, r.WithContext(boardContext)) + }) +} + +func (api *API) AnonymousBoardCreationContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.user.api.context.anonymous_board_creation") + defer span.End() + log := logger.FromContext(ctx) + + userIDValue := ctx.Value(identifiers.UserIdentifier) + userID, ok := userIDValue.(uuid.UUID) + span.SetAttributes( + attribute.String("scrumlr.user.api.context.authenticated.user", userID.String()), + ) + if !ok { + span.SetStatus(codes.Error, "invalid or missing user identifier in context") + span.RecordError(errors.New("invalid or missing user identifier in context")) + log.Errorw("invalid or missing user identifier in context") + common.Throw(w, r, common.InternalServerError) + return + } + + user, err := api.service.Get(ctx, userID) + if err != nil { + span.SetStatus(codes.Error, "could not fetch user") + span.RecordError(err) + log.Errorw("Could not fetch user", "error", err) + common.Throw(w, r, common.InternalServerError) + return + } + + if user.AccountType == common.Anonymous && !api.allowAnonymousBoardCreation { + span.SetStatus(codes.Error, "not authorized to create boards anonymously") + err := errors.New("not authorized to create boards anonymously") + span.RecordError(err) + log.Errorw("anonymous board creation not allowed") + common.Throw(w, r, common.ForbiddenError(err)) + return + } + + next.ServeHTTP(w, r) + }) +} + +func (api *API) AnonymousCustomTemplateCreationContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "scrumlr.user.api.context.anonymous_template_creation") + defer span.End() + log := logger.FromContext(ctx) + + userIDValue := ctx.Value(identifiers.UserIdentifier) + userID, ok := userIDValue.(uuid.UUID) + if !ok { + span.SetStatus(codes.Error, "invalid or missing user identifier in context") + span.RecordError(errors.New("invalid or missing user identifier in context")) + log.Errorw("invalid or missing user identifier in context") + common.Throw(w, r, common.InternalServerError) + return + } + + user, err := api.service.Get(ctx, userID) + if err != nil { + span.SetStatus(codes.Error, "could not fetch user") + span.RecordError(err) + log.Errorw("Could not fetch user", "error", err) + common.Throw(w, r, common.InternalServerError) + return + } + + if user.AccountType == common.Anonymous && !api.allowAnonymousCustomTemplates { + span.SetStatus(codes.Error, "not authorized to create custom templates") + err := errors.New("not authorized to create custom templates anonymously") + span.RecordError(err) + log.Errorw("anonymous custom template creation not allowed") + common.Throw(w, r, common.ForbiddenError(err)) + return + } + + next.ServeHTTP(w, r) + }) +} + +func NewUserApi(service UserService, sessionService sessions.SessionService, allowAnonymousBoardCreation, allowAnonymousCustomTemplates bool) UsersApi { + api := new(API) + api.service = service + api.sessions = sessionService + api.allowAnonymousBoardCreation = allowAnonymousBoardCreation + api.allowAnonymousCustomTemplates = allowAnonymousCustomTemplates + return api } diff --git a/server/src/users/api_test.go b/server/src/users/api_test.go new file mode 100644 index 0000000000..a8dff01850 --- /dev/null +++ b/server/src/users/api_test.go @@ -0,0 +1,428 @@ +package users + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "scrumlr.io/server/common" + "scrumlr.io/server/identifiers" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessions" + "scrumlr.io/server/technical_helper" +) + +func TestGetUser_api(t *testing.T) { + userId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + mockUserService.EXPECT().Get(mock.Anything, userId).Return(&User{ID: userId, AccountType: common.Anonymous}, nil) + + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + userApi.GetUser(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + assert.NotNil(t, rr.Body) + var user User + err := json.Unmarshal(rr.Body.Bytes(), &user) + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) +} + +func TestGetUser_api_InvalidUUID(t *testing.T) { + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + mockUserService.EXPECT().Get(mock.Anything, uuid.Nil).Return(nil, errors.New("uuid required")) + + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, uuid.Nil) + + userApi.GetUser(rr, req.Request()) + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_GetUserByID(t *testing.T) { + user := User{ + ID: uuid.New(), + Name: "Joseph", + Avatar: nil, + AccountType: common.Anonymous, + } + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, user.ID) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("user", user.ID.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + mockUserService.EXPECT().Get(mock.Anything, user.ID).Return(&user, nil) + + userApi.GetUserByID(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var response User + err := json.Unmarshal(rr.Body.Bytes(), &response) + assert.Nil(t, err) + assert.Equal(t, user, response) + +} + +func Test_GetUserByID_api_InvalidUUID(t *testing.T) { + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, uuid.Nil) + + userApi.GetUserByID(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_GetUserByID_ServiceError(t *testing.T) { + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userId := uuid.New() + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("user", userId.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + mockUserService.EXPECT().Get(mock.Anything, userId).Return(nil, errors.New("service error")) + userApi.GetUserByID(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_GetBoardUsers_api(t *testing.T) { + boardID := uuid.New() + mockUsers := []*User{ + {ID: uuid.New(), Name: "User A", AccountType: common.Anonymous}, + {ID: uuid.New(), Name: "User B", AccountType: common.Anonymous}, + } + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + mockUserService.EXPECT().GetBoardUsers(mock.Anything, boardID).Return(mockUsers, nil) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/board/{id}", nil).AddToContext(identifiers.BoardIdentifier, boardID) + + userApi.GetUsersFromBoard(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var users []*User + err := json.Unmarshal(rr.Body.Bytes(), &users) + assert.Nil(t, err) + assert.Len(t, users, 2) + assert.Equal(t, "User A", users[0].Name) +} + +func Test_GetBoardUsers_ServiceError(t *testing.T) { + boardID := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + mockUserService.EXPECT().GetBoardUsers(mock.Anything, boardID).Return(nil, errors.New("db error")) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + + req := technical_helper.NewTestRequestBuilder("GET", "/board/{id}", nil).AddToContext(identifiers.BoardIdentifier, boardID) + + userApi.GetUsersFromBoard(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) + +} + +func Test_UpdateUser_api(t *testing.T) { + userID := uuid.New() + + updateBody := UserUpdateRequest{Name: "Jose", ID: userID} + mockUpdatedUser := &User{ID: userID, Name: "Jose", AccountType: common.Anonymous} + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + mockUserService.EXPECT().Update(mock.Anything, updateBody).Return(mockUpdatedUser, nil) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + + bodyBytes, _ := json.Marshal(updateBody) + req := technical_helper.NewTestRequestBuilder("PUT", "/", bytes.NewReader(bodyBytes)). + AddToContext(identifiers.UserIdentifier, userID) + + userApi.Update(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) + var user User + err := json.Unmarshal(rr.Body.Bytes(), &user) + assert.Nil(t, err) + assert.Equal(t, mockUpdatedUser.Name, user.Name) +} + +func Test_UpdateUser_ServiceError(t *testing.T) { + userID := uuid.New() + + updateBody := UserUpdateRequest{Name: "Jose", ID: userID} + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + mockUserService.EXPECT().Update(mock.Anything, updateBody).Return(nil, errors.New("db error")) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + + bodyBytes, _ := json.Marshal(updateBody) + req := technical_helper.NewTestRequestBuilder("PUT", "/", bytes.NewReader(bodyBytes)). + AddToContext(identifiers.UserIdentifier, userID) + + userApi.Update(rr, req.Request()) + + assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode) +} + +func Test_UpdateUserBoards_ServiceError(t *testing.T) { + userID := uuid.New() + + updateBody := UserUpdateRequest{Name: "Jose", ID: userID} + mockUpdatedUser := &User{ID: userID, Name: "Jose", AccountType: common.Anonymous} + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + + mockUserService.EXPECT().Update(mock.Anything, updateBody).Return(mockUpdatedUser, nil) + + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + + bodyBytes, _ := json.Marshal(updateBody) + req := technical_helper.NewTestRequestBuilder("PUT", "/", bytes.NewReader(bodyBytes)). + AddToContext(identifiers.UserIdentifier, userID) + + userApi.Update(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) +} + +func Test_BoardAuthenticatedContext(t *testing.T) { + userId := uuid.New() + boardId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardId.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + mockUserService.EXPECT().Get(mock.Anything, userId).Return(&User{ID: userId, AccountType: common.Google}, nil) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.BoardAuthenticatedContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) +} + +func Test_BoardAuthenticatedContext_NotAuthenticated(t *testing.T) { + userId := uuid.New() + boardId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardId.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + mockUserService.EXPECT().Get(mock.Anything, userId).Return(&User{ID: userId, AccountType: common.Anonymous}, nil) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.BoardAuthenticatedContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusForbidden, rr.Result().StatusCode) + assert.Error(t, common.ForbiddenError(errors.New("not authorized"))) +} + +func Test_BoardAuthenticatedContext_InvalidBoardID(t *testing.T) { + userId := uuid.New() + boardId := "abc" + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardId) + req.AddToContext(chi.RouteCtxKey, rctx) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.BoardAuthenticatedContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusBadRequest, rr.Result().StatusCode) + assert.Error(t, common.BadRequestError(errors.New("invalid board id"))) +} + +func Test_BoardAuthenticatedContext_InvalidUserID(t *testing.T) { + userId := "abc" + boardId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", boardId.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.BoardAuthenticatedContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusBadRequest, rr.Result().StatusCode) +} + +func Test_AnonymousBoardCreationContext(t *testing.T) { + userId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, true, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", userId.String()) + req.AddToContext(chi.RouteCtxKey, rctx) + + mockUserService.EXPECT().Get(mock.Anything, userId).Return(&User{ID: userId, AccountType: common.Anonymous}, nil) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.AnonymousBoardCreationContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusOK, rr.Result().StatusCode) +} + +func Test_AnonymousBoardCreationContext_NotAllowed(t *testing.T) { + userId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, false, true) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", userId.String()) + + mockUserService.EXPECT().Get(mock.Anything, userId).Return(&User{ID: userId, AccountType: common.Anonymous}, nil) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.AnonymousBoardCreationContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusForbidden, rr.Result().StatusCode) + assert.Error(t, common.ForbiddenError(errors.New("not authorized to create boards anonymously"))) +} + +func Test_AnonymousCustomTemplateCreationContext_NotAllowed(t *testing.T) { + userId := uuid.New() + + mockUserService := NewMockUserService(t) + mockSessionService := sessions.NewMockSessionService(t) + userApi := NewUserApi(mockUserService, mockSessionService, false, false) + rr := httptest.NewRecorder() + req := technical_helper.NewTestRequestBuilder("GET", "/", nil). + AddToContext(identifiers.UserIdentifier, userId) + + mockUserService.EXPECT().Get(mock.Anything, userId).Return(&User{ID: userId, AccountType: common.Anonymous}, nil) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + userApi.AnonymousCustomTemplateCreationContext(next).ServeHTTP(rr, req.Request()) + + assert.Equal(t, http.StatusForbidden, rr.Result().StatusCode) + assert.Error(t, common.ForbiddenError(errors.New("not authorized to create custom templates anonymous"))) +} diff --git a/server/src/users/mock_UsersApi.go b/server/src/users/mock_UsersApi.go new file mode 100644 index 0000000000..ad49a15f82 --- /dev/null +++ b/server/src/users/mock_UsersApi.go @@ -0,0 +1,427 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package users + +import ( + "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// NewMockUsersApi creates a new instance of MockUsersApi. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUsersApi(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUsersApi { + mock := &MockUsersApi{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockUsersApi is an autogenerated mock type for the UsersApi type +type MockUsersApi struct { + mock.Mock +} + +type MockUsersApi_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUsersApi) EXPECT() *MockUsersApi_Expecter { + return &MockUsersApi_Expecter{mock: &_m.Mock} +} + +// AnonymousBoardCreationContext provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) AnonymousBoardCreationContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for AnonymousBoardCreationContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockUsersApi_AnonymousBoardCreationContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnonymousBoardCreationContext' +type MockUsersApi_AnonymousBoardCreationContext_Call struct { + *mock.Call +} + +// AnonymousBoardCreationContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockUsersApi_Expecter) AnonymousBoardCreationContext(next interface{}) *MockUsersApi_AnonymousBoardCreationContext_Call { + return &MockUsersApi_AnonymousBoardCreationContext_Call{Call: _e.mock.On("AnonymousBoardCreationContext", next)} +} + +func (_c *MockUsersApi_AnonymousBoardCreationContext_Call) Run(run func(next http.Handler)) *MockUsersApi_AnonymousBoardCreationContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockUsersApi_AnonymousBoardCreationContext_Call) Return(handler http.Handler) *MockUsersApi_AnonymousBoardCreationContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockUsersApi_AnonymousBoardCreationContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockUsersApi_AnonymousBoardCreationContext_Call { + _c.Call.Return(run) + return _c +} + +// AnonymousCustomTemplateCreationContext provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) AnonymousCustomTemplateCreationContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for AnonymousCustomTemplateCreationContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockUsersApi_AnonymousCustomTemplateCreationContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnonymousCustomTemplateCreationContext' +type MockUsersApi_AnonymousCustomTemplateCreationContext_Call struct { + *mock.Call +} + +// AnonymousCustomTemplateCreationContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockUsersApi_Expecter) AnonymousCustomTemplateCreationContext(next interface{}) *MockUsersApi_AnonymousCustomTemplateCreationContext_Call { + return &MockUsersApi_AnonymousCustomTemplateCreationContext_Call{Call: _e.mock.On("AnonymousCustomTemplateCreationContext", next)} +} + +func (_c *MockUsersApi_AnonymousCustomTemplateCreationContext_Call) Run(run func(next http.Handler)) *MockUsersApi_AnonymousCustomTemplateCreationContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockUsersApi_AnonymousCustomTemplateCreationContext_Call) Return(handler http.Handler) *MockUsersApi_AnonymousCustomTemplateCreationContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockUsersApi_AnonymousCustomTemplateCreationContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockUsersApi_AnonymousCustomTemplateCreationContext_Call { + _c.Call.Return(run) + return _c +} + +// BoardAuthenticatedContext provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) BoardAuthenticatedContext(next http.Handler) http.Handler { + ret := _mock.Called(next) + + if len(ret) == 0 { + panic("no return value specified for BoardAuthenticatedContext") + } + + var r0 http.Handler + if returnFunc, ok := ret.Get(0).(func(http.Handler) http.Handler); ok { + r0 = returnFunc(next) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + return r0 +} + +// MockUsersApi_BoardAuthenticatedContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BoardAuthenticatedContext' +type MockUsersApi_BoardAuthenticatedContext_Call struct { + *mock.Call +} + +// BoardAuthenticatedContext is a helper method to define mock.On call +// - next http.Handler +func (_e *MockUsersApi_Expecter) BoardAuthenticatedContext(next interface{}) *MockUsersApi_BoardAuthenticatedContext_Call { + return &MockUsersApi_BoardAuthenticatedContext_Call{Call: _e.mock.On("BoardAuthenticatedContext", next)} +} + +func (_c *MockUsersApi_BoardAuthenticatedContext_Call) Run(run func(next http.Handler)) *MockUsersApi_BoardAuthenticatedContext_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.Handler + if args[0] != nil { + arg0 = args[0].(http.Handler) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockUsersApi_BoardAuthenticatedContext_Call) Return(handler http.Handler) *MockUsersApi_BoardAuthenticatedContext_Call { + _c.Call.Return(handler) + return _c +} + +func (_c *MockUsersApi_BoardAuthenticatedContext_Call) RunAndReturn(run func(next http.Handler) http.Handler) *MockUsersApi_BoardAuthenticatedContext_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) Delete(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockUsersApi_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockUsersApi_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockUsersApi_Expecter) Delete(w interface{}, r interface{}) *MockUsersApi_Delete_Call { + return &MockUsersApi_Delete_Call{Call: _e.mock.On("Delete", w, r)} +} + +func (_c *MockUsersApi_Delete_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockUsersApi_Delete_Call) Return() *MockUsersApi_Delete_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUsersApi_Delete_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_Delete_Call { + _c.Run(run) + return _c +} + +// GetUser provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) GetUser(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockUsersApi_GetUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUser' +type MockUsersApi_GetUser_Call struct { + *mock.Call +} + +// GetUser is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockUsersApi_Expecter) GetUser(w interface{}, r interface{}) *MockUsersApi_GetUser_Call { + return &MockUsersApi_GetUser_Call{Call: _e.mock.On("GetUser", w, r)} +} + +func (_c *MockUsersApi_GetUser_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_GetUser_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockUsersApi_GetUser_Call) Return() *MockUsersApi_GetUser_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUsersApi_GetUser_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_GetUser_Call { + _c.Run(run) + return _c +} + +// GetUserByID provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) GetUserByID(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockUsersApi_GetUserByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByID' +type MockUsersApi_GetUserByID_Call struct { + *mock.Call +} + +// GetUserByID is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockUsersApi_Expecter) GetUserByID(w interface{}, r interface{}) *MockUsersApi_GetUserByID_Call { + return &MockUsersApi_GetUserByID_Call{Call: _e.mock.On("GetUserByID", w, r)} +} + +func (_c *MockUsersApi_GetUserByID_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_GetUserByID_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockUsersApi_GetUserByID_Call) Return() *MockUsersApi_GetUserByID_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUsersApi_GetUserByID_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_GetUserByID_Call { + _c.Run(run) + return _c +} + +// GetUsersFromBoard provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) GetUsersFromBoard(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockUsersApi_GetUsersFromBoard_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUsersFromBoard' +type MockUsersApi_GetUsersFromBoard_Call struct { + *mock.Call +} + +// GetUsersFromBoard is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockUsersApi_Expecter) GetUsersFromBoard(w interface{}, r interface{}) *MockUsersApi_GetUsersFromBoard_Call { + return &MockUsersApi_GetUsersFromBoard_Call{Call: _e.mock.On("GetUsersFromBoard", w, r)} +} + +func (_c *MockUsersApi_GetUsersFromBoard_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_GetUsersFromBoard_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockUsersApi_GetUsersFromBoard_Call) Return() *MockUsersApi_GetUsersFromBoard_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUsersApi_GetUsersFromBoard_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_GetUsersFromBoard_Call { + _c.Run(run) + return _c +} + +// Update provides a mock function for the type MockUsersApi +func (_mock *MockUsersApi) Update(w http.ResponseWriter, r *http.Request) { + _mock.Called(w, r) + return +} + +// MockUsersApi_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockUsersApi_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - w http.ResponseWriter +// - r *http.Request +func (_e *MockUsersApi_Expecter) Update(w interface{}, r interface{}) *MockUsersApi_Update_Call { + return &MockUsersApi_Update_Call{Call: _e.mock.On("Update", w, r)} +} + +func (_c *MockUsersApi_Update_Call) Run(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 http.ResponseWriter + if args[0] != nil { + arg0 = args[0].(http.ResponseWriter) + } + var arg1 *http.Request + if args[1] != nil { + arg1 = args[1].(*http.Request) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockUsersApi_Update_Call) Return() *MockUsersApi_Update_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUsersApi_Update_Call) RunAndReturn(run func(w http.ResponseWriter, r *http.Request)) *MockUsersApi_Update_Call { + _c.Run(run) + return _c +} diff --git a/server/src/users/routes.go b/server/src/users/routes.go new file mode 100644 index 0000000000..7376c869c0 --- /dev/null +++ b/server/src/users/routes.go @@ -0,0 +1,42 @@ +package users + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "scrumlr.io/server/sessions" +) + +type UsersApi interface { + GetUser(w http.ResponseWriter, r *http.Request) + GetUserByID(w http.ResponseWriter, r *http.Request) + GetUsersFromBoard(w http.ResponseWriter, r *http.Request) + Update(w http.ResponseWriter, r *http.Request) + Delete(w http.ResponseWriter, r *http.Request) + + BoardAuthenticatedContext(next http.Handler) http.Handler + AnonymousBoardCreationContext(next http.Handler) http.Handler + AnonymousCustomTemplateCreationContext(next http.Handler) http.Handler +} +type Router struct { + usersApi UsersApi + sessionApi sessions.SessionApi +} + +func (r *Router) RegisterRoutes() chi.Router { + router := chi.NewRouter() + router.Route("/users", func(router chi.Router) { + router.Get("/", r.usersApi.GetUser) + router.Get("/{user}", r.usersApi.GetUserByID) + router.Put("/", r.usersApi.Update) + router.With(r.sessionApi.BoardParticipantContext).Get("/board/{id}", r.usersApi.GetUsersFromBoard) + }) + return router +} + +func NewUsersRouter(usersApi UsersApi, sessionApi sessions.SessionApi) *Router { + r := new(Router) + r.usersApi = usersApi + r.sessionApi = sessionApi + return r +} diff --git a/server/src/users/service.go b/server/src/users/service.go index 087105695f..c1a7b7e02d 100644 --- a/server/src/users/service.go +++ b/server/src/users/service.go @@ -1,407 +1,408 @@ package users import ( - "context" - "database/sql" - "errors" - "strings" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" - "scrumlr.io/server/sessions" - - "github.com/google/uuid" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" - "scrumlr.io/server/realtime" + "context" + "database/sql" + "errors" + + "strings" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "scrumlr.io/server/sessions" + + "github.com/google/uuid" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" + "scrumlr.io/server/realtime" ) var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/users") var meter metric.Meter = otel.Meter("scrumlr.io/server/users") type UserDatabase interface { - CreateAnonymousUser(ctx context.Context, name string) (DatabaseUser, error) - CreateAppleUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) - CreateAzureAdUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) - CreateGitHubUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) - CreateGoogleUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) - CreateMicrosoftUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) - CreateOIDCUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) - UpdateUser(ctx context.Context, update DatabaseUserUpdate) (DatabaseUser, error) - GetUser(ctx context.Context, id uuid.UUID) (DatabaseUser, error) - GetUsers(ctx context.Context, boardID uuid.UUID) ([]DatabaseUser, error) - - IsUserAnonymous(ctx context.Context, id uuid.UUID) (bool, error) - IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) - SetKeyMigration(ctx context.Context, id uuid.UUID) (DatabaseUser, error) + CreateAnonymousUser(ctx context.Context, name string) (DatabaseUser, error) + CreateAppleUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) + CreateAzureAdUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) + CreateGitHubUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) + CreateGoogleUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) + CreateMicrosoftUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) + CreateOIDCUser(ctx context.Context, id, name, avatarUrl string) (DatabaseUser, error) + UpdateUser(ctx context.Context, update DatabaseUserUpdate) (DatabaseUser, error) + GetUser(ctx context.Context, id uuid.UUID) (DatabaseUser, error) + GetUsers(ctx context.Context, boardID uuid.UUID) ([]DatabaseUser, error) + + IsUserAnonymous(ctx context.Context, id uuid.UUID) (bool, error) + IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) + SetKeyMigration(ctx context.Context, id uuid.UUID) (DatabaseUser, error) } type Service struct { - database UserDatabase - sessionService sessions.SessionService - realtime *realtime.Broker + database UserDatabase + sessionService sessions.SessionService + realtime *realtime.Broker } func NewUserService(db UserDatabase, rt *realtime.Broker, sessionService sessions.SessionService) UserService { - service := new(Service) - service.database = db - service.realtime = rt - service.sessionService = sessionService + service := new(Service) + service.database = db + service.realtime = rt + service.sessionService = sessionService - return service + return service } func (service *Service) CreateAnonymous(ctx context.Context, name string) (*User, error) { - ctx, span := tracer.Start(ctx, "users.service.CreateAnonymous") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, err - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.anonymous.type", string(common.Anonymous)), - attribute.String("scrumlr.users.service.create.anonymous.name", name), - ) - - user, err := service.database.CreateAnonymousUser(ctx, name) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, err - } - - userCreatedCounter.Add(ctx, 1) - anonymousUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "users.service.CreateAnonymous") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, err + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.anonymous.type", string(common.Anonymous)), + attribute.String("scrumlr.users.service.create.anonymous.name", name), + ) + + user, err := service.database.CreateAnonymousUser(ctx, name) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, err + } + + userCreatedCounter.Add(ctx, 1) + anonymousUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) CreateAppleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.apple") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.apple.type", string(common.Apple)), - attribute.String("scrumlr.users.service.create.apple.name", name), - ) - - user, err := service.database.CreateAppleUser(ctx, id, name, avatarUrl) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, common.InternalServerError - } - - userCreatedCounter.Add(ctx, 1) - appleUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.apple") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.apple.type", string(common.Apple)), + attribute.String("scrumlr.users.service.create.apple.name", name), + ) + + user, err := service.database.CreateAppleUser(ctx, id, name, avatarUrl) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, common.InternalServerError + } + + userCreatedCounter.Add(ctx, 1) + appleUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) CreateAzureAdUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.azuread") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.azuread.type", string(common.AzureAd)), - attribute.String("scrumlr.users.service.create.azuread.name", name), - ) - - user, err := service.database.CreateAzureAdUser(ctx, id, name, avatarUrl) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, common.InternalServerError - } - - userCreatedCounter.Add(ctx, 1) - azureAdUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.azuread") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.azuread.type", string(common.AzureAd)), + attribute.String("scrumlr.users.service.create.azuread.name", name), + ) + + user, err := service.database.CreateAzureAdUser(ctx, id, name, avatarUrl) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, common.InternalServerError + } + + userCreatedCounter.Add(ctx, 1) + azureAdUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) CreateGitHubUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.github") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.github.type", string(common.GitHub)), - attribute.String("scrumlr.users.service.create.github.name", name), - ) - - user, err := service.database.CreateGitHubUser(ctx, id, name, avatarUrl) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, common.InternalServerError - } - - userCreatedCounter.Add(ctx, 1) - githubUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.github") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.github.type", string(common.GitHub)), + attribute.String("scrumlr.users.service.create.github.name", name), + ) + + user, err := service.database.CreateGitHubUser(ctx, id, name, avatarUrl) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, common.InternalServerError + } + + userCreatedCounter.Add(ctx, 1) + githubUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) CreateGoogleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.google") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.google.type", string(common.Google)), - attribute.String("scrumlr.users.service.create.google.name", name), - ) - - user, err := service.database.CreateGoogleUser(ctx, id, name, avatarUrl) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, common.InternalServerError - } - - userCreatedCounter.Add(ctx, 1) - googleUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.google") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.google.type", string(common.Google)), + attribute.String("scrumlr.users.service.create.google.name", name), + ) + + user, err := service.database.CreateGoogleUser(ctx, id, name, avatarUrl) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, common.InternalServerError + } + + userCreatedCounter.Add(ctx, 1) + googleUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) CreateMicrosoftUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.microsoft") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.microsoft.type", string(common.Microsoft)), - attribute.String("scrumlr.users.service.create.microsoft.name", name), - ) - - user, err := service.database.CreateMicrosoftUser(ctx, id, name, avatarUrl) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, common.InternalServerError - } - - userCreatedCounter.Add(ctx, 1) - microsoftUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.microsoft") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.microsoft.type", string(common.Microsoft)), + attribute.String("scrumlr.users.service.create.microsoft.name", name), + ) + + user, err := service.database.CreateMicrosoftUser(ctx, id, name, avatarUrl) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, common.InternalServerError + } + + userCreatedCounter.Add(ctx, 1) + microsoftUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) CreateOIDCUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.oidc") - defer span.End() - - err := validateUsername(name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.create.oidc.type", string(common.TypeOIDC)), - attribute.String("scrumlr.users.service.create.oidc.name", name), - ) - - user, err := service.database.CreateOIDCUser(ctx, id, name, avatarUrl) - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - return nil, common.InternalServerError - } - - userCreatedCounter.Add(ctx, 1) - oicdUserCreatedCounter.Add(ctx, 1) - return new(User).From(user), err + ctx, span := tracer.Start(ctx, "scrumlr.users.service.create.oidc") + defer span.End() + + err := validateUsername(name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.create.oidc.type", string(common.TypeOIDC)), + attribute.String("scrumlr.users.service.create.oidc.name", name), + ) + + user, err := service.database.CreateOIDCUser(ctx, id, name, avatarUrl) + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + return nil, common.InternalServerError + } + + userCreatedCounter.Add(ctx, 1) + oicdUserCreatedCounter.Add(ctx, 1) + return new(User).From(user), err } func (service *Service) Update(ctx context.Context, body UserUpdateRequest) (*User, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.users.service.update") - defer span.End() - - err := validateUsername(body.Name) - if err != nil { - span.SetStatus(codes.Error, "failed to validate user name") - span.RecordError(err) - return nil, common.BadRequestError(err) - } - - span.SetAttributes( - attribute.String("scrumlr.users.service.update.id", body.ID.String()), - attribute.String("scrumlr.users.service.update.name", body.Name), - ) - - user, err := service.database.UpdateUser(ctx, DatabaseUserUpdate{ - ID: body.ID, - Name: body.Name, - Avatar: body.Avatar, - }) - - if err != nil { - if err == sql.ErrNoRows { - span.SetStatus(codes.Error, "user to update not found") - span.RecordError(err) - log.Errorw("user to update not found", "user", body.ID, "err", err) - return nil, common.NotFoundError - } - - span.SetStatus(codes.Error, "failed to update user") - span.RecordError(err) - log.Errorw("unable to update user", "user", body.ID, "err", err) - return nil, common.InternalServerError - } - - service.updatedUser(ctx, user) - - return new(User).From(user), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.users.service.update") + defer span.End() + + err := validateUsername(body.Name) + if err != nil { + span.SetStatus(codes.Error, "failed to validate user name") + span.RecordError(err) + return nil, common.BadRequestError(err) + } + + span.SetAttributes( + attribute.String("scrumlr.users.service.update.id", body.ID.String()), + attribute.String("scrumlr.users.service.update.name", body.Name), + ) + + user, err := service.database.UpdateUser(ctx, DatabaseUserUpdate{ + ID: body.ID, + Name: body.Name, + Avatar: body.Avatar, + }) + + if err != nil { + if err == sql.ErrNoRows { + span.SetStatus(codes.Error, "user to update not found") + span.RecordError(err) + log.Errorw("user to update not found", "user", body.ID, "err", err) + return nil, common.NotFoundError + } + + span.SetStatus(codes.Error, "failed to update user") + span.RecordError(err) + log.Errorw("unable to update user", "user", body.ID, "err", err) + return nil, common.InternalServerError + } + + service.updatedUser(ctx, user) + + return new(User).From(user), err } func (service *Service) Get(ctx context.Context, userID uuid.UUID) (*User, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.users.service.get") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.users.service.get.id", userID.String()), - ) - - user, err := service.database.GetUser(ctx, userID) - if err != nil { - if err == sql.ErrNoRows { - span.SetStatus(codes.Error, "user not found") - span.RecordError(err) - return nil, common.NotFoundError - } - - span.SetStatus(codes.Error, "failed to get user") - span.RecordError(err) - log.Errorw("unable to get user", "user", userID, "err", err) - return nil, common.InternalServerError - } - - return new(User).From(user), err + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.users.service.get") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.users.service.get.id", userID.String()), + ) + + user, err := service.database.GetUser(ctx, userID) + if err != nil { + if err == sql.ErrNoRows { + span.SetStatus(codes.Error, "user not found") + span.RecordError(err) + return nil, common.NotFoundError + } + + span.SetStatus(codes.Error, "failed to get user") + span.RecordError(err) + log.Errorw("unable to get user", "user", userID, "err", err) + return nil, common.InternalServerError + } + + return new(User).From(user), err } func (service *Service) GetBoardUsers(ctx context.Context, boardID uuid.UUID) ([]*User, error) { - log := logger.FromContext(ctx) - ctx, span := tracer.Start(ctx, "scrumlr.users.service.multiple") - defer span.End() - - users, err := service.database.GetUsers(ctx, boardID) - if err != nil { - span.SetStatus(codes.Error, "failed to get users") - span.RecordError(err) - log.Errorw("unable to get users", "board", boardID, "err", err) - return nil, common.InternalServerError - } - - return UserSlice(users), nil + log := logger.FromContext(ctx) + ctx, span := tracer.Start(ctx, "scrumlr.users.service.multiple") + defer span.End() + + users, err := service.database.GetUsers(ctx, boardID) + if err != nil { + span.SetStatus(codes.Error, "failed to get users") + span.RecordError(err) + log.Errorw("unable to get users", "board", boardID, "err", err) + return nil, common.InternalServerError + } + + return UserSlice(users), nil } func (service *Service) IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.available_key_migration") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.users.service.available_key_migration") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.users.service.available_key_migration.id", id.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.users.service.available_key_migration.id", id.String()), + ) - return service.database.IsUserAvailableForKeyMigration(ctx, id) + return service.database.IsUserAvailableForKeyMigration(ctx, id) } func (service *Service) SetKeyMigration(ctx context.Context, id uuid.UUID) (*User, error) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.set_key_migration") - defer span.End() + ctx, span := tracer.Start(ctx, "scrumlr.users.service.set_key_migration") + defer span.End() - span.SetAttributes( - attribute.String("scrumlr.users.service.set_key_migration.id", id.String()), - ) + span.SetAttributes( + attribute.String("scrumlr.users.service.set_key_migration.id", id.String()), + ) - user, err := service.database.SetKeyMigration(ctx, id) - if err != nil { - span.SetStatus(codes.Error, "failed to set key migration") - span.RecordError(err) - return nil, err - } + user, err := service.database.SetKeyMigration(ctx, id) + if err != nil { + span.SetStatus(codes.Error, "failed to set key migration") + span.RecordError(err) + return nil, err + } - return new(User).From(user), nil + return new(User).From(user), nil } func (service *Service) updatedUser(ctx context.Context, user DatabaseUser) { - ctx, span := tracer.Start(ctx, "scrumlr.users.service.update") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.users.service.update.id", user.ID.String()), - attribute.String("scrumlr.users.service.update.name", user.Name), - attribute.String("scrumlr.users.service.update.type", string(user.AccountType)), - ) - - connectedBoards, err := service.sessionService.GetUserConnectedBoards(ctx, user.ID) - if err != nil { - span.SetStatus(codes.Error, "failed to get connected boards") - span.RecordError(err) - return - } - - for _, session := range connectedBoards { - userSession, err := service.sessionService.Get(ctx, session.Board, session.UserID) - if err != nil { - span.SetStatus(codes.Error, "failed to sessions") - span.RecordError(err) - logger.Get().Errorw("unable to get board session", "board", userSession.Board, "user", userSession.UserID, "err", err) - } - _ = service.realtime.BroadcastToBoard(ctx, session.Board, realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: session, - }) - } + ctx, span := tracer.Start(ctx, "scrumlr.users.service.update") + defer span.End() + + span.SetAttributes( + attribute.String("scrumlr.users.service.update.id", user.ID.String()), + attribute.String("scrumlr.users.service.update.name", user.Name), + attribute.String("scrumlr.users.service.update.type", string(user.AccountType)), + ) + + connectedBoards, err := service.sessionService.GetUserConnectedBoards(ctx, user.ID) + if err != nil { + span.SetStatus(codes.Error, "failed to get connected boards") + span.RecordError(err) + return + } + + for _, session := range connectedBoards { + userSession, err := service.sessionService.Get(ctx, session.Board, session.UserID) + if err != nil { + span.SetStatus(codes.Error, "failed to sessions") + span.RecordError(err) + logger.Get().Errorw("unable to get board session", "board", userSession.Board, "user", userSession.UserID, "err", err) + } + _ = service.realtime.BroadcastToBoard(ctx, session.Board, realtime.BoardEvent{ + Type: realtime.BoardEventParticipantUpdated, + Data: session, + }) + } } func validateUsername(name string) error { - if strings.TrimSpace(name) == "" { - return errors.New("name may not be empty") - } + if strings.TrimSpace(name) == "" { + return errors.New("name may not be empty") + } - if strings.Contains(name, "\n") { - return errors.New("name may not contain newline characters") - } + if strings.Contains(name, "\n") { + return errors.New("name may not contain newline characters") + } - return nil + return nil }