From 463527d6646d3426aad071eb1e80ad85e02295b7 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 8 Sep 2025 14:09:55 +0200 Subject: [PATCH 01/16] rework session that it only contains userID # Conflicts: # server/src/api/boards.go # server/src/api/router.go # server/src/sessions/service.go --- server/src/api/event_filter.go | 2 +- server/src/api/users.go | 16 ++++++++++++++++ server/src/boards/service.go | 2 +- server/src/sessions/dto_sessions.go | 4 ++-- server/src/sessions/service_sessions.go | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/server/src/api/event_filter.go b/server/src/api/event_filter.go index db582c7a70..51e2af4f98 100644 --- a/server/src/api/event_filter.go +++ b/server/src/api/event_filter.go @@ -199,7 +199,7 @@ func (bs *BoardSubscription) participantUpdated(event *realtime.BoardEvent, isMo if isMod { // Cache the changes of when a participant got updated updatedSessions := technical_helper.MapSlice(bs.boardParticipants, func(boardSession *sessions.BoardSession) *sessions.BoardSession { - if boardSession.User == participantSession.User { + if boardSession.ID == participantSession.ID { return participantSession } else { return boardSession diff --git a/server/src/api/users.go b/server/src/api/users.go index 283c33fb93..83622158b8 100644 --- a/server/src/api/users.go +++ b/server/src/api/users.go @@ -1,6 +1,7 @@ package api import ( + "github.com/go-chi/chi/v5" "net/http" "github.com/go-chi/render" @@ -33,6 +34,21 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) { render.Respond(w, r, user) } +func (s *Server) getUserByID(w http.ResponseWriter, r *http.Request) { + //callerId := r.Context().Value(identifiers.UserIdentifier).(uuid.UUID) + + userParam := chi.URLParam(r, "user") + requestedUserId, err := uuid.Parse(userParam) + user, err := s.users.Get(r.Context(), requestedUserId) + if err != nil { + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, user) +} + func (s *Server) updateUser(w http.ResponseWriter, r *http.Request) { ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.update") defer span.End() diff --git a/server/src/boards/service.go b/server/src/boards/service.go index e0c9b60e15..4701e894dc 100644 --- a/server/src/boards/service.go +++ b/server/src/boards/service.go @@ -289,7 +289,7 @@ func (service *Service) BoardOverview(ctx context.Context, boardIDs []uuid.UUID, columnNum := len(boardColumns) dtoBoard := new(Board).From(board) for _, session := range boardSessions { - if session.User.ID == user { + if session.ID == user { sessionCreated := session.CreatedAt overviewBoards = append(overviewBoards, &BoardOverview{ Board: dtoBoard, diff --git a/server/src/sessions/dto_sessions.go b/server/src/sessions/dto_sessions.go index 5897775747..2d516f6372 100644 --- a/server/src/sessions/dto_sessions.go +++ b/server/src/sessions/dto_sessions.go @@ -10,7 +10,7 @@ import ( // BoardSession is the response for all participant requests. type BoardSession struct { - User User `json:"user"` + ID uuid.UUID `json:"id"` // Flag indicates whether user is online and connected to the board. Connected bool `json:"connected"` @@ -85,7 +85,7 @@ func (b *BoardSession) From(session DatabaseBoardSession) *BoardSession { Avatar: session.Avatar, AccountType: session.AccountType, } - b.User = user + b.ID = user.ID b.Connected = session.Connected b.Ready = session.Ready b.RaisedHand = session.RaisedHand diff --git a/server/src/sessions/service_sessions.go b/server/src/sessions/service_sessions.go index 56acc0255b..a26c640c2c 100644 --- a/server/src/sessions/service_sessions.go +++ b/server/src/sessions/service_sessions.go @@ -528,7 +528,7 @@ func (service *BoardSessionService) updatedSessions(ctx context.Context, board u func CheckSessionRole(clientID uuid.UUID, sessions []*BoardSession, sessionsRoles []common.SessionRole) bool { for _, session := range sessions { - if clientID == session.User.ID { + if clientID == session.ID { if slices.Contains(sessionsRoles, session.Role) { return true } From 67ddd1c928a6e8580d942289af13b9796286c1be Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 8 Sep 2025 14:10:25 +0200 Subject: [PATCH 02/16] edit frontend that it supports sessions with userID only --- src/api/participant.ts | 15 +++ src/store/features/board/thunks.ts | 129 ++++++++++++++++++----- src/store/features/participants/types.ts | 10 ++ 3 files changed, 126 insertions(+), 28 deletions(-) diff --git a/src/api/participant.ts b/src/api/participant.ts index 6177225235..fc5b3cd1ab 100644 --- a/src/api/participant.ts +++ b/src/api/participant.ts @@ -121,4 +121,19 @@ export const ParticipantsAPI = { throw new Error(`unable to update ready states: ${error}`); } }, + + getUserById: async (userId: string) => { + try { + const response = await fetch(`${SERVER_HTTP_URL}/user/${userId}`, { + method: "GET", + credentials: "include", + }); + if (response.status === 200) { + return await response.json(); + } + throw new Error(`unable to fetch user with response status ${response.status}`); + } catch (error) { + throw new Error(`unable to fetch user: ${error}`); + } + }, }; diff --git a/src/store/features/board/thunks.ts b/src/store/features/board/thunks.ts index f6b3c6cead..5111420dde 100644 --- a/src/store/features/board/thunks.ts +++ b/src/store/features/board/thunks.ts @@ -1,7 +1,7 @@ import Socket from "sockette"; import {createAsyncThunk} from "@reduxjs/toolkit"; import {SERVER_WEBSOCKET_URL} from "config"; -import {ServerEvent} from "types/websocket"; +import {BoardInitEvent, ServerEvent} from "types/websocket"; import {API} from "api"; import {Timer} from "utils/timer"; import {ApplicationState, retryable} from "store"; @@ -10,39 +10,43 @@ import {initializeBoard, updatedBoard, updatedBoardTimer} from "./actions"; import {deletedColumn, updatedColumns} from "../columns"; import {deletedNote, syncNotes, updatedNotes} from "../notes"; import {addedReaction, deletedReaction, updatedReaction} from "../reactions"; -import {createdParticipant, setParticipants, updatedParticipant} from "../participants"; +import {createdParticipant, Participant, ParticipantDTO, setParticipants, updatedParticipant} from "../participants"; import {createdVoting, updatedVoting} from "../votings"; import {deletedVotes} from "../votes"; import {createJoinRequest, updateJoinRequest} from "../requests"; import {addedBoardReaction, removeBoardReaction} from "../boardReactions"; import {CreateSessionAccessPolicy, EditBoardRequest} from "./types"; import {TemplateWithColumns} from "../templates"; +import {Auth} from "../auth"; let socket: Socket | null = null; // creates a board from a template and returns board id if successful -export const createBoardFromTemplate = createAsyncThunk( - "board/createBoardFromTemplate", - async (payload) => { - // finally, translate names and descriptions, since only the keys were stored until this point - const translateRecommendedTemplate = (toBeTranslated: TemplateWithColumns): TemplateWithColumns => ({ - template: { - ...toBeTranslated.template, - name: i18n.t(toBeTranslated.template.name, {ns: "templates"}), - description: i18n.t(toBeTranslated.template.description, {ns: "templates"}), - }, - columns: toBeTranslated.columns.map((toBeTranslatedColumn) => ({ - ...toBeTranslatedColumn, - name: i18n.t(toBeTranslatedColumn.name, {ns: "templates"}), - description: i18n.t(toBeTranslatedColumn.description, {ns: "templates"}), - })), - }); - - const translatedTemplateWithColumns = - payload.templateWithColumns.template.type === "RECOMMENDED" ? translateRecommendedTemplate(payload.templateWithColumns) : payload.templateWithColumns; - return API.createBoard(translatedTemplateWithColumns.template.name, payload.accessPolicy, translatedTemplateWithColumns.columns); +export const createBoardFromTemplate = createAsyncThunk< + string, + { + templateWithColumns: TemplateWithColumns; + accessPolicy: CreateSessionAccessPolicy; } -); +>("board/createBoardFromTemplate", async (payload) => { + // finally, translate names and descriptions, since only the keys were stored until this point + const translateRecommendedTemplate = (toBeTranslated: TemplateWithColumns): TemplateWithColumns => ({ + template: { + ...toBeTranslated.template, + name: i18n.t(toBeTranslated.template.name, {ns: "templates"}), + description: i18n.t(toBeTranslated.template.description, {ns: "templates"}), + }, + columns: toBeTranslated.columns.map((toBeTranslatedColumn) => ({ + ...toBeTranslatedColumn, + name: i18n.t(toBeTranslatedColumn.name, {ns: "templates"}), + description: i18n.t(toBeTranslatedColumn.description, {ns: "templates"}), + })), + }); + + const translatedTemplateWithColumns = + payload.templateWithColumns.template.type === "RECOMMENDED" ? translateRecommendedTemplate(payload.templateWithColumns) : payload.templateWithColumns; + return API.createBoard(translatedTemplateWithColumns.template.name, payload.accessPolicy, translatedTemplateWithColumns.columns); +}); export const leaveBoard = createAsyncThunk("board/leaveBoard", async () => { if (socket) { @@ -51,6 +55,26 @@ export const leaveBoard = createAsyncThunk("board/leaveBoard", async () => { } }); +async function participantsWithUser(message: BoardInitEvent) { + const {participants} = message.data; + const participantsWithUser: Participant[] = []; + for (let i = 0; i < participants.length; i++) { + const user = await API.getUserById((participants[i] as unknown as ParticipantDTO).id); + participantsWithUser.push({ + connected: participants[i].connected, + raisedHand: participants[i].raisedHand, + ready: participants[i].ready, + role: participants[i].role, + showHiddenColumns: participants[i].showHiddenColumns, + user, + banned: participants[i].banned, + }); + } + + message.data.participants = participantsWithUser; + return participantsWithUser; +} + // generic args: ("board/importBoard", async (payload, {dispatch}) => { +export const importBoard = createAsyncThunk< + void, + string, + { + state: ApplicationState; + } +>("board/importBoard", async (payload, {dispatch}) => { retryable( () => API.importBoard(payload), dispatch, diff --git a/src/store/features/participants/types.ts b/src/store/features/participants/types.ts index 999131c039..18b3c486e2 100644 --- a/src/store/features/participants/types.ts +++ b/src/store/features/participants/types.ts @@ -12,6 +12,16 @@ export interface Participant { banned?: boolean; } +export interface ParticipantDTO { + id: string; + connected: boolean; + ready: boolean; + raisedHand: boolean; + showHiddenColumns: boolean; + role: ParticipantRole; + banned?: boolean; +} + export type ParticipantsState = { self?: Participant; others?: Participant[]; From 8490c7fa180416889eea883d4d4a4cfa01411d36 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 8 Sep 2025 16:16:52 +0200 Subject: [PATCH 03/16] seperating sessions from users + fixing tests --- server/src/sessions/dto_sessions.go | 9 +- server/src/sessions/service.go | 381 ------------------ server/src/sessions/service_sessions_test.go | 18 +- server/src/{sessions => users}/api.go | 2 +- server/src/{sessions => users}/database.go | 2 +- .../src/{sessions => users}/database_dto.go | 2 +- .../src/{sessions => users}/database_test.go | 2 +- server/src/{sessions => users}/dto.go | 2 +- .../{sessions => users}/mock_UserDatabase.go | 2 +- .../{sessions => users}/mock_UserService.go | 2 +- server/src/users/service.go | 223 ++++++++++ .../src/{sessions => users}/service_test.go | 0 12 files changed, 241 insertions(+), 404 deletions(-) delete mode 100644 server/src/sessions/service.go rename server/src/{sessions => users}/api.go (98%) rename server/src/{sessions => users}/database.go (99%) rename server/src/{sessions => users}/database_dto.go (97%) rename server/src/{sessions => users}/database_test.go (99%) rename server/src/{sessions => users}/dto.go (97%) rename server/src/{sessions => users}/mock_UserDatabase.go (99%) rename server/src/{sessions => users}/mock_UserService.go (99%) create mode 100644 server/src/users/service.go rename server/src/{sessions => users}/service_test.go (100%) diff --git a/server/src/sessions/dto_sessions.go b/server/src/sessions/dto_sessions.go index 2d516f6372..fad88dc80e 100644 --- a/server/src/sessions/dto_sessions.go +++ b/server/src/sessions/dto_sessions.go @@ -79,13 +79,8 @@ type BoardSessionsUpdateRequest struct { } func (b *BoardSession) From(session DatabaseBoardSession) *BoardSession { - user := User{ - ID: session.User, - Name: session.Name, - Avatar: session.Avatar, - AccountType: session.AccountType, - } - b.ID = user.ID + + b.ID = session.User b.Connected = session.Connected b.Ready = session.Ready b.RaisedHand = session.RaisedHand diff --git a/server/src/sessions/service.go b/server/src/sessions/service.go deleted file mode 100644 index 161f872a0f..0000000000 --- a/server/src/sessions/service.go +++ /dev/null @@ -1,381 +0,0 @@ -package sessions - -import ( - "context" - "database/sql" - "errors" - "strings" - - "github.com/google/uuid" - "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" -) - -var userTracer trace.Tracer = otel.Tracer("scrumlr.io/server/users") -var userMeter 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) - - 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 SessionService - realtime *realtime.Broker -} - -func NewUserService(db UserDatabase, rt *realtime.Broker, sessionService SessionService) UserService { - service := new(Service) - service.database = db - service.realtime = rt - service.sessionService = sessionService - - return service -} - -func (service *Service) CreateAnonymous(ctx context.Context, name string) (*User, error) { - ctx, span := userTracer.Start(ctx, "scrumlr.users.service.create.anonymous") - 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 := userTracer.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, 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, err - } - - 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 := userTracer.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, 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, err - } - - 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 := userTracer.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, 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, err - } - - 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 := userTracer.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, 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, err - } - - 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 := userTracer.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, 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, err - } - - 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 := userTracer.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, 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, err - } - - 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 := userTracer.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, 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 { - span.SetStatus(codes.Error, "failed to update user") - span.RecordError(err) - log.Errorw("unable to update user", "user", body.ID, "err", err) - return nil, err - } - - 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 := userTracer.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) IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) { - ctx, span := userTracer.Start(ctx, "scrumlr.users.service.available_key_migration") - defer span.End() - - span.SetAttributes( - attribute.String("scrumlr.users.service.available_key_migration.id", id.String()), - ) - - return service.database.IsUserAvailableForKeyMigration(ctx, id) -} - -func (service *Service) SetKeyMigration(ctx context.Context, id uuid.UUID) (*User, error) { - ctx, span := userTracer.Start(ctx, "scrumlr.users.service.set_key_migration") - defer span.End() - - 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 - } - - return new(User).From(user), nil -} - -func (service *Service) updatedUser(ctx context.Context, user DatabaseUser) { - ctx, span := userTracer.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.User.ID) - 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.User.ID, "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.Contains(name, "\n") { - return errors.New("name may not contain newline characters") - } - - return nil -} diff --git a/server/src/sessions/service_sessions_test.go b/server/src/sessions/service_sessions_test.go index cb34301e2b..1171d17782 100644 --- a/server/src/sessions/service_sessions_test.go +++ b/server/src/sessions/service_sessions_test.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" - mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/mock" "scrumlr.io/server/columns" "scrumlr.io/server/common" "scrumlr.io/server/notes" @@ -38,7 +38,7 @@ func TestGetSession(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) + assert.Equal(t, userId, session.ID) } func TestGetSession_NotFound(t *testing.T) { @@ -116,10 +116,10 @@ func TestGetSessions(t *testing.T) { assert.NotNil(t, boardSessions) assert.Len(t, boardSessions, 2) - assert.Equal(t, firstUserId, boardSessions[0].User.ID) + assert.Equal(t, firstUserId, boardSessions[0].ID) assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, secondUserId, boardSessions[1].User.ID) + assert.Equal(t, secondUserId, boardSessions[1].ID) assert.Equal(t, boardId, boardSessions[1].Board) } @@ -308,7 +308,7 @@ func TestCreateSession(t *testing.T) { assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) + assert.Equal(t, userId, session.ID) assert.Equal(t, common.ParticipantRole, session.Role) } @@ -389,7 +389,7 @@ func TestUpdateSession_Role(t *testing.T) { assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) + assert.Equal(t, userId, session.ID) assert.Equal(t, common.ModeratorRole, session.Role) } @@ -442,7 +442,7 @@ func TestUpdateSession_RaiseHand(t *testing.T) { assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) + assert.Equal(t, userId, session.ID) assert.Equal(t, raisedHand, session.RaisedHand) } @@ -698,11 +698,11 @@ func TestUpdateAllSessions(t *testing.T) { assert.Len(t, boardSessions, 2) assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, firstUserId, boardSessions[0].User.ID) + assert.Equal(t, firstUserId, boardSessions[0].ID) assert.Equal(t, ready, boardSessions[0].Ready) assert.Equal(t, boardId, boardSessions[1].Board) - assert.Equal(t, secondUserId, boardSessions[1].User.ID) + assert.Equal(t, secondUserId, boardSessions[1].ID) assert.Equal(t, ready, boardSessions[1].Ready) } diff --git a/server/src/sessions/api.go b/server/src/users/api.go similarity index 98% rename from server/src/sessions/api.go rename to server/src/users/api.go index 34442a8380..3ac6e25c00 100644 --- a/server/src/sessions/api.go +++ b/server/src/users/api.go @@ -1,4 +1,4 @@ -package sessions +package users import ( "context" diff --git a/server/src/sessions/database.go b/server/src/users/database.go similarity index 99% rename from server/src/sessions/database.go rename to server/src/users/database.go index cbc0b7754f..2c1a951b7c 100644 --- a/server/src/sessions/database.go +++ b/server/src/users/database.go @@ -1,4 +1,4 @@ -package sessions +package users import ( "context" diff --git a/server/src/sessions/database_dto.go b/server/src/users/database_dto.go similarity index 97% rename from server/src/sessions/database_dto.go rename to server/src/users/database_dto.go index 090f613924..d7674dbae1 100644 --- a/server/src/sessions/database_dto.go +++ b/server/src/users/database_dto.go @@ -1,4 +1,4 @@ -package sessions +package users import ( "time" diff --git a/server/src/sessions/database_test.go b/server/src/users/database_test.go similarity index 99% rename from server/src/sessions/database_test.go rename to server/src/users/database_test.go index 4dd8068ac6..8a5591ca59 100644 --- a/server/src/sessions/database_test.go +++ b/server/src/users/database_test.go @@ -1,4 +1,4 @@ -package sessions +package users import ( "context" diff --git a/server/src/sessions/dto.go b/server/src/users/dto.go similarity index 97% rename from server/src/sessions/dto.go rename to server/src/users/dto.go index bb43071dcd..758516226a 100644 --- a/server/src/sessions/dto.go +++ b/server/src/users/dto.go @@ -1,4 +1,4 @@ -package sessions +package users import ( "net/http" diff --git a/server/src/sessions/mock_UserDatabase.go b/server/src/users/mock_UserDatabase.go similarity index 99% rename from server/src/sessions/mock_UserDatabase.go rename to server/src/users/mock_UserDatabase.go index da0d716fd7..4b8f96248e 100644 --- a/server/src/sessions/mock_UserDatabase.go +++ b/server/src/users/mock_UserDatabase.go @@ -2,7 +2,7 @@ // github.com/vektra/mockery // template: testify -package sessions +package users import ( "context" diff --git a/server/src/sessions/mock_UserService.go b/server/src/users/mock_UserService.go similarity index 99% rename from server/src/sessions/mock_UserService.go rename to server/src/users/mock_UserService.go index d796edc31b..713256a8b9 100644 --- a/server/src/sessions/mock_UserService.go +++ b/server/src/users/mock_UserService.go @@ -2,7 +2,7 @@ // github.com/vektra/mockery // template: testify -package sessions +package users import ( "context" diff --git a/server/src/users/service.go b/server/src/users/service.go new file mode 100644 index 0000000000..585cb5e5e4 --- /dev/null +++ b/server/src/users/service.go @@ -0,0 +1,223 @@ +package users + +import ( + "context" + "database/sql" + "errors" + "scrumlr.io/server/sessions" + "strings" + + "github.com/google/uuid" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" + "scrumlr.io/server/realtime" +) + +type UserDatabase interface { + CreateAnonymousUser(name string) (DatabaseUser, error) + CreateAppleUser(id, name, avatarUrl string) (DatabaseUser, error) + CreateAzureAdUser(id, name, avatarUrl string) (DatabaseUser, error) + CreateGitHubUser(id, name, avatarUrl string) (DatabaseUser, error) + CreateGoogleUser(id, name, avatarUrl string) (DatabaseUser, error) + CreateMicrosoftUser(id, name, avatarUrl string) (DatabaseUser, error) + CreateOIDCUser(id, name, avatarUrl string) (DatabaseUser, error) + UpdateUser(update DatabaseUserUpdate) (DatabaseUser, error) + GetUser(id uuid.UUID) (DatabaseUser, error) + + IsUserAnonymous(id uuid.UUID) (bool, error) + IsUserAvailableForKeyMigration(id uuid.UUID) (bool, error) + SetKeyMigration(id uuid.UUID) (DatabaseUser, error) +} + +type Service struct { + 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 + + return service +} + +func (service *Service) CreateAnonymous(ctx context.Context, name string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateAnonymousUser(name) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) CreateAppleUser(_ context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateAppleUser(id, name, avatarUrl) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) CreateAzureAdUser(_ context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateAzureAdUser(id, name, avatarUrl) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) CreateGitHubUser(_ context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateGitHubUser(id, name, avatarUrl) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) CreateGoogleUser(_ context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateGoogleUser(id, name, avatarUrl) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) CreateMicrosoftUser(_ context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateMicrosoftUser(id, name, avatarUrl) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) CreateOIDCUser(_ context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } + + user, err := service.database.CreateOIDCUser(id, name, avatarUrl) + if err != nil { + return nil, err + } + + return new(User).From(user), err +} + +func (service *Service) Update(ctx context.Context, body UserUpdateRequest) (*User, error) { + log := logger.FromContext(ctx) + err := validateUsername(body.Name) + if err != nil { + return nil, err + } + + user, err := service.database.UpdateUser(DatabaseUserUpdate{ + ID: body.ID, + Name: body.Name, + Avatar: body.Avatar, + }) + + if err != nil { + log.Errorw("unable to update user", "user", body.ID, "err", err) + return nil, err + } + + 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) + user, err := service.database.GetUser(userID) + if err != nil { + if err == sql.ErrNoRows { + return nil, common.NotFoundError + } + log.Errorw("unable to get user", "user", userID, "err", err) + return nil, common.InternalServerError + } + + return new(User).From(user), err +} + +func (service *Service) IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) { + return service.database.IsUserAvailableForKeyMigration(id) +} + +func (service *Service) SetKeyMigration(ctx context.Context, id uuid.UUID) (*User, error) { + user, err := service.database.SetKeyMigration(id) + if err != nil { + return nil, err + } + + return new(User).From(user), nil +} + +func (service *Service) updatedUser(ctx context.Context, user DatabaseUser) { + connectedBoards, err := service.sessionService.GetUserConnectedBoards(ctx, user.ID) + if err != nil { + return + } + + for _, session := range connectedBoards { + userSession, err := service.sessionService.Get(ctx, session.Board, session.ID) + if err != nil { + logger.Get().Errorw("unable to get board session", "board", userSession.Board, "user", userSession.ID, "err", err) + } + _ = service.realtime.BroadcastToBoard(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.Contains(name, "\n") { + return errors.New("name may not contain newline characters") + } + + return nil +} diff --git a/server/src/sessions/service_test.go b/server/src/users/service_test.go similarity index 100% rename from server/src/sessions/service_test.go rename to server/src/users/service_test.go From 1101191ccc3b3d62fbb7a7595897ff8e1df5c0a9 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 8 Sep 2025 16:17:07 +0200 Subject: [PATCH 04/16] fixing tests --- server/src/api/board_templates_test.go | 238 ++--- server/src/api/context_test.go | 58 +- server/src/api/event_filter_test.go | 1217 ++++++++++++----------- server/src/api/router.go | 5 +- server/src/api/users.go | 3 +- server/src/auth/auth.go | 6 +- server/src/serviceinitialize/service.go | 7 +- server/src/sessionrequests/dto.go | 6 +- 8 files changed, 772 insertions(+), 768 deletions(-) diff --git a/server/src/api/board_templates_test.go b/server/src/api/board_templates_test.go index 4e38234bc0..b5040404d3 100644 --- a/server/src/api/board_templates_test.go +++ b/server/src/api/board_templates_test.go @@ -6,10 +6,12 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "scrumlr.io/server/users" "testing" "github.com/go-chi/jwtauth/v5" "github.com/google/uuid" + "github.com/markbates/goth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "scrumlr.io/server/auth" @@ -18,8 +20,6 @@ import ( "scrumlr.io/server/columntemplates" "scrumlr.io/server/common" "scrumlr.io/server/identifiers" - "scrumlr.io/server/sessions" - "github.com/markbates/goth" ) // createValidBoardTemplateRequest creates a valid board template request for testing @@ -83,12 +83,12 @@ func (t *testAuthService) Verifier() func(http.Handler) http.Handler { userID = userUUID.String() } } - + // Create JWT token and context using jwtauth tokenAuth := jwtauth.New("HS256", []byte("test-secret"), nil) claims := map[string]interface{}{"id": userID} token, _, _ := tokenAuth.Encode(claims) - + // Set the JWT context the way jwtauth expects it ctx := jwtauth.NewContext(r.Context(), token, nil) next.ServeHTTP(w, r.WithContext(ctx)) @@ -121,49 +121,49 @@ func (t *testAuthService) ExtractUserInformation(accountType common.AccountType, // Test suite for AnonymousCustomTemplateCreationContext middleware func TestAnonymousCustomTemplateCreationContext(t *testing.T) { userID := uuid.New() - + tests := []struct { - name string - allowAnonymousCustomTemplates bool - userAccountType common.AccountType - expectedStatus int - expectedToCallNext bool + name string + allowAnonymousCustomTemplates bool + userAccountType common.AccountType + expectedStatus int + expectedToCallNext bool }{ { - name: "authenticated user can create templates when flag is disabled", - allowAnonymousCustomTemplates: false, - userAccountType: common.Google, - expectedStatus: http.StatusOK, - expectedToCallNext: true, + name: "authenticated user can create templates when flag is disabled", + allowAnonymousCustomTemplates: false, + userAccountType: common.Google, + expectedStatus: http.StatusOK, + expectedToCallNext: true, }, { - name: "authenticated user can create templates when flag is enabled", - allowAnonymousCustomTemplates: true, - userAccountType: common.Google, - expectedStatus: http.StatusOK, - expectedToCallNext: true, + name: "authenticated user can create templates when flag is enabled", + allowAnonymousCustomTemplates: true, + userAccountType: common.Google, + expectedStatus: http.StatusOK, + expectedToCallNext: true, }, { - name: "anonymous user can create templates when flag is enabled", - allowAnonymousCustomTemplates: true, - userAccountType: common.Anonymous, - expectedStatus: http.StatusOK, - expectedToCallNext: true, + name: "anonymous user can create templates when flag is enabled", + allowAnonymousCustomTemplates: true, + userAccountType: common.Anonymous, + expectedStatus: http.StatusOK, + expectedToCallNext: true, }, { - name: "anonymous user receives 403 forbidden when allowAnonymousCustomTemplates is false", - allowAnonymousCustomTemplates: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, - expectedToCallNext: false, + name: "anonymous user receives 403 forbidden when allowAnonymousCustomTemplates is false", + allowAnonymousCustomTemplates: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, + expectedToCallNext: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock user service with test user - mockUsers := sessions.NewMockUserService(t) - mockUsers.EXPECT().Get(mock.Anything, userID).Return(&sessions.User{ + mockUsers := users.NewMockUserService(t) + mockUsers.EXPECT().Get(mock.Anything, userID).Return(&users.User{ ID: userID, Name: "Test User", AccountType: tt.userAccountType, @@ -209,7 +209,7 @@ func TestAnonymousCustomTemplateCreationContext_UserNotFound(t *testing.T) { userID := uuid.New() // Create mock user service that returns error when user not found - mockUsers := sessions.NewMockUserService(t) + mockUsers := users.NewMockUserService(t) mockUsers.EXPECT().Get(mock.Anything, userID).Return(nil, assert.AnError) server := &Server{ @@ -265,80 +265,80 @@ func TestAnonymousCustomTemplateCreationContext_MissingUserContext(t *testing.T) // Integration test for the middleware applied to all /templates routes func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { tests := []struct { - name string - method string - path string - allowAnonymousCustomTemplates bool - userAccountType common.AccountType - expectedStatus int - needsRequestBody bool - requestBodyType string // "create" or "update" + name string + method string + path string + allowAnonymousCustomTemplates bool + userAccountType common.AccountType + expectedStatus int + needsRequestBody bool + requestBodyType string // "create" or "update" }{ // POST /templates { - name: "POST /templates - anonymous user blocked when flag disabled", - method: "POST", - path: "/templates", - allowAnonymousCustomTemplates: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, + name: "POST /templates - anonymous user blocked when flag disabled", + method: "POST", + path: "/templates", + allowAnonymousCustomTemplates: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, }, { - name: "POST /templates - anonymous user allowed when flag enabled", - method: "POST", - path: "/templates", - allowAnonymousCustomTemplates: true, - userAccountType: common.Anonymous, - expectedStatus: http.StatusCreated, - needsRequestBody: true, - requestBodyType: "create", + name: "POST /templates - anonymous user allowed when flag enabled", + method: "POST", + path: "/templates", + allowAnonymousCustomTemplates: true, + userAccountType: common.Anonymous, + expectedStatus: http.StatusCreated, + needsRequestBody: true, + requestBodyType: "create", }, // GET /templates { - name: "GET /templates - anonymous user blocked when flag disabled", - method: "GET", - path: "/templates", - allowAnonymousCustomTemplates: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, + name: "GET /templates - anonymous user blocked when flag disabled", + method: "GET", + path: "/templates", + allowAnonymousCustomTemplates: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, }, // GET /templates/{id} { - name: "GET /templates/id - anonymous user blocked when flag disabled", - method: "GET", - path: "/templates/" + uuid.New().String(), - allowAnonymousCustomTemplates: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, + name: "GET /templates/id - anonymous user blocked when flag disabled", + method: "GET", + path: "/templates/" + uuid.New().String(), + allowAnonymousCustomTemplates: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, }, // PUT /templates/{id} { - name: "PUT /templates/id - anonymous user blocked when flag disabled", - method: "PUT", - path: "/templates/" + uuid.New().String(), - allowAnonymousCustomTemplates: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, + name: "PUT /templates/id - anonymous user blocked when flag disabled", + method: "PUT", + path: "/templates/" + uuid.New().String(), + allowAnonymousCustomTemplates: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, }, // DELETE /templates/{id} { - name: "DELETE /templates/id - anonymous user blocked when flag disabled", - method: "DELETE", - path: "/templates/" + uuid.New().String(), - allowAnonymousCustomTemplates: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, + name: "DELETE /templates/id - anonymous user blocked when flag disabled", + method: "DELETE", + path: "/templates/" + uuid.New().String(), + allowAnonymousCustomTemplates: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, }, // Authenticated users should always pass { - name: "Authenticated user always allowed", - method: "POST", - path: "/templates", - allowAnonymousCustomTemplates: false, - userAccountType: common.Google, - expectedStatus: http.StatusCreated, - needsRequestBody: true, - requestBodyType: "create", + name: "Authenticated user always allowed", + method: "POST", + path: "/templates", + allowAnonymousCustomTemplates: false, + userAccountType: common.Google, + expectedStatus: http.StatusCreated, + needsRequestBody: true, + requestBodyType: "create", }, } @@ -347,8 +347,8 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { userID := uuid.New() // Create mock user service - mockUsers := sessions.NewMockUserService(t) - mockUsers.EXPECT().Get(mock.Anything, userID).Return(&sessions.User{ + mockUsers := users.NewMockUserService(t) + mockUsers.EXPECT().Get(mock.Anything, userID).Return(&users.User{ ID: userID, Name: "Test User", AccountType: tt.userAccountType, @@ -357,16 +357,16 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { // Create mock services for dependencies required by the actual router mockBoardTemplates := boardtemplates.NewMockBoardTemplateService(t) mockColumnTemplates := columntemplates.NewMockColumnTemplateService(t) - + // Create a simple auth mock that allows all requests to pass mockAuth := createTestAuth() - + // Create mock handlers that return proper template objects templateID := uuid.New() templateName := "Test Template" templateDescription := "Test Description" favourite := false - + // Mock template response mockTemplate := &boardtemplates.BoardTemplate{ ID: templateID, @@ -375,43 +375,43 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { Description: &templateDescription, Favourite: &favourite, } - + // Mock template full response for GetAll mockTemplateFull := &boardtemplates.BoardTemplateFull{ Template: mockTemplate, ColumnTemplates: []*columntemplates.ColumnTemplate{}, } - + mockBoardTemplates.EXPECT().Create(mock.Anything, mock.Anything).Return(mockTemplate, nil).Maybe() mockBoardTemplates.EXPECT().GetAll(mock.Anything, mock.Anything).Return([]*boardtemplates.BoardTemplateFull{mockTemplateFull}, nil).Maybe() mockBoardTemplates.EXPECT().Get(mock.Anything, mock.Anything).Return(mockTemplate, nil).Maybe() mockBoardTemplates.EXPECT().Update(mock.Anything, mock.Anything).Return(mockTemplate, nil).Maybe() mockBoardTemplates.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil).Maybe() - + // Use the actual router from router.go with minimal mocked dependencies r := New( - "/", // basePath - nil, // realtime (not needed for templates) - mockAuth, // auth - nil, // boards - nil, // columns - nil, // votings - mockUsers, // users - nil, // notes - nil, // reactions - nil, // sessions - nil, // sessionRequests - nil, // health - nil, // feedback - nil, // boardReactions - mockBoardTemplates, // boardTemplates - mockColumnTemplates, // columntemplates - false, // verbose - true, // checkOrigin - false, // anonymousLoginDisabled + "/", // basePath + nil, // realtime (not needed for templates) + mockAuth, // auth + nil, // boards + nil, // columns + nil, // votings + mockUsers, // users + nil, // notes + nil, // reactions + nil, // sessions + nil, // sessionRequests + nil, // health + nil, // feedback + nil, // boardReactions + mockBoardTemplates, // boardTemplates + mockColumnTemplates, // columntemplates + false, // verbose + true, // checkOrigin + false, // anonymousLoginDisabled tt.allowAnonymousCustomTemplates, // allowAnonymousCustomTemplates - false, // allowAnonymousBoardCreation - false, // experimentalFileSystemStore + false, // allowAnonymousBoardCreation + false, // experimentalFileSystemStore ) // Create request with body if needed @@ -424,14 +424,14 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { case "update": body = createValidBoardTemplateUpdateRequest() } - + bodyBytes, _ := json.Marshal(body) req = httptest.NewRequest(tt.method, tt.path, bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(tt.method, tt.path, nil) } - + ctx := context.WithValue(req.Context(), identifiers.UserIdentifier, userID) req = req.WithContext(ctx) @@ -449,4 +449,4 @@ func TestTemplateRoutesMiddlewareIntegration(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/server/src/api/context_test.go b/server/src/api/context_test.go index 320f1a6eb0..3b1d7502e3 100644 --- a/server/src/api/context_test.go +++ b/server/src/api/context_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "scrumlr.io/server/users" "testing" "github.com/google/uuid" @@ -11,54 +12,53 @@ import ( "github.com/stretchr/testify/mock" "scrumlr.io/server/common" "scrumlr.io/server/identifiers" - "scrumlr.io/server/sessions" ) func TestAnonymousBoardCreationContext(t *testing.T) { userID := uuid.New() tests := []struct { - name string - allowAnonymousBoardCreation bool - userAccountType common.AccountType - expectedStatus int - expectedToCallNext bool + name string + allowAnonymousBoardCreation bool + userAccountType common.AccountType + expectedStatus int + expectedToCallNext bool }{ { - name: "authenticated user can create boards when flag is disabled", - allowAnonymousBoardCreation: false, - userAccountType: common.Google, - expectedStatus: http.StatusOK, - expectedToCallNext: true, + name: "authenticated user can create boards when flag is disabled", + allowAnonymousBoardCreation: false, + userAccountType: common.Google, + expectedStatus: http.StatusOK, + expectedToCallNext: true, }, { - name: "authenticated user can create boards when flag is enabled", - allowAnonymousBoardCreation: true, - userAccountType: common.Google, - expectedStatus: http.StatusOK, - expectedToCallNext: true, + name: "authenticated user can create boards when flag is enabled", + allowAnonymousBoardCreation: true, + userAccountType: common.Google, + expectedStatus: http.StatusOK, + expectedToCallNext: true, }, { - name: "anonymous user can create boards when flag is enabled", - allowAnonymousBoardCreation: true, - userAccountType: common.Anonymous, - expectedStatus: http.StatusOK, - expectedToCallNext: true, + name: "anonymous user can create boards when flag is enabled", + allowAnonymousBoardCreation: true, + userAccountType: common.Anonymous, + expectedStatus: http.StatusOK, + expectedToCallNext: true, }, { - name: "anonymous user receives 403 forbidden when allowAnonymousBoardCreation is false", - allowAnonymousBoardCreation: false, - userAccountType: common.Anonymous, - expectedStatus: http.StatusForbidden, - expectedToCallNext: false, + name: "anonymous user receives 403 forbidden when allowAnonymousBoardCreation is false", + allowAnonymousBoardCreation: false, + userAccountType: common.Anonymous, + expectedStatus: http.StatusForbidden, + expectedToCallNext: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock user service with test user - mockUsers := sessions.NewMockUserService(t) - mockUsers.EXPECT().Get(mock.Anything, userID).Return(&sessions.User{ + mockUsers := users.NewMockUserService(t) + mockUsers.EXPECT().Get(mock.Anything, userID).Return(&users.User{ ID: userID, Name: "Test User", AccountType: tt.userAccountType, @@ -104,7 +104,7 @@ func TestAnonymousBoardCreationContext_UserNotFound(t *testing.T) { userID := uuid.New() // Create mock user service that returns error when user not found - mockUsers := sessions.NewMockUserService(t) + mockUsers := users.NewMockUserService(t) mockUsers.EXPECT().Get(mock.Anything, userID).Return(nil, assert.AnError) server := &Server{ diff --git a/server/src/api/event_filter_test.go b/server/src/api/event_filter_test.go index d5c9476f20..b8d32c65a2 100644 --- a/server/src/api/event_filter_test.go +++ b/server/src/api/event_filter_test.go @@ -1,793 +1,794 @@ package api import ( - "math/rand" - "testing" - - "scrumlr.io/server/sessions" - - "scrumlr.io/server/boards" - "scrumlr.io/server/common" - "scrumlr.io/server/votings" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "scrumlr.io/server/columns" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessionrequests" - "scrumlr.io/server/technical_helper" + "math/rand" + "scrumlr.io/server/users" + "testing" + + "scrumlr.io/server/sessions" + + "scrumlr.io/server/boards" + "scrumlr.io/server/common" + "scrumlr.io/server/votings" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "scrumlr.io/server/columns" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessionrequests" + "scrumlr.io/server/technical_helper" ) var ( - moderatorUser = sessions.User{ - ID: uuid.New(), - } - ownerUser = sessions.User{ - ID: uuid.New(), - } - participantUser = sessions.User{ - ID: uuid.New(), - AccountType: common.Anonymous, - } - moderatorBoardSession = sessions.BoardSession{ - User: moderatorUser, - Role: common.ModeratorRole, - } - ownerBoardSession = sessions.BoardSession{ - User: ownerUser, - Role: common.OwnerRole, - } - participantBoardSession = sessions.BoardSession{ - User: participantUser, - Role: common.ParticipantRole, - } - boardSessions = []*sessions.BoardSession{ - &participantBoardSession, - &ownerBoardSession, - &moderatorBoardSession, - } - boardSettings = &boards.Board{ - ID: uuid.New(), - AccessPolicy: boards.Public, - ShowAuthors: true, - ShowNotesOfOtherUsers: true, - AllowStacking: true, - } - aSeeableColumn = columns.Column{ - ID: uuid.New(), - Name: "Main Thread", - Color: "backlog-blue", - Visible: true, - Index: 0, - } - aModeratorNote = notes.Note{ - ID: uuid.New(), - Author: moderatorUser.ID, - Text: "Moderator Text", - Position: notes.NotePosition{ - Column: aSeeableColumn.ID, - Stack: uuid.NullUUID{}, - Rank: 1, - }, - } - aParticipantNote = notes.Note{ - ID: uuid.New(), - Author: participantUser.ID, - Text: "User Text", - Position: notes.NotePosition{ - Column: aSeeableColumn.ID, - Stack: uuid.NullUUID{}, - Rank: 0, - }, - } - aHiddenColumn = columns.Column{ - ID: uuid.New(), - Name: "Lean Coffee", - Color: "poker-purple", - Visible: false, - Index: 1, - } - aOwnerNote = notes.Note{ - ID: uuid.New(), - Author: ownerUser.ID, - Text: "Owner Text", - Position: notes.NotePosition{ - Column: aHiddenColumn.ID, - Rank: 1, - Stack: uuid.NullUUID{}, - }, - } - boardSub = &BoardSubscription{ - boardParticipants: []*sessions.BoardSession{&moderatorBoardSession, &ownerBoardSession, &participantBoardSession}, - boardColumns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - boardNotes: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - boardSettings: &boards.Board{ - ShowNotesOfOtherUsers: false, - }, - } - boardEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventBoardUpdated, - Data: boardSettings, - } - columnEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - } - noteEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - } - votingID = uuid.New() - votingData = &votings.VotingUpdated{ - Notes: []votings.Note{ - {ID: aParticipantNote.ID, Author: aParticipantNote.Author, Text: aParticipantNote.Text, Edited: aParticipantNote.Edited, Position: votings.NotePosition{Column: aParticipantNote.Position.Column, Stack: aParticipantNote.Position.Stack, Rank: aParticipantNote.Position.Rank}}, - {ID: aModeratorNote.ID, Author: aModeratorNote.Author, Text: aModeratorNote.Text, Edited: aModeratorNote.Edited, Position: votings.NotePosition{Column: aModeratorNote.Position.Column, Stack: aModeratorNote.Position.Stack, Rank: aModeratorNote.Position.Rank}}, - {ID: aOwnerNote.ID, Author: aOwnerNote.Author, Text: aOwnerNote.Text, Edited: aOwnerNote.Edited, Position: votings.NotePosition{Column: aOwnerNote.Position.Column, Stack: aOwnerNote.Position.Stack, Rank: aOwnerNote.Position.Rank}}, - }, - Voting: &votings.Voting{ - ID: votingID, - VoteLimit: 5, - AllowMultipleVotes: true, - ShowVotesOfOthers: false, - Status: "CLOSED", - VotingResults: &votings.VotingResults{ - Total: 5, - Votes: map[uuid.UUID]votings.VotingResultsPerNote{ - aParticipantNote.ID: { - Total: 2, - Users: nil, - }, - aModeratorNote.ID: { - Total: 1, - Users: nil, - }, - aOwnerNote.ID: { - Total: 2, - Users: nil, - }, - }, - }, - }, - } - votingEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: votingData, - } - initEvent = InitEvent{ - Type: realtime.BoardEventInit, - Data: boards.FullBoard{ - Board: &boards.Board{}, - Columns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - Notes: []*notes.Note{&aOwnerNote, &aModeratorNote, &aParticipantNote}, - Votings: []*votings.Voting{votingData.Voting}, - Votes: []*votings.Vote{}, - BoardSessions: boardSessions, - BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, - }, - } + moderatorUser = users.User{ + ID: uuid.New(), + } + ownerUser = users.User{ + ID: uuid.New(), + } + participantUser = users.User{ + ID: uuid.New(), + AccountType: common.Anonymous, + } + moderatorBoardSession = sessions.BoardSession{ + ID: moderatorUser.ID, + Role: common.ModeratorRole, + } + ownerBoardSession = sessions.BoardSession{ + ID: ownerUser.ID, + Role: common.OwnerRole, + } + participantBoardSession = sessions.BoardSession{ + ID: participantUser.ID, + Role: common.ParticipantRole, + } + boardSessions = []*sessions.BoardSession{ + &participantBoardSession, + &ownerBoardSession, + &moderatorBoardSession, + } + boardSettings = &boards.Board{ + ID: uuid.New(), + AccessPolicy: boards.Public, + ShowAuthors: true, + ShowNotesOfOtherUsers: true, + AllowStacking: true, + } + aSeeableColumn = columns.Column{ + ID: uuid.New(), + Name: "Main Thread", + Color: "backlog-blue", + Visible: true, + Index: 0, + } + aModeratorNote = notes.Note{ + ID: uuid.New(), + Author: moderatorUser.ID, + Text: "Moderator Text", + Position: notes.NotePosition{ + Column: aSeeableColumn.ID, + Stack: uuid.NullUUID{}, + Rank: 1, + }, + } + aParticipantNote = notes.Note{ + ID: uuid.New(), + Author: participantUser.ID, + Text: "User Text", + Position: notes.NotePosition{ + Column: aSeeableColumn.ID, + Stack: uuid.NullUUID{}, + Rank: 0, + }, + } + aHiddenColumn = columns.Column{ + ID: uuid.New(), + Name: "Lean Coffee", + Color: "poker-purple", + Visible: false, + Index: 1, + } + aOwnerNote = notes.Note{ + ID: uuid.New(), + Author: ownerUser.ID, + Text: "Owner Text", + Position: notes.NotePosition{ + Column: aHiddenColumn.ID, + Rank: 1, + Stack: uuid.NullUUID{}, + }, + } + boardSub = &BoardSubscription{ + boardParticipants: []*sessions.BoardSession{&moderatorBoardSession, &ownerBoardSession, &participantBoardSession}, + boardColumns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + boardNotes: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + boardSettings: &boards.Board{ + ShowNotesOfOtherUsers: false, + }, + } + boardEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventBoardUpdated, + Data: boardSettings, + } + columnEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + } + noteEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + } + votingID = uuid.New() + votingData = &votings.VotingUpdated{ + Notes: []votings.Note{ + {ID: aParticipantNote.ID, Author: aParticipantNote.Author, Text: aParticipantNote.Text, Edited: aParticipantNote.Edited, Position: votings.NotePosition{Column: aParticipantNote.Position.Column, Stack: aParticipantNote.Position.Stack, Rank: aParticipantNote.Position.Rank}}, + {ID: aModeratorNote.ID, Author: aModeratorNote.Author, Text: aModeratorNote.Text, Edited: aModeratorNote.Edited, Position: votings.NotePosition{Column: aModeratorNote.Position.Column, Stack: aModeratorNote.Position.Stack, Rank: aModeratorNote.Position.Rank}}, + {ID: aOwnerNote.ID, Author: aOwnerNote.Author, Text: aOwnerNote.Text, Edited: aOwnerNote.Edited, Position: votings.NotePosition{Column: aOwnerNote.Position.Column, Stack: aOwnerNote.Position.Stack, Rank: aOwnerNote.Position.Rank}}, + }, + Voting: &votings.Voting{ + ID: votingID, + VoteLimit: 5, + AllowMultipleVotes: true, + ShowVotesOfOthers: false, + Status: "CLOSED", + VotingResults: &votings.VotingResults{ + Total: 5, + Votes: map[uuid.UUID]votings.VotingResultsPerNote{ + aParticipantNote.ID: { + Total: 2, + Users: nil, + }, + aModeratorNote.ID: { + Total: 1, + Users: nil, + }, + aOwnerNote.ID: { + Total: 2, + Users: nil, + }, + }, + }, + }, + } + votingEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: votingData, + } + initEvent = InitEvent{ + Type: realtime.BoardEventInit, + Data: boards.FullBoard{ + Board: &boards.Board{}, + Columns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + Notes: []*notes.Note{&aOwnerNote, &aModeratorNote, &aParticipantNote}, + Votings: []*votings.Voting{votingData.Voting}, + Votes: []*votings.Vote{}, + BoardSessions: boardSessions, + BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, + }, + } ) -func getUserById(id uuid.UUID) sessions.User { - if ownerUser.ID == id { - return ownerUser - } else if participantUser.ID == id { - return participantUser - } else if moderatorUser.ID == id { - return moderatorUser - } - return sessions.User{} +func getUserById(id uuid.UUID) users.User { + if ownerUser.ID == id { + return ownerUser + } else if participantUser.ID == id { + return participantUser + } else if moderatorUser.ID == id { + return moderatorUser + } + return users.User{} } func TestEventFilter(t *testing.T) { - t.Run("TestIsOwnerModerator", testIsOwnerModerator) - t.Run("TestIsModModerator", testIsModModerator) - t.Run("TestIsParticipantModerator", testIsParticipantModerator) - t.Run("TestIsUnknownUuidModerator", testIsUnknownUuidModerator) - t.Run("TestParseBoardSettingsData", testParseBoardSettingsData) - t.Run("TestParseColumnData", testParseColumnData) - t.Run("TestParseNoteData", testParseNoteData) - t.Run("TestParseVotingData", testParseVotingData) - t.Run("TestFilterColumnsAsOwner", testColumnFilterAsOwner) - t.Run("TestFilterColumnsAsModerator", testColumnFilterAsModerator) - t.Run("TestFilterColumnsAsParticipant", testColumnFilterAsParticipant) - t.Run("TestFilterNotesAsOwner", testNoteFilterAsOwner) - t.Run("TestFilterNotesAsModerator", testNoteFilterAsModerator) - t.Run("TestFilterNotesAsParticipant", testNoteFilterAsParticipant) - t.Run("TestFilterVotingUpdatedAsOwner", testFilterVotingUpdatedAsOwner) - t.Run("TestFilterVotingUpdatedAsModerator", testFilterVotingUpdatedAsModerator) - t.Run("TestFilterVotingUpdatedAsParticipant", testFilterVotingUpdatedAsParticipant) - t.Run("TestInitEventAsOwner", testInitFilterAsOwner) - t.Run("TestInitEventAsModerator", testInitFilterAsModerator) - t.Run("TestInitEventAsParticipant", testInitFilterAsParticipant) - t.Run("TestRaiseHandShouldBeUpdatedAfterParticipantUpdated", testRaiseHandShouldBeUpdatedAfterParticipantUpdated) - t.Run("TestParticipantUpdatedShouldHandleError", testParticipantUpdatedShouldHandleError) + t.Run("TestIsOwnerModerator", testIsOwnerModerator) + t.Run("TestIsModModerator", testIsModModerator) + t.Run("TestIsParticipantModerator", testIsParticipantModerator) + t.Run("TestIsUnknownUuidModerator", testIsUnknownUuidModerator) + t.Run("TestParseBoardSettingsData", testParseBoardSettingsData) + t.Run("TestParseColumnData", testParseColumnData) + t.Run("TestParseNoteData", testParseNoteData) + t.Run("TestParseVotingData", testParseVotingData) + t.Run("TestFilterColumnsAsOwner", testColumnFilterAsOwner) + t.Run("TestFilterColumnsAsModerator", testColumnFilterAsModerator) + t.Run("TestFilterColumnsAsParticipant", testColumnFilterAsParticipant) + t.Run("TestFilterNotesAsOwner", testNoteFilterAsOwner) + t.Run("TestFilterNotesAsModerator", testNoteFilterAsModerator) + t.Run("TestFilterNotesAsParticipant", testNoteFilterAsParticipant) + t.Run("TestFilterVotingUpdatedAsOwner", testFilterVotingUpdatedAsOwner) + t.Run("TestFilterVotingUpdatedAsModerator", testFilterVotingUpdatedAsModerator) + t.Run("TestFilterVotingUpdatedAsParticipant", testFilterVotingUpdatedAsParticipant) + t.Run("TestInitEventAsOwner", testInitFilterAsOwner) + t.Run("TestInitEventAsModerator", testInitFilterAsModerator) + t.Run("TestInitEventAsParticipant", testInitFilterAsParticipant) + t.Run("TestRaiseHandShouldBeUpdatedAfterParticipantUpdated", testRaiseHandShouldBeUpdatedAfterParticipantUpdated) + t.Run("TestParticipantUpdatedShouldHandleError", testParticipantUpdatedShouldHandleError) } func testRaiseHandShouldBeUpdatedAfterParticipantUpdated(t *testing.T) { - originalParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { - user := getUserById(session.User.ID) - return user.AccountType == common.Anonymous - })[0] + originalParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { + user := getUserById(session.ID) + return user.AccountType == common.Anonymous + })[0] - updateEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: sessions.BoardSession{ - RaisedHand: true, - User: originalParticipantSession.User, - Role: common.ParticipantRole, - }, - } + updateEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventParticipantUpdated, + Data: sessions.BoardSession{ + RaisedHand: true, + ID: originalParticipantSession.ID, + Role: common.ParticipantRole, + }, + } - isUpdated := boardSub.participantUpdated(updateEvent, true) + isUpdated := boardSub.participantUpdated(updateEvent, true) - updatedParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { - user := getUserById(session.User.ID) - return user.AccountType == common.Anonymous - })[0] + updatedParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { + user := getUserById(session.ID) + return user.AccountType == common.Anonymous + })[0] - assert.Equal(t, true, isUpdated) - assert.Equal(t, false, originalParticipantSession.RaisedHand) - assert.Equal(t, true, updatedParticipantSession.RaisedHand) + assert.Equal(t, true, isUpdated) + assert.Equal(t, false, originalParticipantSession.RaisedHand) + assert.Equal(t, true, updatedParticipantSession.RaisedHand) } func testParticipantUpdatedShouldHandleError(t *testing.T) { - updateEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: "SHOULD FAIL", - } + updateEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventParticipantUpdated, + Data: "SHOULD FAIL", + } - isUpdated := boardSub.participantUpdated(updateEvent, true) + isUpdated := boardSub.participantUpdated(updateEvent, true) - assert.Equal(t, false, isUpdated) + assert.Equal(t, false, isUpdated) } func testIsModModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(moderatorBoardSession.User.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(moderatorBoardSession.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.True(t, isMod) - assert.Equal(t, common.ModeratorRole, moderatorBoardSession.Role) + assert.NotNil(t, isMod) + assert.True(t, isMod) + assert.Equal(t, common.ModeratorRole, moderatorBoardSession.Role) } func testIsOwnerModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(ownerBoardSession.User.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(ownerBoardSession.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.True(t, isMod) - assert.Equal(t, common.OwnerRole, ownerBoardSession.Role) + assert.NotNil(t, isMod) + assert.True(t, isMod) + assert.Equal(t, common.OwnerRole, ownerBoardSession.Role) } func testIsParticipantModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(participantBoardSession.User.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(participantBoardSession.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.False(t, isMod) + assert.NotNil(t, isMod) + assert.False(t, isMod) } func testIsUnknownUuidModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(uuid.New(), boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(uuid.New(), boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.False(t, isMod) + assert.NotNil(t, isMod) + assert.False(t, isMod) } func testParseBoardSettingsData(t *testing.T) { - expectedBoardSettings := boardSettings - actualBoardSettings, err := technical_helper.Unmarshal[boards.Board](boardEvent.Data) + expectedBoardSettings := boardSettings + actualBoardSettings, err := technical_helper.Unmarshal[boards.Board](boardEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualBoardSettings) - assert.Equal(t, expectedBoardSettings, actualBoardSettings) + assert.Nil(t, err) + assert.NotNil(t, actualBoardSettings) + assert.Equal(t, expectedBoardSettings, actualBoardSettings) } func testParseColumnData(t *testing.T) { - expectedColumns := []*columns.Column{&aSeeableColumn, &aHiddenColumn} - actualColumns, err := technical_helper.UnmarshalSlice[columns.Column](columnEvent.Data) + expectedColumns := []*columns.Column{&aSeeableColumn, &aHiddenColumn} + actualColumns, err := technical_helper.UnmarshalSlice[columns.Column](columnEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualColumns) - assert.Equal(t, expectedColumns, actualColumns) + assert.Nil(t, err) + assert.NotNil(t, actualColumns) + assert.Equal(t, expectedColumns, actualColumns) } func testParseNoteData(t *testing.T) { - expectedNotes := []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote} - actualNotes, err := technical_helper.UnmarshalSlice[notes.Note](noteEvent.Data) + expectedNotes := []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote} + actualNotes, err := technical_helper.UnmarshalSlice[notes.Note](noteEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualNotes) - assert.Equal(t, expectedNotes, actualNotes) + assert.Nil(t, err) + assert.NotNil(t, actualNotes) + assert.Equal(t, expectedNotes, actualNotes) } func testParseVotingData(t *testing.T) { - expectedVoting := votingData - actualVoting, err := technical_helper.Unmarshal[votings.VotingUpdated](votingEvent.Data) + expectedVoting := votingData + actualVoting, err := technical_helper.Unmarshal[votings.VotingUpdated](votingEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualVoting) - assert.Equal(t, expectedVoting, actualVoting) + assert.Nil(t, err) + assert.NotNil(t, actualVoting) + assert.Equal(t, expectedVoting, actualVoting) } func testColumnFilterAsParticipant(t *testing.T) { - expectedColumnEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn}, - } - returnedColumnEvent := boardSub.eventFilter(columnEvent, participantBoardSession.User.ID) + expectedColumnEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn}, + } + returnedColumnEvent := boardSub.eventFilter(columnEvent, participantBoardSession.ID) - assert.Equal(t, expectedColumnEvent, returnedColumnEvent) + assert.Equal(t, expectedColumnEvent, returnedColumnEvent) } func testColumnFilterAsOwner(t *testing.T) { - expectedColumnEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - } - returnedColumnEvent := boardSub.eventFilter(columnEvent, ownerBoardSession.User.ID) + expectedColumnEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + } + returnedColumnEvent := boardSub.eventFilter(columnEvent, ownerBoardSession.ID) - assert.Equal(t, expectedColumnEvent, returnedColumnEvent) + assert.Equal(t, expectedColumnEvent, returnedColumnEvent) } func testColumnFilterAsModerator(t *testing.T) { - expectedColumnEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - } + expectedColumnEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + } - returnedColumnEvent := boardSub.eventFilter(columnEvent, moderatorBoardSession.User.ID) + returnedColumnEvent := boardSub.eventFilter(columnEvent, moderatorBoardSession.ID) - assert.Equal(t, expectedColumnEvent, returnedColumnEvent) + assert.Equal(t, expectedColumnEvent, returnedColumnEvent) } func testNoteFilterAsParticipant(t *testing.T) { - expectedNoteEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: notes.NoteSlice{&aParticipantNote}, - } - returnedNoteEvent := boardSub.eventFilter(noteEvent, participantBoardSession.User.ID) + expectedNoteEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: notes.NoteSlice{&aParticipantNote}, + } + returnedNoteEvent := boardSub.eventFilter(noteEvent, participantBoardSession.ID) - assert.Equal(t, expectedNoteEvent, returnedNoteEvent) + assert.Equal(t, expectedNoteEvent, returnedNoteEvent) } func testNoteFilterAsOwner(t *testing.T) { - expectedNoteEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - } - returnedNoteEvent := boardSub.eventFilter(noteEvent, ownerBoardSession.User.ID) + expectedNoteEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + } + returnedNoteEvent := boardSub.eventFilter(noteEvent, ownerBoardSession.ID) - assert.Equal(t, expectedNoteEvent, returnedNoteEvent) + assert.Equal(t, expectedNoteEvent, returnedNoteEvent) } func testNoteFilterAsModerator(t *testing.T) { - expectedNoteEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - } - returnedNoteEvent := boardSub.eventFilter(noteEvent, moderatorBoardSession.User.ID) + expectedNoteEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + } + returnedNoteEvent := boardSub.eventFilter(noteEvent, moderatorBoardSession.ID) - assert.Equal(t, expectedNoteEvent, returnedNoteEvent) + assert.Equal(t, expectedNoteEvent, returnedNoteEvent) } func testFilterVotingUpdatedAsOwner(t *testing.T) { - expectedVotingEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: votingData, - } - returnedVoteEvent := boardSub.eventFilter(votingEvent, ownerBoardSession.User.ID) + expectedVotingEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: votingData, + } + returnedVoteEvent := boardSub.eventFilter(votingEvent, ownerBoardSession.ID) - assert.NotNil(t, returnedVoteEvent) - assert.Equal(t, expectedVotingEvent, returnedVoteEvent) + assert.NotNil(t, returnedVoteEvent) + assert.Equal(t, expectedVotingEvent, returnedVoteEvent) } func testFilterVotingUpdatedAsModerator(t *testing.T) { - expectedVotingEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: votingData, - } - returnedVoteEvent := boardSub.eventFilter(votingEvent, moderatorBoardSession.User.ID) + expectedVotingEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: votingData, + } + returnedVoteEvent := boardSub.eventFilter(votingEvent, moderatorBoardSession.ID) - assert.NotNil(t, returnedVoteEvent) - assert.Equal(t, expectedVotingEvent, returnedVoteEvent) + assert.NotNil(t, returnedVoteEvent) + assert.Equal(t, expectedVotingEvent, returnedVoteEvent) } func testFilterVotingUpdatedAsParticipant(t *testing.T) { - expectedVoting := &votings.VotingUpdated{ - Notes: []votings.Note{ - { - ID: aParticipantNote.ID, - Author: aParticipantNote.Author, - Text: aParticipantNote.Text, - Edited: aParticipantNote.Edited, - Position: votings.NotePosition{ - Column: aParticipantNote.Position.Column, - Stack: aParticipantNote.Position.Stack, - Rank: aParticipantNote.Position.Rank, - }, - }, - }, - Voting: &votings.Voting{ - ID: votingID, - VoteLimit: 5, - AllowMultipleVotes: true, - ShowVotesOfOthers: false, - Status: "CLOSED", - VotingResults: &votings.VotingResults{ - Total: 2, - Votes: map[uuid.UUID]votings.VotingResultsPerNote{ - aParticipantNote.ID: { - Total: 2, - Users: nil, - }, - }, - }, - }, - } - expectedVotingEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: expectedVoting, - } - returnedVoteEvent := boardSub.eventFilter(votingEvent, participantBoardSession.User.ID) - - assert.NotNil(t, returnedVoteEvent) - assert.Equal(t, expectedVotingEvent, returnedVoteEvent) + expectedVoting := &votings.VotingUpdated{ + Notes: []votings.Note{ + { + ID: aParticipantNote.ID, + Author: aParticipantNote.Author, + Text: aParticipantNote.Text, + Edited: aParticipantNote.Edited, + Position: votings.NotePosition{ + Column: aParticipantNote.Position.Column, + Stack: aParticipantNote.Position.Stack, + Rank: aParticipantNote.Position.Rank, + }, + }, + }, + Voting: &votings.Voting{ + ID: votingID, + VoteLimit: 5, + AllowMultipleVotes: true, + ShowVotesOfOthers: false, + Status: "CLOSED", + VotingResults: &votings.VotingResults{ + Total: 2, + Votes: map[uuid.UUID]votings.VotingResultsPerNote{ + aParticipantNote.ID: { + Total: 2, + Users: nil, + }, + }, + }, + }, + } + expectedVotingEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: expectedVoting, + } + returnedVoteEvent := boardSub.eventFilter(votingEvent, participantBoardSession.ID) + + assert.NotNil(t, returnedVoteEvent) + assert.Equal(t, expectedVotingEvent, returnedVoteEvent) } func testInitFilterAsOwner(t *testing.T) { - expectedInitEvent := initEvent - returnedInitEvent := eventInitFilter(initEvent, ownerBoardSession.User.ID) + expectedInitEvent := initEvent + returnedInitEvent := eventInitFilter(initEvent, ownerBoardSession.ID) - assert.Equal(t, expectedInitEvent, returnedInitEvent) + assert.Equal(t, expectedInitEvent, returnedInitEvent) } func testInitFilterAsModerator(t *testing.T) { - expectedInitEvent := initEvent - returnedInitEvent := eventInitFilter(initEvent, moderatorBoardSession.User.ID) + expectedInitEvent := initEvent + returnedInitEvent := eventInitFilter(initEvent, moderatorBoardSession.ID) - assert.Equal(t, expectedInitEvent, returnedInitEvent) + assert.Equal(t, expectedInitEvent, returnedInitEvent) } func testInitFilterAsParticipant(t *testing.T) { - expectedVoting := votings.Voting{ - ID: votingID, - VoteLimit: 5, - AllowMultipleVotes: true, - ShowVotesOfOthers: false, - Status: "CLOSED", - VotingResults: &votings.VotingResults{ - Total: 2, - Votes: map[uuid.UUID]votings.VotingResultsPerNote{ - aParticipantNote.ID: { - Total: 2, - Users: nil, - }, - }, - }, - } - expectedInitEvent := InitEvent{ - Type: realtime.BoardEventInit, - Data: boards.FullBoard{ - Board: &boards.Board{}, - Columns: []*columns.Column{&aSeeableColumn}, - Notes: []*notes.Note{&aParticipantNote}, - Votings: []*votings.Voting{&expectedVoting}, - Votes: []*votings.Vote{}, - BoardSessions: boardSessions, - BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, - }, - } - returnedInitEvent := eventInitFilter(initEvent, participantBoardSession.User.ID) - - assert.Equal(t, expectedInitEvent, returnedInitEvent) + expectedVoting := votings.Voting{ + ID: votingID, + VoteLimit: 5, + AllowMultipleVotes: true, + ShowVotesOfOthers: false, + Status: "CLOSED", + VotingResults: &votings.VotingResults{ + Total: 2, + Votes: map[uuid.UUID]votings.VotingResultsPerNote{ + aParticipantNote.ID: { + Total: 2, + Users: nil, + }, + }, + }, + } + expectedInitEvent := InitEvent{ + Type: realtime.BoardEventInit, + Data: boards.FullBoard{ + Board: &boards.Board{}, + Columns: []*columns.Column{&aSeeableColumn}, + Notes: []*notes.Note{&aParticipantNote}, + Votings: []*votings.Voting{&expectedVoting}, + Votes: []*votings.Vote{}, + BoardSessions: boardSessions, + BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, + }, + } + returnedInitEvent := eventInitFilter(initEvent, participantBoardSession.ID) + + assert.Equal(t, expectedInitEvent, returnedInitEvent) } func TestShouldFailBecauseOfInvalidBordData(t *testing.T) { - event := buildBoardEvent(buildBoardDto(nil, nil, "lorem ipsum", false), realtime.BoardEventBoardUpdated) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(buildBoardDto(nil, nil, "lorem ipsum", false), realtime.BoardEventBoardUpdated) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.boardUpdated(event, false) + _, success := bordSubscription.boardUpdated(event, false) - assert.False(t, success) + assert.False(t, success) } func TestShouldUpdateBordSubscriptionAsModerator(t *testing.T) { - nameForUpdate := randSeq(10) - descriptionForUpdate := randSeq(10) + nameForUpdate := randSeq(10) + descriptionForUpdate := randSeq(10) - event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.boardUpdated(event, true) + _, success := bordSubscription.boardUpdated(event, true) - assert.Equal(t, nameForUpdate, bordSubscription.boardSettings.Name) - assert.Equal(t, descriptionForUpdate, bordSubscription.boardSettings.Description) - assert.True(t, success) + assert.Equal(t, nameForUpdate, bordSubscription.boardSettings.Name) + assert.Equal(t, descriptionForUpdate, bordSubscription.boardSettings.Description) + assert.True(t, success) } func TestShouldNotUpdateBordSubscriptionWithoutModeratorRights(t *testing.T) { - nameForUpdate := randSeq(10) - descriptionForUpdate := randSeq(10) + nameForUpdate := randSeq(10) + descriptionForUpdate := randSeq(10) - event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.boardUpdated(event, false) + _, success := bordSubscription.boardUpdated(event, false) - assert.Nil(t, bordSubscription.boardSettings.Name) - assert.Nil(t, bordSubscription.boardSettings.Description) - assert.True(t, success) + assert.Nil(t, bordSubscription.boardSettings.Name) + assert.Nil(t, bordSubscription.boardSettings.Description) + assert.True(t, success) } func TestShouldOnlyInsertLatestVotingInInitEventStatusClosed(t *testing.T) { - latestVotingId := uuid.New() - newestVotingId := uuid.New() - clientId := uuid.New() - client := sessions.User{ - ID: clientId, - } - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ModeratorRole, - User: client, - }, - }, - Votings: []*votings.Voting{ - buildVoting(latestVotingId, votings.Closed), - buildVoting(newestVotingId, votings.Closed), - }, - Votes: []*votings.Vote{ - buildVote(latestVotingId, uuid.New(), uuid.New()), - buildVote(newestVotingId, uuid.New(), uuid.New()), - }, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) + latestVotingId := uuid.New() + newestVotingId := uuid.New() + clientId := uuid.New() + client := users.User{ + ID: clientId, + } + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ModeratorRole, + ID: client.ID, + }, + }, + Votings: []*votings.Voting{ + buildVoting(latestVotingId, votings.Closed), + buildVoting(newestVotingId, votings.Closed), + }, + Votes: []*votings.Vote{ + buildVote(latestVotingId, uuid.New(), uuid.New()), + buildVote(newestVotingId, uuid.New(), uuid.New()), + }, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) } func TestShouldOnlyInsertLatestVotingInInitEventStatusOpen(t *testing.T) { - latestVotingId := uuid.New() - newestVotingId := uuid.New() - clientId := uuid.New() - client := sessions.User{ - ID: clientId, - } - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ModeratorRole, - User: client, - }, - }, - Votings: []*votings.Voting{ - buildVoting(latestVotingId, votings.Open), - buildVoting(newestVotingId, votings.Closed), - }, - Votes: []*votings.Vote{ - buildVote(latestVotingId, clientId, uuid.New()), - buildVote(newestVotingId, uuid.New(), uuid.New()), - }, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) + latestVotingId := uuid.New() + newestVotingId := uuid.New() + clientId := uuid.New() + client := users.User{ + ID: clientId, + } + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ModeratorRole, + ID: client.ID, + }, + }, + Votings: []*votings.Voting{ + buildVoting(latestVotingId, votings.Open), + buildVoting(newestVotingId, votings.Closed), + }, + Votes: []*votings.Vote{ + buildVote(latestVotingId, clientId, uuid.New()), + buildVote(newestVotingId, uuid.New(), uuid.New()), + }, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) } func TestShouldBeEmptyVotesInInitEventBecauseIdsDiffer(t *testing.T) { - clientId := uuid.New() - latestVotingId := uuid.New() - client := sessions.User{ - ID: clientId, - } - orgVoting := []*votings.Voting{ - buildVoting(latestVotingId, votings.Open), - buildVoting(uuid.New(), votings.Closed), - } - orgVote := []*votings.Vote{ - buildVote(uuid.New(), uuid.New(), uuid.New()), - buildVote(uuid.New(), uuid.New(), uuid.New()), - } - - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ModeratorRole, - User: client, - }, - }, - Votings: orgVoting, - Votes: orgVote, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Empty(t, updatedInitEvent.Data.Votes) - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) + clientId := uuid.New() + latestVotingId := uuid.New() + client := users.User{ + ID: clientId, + } + orgVoting := []*votings.Voting{ + buildVoting(latestVotingId, votings.Open), + buildVoting(uuid.New(), votings.Closed), + } + orgVote := []*votings.Vote{ + buildVote(uuid.New(), uuid.New(), uuid.New()), + buildVote(uuid.New(), uuid.New(), uuid.New()), + } + + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ModeratorRole, + ID: client.ID, + }, + }, + Votings: orgVoting, + Votes: orgVote, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Empty(t, updatedInitEvent.Data.Votes) + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) } func TestShouldCreateNewInitEventBecauseNoModeratorRightsWithVisibleVotes(t *testing.T) { - latestVotingId := uuid.New() - newestVotingId := uuid.New() - noteId := uuid.New() - columnId := uuid.New() - clientId := uuid.New() - client := sessions.User{ - ID: clientId, - } - nameForUpdate := randSeq(10) - descriptionForUpdate := randSeq(10) - - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - Columns: []*columns.Column{buildColumn(columnId, true)}, - Board: buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, true), - Notes: []*notes.Note{buildNote(noteId, columnId)}, - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ParticipantRole, - User: client, - }, - }, - Votings: []*votings.Voting{ - buildVoting(latestVotingId, votings.Open), - buildVoting(newestVotingId, votings.Closed), - }, - Votes: []*votings.Vote{ - buildVote(latestVotingId, clientId, noteId), - buildVote(newestVotingId, uuid.New(), uuid.New()), - }, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Equal(t, noteId, updatedInitEvent.Data.Votes[0].Note) - assert.Equal(t, clientId, updatedInitEvent.Data.Votes[0].User) + latestVotingId := uuid.New() + newestVotingId := uuid.New() + noteId := uuid.New() + columnId := uuid.New() + clientId := uuid.New() + client := users.User{ + ID: clientId, + } + nameForUpdate := randSeq(10) + descriptionForUpdate := randSeq(10) + + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + Columns: []*columns.Column{buildColumn(columnId, true)}, + Board: buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, true), + Notes: []*notes.Note{buildNote(noteId, columnId)}, + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ParticipantRole, + ID: client.ID, + }, + }, + Votings: []*votings.Voting{ + buildVoting(latestVotingId, votings.Open), + buildVoting(newestVotingId, votings.Closed), + }, + Votes: []*votings.Vote{ + buildVote(latestVotingId, clientId, noteId), + buildVote(newestVotingId, uuid.New(), uuid.New()), + }, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Equal(t, noteId, updatedInitEvent.Data.Votes[0].Note) + assert.Equal(t, clientId, updatedInitEvent.Data.Votes[0].User) } func TestShouldFailBecauseOfInvalidVoteData(t *testing.T) { - event := buildBoardEvent(*buildVote(uuid.New(), uuid.New(), uuid.New()), realtime.BoardEventVotesDeleted) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(*buildVote(uuid.New(), uuid.New(), uuid.New()), realtime.BoardEventVotesDeleted) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.votesDeleted(event, uuid.New()) + _, success := bordSubscription.votesDeleted(event, uuid.New()) - assert.False(t, success) + assert.False(t, success) } func TestShouldReturnEmptyVotesBecauseUserIdNotMatched(t *testing.T) { - event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), uuid.New(), uuid.New())}, realtime.BoardEventVotesDeleted) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), uuid.New(), uuid.New())}, realtime.BoardEventVotesDeleted) + bordSubscription := buildBordSubscription(boards.Public) - updatedBordEvent, success := bordSubscription.votesDeleted(event, uuid.New()) + updatedBordEvent, success := bordSubscription.votesDeleted(event, uuid.New()) - assert.True(t, success) - assert.Equal(t, 0, len(updatedBordEvent.Data.([]*votings.Vote))) + assert.True(t, success) + assert.Equal(t, 0, len(updatedBordEvent.Data.([]*votings.Vote))) } func TestVotesDeleted(t *testing.T) { - userId := uuid.New() - event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), userId, uuid.New())}, realtime.BoardEventVotesDeleted) - bordSubscription := buildBordSubscription(boards.Public) + userId := uuid.New() + event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), userId, uuid.New())}, realtime.BoardEventVotesDeleted) + bordSubscription := buildBordSubscription(boards.Public) - updatedBordEvent, success := bordSubscription.votesDeleted(event, userId) + updatedBordEvent, success := bordSubscription.votesDeleted(event, userId) - assert.True(t, success) - assert.Equal(t, 1, len(updatedBordEvent.Data.([]*votings.Vote))) + assert.True(t, success) + assert.Equal(t, 1, len(updatedBordEvent.Data.([]*votings.Vote))) } func buildNote(id uuid.UUID, columnId uuid.UUID) *notes.Note { - return ¬es.Note{ - ID: id, - Author: uuid.New(), - Text: "lorem in ipsum", - Edited: false, - Position: notes.NotePosition{ - Column: columnId, - Stack: uuid.NullUUID{ - UUID: uuid.New(), - Valid: true, - }, - Rank: 0, - }, - } + return ¬es.Note{ + ID: id, + Author: uuid.New(), + Text: "lorem in ipsum", + Edited: false, + Position: notes.NotePosition{ + Column: columnId, + Stack: uuid.NullUUID{ + UUID: uuid.New(), + Valid: true, + }, + Rank: 0, + }, + } } func buildColumn(id uuid.UUID, visible bool) *columns.Column { - return &columns.Column{ - ID: id, - Visible: visible, - } + return &columns.Column{ + ID: id, + Visible: visible, + } } func buildVote(votingId uuid.UUID, userId uuid.UUID, noteId uuid.UUID) *votings.Vote { - return &votings.Vote{ - Voting: votingId, - User: userId, - Note: noteId, - } + return &votings.Vote{ + Voting: votingId, + User: userId, + Note: noteId, + } } func buildVoting(id uuid.UUID, status votings.VotingStatus) *votings.Voting { - return &votings.Voting{ - ID: id, - Status: status, - } + return &votings.Voting{ + ID: id, + Status: status, + } } func buildBordSubscription(accessPolicy boards.AccessPolicy) BoardSubscription { - return BoardSubscription{ - subscription: nil, - clients: nil, - boardParticipants: nil, - boardSettings: buildBoardDto(nil, nil, accessPolicy, false), - boardColumns: nil, - boardNotes: nil, - boardReactions: nil, - } + return BoardSubscription{ + subscription: nil, + clients: nil, + boardParticipants: nil, + boardSettings: buildBoardDto(nil, nil, accessPolicy, false), + boardColumns: nil, + boardNotes: nil, + boardReactions: nil, + } } func buildBoardEvent(data interface{}, eventType realtime.BoardEventType) *realtime.BoardEvent { - return &realtime.BoardEvent{ - Type: eventType, - Data: data, - } + return &realtime.BoardEvent{ + Type: eventType, + Data: data, + } } func buildBoardDto(name *string, description *string, accessPolicy boards.AccessPolicy, showNotesOfOtherUsers bool) *boards.Board { - return &boards.Board{ - ID: uuid.UUID{}, - Name: name, - Description: description, - AccessPolicy: accessPolicy, - ShowAuthors: false, - ShowNotesOfOtherUsers: showNotesOfOtherUsers, - ShowNoteReactions: false, - AllowStacking: false, - IsLocked: false, - TimerStart: nil, - TimerEnd: nil, - SharedNote: uuid.NullUUID{}, - ShowVoting: uuid.NullUUID{}, - Passphrase: nil, - Salt: nil, - } + return &boards.Board{ + ID: uuid.UUID{}, + Name: name, + Description: description, + AccessPolicy: accessPolicy, + ShowAuthors: false, + ShowNotesOfOtherUsers: showNotesOfOtherUsers, + ShowNoteReactions: false, + AllowStacking: false, + IsLocked: false, + TimerStart: nil, + TimerEnd: nil, + SharedNote: uuid.NullUUID{}, + ShowVoting: uuid.NullUUID{}, + Passphrase: nil, + Salt: nil, + } } func randSeq(n int) *string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } - s := string(b) - return &s + s := string(b) + return &s } diff --git a/server/src/api/router.go b/server/src/api/router.go index 414d787611..4cb421d78a 100644 --- a/server/src/api/router.go +++ b/server/src/api/router.go @@ -3,6 +3,7 @@ package api import ( "net/http" "os" + "scrumlr.io/server/users" "time" "scrumlr.io/server/sessions" @@ -48,7 +49,7 @@ type Server struct { boards boards.BoardService columns columns.ColumnService votings votings.VotingService - users sessions.UserService + users users.UserService notes notes.NotesService reactions reactions.ReactionService sessions sessions.SessionService @@ -80,7 +81,7 @@ func New( boards boards.BoardService, columns columns.ColumnService, votings votings.VotingService, - users sessions.UserService, + users users.UserService, notes notes.NotesService, reactions reactions.ReactionService, sessions sessions.SessionService, diff --git a/server/src/api/users.go b/server/src/api/users.go index 83622158b8..395871225d 100644 --- a/server/src/api/users.go +++ b/server/src/api/users.go @@ -3,6 +3,7 @@ package api import ( "github.com/go-chi/chi/v5" "net/http" + "scrumlr.io/server/users" "github.com/go-chi/render" "github.com/google/uuid" @@ -56,7 +57,7 @@ func (s *Server) updateUser(w http.ResponseWriter, r *http.Request) { user := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) - var body sessions.UserUpdateRequest + var body users.UserUpdateRequest if err := render.Decode(r, &body); err != nil { span.SetStatus(codes.Error, "unable to decode body") span.RecordError(err) diff --git a/server/src/auth/auth.go b/server/src/auth/auth.go index 30857d4bbd..452bec3e69 100644 --- a/server/src/auth/auth.go +++ b/server/src/auth/auth.go @@ -9,7 +9,7 @@ import ( "io" "math" "net/http" - "scrumlr.io/server/sessions" + "scrumlr.io/server/users" "strings" "github.com/uptrace/bun" @@ -57,7 +57,7 @@ type AuthConfiguration struct { unsafeAuth *jwtauth.JWTAuth auth *jwtauth.JWTAuth database *bun.DB - userService sessions.UserService + userService users.UserService } type UserInformation struct { @@ -65,7 +65,7 @@ type UserInformation struct { Ident, Name, AvatarURL string } -func NewAuthConfiguration(providers map[string]AuthProviderConfiguration, unsafePrivateKey, privateKey string, database *bun.DB, userService sessions.UserService) (Auth, error) { +func NewAuthConfiguration(providers map[string]AuthProviderConfiguration, unsafePrivateKey, privateKey string, database *bun.DB, userService users.UserService) (Auth, error) { a := new(AuthConfiguration) a.providers = providers a.unsafePrivateKey = unsafePrivateKey diff --git a/server/src/serviceinitialize/service.go b/server/src/serviceinitialize/service.go index f39824ca6d..4d13ed59c2 100644 --- a/server/src/serviceinitialize/service.go +++ b/server/src/serviceinitialize/service.go @@ -2,6 +2,7 @@ package serviceinitialize import ( "net/http" + "scrumlr.io/server/users" "scrumlr.io/server/boards" "scrumlr.io/server/sessions" @@ -121,9 +122,9 @@ func (init *ServiceInitializer) InitializeWebsocket() sessionrequests.Websocket return websocket } -func (init *ServiceInitializer) InitializeUserService(sessionService sessions.SessionService) sessions.UserService { - userDb := sessions.NewUserDatabase(init.db) - userService := sessions.NewUserService(userDb, init.rt, sessionService) +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/dto.go b/server/src/sessionrequests/dto.go index c46ace9389..992929b943 100644 --- a/server/src/sessionrequests/dto.go +++ b/server/src/sessionrequests/dto.go @@ -2,13 +2,13 @@ package sessionrequests import ( "net/http" - "scrumlr.io/server/sessions" + "scrumlr.io/server/users" "github.com/google/uuid" ) type BoardSessionRequest struct { - User sessions.User `json:"user"` + User users.User `json:"user"` Status RequestStatus `json:"status"` } @@ -19,7 +19,7 @@ type BoardSessionRequestUpdate struct { } func (r *BoardSessionRequest) From(request DatabaseBoardSessionRequest) *BoardSessionRequest { - r.User = sessions.User{ + r.User = users.User{ ID: request.User, Name: request.Name, } From e34cc3fa6d9aacea1a0693265d34e7a3b4ca54e1 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 12 Sep 2025 13:37:07 +0200 Subject: [PATCH 05/16] clean up --- src/store/features/board/thunks.ts | 92 +++++++++++------------------- 1 file changed, 33 insertions(+), 59 deletions(-) diff --git a/src/store/features/board/thunks.ts b/src/store/features/board/thunks.ts index 5111420dde..49d9dffd67 100644 --- a/src/store/features/board/thunks.ts +++ b/src/store/features/board/thunks.ts @@ -55,25 +55,26 @@ export const leaveBoard = createAsyncThunk("board/leaveBoard", async () => { } }); -async function participantsWithUser(message: BoardInitEvent) { - const {participants} = message.data; - const participantsWithUser: Participant[] = []; - for (let i = 0; i < participants.length; i++) { - const user = await API.getUserById((participants[i] as unknown as ParticipantDTO).id); - participantsWithUser.push({ - connected: participants[i].connected, - raisedHand: participants[i].raisedHand, - ready: participants[i].ready, - role: participants[i].role, - showHiddenColumns: participants[i].showHiddenColumns, - user, - banned: participants[i].banned, - }); - } +const dtoToParticipant = async (dto: ParticipantDTO): Promise => { + const user: Auth = await API.getUserById(dto.id); + return { + user, + connected: dto.connected, + raisedHand: dto.raisedHand, + ready: dto.ready, + showHiddenColumns: dto.showHiddenColumns, + role: dto.role, + banned: dto.banned, + }; +}; - message.data.participants = participantsWithUser; - return participantsWithUser; -} +const mapParticipantsWithUsers = async (message: BoardInitEvent): Promise => { + const {participants} = message.data; + const asDTOs = participants as unknown as ParticipantDTO[]; + const mapped = await Promise.all(asDTOs.map(dtoToParticipant)); + message.data.participants = mapped; + return mapped; +}; // generic args: Date: Fri, 12 Sep 2025 13:58:28 +0200 Subject: [PATCH 06/16] renaming of session files --- server/src/sessions/{api_sessions.go => api.go} | 0 server/src/sessions/{database_sessions.go => database.go} | 0 server/src/sessions/{database_dto_sessions.go => database_dto.go} | 0 .../src/sessions/{database_session_test.go => database_test.go} | 0 server/src/sessions/{dto_sessions.go => dto.go} | 0 server/src/sessions/{service_sessions.go => service.go} | 0 server/src/sessions/{service_sessions_test.go => service_test.go} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename server/src/sessions/{api_sessions.go => api.go} (100%) rename server/src/sessions/{database_sessions.go => database.go} (100%) rename server/src/sessions/{database_dto_sessions.go => database_dto.go} (100%) rename server/src/sessions/{database_session_test.go => database_test.go} (100%) rename server/src/sessions/{dto_sessions.go => dto.go} (100%) rename server/src/sessions/{service_sessions.go => service.go} (100%) rename server/src/sessions/{service_sessions_test.go => service_test.go} (100%) diff --git a/server/src/sessions/api_sessions.go b/server/src/sessions/api.go similarity index 100% rename from server/src/sessions/api_sessions.go rename to server/src/sessions/api.go diff --git a/server/src/sessions/database_sessions.go b/server/src/sessions/database.go similarity index 100% rename from server/src/sessions/database_sessions.go rename to server/src/sessions/database.go diff --git a/server/src/sessions/database_dto_sessions.go b/server/src/sessions/database_dto.go similarity index 100% rename from server/src/sessions/database_dto_sessions.go rename to server/src/sessions/database_dto.go diff --git a/server/src/sessions/database_session_test.go b/server/src/sessions/database_test.go similarity index 100% rename from server/src/sessions/database_session_test.go rename to server/src/sessions/database_test.go diff --git a/server/src/sessions/dto_sessions.go b/server/src/sessions/dto.go similarity index 100% rename from server/src/sessions/dto_sessions.go rename to server/src/sessions/dto.go diff --git a/server/src/sessions/service_sessions.go b/server/src/sessions/service.go similarity index 100% rename from server/src/sessions/service_sessions.go rename to server/src/sessions/service.go diff --git a/server/src/sessions/service_sessions_test.go b/server/src/sessions/service_test.go similarity index 100% rename from server/src/sessions/service_sessions_test.go rename to server/src/sessions/service_test.go From 207eb3a240077b1c27433559e47fe90cf91d3fa9 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 26 Sep 2025 09:29:47 +0200 Subject: [PATCH 07/16] rebase --- server/src/api/boards.go | 4 +- server/src/api/login.go | 277 ++++++----- server/src/api/router.go | 685 ++++++++++++++-------------- server/src/sessions/otel_counter.go | 94 ++-- server/src/users/service.go | 306 ++++++------- server/src/users/service_test.go | 2 +- 6 files changed, 684 insertions(+), 684 deletions(-) diff --git a/server/src/api/boards.go b/server/src/api/boards.go index 2b10e089d9..031dae7787 100644 --- a/server/src/api/boards.go +++ b/server/src/api/boards.go @@ -477,8 +477,8 @@ func (s *Server) exportBoard(w http.ResponseWriter, r *http.Request) { author := note.Author.String() for _, session := range fullBoard.BoardSessions { - if session.User.ID == note.Author { - user, _ := s.users.Get(ctx, session.User.ID) // TODO handle error + if session.ID == note.Author { + user, _ := s.users.Get(ctx, session.ID) // TODO handle error author = user.Name } } diff --git a/server/src/api/login.go b/server/src/api/login.go index eb467669ee..c2e810ccce 100644 --- a/server/src/api/login.go +++ b/server/src/api/login.go @@ -1,164 +1,163 @@ package api import ( - "fmt" - "math" - "net/http" - "strings" - "time" - - "go.opentelemetry.io/otel/codes" - "scrumlr.io/server/sessions" - - "github.com/go-chi/render" - "github.com/markbates/goth/gothic" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" + "fmt" + "math" + "net/http" + "scrumlr.io/server/users" + "strings" + "time" + + "github.com/go-chi/render" + "github.com/markbates/goth/gothic" + "go.opentelemetry.io/otel/codes" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" ) //var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/api") // AnonymousSignUpRequest represents the request to create a new anonymous user. type AnonymousSignUpRequest struct { - // The display name of the user. - Name string + // The display name of the user. + Name string } // signInAnonymously create a new anonymous user func (s *Server) signInAnonymously(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.signin.anonymous") - defer span.End() - log := logger.FromContext(ctx) - - var body AnonymousSignUpRequest - 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) - w.WriteHeader(http.StatusBadRequest) - return - } - - user, err := s.users.CreateAnonymous(ctx, body.Name) - if err != nil { - span.SetStatus(codes.Error, "failed to create anonyoums user") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - tokenString, err := s.auth.Sign(map[string]interface{}{"id": user.ID}) - if err != nil { - span.SetStatus(codes.Error, "failed to generate token string") - span.RecordError(err) - log.Errorw("unable to generate token string", "err", err) - common.Throw(w, r, common.InternalServerError) - return - } - - cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", HttpOnly: true, MaxAge: math.MaxInt32} - common.SealCookie(r, &cookie) - http.SetCookie(w, &cookie) - - render.Status(r, http.StatusCreated) - render.Respond(w, r, user) + ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.signin.anonymous") + defer span.End() + log := logger.FromContext(ctx) + + var body AnonymousSignUpRequest + 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) + w.WriteHeader(http.StatusBadRequest) + return + } + + user, err := s.users.CreateAnonymous(ctx, body.Name) + if err != nil { + span.SetStatus(codes.Error, "failed to create anonyoums user") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + tokenString, err := s.auth.Sign(map[string]interface{}{"id": user.ID}) + if err != nil { + span.SetStatus(codes.Error, "failed to generate token string") + span.RecordError(err) + log.Errorw("unable to generate token string", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", HttpOnly: true, MaxAge: math.MaxInt32} + common.SealCookie(r, &cookie) + http.SetCookie(w, &cookie) + + render.Status(r, http.StatusCreated) + render.Respond(w, r, user) } func (s *Server) logout(w http.ResponseWriter, r *http.Request) { - _, span := tracer.Start(r.Context(), "scrumlr.login.api.logout") - defer span.End() - - cookie := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} - common.SealCookie(r, &cookie) - http.SetCookie(w, &cookie) - - if common.GetHostWithoutPort(r) != common.GetTopLevelHost(r) { - cookieWithSubdomain := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} - common.SealCookie(r, &cookieWithSubdomain) - cookieWithSubdomain.Domain = common.GetHostWithoutPort(r) - http.SetCookie(w, &cookieWithSubdomain) - } - - render.Status(r, http.StatusNoContent) - render.Respond(w, r, nil) + _, span := tracer.Start(r.Context(), "scrumlr.login.api.logout") + defer span.End() + + cookie := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} + common.SealCookie(r, &cookie) + http.SetCookie(w, &cookie) + + if common.GetHostWithoutPort(r) != common.GetTopLevelHost(r) { + cookieWithSubdomain := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} + common.SealCookie(r, &cookieWithSubdomain) + cookieWithSubdomain.Domain = common.GetHostWithoutPort(r) + http.SetCookie(w, &cookieWithSubdomain) + } + + render.Status(r, http.StatusNoContent) + render.Respond(w, r, nil) } // beginAuthProviderVerification will redirect the user to the specified auth provider consent page func (s *Server) beginAuthProviderVerification(w http.ResponseWriter, r *http.Request) { - gothic.BeginAuthHandler(w, r) + gothic.BeginAuthHandler(w, r) } // verifyAuthProviderCallback will verify the auth provider call, create or update a user and redirect to the page provider with the state func (s *Server) verifyAuthProviderCallback(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.verify_auth_provider") - defer span.End() - log := logger.FromContext(ctx) - - externalUser, err := gothic.CompleteUserAuth(w, r) - if err != nil { - span.SetStatus(codes.Error, "failed to complete user auth") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("could not complete user auth", "err", err) - return - } - - provider, err := common.NewAccountType(externalUser.Provider) - if err != nil { - span.SetStatus(codes.Error, "user provider not supported") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("unsupported user provider", "err", err) - return - } - - userInfo, err := s.auth.ExtractUserInformation(provider, &externalUser) - if err != nil { - span.SetStatus(codes.Error, "insufficient user information from external auth source") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("insufficient user information from external auth source", "err", err) - return - } - - var internalUser *sessions.User - switch provider { - case common.Google: - internalUser, err = s.users.CreateGoogleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.GitHub: - internalUser, err = s.users.CreateGitHubUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.Microsoft: - internalUser, err = s.users.CreateMicrosoftUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.AzureAd: - internalUser, err = s.users.CreateAzureAdUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.Apple: - internalUser, err = s.users.CreateAppleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.TypeOIDC: - internalUser, err = s.users.CreateOIDCUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - } - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("could not create user", "err", err) - return - } - - tokenString, _ := s.auth.Sign(map[string]interface{}{"id": internalUser.ID}) - cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", Expires: time.Now().AddDate(0, 0, 3*7)} - common.SealCookie(r, &cookie) - http.SetCookie(w, &cookie) - - state := gothic.GetState(r) - stateSplit := strings.Split(state, "__") - if len(stateSplit) > 1 { - w.Header().Set("Location", stateSplit[1]) - w.WriteHeader(http.StatusSeeOther) - } - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/", common.GetProtocol(r), r.Host)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/", common.GetProtocol(r), r.Host, s.basePath)) - } - w.WriteHeader(http.StatusSeeOther) + ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.verify_auth_provider") + defer span.End() + log := logger.FromContext(ctx) + + externalUser, err := gothic.CompleteUserAuth(w, r) + if err != nil { + span.SetStatus(codes.Error, "failed to complete user auth") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("could not complete user auth", "err", err) + return + } + + provider, err := common.NewAccountType(externalUser.Provider) + if err != nil { + span.SetStatus(codes.Error, "user provider not supported") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("unsupported user provider", "err", err) + return + } + + userInfo, err := s.auth.ExtractUserInformation(provider, &externalUser) + if err != nil { + span.SetStatus(codes.Error, "insufficient user information from external auth source") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("insufficient user information from external auth source", "err", err) + return + } + + var internalUser *users.User + switch provider { + case common.Google: + internalUser, err = s.users.CreateGoogleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.GitHub: + internalUser, err = s.users.CreateGitHubUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.Microsoft: + internalUser, err = s.users.CreateMicrosoftUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.AzureAd: + internalUser, err = s.users.CreateAzureAdUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.Apple: + internalUser, err = s.users.CreateAppleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.TypeOIDC: + internalUser, err = s.users.CreateOIDCUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + } + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("could not create user", "err", err) + return + } + + tokenString, _ := s.auth.Sign(map[string]interface{}{"id": internalUser.ID}) + cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", Expires: time.Now().AddDate(0, 0, 3*7)} + common.SealCookie(r, &cookie) + http.SetCookie(w, &cookie) + + state := gothic.GetState(r) + stateSplit := strings.Split(state, "__") + if len(stateSplit) > 1 { + w.Header().Set("Location", stateSplit[1]) + w.WriteHeader(http.StatusSeeOther) + } + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/", common.GetProtocol(r), r.Host)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/", common.GetProtocol(r), r.Host, s.basePath)) + } + w.WriteHeader(http.StatusSeeOther) } diff --git a/server/src/api/router.go b/server/src/api/router.go index 4cb421d78a..0a8ed1efc2 100644 --- a/server/src/api/router.go +++ b/server/src/api/router.go @@ -1,390 +1,391 @@ package api import ( - "net/http" - "os" - "scrumlr.io/server/users" - "time" - - "scrumlr.io/server/sessions" - - "scrumlr.io/server/boards" - - "scrumlr.io/server/votings" - - "scrumlr.io/server/boardreactions" - "scrumlr.io/server/boardtemplates" - "scrumlr.io/server/columns" - "scrumlr.io/server/columntemplates" - "scrumlr.io/server/notes" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/markbates/goth/gothic" - - "github.com/go-chi/cors" - "github.com/go-chi/httprate" - "github.com/go-chi/render" - "github.com/google/uuid" - gorillaSessions "github.com/gorilla/sessions" - "github.com/gorilla/websocket" - - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - - "scrumlr.io/server/auth" - "scrumlr.io/server/feedback" - "scrumlr.io/server/health" - "scrumlr.io/server/logger" - "scrumlr.io/server/reactions" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessionrequests" + "net/http" + "os" + "scrumlr.io/server/users" + "time" + + "scrumlr.io/server/sessions" + + "scrumlr.io/server/boards" + + "scrumlr.io/server/votings" + + "scrumlr.io/server/boardreactions" + "scrumlr.io/server/boardtemplates" + "scrumlr.io/server/columns" + "scrumlr.io/server/columntemplates" + "scrumlr.io/server/notes" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/markbates/goth/gothic" + + "github.com/go-chi/cors" + "github.com/go-chi/httprate" + "github.com/go-chi/render" + "github.com/google/uuid" + gorillaSessions "github.com/gorilla/sessions" + "github.com/gorilla/websocket" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "scrumlr.io/server/auth" + "scrumlr.io/server/feedback" + "scrumlr.io/server/health" + "scrumlr.io/server/logger" + "scrumlr.io/server/reactions" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessionrequests" ) type Server struct { - basePath string - - realtime *realtime.Broker - auth auth.Auth - - boards boards.BoardService - columns columns.ColumnService - votings votings.VotingService - users users.UserService - notes notes.NotesService - reactions reactions.ReactionService - sessions sessions.SessionService - sessionRequests sessionrequests.SessionRequestService - health health.HealthService - feedback feedback.FeedbackService - boardReactions boardreactions.BoardReactionService - boardTemplates boardtemplates.BoardTemplateService - columntemplates columntemplates.ColumnTemplateService - - upgrader websocket.Upgrader - - // map of boardSubscriptions with maps of users with connections - boardSubscriptions map[uuid.UUID]*BoardSubscription - boardSessionRequestSubscriptions map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription - - // note: if more options come with time, it might be sensible to wrap them into a struct - anonymousLoginDisabled bool - allowAnonymousCustomTemplates bool - allowAnonymousBoardCreation bool - experimentalFileSystemStore bool + basePath string + + realtime *realtime.Broker + auth auth.Auth + + boards boards.BoardService + columns columns.ColumnService + votings votings.VotingService + users users.UserService + notes notes.NotesService + reactions reactions.ReactionService + sessions sessions.SessionService + sessionRequests sessionrequests.SessionRequestService + health health.HealthService + feedback feedback.FeedbackService + boardReactions boardreactions.BoardReactionService + boardTemplates boardtemplates.BoardTemplateService + columntemplates columntemplates.ColumnTemplateService + + upgrader websocket.Upgrader + + // map of boardSubscriptions with maps of users with connections + boardSubscriptions map[uuid.UUID]*BoardSubscription + boardSessionRequestSubscriptions map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription + + // note: if more options come with time, it might be sensible to wrap them into a struct + anonymousLoginDisabled bool + allowAnonymousCustomTemplates bool + allowAnonymousBoardCreation bool + experimentalFileSystemStore bool } func New( - basePath string, - rt *realtime.Broker, - auth auth.Auth, - - boards boards.BoardService, - columns columns.ColumnService, - votings votings.VotingService, - users users.UserService, - notes notes.NotesService, - reactions reactions.ReactionService, - sessions sessions.SessionService, - sessionRequests sessionrequests.SessionRequestService, - health health.HealthService, - feedback feedback.FeedbackService, - boardReactions boardreactions.BoardReactionService, - boardTemplates boardtemplates.BoardTemplateService, - columntemplates columntemplates.ColumnTemplateService, - - verbose bool, - checkOrigin bool, - anonymousLoginDisabled bool, - allowAnonymousCustomTemplates bool, - allowAnonymousBoardCreation bool, - experimentalFileSystemStore bool, + basePath string, + rt *realtime.Broker, + auth auth.Auth, + + boards boards.BoardService, + columns columns.ColumnService, + votings votings.VotingService, + users users.UserService, + notes notes.NotesService, + reactions reactions.ReactionService, + sessions sessions.SessionService, + sessionRequests sessionrequests.SessionRequestService, + health health.HealthService, + feedback feedback.FeedbackService, + boardReactions boardreactions.BoardReactionService, + boardTemplates boardtemplates.BoardTemplateService, + columntemplates columntemplates.ColumnTemplateService, + + verbose bool, + checkOrigin bool, + anonymousLoginDisabled bool, + allowAnonymousCustomTemplates bool, + allowAnonymousBoardCreation bool, + experimentalFileSystemStore bool, ) chi.Router { - r := chi.NewRouter() - r.Use(middleware.Recoverer) - r.Use(middleware.RequestID) - r.Use(logger.RequestIDMiddleware) - r.Use(render.SetContentType(render.ContentTypeJSON)) - r.Use(otelhttp.NewMiddleware("scrumlr")) - - if !checkOrigin { - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"https://*", "http://*"}, - - // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link", "Set-Cookie"}, - AllowCredentials: true, - MaxAge: 300, - })) - } - - if verbose { - r.Use(logger.ChiZapLogger()) - } - - s := Server{ - basePath: basePath, - realtime: rt, - boardSubscriptions: make(map[uuid.UUID]*BoardSubscription), - boardSessionRequestSubscriptions: make(map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription), - auth: auth, - boards: boards, - columns: columns, - votings: votings, - users: users, - notes: notes, - reactions: reactions, - sessions: sessions, - sessionRequests: sessionRequests, - health: health, - feedback: feedback, - boardReactions: boardReactions, - boardTemplates: boardTemplates, - columntemplates: columntemplates, - - anonymousLoginDisabled: anonymousLoginDisabled, - allowAnonymousCustomTemplates: allowAnonymousCustomTemplates, - allowAnonymousBoardCreation: allowAnonymousBoardCreation, - experimentalFileSystemStore: experimentalFileSystemStore, - } - - // initialize websocket upgrader with origin check depending on options - s.upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - } - - // if enabled, this experimental feature allows for larger session cookies *during OAuth authentication* by storing them in a file store. - // this might be required when using some OIDC providers which exceed the 4KB limit. - // see https://github.com/markbates/goth/pull/141 - if s.experimentalFileSystemStore { - logger.Get().Infow("using experimental file system store") - store := gorillaSessions.NewFilesystemStore(os.TempDir(), []byte("scrumlr.io")) - store.MaxLength(0x8000) // 32KB should be plenty of space - gothic.Store = store - } - - if checkOrigin { - s.upgrader.CheckOrigin = nil - } else { - s.upgrader.CheckOrigin = func(r *http.Request) bool { - return true - } - } - if s.basePath == "/" { - s.publicRoutes(r) - s.protectedRoutes(r) - } else { - r.Route(s.basePath, func(router chi.Router) { - s.publicRoutes(router) - s.protectedRoutes(router) - }) - } - return r + r := chi.NewRouter() + r.Use(middleware.Recoverer) + r.Use(middleware.RequestID) + r.Use(logger.RequestIDMiddleware) + r.Use(render.SetContentType(render.ContentTypeJSON)) + r.Use(otelhttp.NewMiddleware("scrumlr")) + + if !checkOrigin { + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link", "Set-Cookie"}, + AllowCredentials: true, + MaxAge: 300, + })) + } + + if verbose { + r.Use(logger.ChiZapLogger()) + } + + s := Server{ + basePath: basePath, + realtime: rt, + boardSubscriptions: make(map[uuid.UUID]*BoardSubscription), + boardSessionRequestSubscriptions: make(map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription), + auth: auth, + boards: boards, + columns: columns, + votings: votings, + users: users, + notes: notes, + reactions: reactions, + sessions: sessions, + sessionRequests: sessionRequests, + health: health, + feedback: feedback, + boardReactions: boardReactions, + boardTemplates: boardTemplates, + columntemplates: columntemplates, + + anonymousLoginDisabled: anonymousLoginDisabled, + allowAnonymousCustomTemplates: allowAnonymousCustomTemplates, + allowAnonymousBoardCreation: allowAnonymousBoardCreation, + experimentalFileSystemStore: experimentalFileSystemStore, + } + + // initialize websocket upgrader with origin check depending on options + s.upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + // if enabled, this experimental feature allows for larger session cookies *during OAuth authentication* by storing them in a file store. + // this might be required when using some OIDC providers which exceed the 4KB limit. + // see https://github.com/markbates/goth/pull/141 + if s.experimentalFileSystemStore { + logger.Get().Infow("using experimental file system store") + store := gorillaSessions.NewFilesystemStore(os.TempDir(), []byte("scrumlr.io")) + store.MaxLength(0x8000) // 32KB should be plenty of space + gothic.Store = store + } + + if checkOrigin { + s.upgrader.CheckOrigin = nil + } else { + s.upgrader.CheckOrigin = func(r *http.Request) bool { + return true + } + } + if s.basePath == "/" { + s.publicRoutes(r) + s.protectedRoutes(r) + } else { + r.Route(s.basePath, func(router chi.Router) { + s.publicRoutes(router) + s.protectedRoutes(router) + }) + } + return r } func (s *Server) publicRoutes(r chi.Router) chi.Router { - return r.Group(func(r chi.Router) { - r.Get("/info", s.getServerInfo) - r.Get("/health", s.healthCheck) - r.Post("/feedback", s.createFeedback) - r.Route("/login", func(r chi.Router) { - r.Delete("/", s.logout) - r.With(s.AnonymousLoginDisabledContext).Post("/anonymous", s.signInAnonymously) - - r.Route("/{provider}", func(r chi.Router) { - r.Get("/", s.beginAuthProviderVerification) - r.Get("/callback", s.verifyAuthProviderCallback) - }) - }) - }) + return r.Group(func(r chi.Router) { + r.Get("/info", s.getServerInfo) + r.Get("/health", s.healthCheck) + r.Post("/feedback", s.createFeedback) + r.Route("/login", func(r chi.Router) { + r.Delete("/", s.logout) + r.With(s.AnonymousLoginDisabledContext).Post("/anonymous", s.signInAnonymously) + + r.Route("/{provider}", func(r chi.Router) { + r.Get("/", s.beginAuthProviderVerification) + r.Get("/callback", s.verifyAuthProviderCallback) + }) + }) + }) } func (s *Server) protectedRoutes(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(s.auth.Verifier()) - r.Use(s.auth.Authenticator()) - r.Use(auth.AuthContext) - - r.Route("/templates", func(r chi.Router) { - r.Use(s.BoardTemplateRateLimiter) - r.Use(s.AnonymousCustomTemplateCreationContext) - - r.Post("/", s.createBoardTemplate) - r.Get("/", s.getBoardTemplates) - - r.Route("/{id}", func(r chi.Router) { - r.Use(s.BoardTemplateContext) - - r.Get("/", s.getBoardTemplate) - r.Put("/", s.updateBoardTemplate) - r.Delete("/", s.deleteBoardTemplate) - - r.Route("/columns", func(r chi.Router) { - r.Post("/", s.createColumnTemplate) - r.Get("/", s.getColumnTemplates) - - r.Route("/{columnTemplate}", func(r chi.Router) { - r.Use(s.ColumnTemplateContext) - - r.Get("/", s.getColumnTemplate) - r.Put("/", s.updateColumnTemplate) - r.Delete("/", s.deleteColumnTemplate) - }) - }) - }) - }) - - r.With(s.AnonymousBoardCreationContext).Post("/boards", s.createBoard) - r.With(s.AnonymousBoardCreationContext).Post("/import", s.importBoard) - r.Get("/boards", s.getBoards) - r.Route("/boards/{id}", func(r chi.Router) { - r.With(s.BoardParticipantContext).Get("/", s.getBoard) - r.With(s.BoardParticipantContext).Get("/export", s.exportBoard) - r.With(s.BoardModeratorContext).Post("/timer", s.setTimer) - r.With(s.BoardModeratorContext).Delete("/timer", s.deleteTimer) - r.With(s.BoardModeratorContext).Post("/timer/increment", s.incrementTimer) - r.With(s.BoardModeratorContext).Put("/", s.updateBoard) - r.With(s.BoardModeratorContext).Delete("/", s.deleteBoard) - - s.initBoardSessionRequestResources(r) - s.initBoardSessionResources(r) - s.initColumnResources(r) - s.initNoteResources(r) - s.initReactionResources(r) - s.initVotingResources(r) - s.initVoteResources(r) - s.initBoardReactionResources(r) - }) - - r.Route("/user", func(r chi.Router) { - r.Get("/", s.getUser) - r.Put("/", s.updateUser) - }) - }) + r.Group(func(r chi.Router) { + r.Use(s.auth.Verifier()) + r.Use(s.auth.Authenticator()) + r.Use(auth.AuthContext) + + r.Route("/templates", func(r chi.Router) { + r.Use(s.BoardTemplateRateLimiter) + r.Use(s.AnonymousCustomTemplateCreationContext) + + r.Post("/", s.createBoardTemplate) + r.Get("/", s.getBoardTemplates) + + r.Route("/{id}", func(r chi.Router) { + r.Use(s.BoardTemplateContext) + + r.Get("/", s.getBoardTemplate) + r.Put("/", s.updateBoardTemplate) + r.Delete("/", s.deleteBoardTemplate) + + r.Route("/columns", func(r chi.Router) { + r.Post("/", s.createColumnTemplate) + r.Get("/", s.getColumnTemplates) + + r.Route("/{columnTemplate}", func(r chi.Router) { + r.Use(s.ColumnTemplateContext) + + r.Get("/", s.getColumnTemplate) + r.Put("/", s.updateColumnTemplate) + r.Delete("/", s.deleteColumnTemplate) + }) + }) + }) + }) + + r.With(s.AnonymousBoardCreationContext).Post("/boards", s.createBoard) + r.With(s.AnonymousBoardCreationContext).Post("/import", s.importBoard) + r.Get("/boards", s.getBoards) + r.Route("/boards/{id}", func(r chi.Router) { + r.With(s.BoardParticipantContext).Get("/", s.getBoard) + r.With(s.BoardParticipantContext).Get("/export", s.exportBoard) + r.With(s.BoardModeratorContext).Post("/timer", s.setTimer) + r.With(s.BoardModeratorContext).Delete("/timer", s.deleteTimer) + r.With(s.BoardModeratorContext).Post("/timer/increment", s.incrementTimer) + r.With(s.BoardModeratorContext).Put("/", s.updateBoard) + r.With(s.BoardModeratorContext).Delete("/", s.deleteBoard) + + s.initBoardSessionRequestResources(r) + s.initBoardSessionResources(r) + s.initColumnResources(r) + s.initNoteResources(r) + s.initReactionResources(r) + s.initVotingResources(r) + s.initVoteResources(r) + s.initBoardReactionResources(r) + }) + + r.Route("/user", func(r chi.Router) { + r.Get("/", s.getUser) + r.Put("/", s.updateUser) + r.Get("/{user}", s.getUserByID) + }) + }) } func (s *Server) initVoteResources(r chi.Router) { - r.Route("/votes", func(r chi.Router) { - r.Use(s.BoardParticipantContext) - r.Get("/", s.getVotes) - - r.Group(func(r chi.Router) { - r.Use(s.BoardEditableContext) - r.Post("/", s.addVote) - r.Delete("/", s.removeVote) - }) - }) + r.Route("/votes", func(r chi.Router) { + r.Use(s.BoardParticipantContext) + r.Get("/", s.getVotes) + + r.Group(func(r chi.Router) { + r.Use(s.BoardEditableContext) + r.Post("/", s.addVote) + r.Delete("/", s.removeVote) + }) + }) } func (s *Server) initVotingResources(r chi.Router) { - r.Route("/votings", func(r chi.Router) { - r.With(s.BoardParticipantContext).Get("/", s.getVotings) - r.With(s.BoardModeratorContext).Post("/", s.createVoting) - r.With(s.BoardModeratorContext).Put("/", s.updateVoting) - - r.Route("/{voting}", func(r chi.Router) { - r.Use(s.VotingContext) - r.With(s.BoardParticipantContext).Get("/", s.getVoting) - r.With(s.BoardModeratorContext).Put("/", s.updateVoting) - }) - }) + r.Route("/votings", func(r chi.Router) { + r.With(s.BoardParticipantContext).Get("/", s.getVotings) + r.With(s.BoardModeratorContext).Post("/", s.createVoting) + r.With(s.BoardModeratorContext).Put("/", s.updateVoting) + + r.Route("/{voting}", func(r chi.Router) { + r.Use(s.VotingContext) + r.With(s.BoardParticipantContext).Get("/", s.getVoting) + r.With(s.BoardModeratorContext).Put("/", s.updateVoting) + }) + }) } func (s *Server) initBoardSessionResources(r chi.Router) { - r.Route("/participants", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(httprate.Limit( - 3, - 5*time.Second, - httprate.WithKeyFuncs(httprate.KeyByIP), - httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusTooManyRequests) - _, err := w.Write([]byte(`{"error": "Too many requests"}`)) - if err != nil { - log := logger.FromRequest(r) - log.Errorw("Could not write error", "error", err) - return - } - }), - )) - - 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.Route("/participants", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(httprate.Limit( + 3, + 5*time.Second, + httprate.WithKeyFuncs(httprate.KeyByIP), + httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTooManyRequests) + _, err := w.Write([]byte(`{"error": "Too many requests"}`)) + if err != nil { + log := logger.FromRequest(r) + log.Errorw("Could not write error", "error", err) + return + } + }), + )) + + 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) + }) + }) } func (s *Server) initBoardSessionRequestResources(r chi.Router) { - r.Route("/requests", func(r chi.Router) { - r.With(s.BoardModeratorContext).Get("/", s.getBoardSessionRequests) - r.With(s.BoardCandidateContext).Get("/{user}", s.getBoardSessionRequest) - r.With(s.BoardModeratorContext).Put("/{user}", s.updateBoardSessionRequest) - }) + r.Route("/requests", func(r chi.Router) { + r.With(s.BoardModeratorContext).Get("/", s.getBoardSessionRequests) + r.With(s.BoardCandidateContext).Get("/{user}", s.getBoardSessionRequest) + r.With(s.BoardModeratorContext).Put("/{user}", s.updateBoardSessionRequest) + }) } func (s *Server) initColumnResources(r chi.Router) { - r.Route("/columns", func(r chi.Router) { - r.With(s.BoardParticipantContext).Get("/", s.getColumns) - r.With(s.BoardModeratorContext).Post("/", s.createColumn) - - r.Route("/{column}", func(r chi.Router) { - r.Use(s.ColumnContext) - - r.With(s.BoardParticipantContext).Get("/", s.getColumn) - r.With(s.BoardModeratorContext).Put("/", s.updateColumn) - r.With(s.BoardModeratorContext).Delete("/", s.deleteColumn) - }) - }) + r.Route("/columns", func(r chi.Router) { + r.With(s.BoardParticipantContext).Get("/", s.getColumns) + r.With(s.BoardModeratorContext).Post("/", s.createColumn) + + r.Route("/{column}", func(r chi.Router) { + r.Use(s.ColumnContext) + + r.With(s.BoardParticipantContext).Get("/", s.getColumn) + r.With(s.BoardModeratorContext).Put("/", s.updateColumn) + r.With(s.BoardModeratorContext).Delete("/", s.deleteColumn) + }) + }) } func (s *Server) initNoteResources(r chi.Router) { - r.Route("/notes", func(r chi.Router) { - r.Use(s.BoardParticipantContext) + r.Route("/notes", func(r chi.Router) { + r.Use(s.BoardParticipantContext) - r.Get("/", s.getNotes) - r.With(s.BoardEditableContext).Post("/", s.createNote) + r.Get("/", s.getNotes) + r.With(s.BoardEditableContext).Post("/", s.createNote) - r.Route("/{note}", func(r chi.Router) { - r.Use(s.NoteContext) + r.Route("/{note}", func(r chi.Router) { + r.Use(s.NoteContext) - r.Get("/", s.getNote) - r.With(s.BoardEditableContext).Put("/", s.updateNote) - r.With(s.BoardEditableContext).Delete("/", s.deleteNote) - }) - }) + r.Get("/", s.getNote) + r.With(s.BoardEditableContext).Put("/", s.updateNote) + r.With(s.BoardEditableContext).Delete("/", s.deleteNote) + }) + }) } func (s *Server) initReactionResources(r chi.Router) { - r.Route("/reactions", func(r chi.Router) { - r.Use(s.BoardParticipantContext) + r.Route("/reactions", func(r chi.Router) { + r.Use(s.BoardParticipantContext) - r.Get("/", s.getReactions) - r.With(s.BoardEditableContext).Post("/", s.createReaction) + r.Get("/", s.getReactions) + r.With(s.BoardEditableContext).Post("/", s.createReaction) - r.Route("/{reaction}", func(r chi.Router) { - r.Use(s.ReactionContext) + r.Route("/{reaction}", func(r chi.Router) { + r.Use(s.ReactionContext) - r.Get("/", s.getReaction) - r.With(s.BoardEditableContext).Delete("/", s.removeReaction) - r.With(s.BoardEditableContext).Put("/", s.updateReaction) - }) - }) + r.Get("/", s.getReaction) + r.With(s.BoardEditableContext).Delete("/", s.removeReaction) + r.With(s.BoardEditableContext).Put("/", s.updateReaction) + }) + }) } func (s *Server) initBoardReactionResources(r chi.Router) { - r.Route("/board-reactions", func(r chi.Router) { - r.Use(s.BoardParticipantContext) + r.Route("/board-reactions", func(r chi.Router) { + r.Use(s.BoardParticipantContext) - r.Post("/", s.createBoardReaction) - }) + r.Post("/", s.createBoardReaction) + }) } diff --git a/server/src/sessions/otel_counter.go b/server/src/sessions/otel_counter.go index e9c4d51085..e5f03bb433 100644 --- a/server/src/sessions/otel_counter.go +++ b/server/src/sessions/otel_counter.go @@ -20,50 +20,50 @@ var bannedSessionsCounter, _ = meter.Int64Counter( metric.WithUnit("sessions"), ) -var userCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.created.counter", - metric.WithDescription("Number of created users"), - metric.WithUnit("users"), -) - -var anonymousUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.anonymous.created.counter", - metric.WithDescription("Number of anonymous users created"), - metric.WithUnit("users"), -) - -var appleUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.aplle.created.counter", - metric.WithDescription("Number of apple users created"), - metric.WithUnit("users"), -) - -var azureAdUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.azuread.created.counter", - metric.WithDescription("Number of azuread users created"), - metric.WithUnit("users"), -) - -var githubUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.github.created.counter", - metric.WithDescription("Number of github users created"), - metric.WithUnit("users"), -) - -var googleUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.google.created.counter", - metric.WithDescription("Number of google users created"), - metric.WithUnit("users"), -) - -var microsoftUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.microsoft.created.counter", - metric.WithDescription("Number of anonymous users created"), - metric.WithUnit("users"), -) - -var oicdUserCreatedCounter, _ = userMeter.Int64Counter( - "scrumlr.users.oicd.created.counter", - metric.WithDescription("Number of anonymous users created"), - metric.WithUnit("users"), -) +//var userCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.created.counter", +// metric.WithDescription("Number of created users"), +// metric.WithUnit("users"), +//) +// +//var anonymousUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.anonymous.created.counter", +// metric.WithDescription("Number of anonymous users created"), +// metric.WithUnit("users"), +//) +// +//var appleUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.aplle.created.counter", +// metric.WithDescription("Number of apple users created"), +// metric.WithUnit("users"), +//) +// +//var azureAdUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.azuread.created.counter", +// metric.WithDescription("Number of azuread users created"), +// metric.WithUnit("users"), +//) +// +//var githubUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.github.created.counter", +// metric.WithDescription("Number of github users created"), +// metric.WithUnit("users"), +//) +// +//var googleUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.google.created.counter", +// metric.WithDescription("Number of google users created"), +// metric.WithUnit("users"), +//) +// +//var microsoftUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.microsoft.created.counter", +// metric.WithDescription("Number of anonymous users created"), +// metric.WithUnit("users"), +//) +// +//var oicdUserCreatedCounter, _ = userMeter.Int64Counter( +// "scrumlr.users.oicd.created.counter", +// metric.WithDescription("Number of anonymous users created"), +// metric.WithUnit("users"), +//) diff --git a/server/src/users/service.go b/server/src/users/service.go index 585cb5e5e4..a904018051 100644 --- a/server/src/users/service.go +++ b/server/src/users/service.go @@ -1,223 +1,223 @@ package users import ( - "context" - "database/sql" - "errors" - "scrumlr.io/server/sessions" - "strings" - - "github.com/google/uuid" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" - "scrumlr.io/server/realtime" + "context" + "database/sql" + "errors" + "scrumlr.io/server/sessions" + "strings" + + "github.com/google/uuid" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" + "scrumlr.io/server/realtime" ) type UserDatabase interface { - CreateAnonymousUser(name string) (DatabaseUser, error) - CreateAppleUser(id, name, avatarUrl string) (DatabaseUser, error) - CreateAzureAdUser(id, name, avatarUrl string) (DatabaseUser, error) - CreateGitHubUser(id, name, avatarUrl string) (DatabaseUser, error) - CreateGoogleUser(id, name, avatarUrl string) (DatabaseUser, error) - CreateMicrosoftUser(id, name, avatarUrl string) (DatabaseUser, error) - CreateOIDCUser(id, name, avatarUrl string) (DatabaseUser, error) - UpdateUser(update DatabaseUserUpdate) (DatabaseUser, error) - GetUser(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) - IsUserAnonymous(id uuid.UUID) (bool, error) - IsUserAvailableForKeyMigration(id uuid.UUID) (bool, error) - SetKeyMigration(id 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) { - err := validateUsername(name) - if err != nil { - return nil, err - } + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateAnonymousUser(name) - if err != nil { - return nil, err - } + user, err := service.database.CreateAnonymousUser(ctx, name) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } -func (service *Service) CreateAppleUser(_ context.Context, id, name, avatarUrl string) (*User, error) { - err := validateUsername(name) - if err != nil { - return nil, err - } +func (service *Service) CreateAppleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateAppleUser(id, name, avatarUrl) - if err != nil { - return nil, err - } + user, err := service.database.CreateAppleUser(ctx, id, name, avatarUrl) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } -func (service *Service) CreateAzureAdUser(_ context.Context, id, name, avatarUrl string) (*User, error) { - err := validateUsername(name) - if err != nil { - return nil, err - } +func (service *Service) CreateAzureAdUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateAzureAdUser(id, name, avatarUrl) - if err != nil { - return nil, err - } + user, err := service.database.CreateAzureAdUser(ctx, id, name, avatarUrl) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } -func (service *Service) CreateGitHubUser(_ context.Context, id, name, avatarUrl string) (*User, error) { - err := validateUsername(name) - if err != nil { - return nil, err - } +func (service *Service) CreateGitHubUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateGitHubUser(id, name, avatarUrl) - if err != nil { - return nil, err - } + user, err := service.database.CreateGitHubUser(ctx, id, name, avatarUrl) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } -func (service *Service) CreateGoogleUser(_ context.Context, id, name, avatarUrl string) (*User, error) { - err := validateUsername(name) - if err != nil { - return nil, err - } +func (service *Service) CreateGoogleUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateGoogleUser(id, name, avatarUrl) - if err != nil { - return nil, err - } + user, err := service.database.CreateGoogleUser(ctx, id, name, avatarUrl) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } -func (service *Service) CreateMicrosoftUser(_ context.Context, id, name, avatarUrl string) (*User, error) { - err := validateUsername(name) - if err != nil { - return nil, err - } +func (service *Service) CreateMicrosoftUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateMicrosoftUser(id, name, avatarUrl) - if err != nil { - return nil, err - } + user, err := service.database.CreateMicrosoftUser(ctx, id, name, avatarUrl) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } -func (service *Service) CreateOIDCUser(_ context.Context, id, name, avatarUrl string) (*User, error) { - err := validateUsername(name) - if err != nil { - return nil, err - } +func (service *Service) CreateOIDCUser(ctx context.Context, id, name, avatarUrl string) (*User, error) { + err := validateUsername(name) + if err != nil { + return nil, err + } - user, err := service.database.CreateOIDCUser(id, name, avatarUrl) - if err != nil { - return nil, err - } + user, err := service.database.CreateOIDCUser(ctx, id, name, avatarUrl) + if err != nil { + return nil, err + } - return new(User).From(user), err + return new(User).From(user), err } func (service *Service) Update(ctx context.Context, body UserUpdateRequest) (*User, error) { - log := logger.FromContext(ctx) - err := validateUsername(body.Name) - if err != nil { - return nil, err - } + log := logger.FromContext(ctx) + err := validateUsername(body.Name) + if err != nil { + return nil, err + } - user, err := service.database.UpdateUser(DatabaseUserUpdate{ - ID: body.ID, - Name: body.Name, - Avatar: body.Avatar, - }) + user, err := service.database.UpdateUser(ctx, DatabaseUserUpdate{ + ID: body.ID, + Name: body.Name, + Avatar: body.Avatar, + }) - if err != nil { - log.Errorw("unable to update user", "user", body.ID, "err", err) - return nil, err - } + if err != nil { + log.Errorw("unable to update user", "user", body.ID, "err", err) + return nil, err + } - service.updatedUser(ctx, user) + service.updatedUser(ctx, user) - return new(User).From(user), err + return new(User).From(user), err } func (service *Service) Get(ctx context.Context, userID uuid.UUID) (*User, error) { - log := logger.FromContext(ctx) - user, err := service.database.GetUser(userID) - if err != nil { - if err == sql.ErrNoRows { - return nil, common.NotFoundError - } - log.Errorw("unable to get user", "user", userID, "err", err) - return nil, common.InternalServerError - } + log := logger.FromContext(ctx) + user, err := service.database.GetUser(ctx, userID) + if err != nil { + if err == sql.ErrNoRows { + return nil, common.NotFoundError + } + log.Errorw("unable to get user", "user", userID, "err", err) + return nil, common.InternalServerError + } - return new(User).From(user), err + return new(User).From(user), err } func (service *Service) IsUserAvailableForKeyMigration(ctx context.Context, id uuid.UUID) (bool, error) { - return service.database.IsUserAvailableForKeyMigration(id) + return service.database.IsUserAvailableForKeyMigration(ctx, id) } func (service *Service) SetKeyMigration(ctx context.Context, id uuid.UUID) (*User, error) { - user, err := service.database.SetKeyMigration(id) - if err != nil { - return nil, err - } + user, err := service.database.SetKeyMigration(ctx, id) + if err != nil { + return nil, err + } - return new(User).From(user), nil + return new(User).From(user), nil } func (service *Service) updatedUser(ctx context.Context, user DatabaseUser) { - connectedBoards, err := service.sessionService.GetUserConnectedBoards(ctx, user.ID) - if err != nil { - return - } - - for _, session := range connectedBoards { - userSession, err := service.sessionService.Get(ctx, session.Board, session.ID) - if err != nil { - logger.Get().Errorw("unable to get board session", "board", userSession.Board, "user", userSession.ID, "err", err) - } - _ = service.realtime.BroadcastToBoard(session.Board, realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: session, - }) - } + connectedBoards, err := service.sessionService.GetUserConnectedBoards(ctx, user.ID) + if err != nil { + return + } + + for _, session := range connectedBoards { + userSession, err := service.sessionService.Get(ctx, session.Board, session.ID) + if err != nil { + logger.Get().Errorw("unable to get board session", "board", userSession.Board, "user", userSession.ID, "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 } diff --git a/server/src/users/service_test.go b/server/src/users/service_test.go index ff2397a8af..1e6c4a2b1b 100644 --- a/server/src/users/service_test.go +++ b/server/src/users/service_test.go @@ -1,4 +1,4 @@ -package sessions +package users import ( "context" From 987659e6ed1865e2899bc81407d68660698cf7ea Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 26 Sep 2025 10:45:52 +0200 Subject: [PATCH 08/16] fix tests --- server/src/.mockery.yaml | 12 +- server/src/api/login.go | 276 +++---- server/src/api/router.go | 686 +++++++++--------- .../service_integration_test.go | 19 +- server/src/sessionrequests/service_test.go | 5 +- .../src/sessions/service_integration_test.go | 287 ++++---- .../service_sessions_integration_test.go | 464 ------------ server/src/users/service_integration_test.go | 449 ++++++++++++ server/src/users/service_test.go | 99 +-- 9 files changed, 1155 insertions(+), 1142 deletions(-) delete mode 100644 server/src/sessions/service_sessions_integration_test.go create mode 100644 server/src/users/service_integration_test.go diff --git a/server/src/.mockery.yaml b/server/src/.mockery.yaml index 0d23ffdc20..d90db0830c 100644 --- a/server/src/.mockery.yaml +++ b/server/src/.mockery.yaml @@ -37,8 +37,6 @@ packages: interfaces: SessionService: SessionDatabase: - UserService: - UserDatabase: scrumlr.io/server/sessionrequests: interfaces: @@ -50,11 +48,11 @@ packages: interfaces: NotesService: NotesDatabase: -# todo: reimplement this interface -# scrumlr.io/server/sessions: -# interfaces: -# UserService: -# UserDatabase: + + scrumlr.io/server/users: + interfaces: + UserService: + UserDatabase: scrumlr.io/server/votings: interfaces: diff --git a/server/src/api/login.go b/server/src/api/login.go index c2e810ccce..508a679cc5 100644 --- a/server/src/api/login.go +++ b/server/src/api/login.go @@ -1,163 +1,163 @@ package api import ( - "fmt" - "math" - "net/http" - "scrumlr.io/server/users" - "strings" - "time" - - "github.com/go-chi/render" - "github.com/markbates/goth/gothic" - "go.opentelemetry.io/otel/codes" - "scrumlr.io/server/common" - "scrumlr.io/server/logger" + "fmt" + "math" + "net/http" + "scrumlr.io/server/users" + "strings" + "time" + + "github.com/go-chi/render" + "github.com/markbates/goth/gothic" + "go.opentelemetry.io/otel/codes" + "scrumlr.io/server/common" + "scrumlr.io/server/logger" ) //var tracer trace.Tracer = otel.Tracer("scrumlr.io/server/api") // AnonymousSignUpRequest represents the request to create a new anonymous user. type AnonymousSignUpRequest struct { - // The display name of the user. - Name string + // The display name of the user. + Name string } // signInAnonymously create a new anonymous user func (s *Server) signInAnonymously(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.signin.anonymous") - defer span.End() - log := logger.FromContext(ctx) - - var body AnonymousSignUpRequest - 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) - w.WriteHeader(http.StatusBadRequest) - return - } - - user, err := s.users.CreateAnonymous(ctx, body.Name) - if err != nil { - span.SetStatus(codes.Error, "failed to create anonyoums user") - span.RecordError(err) - common.Throw(w, r, common.InternalServerError) - return - } - - tokenString, err := s.auth.Sign(map[string]interface{}{"id": user.ID}) - if err != nil { - span.SetStatus(codes.Error, "failed to generate token string") - span.RecordError(err) - log.Errorw("unable to generate token string", "err", err) - common.Throw(w, r, common.InternalServerError) - return - } - - cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", HttpOnly: true, MaxAge: math.MaxInt32} - common.SealCookie(r, &cookie) - http.SetCookie(w, &cookie) - - render.Status(r, http.StatusCreated) - render.Respond(w, r, user) + ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.signin.anonymous") + defer span.End() + log := logger.FromContext(ctx) + + var body AnonymousSignUpRequest + 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) + w.WriteHeader(http.StatusBadRequest) + return + } + + user, err := s.users.CreateAnonymous(ctx, body.Name) + if err != nil { + span.SetStatus(codes.Error, "failed to create anonyoums user") + span.RecordError(err) + common.Throw(w, r, common.InternalServerError) + return + } + + tokenString, err := s.auth.Sign(map[string]interface{}{"id": user.ID}) + if err != nil { + span.SetStatus(codes.Error, "failed to generate token string") + span.RecordError(err) + log.Errorw("unable to generate token string", "err", err) + common.Throw(w, r, common.InternalServerError) + return + } + + cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", HttpOnly: true, MaxAge: math.MaxInt32} + common.SealCookie(r, &cookie) + http.SetCookie(w, &cookie) + + render.Status(r, http.StatusCreated) + render.Respond(w, r, user) } func (s *Server) logout(w http.ResponseWriter, r *http.Request) { - _, span := tracer.Start(r.Context(), "scrumlr.login.api.logout") - defer span.End() - - cookie := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} - common.SealCookie(r, &cookie) - http.SetCookie(w, &cookie) - - if common.GetHostWithoutPort(r) != common.GetTopLevelHost(r) { - cookieWithSubdomain := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} - common.SealCookie(r, &cookieWithSubdomain) - cookieWithSubdomain.Domain = common.GetHostWithoutPort(r) - http.SetCookie(w, &cookieWithSubdomain) - } - - render.Status(r, http.StatusNoContent) - render.Respond(w, r, nil) + _, span := tracer.Start(r.Context(), "scrumlr.login.api.logout") + defer span.End() + + cookie := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} + common.SealCookie(r, &cookie) + http.SetCookie(w, &cookie) + + if common.GetHostWithoutPort(r) != common.GetTopLevelHost(r) { + cookieWithSubdomain := http.Cookie{Name: "jwt", Value: "deleted", Path: "/", MaxAge: -1, Expires: time.UnixMilli(0)} + common.SealCookie(r, &cookieWithSubdomain) + cookieWithSubdomain.Domain = common.GetHostWithoutPort(r) + http.SetCookie(w, &cookieWithSubdomain) + } + + render.Status(r, http.StatusNoContent) + render.Respond(w, r, nil) } // beginAuthProviderVerification will redirect the user to the specified auth provider consent page func (s *Server) beginAuthProviderVerification(w http.ResponseWriter, r *http.Request) { - gothic.BeginAuthHandler(w, r) + gothic.BeginAuthHandler(w, r) } // verifyAuthProviderCallback will verify the auth provider call, create or update a user and redirect to the page provider with the state func (s *Server) verifyAuthProviderCallback(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.verify_auth_provider") - defer span.End() - log := logger.FromContext(ctx) - - externalUser, err := gothic.CompleteUserAuth(w, r) - if err != nil { - span.SetStatus(codes.Error, "failed to complete user auth") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("could not complete user auth", "err", err) - return - } - - provider, err := common.NewAccountType(externalUser.Provider) - if err != nil { - span.SetStatus(codes.Error, "user provider not supported") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("unsupported user provider", "err", err) - return - } - - userInfo, err := s.auth.ExtractUserInformation(provider, &externalUser) - if err != nil { - span.SetStatus(codes.Error, "insufficient user information from external auth source") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("insufficient user information from external auth source", "err", err) - return - } - - var internalUser *users.User - switch provider { - case common.Google: - internalUser, err = s.users.CreateGoogleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.GitHub: - internalUser, err = s.users.CreateGitHubUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.Microsoft: - internalUser, err = s.users.CreateMicrosoftUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.AzureAd: - internalUser, err = s.users.CreateAzureAdUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.Apple: - internalUser, err = s.users.CreateAppleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - case common.TypeOIDC: - internalUser, err = s.users.CreateOIDCUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) - } - if err != nil { - span.SetStatus(codes.Error, "failed to create user") - span.RecordError(err) - w.WriteHeader(http.StatusInternalServerError) - log.Errorw("could not create user", "err", err) - return - } - - tokenString, _ := s.auth.Sign(map[string]interface{}{"id": internalUser.ID}) - cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", Expires: time.Now().AddDate(0, 0, 3*7)} - common.SealCookie(r, &cookie) - http.SetCookie(w, &cookie) - - state := gothic.GetState(r) - stateSplit := strings.Split(state, "__") - if len(stateSplit) > 1 { - w.Header().Set("Location", stateSplit[1]) - w.WriteHeader(http.StatusSeeOther) - } - if s.basePath == "/" { - w.Header().Set("Location", fmt.Sprintf("%s://%s/", common.GetProtocol(r), r.Host)) - } else { - w.Header().Set("Location", fmt.Sprintf("%s://%s%s/", common.GetProtocol(r), r.Host, s.basePath)) - } - w.WriteHeader(http.StatusSeeOther) + ctx, span := tracer.Start(r.Context(), "scrumlr.login.api.verify_auth_provider") + defer span.End() + log := logger.FromContext(ctx) + + externalUser, err := gothic.CompleteUserAuth(w, r) + if err != nil { + span.SetStatus(codes.Error, "failed to complete user auth") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("could not complete user auth", "err", err) + return + } + + provider, err := common.NewAccountType(externalUser.Provider) + if err != nil { + span.SetStatus(codes.Error, "user provider not supported") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("unsupported user provider", "err", err) + return + } + + userInfo, err := s.auth.ExtractUserInformation(provider, &externalUser) + if err != nil { + span.SetStatus(codes.Error, "insufficient user information from external auth source") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("insufficient user information from external auth source", "err", err) + return + } + + var internalUser *users.User + switch provider { + case common.Google: + internalUser, err = s.users.CreateGoogleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.GitHub: + internalUser, err = s.users.CreateGitHubUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.Microsoft: + internalUser, err = s.users.CreateMicrosoftUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.AzureAd: + internalUser, err = s.users.CreateAzureAdUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.Apple: + internalUser, err = s.users.CreateAppleUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + case common.TypeOIDC: + internalUser, err = s.users.CreateOIDCUser(ctx, userInfo.Ident, userInfo.Name, userInfo.AvatarURL) + } + if err != nil { + span.SetStatus(codes.Error, "failed to create user") + span.RecordError(err) + w.WriteHeader(http.StatusInternalServerError) + log.Errorw("could not create user", "err", err) + return + } + + tokenString, _ := s.auth.Sign(map[string]interface{}{"id": internalUser.ID}) + cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", Expires: time.Now().AddDate(0, 0, 3*7)} + common.SealCookie(r, &cookie) + http.SetCookie(w, &cookie) + + state := gothic.GetState(r) + stateSplit := strings.Split(state, "__") + if len(stateSplit) > 1 { + w.Header().Set("Location", stateSplit[1]) + w.WriteHeader(http.StatusSeeOther) + } + if s.basePath == "/" { + w.Header().Set("Location", fmt.Sprintf("%s://%s/", common.GetProtocol(r), r.Host)) + } else { + w.Header().Set("Location", fmt.Sprintf("%s://%s%s/", common.GetProtocol(r), r.Host, s.basePath)) + } + w.WriteHeader(http.StatusSeeOther) } diff --git a/server/src/api/router.go b/server/src/api/router.go index 0a8ed1efc2..732df42b51 100644 --- a/server/src/api/router.go +++ b/server/src/api/router.go @@ -1,391 +1,391 @@ package api import ( - "net/http" - "os" - "scrumlr.io/server/users" - "time" - - "scrumlr.io/server/sessions" - - "scrumlr.io/server/boards" - - "scrumlr.io/server/votings" - - "scrumlr.io/server/boardreactions" - "scrumlr.io/server/boardtemplates" - "scrumlr.io/server/columns" - "scrumlr.io/server/columntemplates" - "scrumlr.io/server/notes" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/markbates/goth/gothic" - - "github.com/go-chi/cors" - "github.com/go-chi/httprate" - "github.com/go-chi/render" - "github.com/google/uuid" - gorillaSessions "github.com/gorilla/sessions" - "github.com/gorilla/websocket" - - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - - "scrumlr.io/server/auth" - "scrumlr.io/server/feedback" - "scrumlr.io/server/health" - "scrumlr.io/server/logger" - "scrumlr.io/server/reactions" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessionrequests" + "net/http" + "os" + "scrumlr.io/server/users" + "time" + + "scrumlr.io/server/sessions" + + "scrumlr.io/server/boards" + + "scrumlr.io/server/votings" + + "scrumlr.io/server/boardreactions" + "scrumlr.io/server/boardtemplates" + "scrumlr.io/server/columns" + "scrumlr.io/server/columntemplates" + "scrumlr.io/server/notes" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/markbates/goth/gothic" + + "github.com/go-chi/cors" + "github.com/go-chi/httprate" + "github.com/go-chi/render" + "github.com/google/uuid" + gorillaSessions "github.com/gorilla/sessions" + "github.com/gorilla/websocket" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "scrumlr.io/server/auth" + "scrumlr.io/server/feedback" + "scrumlr.io/server/health" + "scrumlr.io/server/logger" + "scrumlr.io/server/reactions" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessionrequests" ) type Server struct { - basePath string - - realtime *realtime.Broker - auth auth.Auth - - boards boards.BoardService - columns columns.ColumnService - votings votings.VotingService - users users.UserService - notes notes.NotesService - reactions reactions.ReactionService - sessions sessions.SessionService - sessionRequests sessionrequests.SessionRequestService - health health.HealthService - feedback feedback.FeedbackService - boardReactions boardreactions.BoardReactionService - boardTemplates boardtemplates.BoardTemplateService - columntemplates columntemplates.ColumnTemplateService - - upgrader websocket.Upgrader - - // map of boardSubscriptions with maps of users with connections - boardSubscriptions map[uuid.UUID]*BoardSubscription - boardSessionRequestSubscriptions map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription - - // note: if more options come with time, it might be sensible to wrap them into a struct - anonymousLoginDisabled bool - allowAnonymousCustomTemplates bool - allowAnonymousBoardCreation bool - experimentalFileSystemStore bool + basePath string + + realtime *realtime.Broker + auth auth.Auth + + boards boards.BoardService + columns columns.ColumnService + votings votings.VotingService + users users.UserService + notes notes.NotesService + reactions reactions.ReactionService + sessions sessions.SessionService + sessionRequests sessionrequests.SessionRequestService + health health.HealthService + feedback feedback.FeedbackService + boardReactions boardreactions.BoardReactionService + boardTemplates boardtemplates.BoardTemplateService + columntemplates columntemplates.ColumnTemplateService + + upgrader websocket.Upgrader + + // map of boardSubscriptions with maps of users with connections + boardSubscriptions map[uuid.UUID]*BoardSubscription + boardSessionRequestSubscriptions map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription + + // note: if more options come with time, it might be sensible to wrap them into a struct + anonymousLoginDisabled bool + allowAnonymousCustomTemplates bool + allowAnonymousBoardCreation bool + experimentalFileSystemStore bool } func New( - basePath string, - rt *realtime.Broker, - auth auth.Auth, - - boards boards.BoardService, - columns columns.ColumnService, - votings votings.VotingService, - users users.UserService, - notes notes.NotesService, - reactions reactions.ReactionService, - sessions sessions.SessionService, - sessionRequests sessionrequests.SessionRequestService, - health health.HealthService, - feedback feedback.FeedbackService, - boardReactions boardreactions.BoardReactionService, - boardTemplates boardtemplates.BoardTemplateService, - columntemplates columntemplates.ColumnTemplateService, - - verbose bool, - checkOrigin bool, - anonymousLoginDisabled bool, - allowAnonymousCustomTemplates bool, - allowAnonymousBoardCreation bool, - experimentalFileSystemStore bool, + basePath string, + rt *realtime.Broker, + auth auth.Auth, + + boards boards.BoardService, + columns columns.ColumnService, + votings votings.VotingService, + users users.UserService, + notes notes.NotesService, + reactions reactions.ReactionService, + sessions sessions.SessionService, + sessionRequests sessionrequests.SessionRequestService, + health health.HealthService, + feedback feedback.FeedbackService, + boardReactions boardreactions.BoardReactionService, + boardTemplates boardtemplates.BoardTemplateService, + columntemplates columntemplates.ColumnTemplateService, + + verbose bool, + checkOrigin bool, + anonymousLoginDisabled bool, + allowAnonymousCustomTemplates bool, + allowAnonymousBoardCreation bool, + experimentalFileSystemStore bool, ) chi.Router { - r := chi.NewRouter() - r.Use(middleware.Recoverer) - r.Use(middleware.RequestID) - r.Use(logger.RequestIDMiddleware) - r.Use(render.SetContentType(render.ContentTypeJSON)) - r.Use(otelhttp.NewMiddleware("scrumlr")) - - if !checkOrigin { - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"https://*", "http://*"}, - - // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link", "Set-Cookie"}, - AllowCredentials: true, - MaxAge: 300, - })) - } - - if verbose { - r.Use(logger.ChiZapLogger()) - } - - s := Server{ - basePath: basePath, - realtime: rt, - boardSubscriptions: make(map[uuid.UUID]*BoardSubscription), - boardSessionRequestSubscriptions: make(map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription), - auth: auth, - boards: boards, - columns: columns, - votings: votings, - users: users, - notes: notes, - reactions: reactions, - sessions: sessions, - sessionRequests: sessionRequests, - health: health, - feedback: feedback, - boardReactions: boardReactions, - boardTemplates: boardTemplates, - columntemplates: columntemplates, - - anonymousLoginDisabled: anonymousLoginDisabled, - allowAnonymousCustomTemplates: allowAnonymousCustomTemplates, - allowAnonymousBoardCreation: allowAnonymousBoardCreation, - experimentalFileSystemStore: experimentalFileSystemStore, - } - - // initialize websocket upgrader with origin check depending on options - s.upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - } - - // if enabled, this experimental feature allows for larger session cookies *during OAuth authentication* by storing them in a file store. - // this might be required when using some OIDC providers which exceed the 4KB limit. - // see https://github.com/markbates/goth/pull/141 - if s.experimentalFileSystemStore { - logger.Get().Infow("using experimental file system store") - store := gorillaSessions.NewFilesystemStore(os.TempDir(), []byte("scrumlr.io")) - store.MaxLength(0x8000) // 32KB should be plenty of space - gothic.Store = store - } - - if checkOrigin { - s.upgrader.CheckOrigin = nil - } else { - s.upgrader.CheckOrigin = func(r *http.Request) bool { - return true - } - } - if s.basePath == "/" { - s.publicRoutes(r) - s.protectedRoutes(r) - } else { - r.Route(s.basePath, func(router chi.Router) { - s.publicRoutes(router) - s.protectedRoutes(router) - }) - } - return r + r := chi.NewRouter() + r.Use(middleware.Recoverer) + r.Use(middleware.RequestID) + r.Use(logger.RequestIDMiddleware) + r.Use(render.SetContentType(render.ContentTypeJSON)) + r.Use(otelhttp.NewMiddleware("scrumlr")) + + if !checkOrigin { + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link", "Set-Cookie"}, + AllowCredentials: true, + MaxAge: 300, + })) + } + + if verbose { + r.Use(logger.ChiZapLogger()) + } + + s := Server{ + basePath: basePath, + realtime: rt, + boardSubscriptions: make(map[uuid.UUID]*BoardSubscription), + boardSessionRequestSubscriptions: make(map[uuid.UUID]*sessionrequests.BoardSessionRequestSubscription), + auth: auth, + boards: boards, + columns: columns, + votings: votings, + users: users, + notes: notes, + reactions: reactions, + sessions: sessions, + sessionRequests: sessionRequests, + health: health, + feedback: feedback, + boardReactions: boardReactions, + boardTemplates: boardTemplates, + columntemplates: columntemplates, + + anonymousLoginDisabled: anonymousLoginDisabled, + allowAnonymousCustomTemplates: allowAnonymousCustomTemplates, + allowAnonymousBoardCreation: allowAnonymousBoardCreation, + experimentalFileSystemStore: experimentalFileSystemStore, + } + + // initialize websocket upgrader with origin check depending on options + s.upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + // if enabled, this experimental feature allows for larger session cookies *during OAuth authentication* by storing them in a file store. + // this might be required when using some OIDC providers which exceed the 4KB limit. + // see https://github.com/markbates/goth/pull/141 + if s.experimentalFileSystemStore { + logger.Get().Infow("using experimental file system store") + store := gorillaSessions.NewFilesystemStore(os.TempDir(), []byte("scrumlr.io")) + store.MaxLength(0x8000) // 32KB should be plenty of space + gothic.Store = store + } + + if checkOrigin { + s.upgrader.CheckOrigin = nil + } else { + s.upgrader.CheckOrigin = func(r *http.Request) bool { + return true + } + } + if s.basePath == "/" { + s.publicRoutes(r) + s.protectedRoutes(r) + } else { + r.Route(s.basePath, func(router chi.Router) { + s.publicRoutes(router) + s.protectedRoutes(router) + }) + } + return r } func (s *Server) publicRoutes(r chi.Router) chi.Router { - return r.Group(func(r chi.Router) { - r.Get("/info", s.getServerInfo) - r.Get("/health", s.healthCheck) - r.Post("/feedback", s.createFeedback) - r.Route("/login", func(r chi.Router) { - r.Delete("/", s.logout) - r.With(s.AnonymousLoginDisabledContext).Post("/anonymous", s.signInAnonymously) - - r.Route("/{provider}", func(r chi.Router) { - r.Get("/", s.beginAuthProviderVerification) - r.Get("/callback", s.verifyAuthProviderCallback) - }) - }) - }) + return r.Group(func(r chi.Router) { + r.Get("/info", s.getServerInfo) + r.Get("/health", s.healthCheck) + r.Post("/feedback", s.createFeedback) + r.Route("/login", func(r chi.Router) { + r.Delete("/", s.logout) + r.With(s.AnonymousLoginDisabledContext).Post("/anonymous", s.signInAnonymously) + + r.Route("/{provider}", func(r chi.Router) { + r.Get("/", s.beginAuthProviderVerification) + r.Get("/callback", s.verifyAuthProviderCallback) + }) + }) + }) } func (s *Server) protectedRoutes(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(s.auth.Verifier()) - r.Use(s.auth.Authenticator()) - r.Use(auth.AuthContext) - - r.Route("/templates", func(r chi.Router) { - r.Use(s.BoardTemplateRateLimiter) - r.Use(s.AnonymousCustomTemplateCreationContext) - - r.Post("/", s.createBoardTemplate) - r.Get("/", s.getBoardTemplates) - - r.Route("/{id}", func(r chi.Router) { - r.Use(s.BoardTemplateContext) - - r.Get("/", s.getBoardTemplate) - r.Put("/", s.updateBoardTemplate) - r.Delete("/", s.deleteBoardTemplate) - - r.Route("/columns", func(r chi.Router) { - r.Post("/", s.createColumnTemplate) - r.Get("/", s.getColumnTemplates) - - r.Route("/{columnTemplate}", func(r chi.Router) { - r.Use(s.ColumnTemplateContext) - - r.Get("/", s.getColumnTemplate) - r.Put("/", s.updateColumnTemplate) - r.Delete("/", s.deleteColumnTemplate) - }) - }) - }) - }) - - r.With(s.AnonymousBoardCreationContext).Post("/boards", s.createBoard) - r.With(s.AnonymousBoardCreationContext).Post("/import", s.importBoard) - r.Get("/boards", s.getBoards) - r.Route("/boards/{id}", func(r chi.Router) { - r.With(s.BoardParticipantContext).Get("/", s.getBoard) - r.With(s.BoardParticipantContext).Get("/export", s.exportBoard) - r.With(s.BoardModeratorContext).Post("/timer", s.setTimer) - r.With(s.BoardModeratorContext).Delete("/timer", s.deleteTimer) - r.With(s.BoardModeratorContext).Post("/timer/increment", s.incrementTimer) - r.With(s.BoardModeratorContext).Put("/", s.updateBoard) - r.With(s.BoardModeratorContext).Delete("/", s.deleteBoard) - - s.initBoardSessionRequestResources(r) - s.initBoardSessionResources(r) - s.initColumnResources(r) - s.initNoteResources(r) - s.initReactionResources(r) - s.initVotingResources(r) - s.initVoteResources(r) - s.initBoardReactionResources(r) - }) - - r.Route("/user", func(r chi.Router) { - r.Get("/", s.getUser) - r.Put("/", s.updateUser) - r.Get("/{user}", s.getUserByID) - }) - }) + r.Group(func(r chi.Router) { + r.Use(s.auth.Verifier()) + r.Use(s.auth.Authenticator()) + r.Use(auth.AuthContext) + + r.Route("/templates", func(r chi.Router) { + r.Use(s.BoardTemplateRateLimiter) + r.Use(s.AnonymousCustomTemplateCreationContext) + + r.Post("/", s.createBoardTemplate) + r.Get("/", s.getBoardTemplates) + + r.Route("/{id}", func(r chi.Router) { + r.Use(s.BoardTemplateContext) + + r.Get("/", s.getBoardTemplate) + r.Put("/", s.updateBoardTemplate) + r.Delete("/", s.deleteBoardTemplate) + + r.Route("/columns", func(r chi.Router) { + r.Post("/", s.createColumnTemplate) + r.Get("/", s.getColumnTemplates) + + r.Route("/{columnTemplate}", func(r chi.Router) { + r.Use(s.ColumnTemplateContext) + + r.Get("/", s.getColumnTemplate) + r.Put("/", s.updateColumnTemplate) + r.Delete("/", s.deleteColumnTemplate) + }) + }) + }) + }) + + r.With(s.AnonymousBoardCreationContext).Post("/boards", s.createBoard) + r.With(s.AnonymousBoardCreationContext).Post("/import", s.importBoard) + r.Get("/boards", s.getBoards) + r.Route("/boards/{id}", func(r chi.Router) { + r.With(s.BoardParticipantContext).Get("/", s.getBoard) + r.With(s.BoardParticipantContext).Get("/export", s.exportBoard) + r.With(s.BoardModeratorContext).Post("/timer", s.setTimer) + r.With(s.BoardModeratorContext).Delete("/timer", s.deleteTimer) + r.With(s.BoardModeratorContext).Post("/timer/increment", s.incrementTimer) + r.With(s.BoardModeratorContext).Put("/", s.updateBoard) + r.With(s.BoardModeratorContext).Delete("/", s.deleteBoard) + + s.initBoardSessionRequestResources(r) + s.initBoardSessionResources(r) + s.initColumnResources(r) + s.initNoteResources(r) + s.initReactionResources(r) + s.initVotingResources(r) + s.initVoteResources(r) + s.initBoardReactionResources(r) + }) + + r.Route("/user", func(r chi.Router) { + r.Get("/", s.getUser) + r.Put("/", s.updateUser) + r.Get("/{user}", s.getUserByID) + }) + }) } func (s *Server) initVoteResources(r chi.Router) { - r.Route("/votes", func(r chi.Router) { - r.Use(s.BoardParticipantContext) - r.Get("/", s.getVotes) - - r.Group(func(r chi.Router) { - r.Use(s.BoardEditableContext) - r.Post("/", s.addVote) - r.Delete("/", s.removeVote) - }) - }) + r.Route("/votes", func(r chi.Router) { + r.Use(s.BoardParticipantContext) + r.Get("/", s.getVotes) + + r.Group(func(r chi.Router) { + r.Use(s.BoardEditableContext) + r.Post("/", s.addVote) + r.Delete("/", s.removeVote) + }) + }) } func (s *Server) initVotingResources(r chi.Router) { - r.Route("/votings", func(r chi.Router) { - r.With(s.BoardParticipantContext).Get("/", s.getVotings) - r.With(s.BoardModeratorContext).Post("/", s.createVoting) - r.With(s.BoardModeratorContext).Put("/", s.updateVoting) - - r.Route("/{voting}", func(r chi.Router) { - r.Use(s.VotingContext) - r.With(s.BoardParticipantContext).Get("/", s.getVoting) - r.With(s.BoardModeratorContext).Put("/", s.updateVoting) - }) - }) + r.Route("/votings", func(r chi.Router) { + r.With(s.BoardParticipantContext).Get("/", s.getVotings) + r.With(s.BoardModeratorContext).Post("/", s.createVoting) + r.With(s.BoardModeratorContext).Put("/", s.updateVoting) + + r.Route("/{voting}", func(r chi.Router) { + r.Use(s.VotingContext) + r.With(s.BoardParticipantContext).Get("/", s.getVoting) + r.With(s.BoardModeratorContext).Put("/", s.updateVoting) + }) + }) } func (s *Server) initBoardSessionResources(r chi.Router) { - r.Route("/participants", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(httprate.Limit( - 3, - 5*time.Second, - httprate.WithKeyFuncs(httprate.KeyByIP), - httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusTooManyRequests) - _, err := w.Write([]byte(`{"error": "Too many requests"}`)) - if err != nil { - log := logger.FromRequest(r) - log.Errorw("Could not write error", "error", err) - return - } - }), - )) - - 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.Route("/participants", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(httprate.Limit( + 3, + 5*time.Second, + httprate.WithKeyFuncs(httprate.KeyByIP), + httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTooManyRequests) + _, err := w.Write([]byte(`{"error": "Too many requests"}`)) + if err != nil { + log := logger.FromRequest(r) + log.Errorw("Could not write error", "error", err) + return + } + }), + )) + + 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) + }) + }) } func (s *Server) initBoardSessionRequestResources(r chi.Router) { - r.Route("/requests", func(r chi.Router) { - r.With(s.BoardModeratorContext).Get("/", s.getBoardSessionRequests) - r.With(s.BoardCandidateContext).Get("/{user}", s.getBoardSessionRequest) - r.With(s.BoardModeratorContext).Put("/{user}", s.updateBoardSessionRequest) - }) + r.Route("/requests", func(r chi.Router) { + r.With(s.BoardModeratorContext).Get("/", s.getBoardSessionRequests) + r.With(s.BoardCandidateContext).Get("/{user}", s.getBoardSessionRequest) + r.With(s.BoardModeratorContext).Put("/{user}", s.updateBoardSessionRequest) + }) } func (s *Server) initColumnResources(r chi.Router) { - r.Route("/columns", func(r chi.Router) { - r.With(s.BoardParticipantContext).Get("/", s.getColumns) - r.With(s.BoardModeratorContext).Post("/", s.createColumn) - - r.Route("/{column}", func(r chi.Router) { - r.Use(s.ColumnContext) - - r.With(s.BoardParticipantContext).Get("/", s.getColumn) - r.With(s.BoardModeratorContext).Put("/", s.updateColumn) - r.With(s.BoardModeratorContext).Delete("/", s.deleteColumn) - }) - }) + r.Route("/columns", func(r chi.Router) { + r.With(s.BoardParticipantContext).Get("/", s.getColumns) + r.With(s.BoardModeratorContext).Post("/", s.createColumn) + + r.Route("/{column}", func(r chi.Router) { + r.Use(s.ColumnContext) + + r.With(s.BoardParticipantContext).Get("/", s.getColumn) + r.With(s.BoardModeratorContext).Put("/", s.updateColumn) + r.With(s.BoardModeratorContext).Delete("/", s.deleteColumn) + }) + }) } func (s *Server) initNoteResources(r chi.Router) { - r.Route("/notes", func(r chi.Router) { - r.Use(s.BoardParticipantContext) + r.Route("/notes", func(r chi.Router) { + r.Use(s.BoardParticipantContext) - r.Get("/", s.getNotes) - r.With(s.BoardEditableContext).Post("/", s.createNote) + r.Get("/", s.getNotes) + r.With(s.BoardEditableContext).Post("/", s.createNote) - r.Route("/{note}", func(r chi.Router) { - r.Use(s.NoteContext) + r.Route("/{note}", func(r chi.Router) { + r.Use(s.NoteContext) - r.Get("/", s.getNote) - r.With(s.BoardEditableContext).Put("/", s.updateNote) - r.With(s.BoardEditableContext).Delete("/", s.deleteNote) - }) - }) + r.Get("/", s.getNote) + r.With(s.BoardEditableContext).Put("/", s.updateNote) + r.With(s.BoardEditableContext).Delete("/", s.deleteNote) + }) + }) } func (s *Server) initReactionResources(r chi.Router) { - r.Route("/reactions", func(r chi.Router) { - r.Use(s.BoardParticipantContext) + r.Route("/reactions", func(r chi.Router) { + r.Use(s.BoardParticipantContext) - r.Get("/", s.getReactions) - r.With(s.BoardEditableContext).Post("/", s.createReaction) + r.Get("/", s.getReactions) + r.With(s.BoardEditableContext).Post("/", s.createReaction) - r.Route("/{reaction}", func(r chi.Router) { - r.Use(s.ReactionContext) + r.Route("/{reaction}", func(r chi.Router) { + r.Use(s.ReactionContext) - r.Get("/", s.getReaction) - r.With(s.BoardEditableContext).Delete("/", s.removeReaction) - r.With(s.BoardEditableContext).Put("/", s.updateReaction) - }) - }) + r.Get("/", s.getReaction) + r.With(s.BoardEditableContext).Delete("/", s.removeReaction) + r.With(s.BoardEditableContext).Put("/", s.updateReaction) + }) + }) } func (s *Server) initBoardReactionResources(r chi.Router) { - r.Route("/board-reactions", func(r chi.Router) { - r.Use(s.BoardParticipantContext) + r.Route("/board-reactions", func(r chi.Router) { + r.Use(s.BoardParticipantContext) - r.Post("/", s.createBoardReaction) - }) + r.Post("/", s.createBoardReaction) + }) } diff --git a/server/src/sessionrequests/service_integration_test.go b/server/src/sessionrequests/service_integration_test.go index 0fa8e9434c..5920f1315e 100644 --- a/server/src/sessionrequests/service_integration_test.go +++ b/server/src/sessionrequests/service_integration_test.go @@ -3,6 +3,7 @@ package sessionrequests import ( "context" "log" + "scrumlr.io/server/users" "testing" "github.com/google/uuid" @@ -28,7 +29,7 @@ type SessionRequestServiceIntegrationTestSuite struct { natsContainer *nats.NATSContainer db *bun.DB natsConnectionString string - users map[string]sessions.User + users map[string]users.User boards map[string]TestBoard sessionsRequests map[string]DatabaseBoardSessionRequest } @@ -138,7 +139,7 @@ func (suite *SessionRequestServiceIntegrationTestSuite) Test_Update() { sessionData, err := technical_helper.Unmarshal[sessions.BoardSession](sessionMsg.Data) assert.Nil(t, err) - assert.Equal(t, userId, sessionData.User.ID) + assert.Equal(t, userId, sessionData.ID) updatedMsg := <-events assert.Equal(t, realtime.BoardEventSessionRequestUpdated, updatedMsg.Type) @@ -253,13 +254,13 @@ func (suite *SessionRequestServiceIntegrationTestSuite) Test_Exists() { func (suite *SessionRequestServiceIntegrationTestSuite) SeedDatabase(db *bun.DB) { // tests users - suite.users = make(map[string]sessions.User, 6) - suite.users["Stan"] = sessions.User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} - suite.users["Friend"] = sessions.User{ID: uuid.New(), Name: "Friend", AccountType: common.Anonymous} - suite.users["Santa"] = sessions.User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} - suite.users["Bob"] = sessions.User{ID: uuid.New(), Name: "Bob", AccountType: common.Anonymous} - suite.users["Luke"] = sessions.User{ID: uuid.New(), Name: "Luke", AccountType: common.Anonymous} - suite.users["Leia"] = sessions.User{ID: uuid.New(), Name: "Leia", AccountType: common.Anonymous} + suite.users = make(map[string]users.User, 6) + suite.users["Stan"] = users.User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} + suite.users["Friend"] = users.User{ID: uuid.New(), Name: "Friend", AccountType: common.Anonymous} + suite.users["Santa"] = users.User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} + suite.users["Bob"] = users.User{ID: uuid.New(), Name: "Bob", AccountType: common.Anonymous} + suite.users["Luke"] = users.User{ID: uuid.New(), Name: "Luke", AccountType: common.Anonymous} + suite.users["Leia"] = users.User{ID: uuid.New(), Name: "Leia", AccountType: common.Anonymous} // test boards suite.boards = make(map[string]TestBoard, 2) diff --git a/server/src/sessionrequests/service_test.go b/server/src/sessionrequests/service_test.go index 17343c69a0..f32d438cf6 100644 --- a/server/src/sessionrequests/service_test.go +++ b/server/src/sessionrequests/service_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "scrumlr.io/server/users" "testing" "time" @@ -265,7 +266,7 @@ func TestUpdatesessionRequest(t *testing.T) { boardId := uuid.New() userId := uuid.New() - user := sessions.User{ + user := users.User{ ID: userId, } mockSessionRequestDb := NewMockSessionRequestDatabase(t) @@ -274,7 +275,7 @@ func TestUpdatesessionRequest(t *testing.T) { mockSessionService := sessions.NewMockSessionService(t) mockSessionService.EXPECT().Create(mock.Anything, boardId, userId). - Return(&sessions.BoardSession{Board: boardId, User: user}, nil) + Return(&sessions.BoardSession{Board: boardId, ID: user.ID}, nil) mockBroker := realtime.NewMockClient(t) mockBroker.EXPECT().Publish(mock.Anything, mock.AnythingOfType("string"), mock.Anything).Return(nil) diff --git a/server/src/sessions/service_integration_test.go b/server/src/sessions/service_integration_test.go index 71c4395f37..fcba800788 100644 --- a/server/src/sessions/service_integration_test.go +++ b/server/src/sessions/service_integration_test.go @@ -16,25 +16,26 @@ import ( "scrumlr.io/server/initialize" "scrumlr.io/server/notes" "scrumlr.io/server/realtime" + "scrumlr.io/server/technical_helper" "scrumlr.io/server/votings" ) -type UserServiceIntegrationTestsuite struct { +type SessionServiceIntegrationTestSuite struct { suite.Suite dbContainer *postgres.PostgresContainer natsContainer *nats.NATSContainer db *bun.DB natsConnectionString string - users map[string]User + users map[string]uuid.UUID boards map[string]TestBoard sessions map[string]BoardSession } -func TestUserServiceIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(UserServiceIntegrationTestsuite)) +func TestSessionServiceIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(SessionServiceIntegrationTestSuite)) } -func (suite *UserServiceIntegrationTestsuite) SetupSuite() { +func (suite *SessionServiceIntegrationTestSuite) SetupSuite() { dbContainer, bun := initialize.StartTestDatabase() suite.SeedDatabase(bun) natsContainer, connectionString := initialize.StartTestNats() @@ -45,22 +46,25 @@ func (suite *UserServiceIntegrationTestsuite) SetupSuite() { suite.natsConnectionString = connectionString } -func (suite *UserServiceIntegrationTestsuite) TeardownSuite() { +func (suite *SessionServiceIntegrationTestSuite) TearDownSuite() { initialize.StopTestDatabase(suite.dbContainer) initialize.StopTestNats(suite.natsContainer) } -func (suite *UserServiceIntegrationTestsuite) Test_CreateAnonymous() { +func (suite *SessionServiceIntegrationTestSuite) Test_Create() { t := suite.T() ctx := context.Background() - userName := "Test User" + boardId := suite.boards["Write"].id + userId := suite.users["Luke"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { log.Fatalf("Faild to connect to nats server %s", err) } + events := broker.GetBoardChannel(ctx, boardId) + voteDatabase := votings.NewVotingDatabase(suite.db) voteService := votings.NewVotingService(voteDatabase, broker) noteDatabase := notes.NewNotesDatabase(suite.db) @@ -69,27 +73,38 @@ func (suite *UserServiceIntegrationTestsuite) Test_CreateAnonymous() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.CreateAnonymous(ctx, userName) + session, err := sessionService.Create(ctx, boardId, userId) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.ID) + assert.Equal(t, common.ParticipantRole, session.Role) + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantCreated, msg.Type) + sessionData, err := technical_helper.Unmarshal[BoardSession](msg.Data) assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Anonymous, user.AccountType) + assert.Equal(t, userId, sessionData.ID) + assert.Equal(t, common.ParticipantRole, sessionData.Role) } -func (suite *UserServiceIntegrationTestsuite) Test_CreateAppleUser() { +func (suite *SessionServiceIntegrationTestSuite) Test_Update() { t := suite.T() ctx := context.Background() - userName := "Test User" + boardId := suite.boards["Update"].id + userId := suite.users["Luke"] + callerId := suite.users["Stan"] + role := common.ModeratorRole broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { log.Fatalf("Faild to connect to nats server %s", err) } + events := broker.GetBoardChannel(ctx, boardId) + voteDatabase := votings.NewVotingDatabase(suite.db) voteService := votings.NewVotingService(voteDatabase, broker) noteDatabase := notes.NewNotesDatabase(suite.db) @@ -98,27 +113,41 @@ func (suite *UserServiceIntegrationTestsuite) Test_CreateAppleUser() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.CreateAppleUser(ctx, "appleId", userName, "") + session, err := sessionService.Update(ctx, BoardSessionUpdateRequest{Caller: callerId, Board: boardId, User: userId, Role: &role}) assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Apple, user.AccountType) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.ID) + assert.Equal(t, common.ModeratorRole, session.Role) + + msgSession := <-events + msgColumns := <-events + msgNotes := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msgSession.Type) + assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumns.Type) + assert.Equal(t, realtime.BoardEventNotesSync, msgNotes.Type) + sessionData, err := technical_helper.Unmarshal[BoardSession](msgSession.Data) + assert.Nil(t, err) + assert.Equal(t, userId, session.ID) + assert.Equal(t, common.ModeratorRole, sessionData.Role) } -func (suite *UserServiceIntegrationTestsuite) Test_CreateAzureadUser() { +func (suite *SessionServiceIntegrationTestSuite) Test_UpdateAll() { t := suite.T() ctx := context.Background() - userName := "Test User" + boardId := suite.boards["UpdateAll"].id + ready := false + raisedHand := false broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { log.Fatalf("Faild to connect to nats server %s", err) } + events := broker.GetBoardChannel(ctx, boardId) + voteDatabase := votings.NewVotingDatabase(suite.db) voteService := votings.NewVotingService(voteDatabase, broker) noteDatabase := notes.NewNotesDatabase(suite.db) @@ -127,50 +156,29 @@ func (suite *UserServiceIntegrationTestsuite) Test_CreateAzureadUser() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.CreateAzureAdUser(ctx, "azureId", userName, "") + sessions, err := sessionService.UpdateAll(ctx, BoardSessionsUpdateRequest{Board: boardId, Ready: &ready, RaisedHand: &raisedHand}) assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.AzureAd, user.AccountType) -} + assert.Len(t, sessions, 4) -func (suite *UserServiceIntegrationTestsuite) Test_CreateGitHubUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantsUpdated, msg.Type) + sessionData, err := technical_helper.UnmarshalSlice[BoardSession](msg.Data) + assert.Nil(t, err) + assert.Len(t, sessionData, 4) +} - user, err := userService.CreateGitHubUser(ctx, "githubId", userName, "") +func (suite *SessionServiceIntegrationTestSuite) Test_UpdateUserBoard() { - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.GitHub, user.AccountType) } -func (suite *UserServiceIntegrationTestsuite) Test_CreateGoogleUser() { +func (suite *SessionServiceIntegrationTestSuite) Test_Get() { t := suite.T() ctx := context.Background() - userName := "Test User" + boardId := suite.boards["Read"].id + userId := suite.users["Santa"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { @@ -185,21 +193,19 @@ func (suite *UserServiceIntegrationTestsuite) Test_CreateGoogleUser() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.CreateGoogleUser(ctx, "googleId", userName, "") + session, err := sessionService.Get(ctx, boardId, userId) assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Google, user.AccountType) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.ID) } -func (suite *UserServiceIntegrationTestsuite) Test_CreateMicrosoft() { +func (suite *SessionServiceIntegrationTestSuite) Test_GetAll() { t := suite.T() ctx := context.Background() - userName := "Test User" + boardId := suite.boards["Read"].id broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { @@ -214,21 +220,17 @@ func (suite *UserServiceIntegrationTestsuite) Test_CreateMicrosoft() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateMicrosoftUser(ctx, "microsoftId", userName, "") + sessions, err := sessionService.GetAll(ctx, boardId, BoardSessionFilter{}) assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Microsoft, user.AccountType) + assert.Len(t, sessions, 4) } -func (suite *UserServiceIntegrationTestsuite) Test_CreateOIDCUser() { +func (suite *SessionServiceIntegrationTestSuite) Test_GetUserConnectedBoards() { t := suite.T() ctx := context.Background() - userName := "Test User" + userId := suite.users["Stan"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { @@ -243,29 +245,27 @@ func (suite *UserServiceIntegrationTestsuite) Test_CreateOIDCUser() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.CreateOIDCUser(ctx, "oidcId", userName, "") + sessions, err := sessionService.GetUserConnectedBoards(ctx, userId) assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.TypeOIDC, user.AccountType) + assert.Len(t, sessions, 2) } -func (suite *UserServiceIntegrationTestsuite) Test_Update() { +func (suite *SessionServiceIntegrationTestSuite) Test_Connect() { t := suite.T() ctx := context.Background() - userId := suite.users["Update"].ID boardId := suite.boards["Update"].id - userName := "Test User" + userId := suite.users["Luke"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { log.Fatalf("Faild to connect to nats server %s", err) } + events := broker.GetBoardChannel(ctx, boardId) + voteDatabase := votings.NewVotingDatabase(suite.db) voteService := votings.NewVotingService(voteDatabase, broker) noteDatabase := notes.NewNotesDatabase(suite.db) @@ -274,35 +274,29 @@ func (suite *UserServiceIntegrationTestsuite) Test_Update() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - events := broker.GetBoardChannel(ctx, boardId) - user, err := userService.Update(ctx, UserUpdateRequest{ID: userId, Name: userName}) + err = sessionService.Connect(ctx, boardId, userId) assert.Nil(t, err) - assert.Equal(t, userId, user.ID) - assert.Equal(t, userName, user.Name) msg := <-events assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) - sessionData := msg.Data.(map[string]interface{}) - assert.True(t, sessionData["connected"].(bool)) - assert.Equal(t, string(common.OwnerRole), sessionData["role"].(string)) } -func (suite *UserServiceIntegrationTestsuite) Test_Get() { +func (suite *SessionServiceIntegrationTestSuite) Test_Disconnect() { t := suite.T() ctx := context.Background() - userId := suite.users["Stan"].ID + boardId := suite.boards["Update"].id + userId := suite.users["Leia"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { log.Fatalf("Faild to connect to nats server %s", err) } + events := broker.GetBoardChannel(ctx, boardId) + voteDatabase := votings.NewVotingDatabase(suite.db) voteService := votings.NewVotingService(voteDatabase, broker) noteDatabase := notes.NewNotesDatabase(suite.db) @@ -311,21 +305,23 @@ func (suite *UserServiceIntegrationTestsuite) Test_Get() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.Get(ctx, userId) + err = sessionService.Disconnect(ctx, boardId, userId) assert.Nil(t, err) - assert.Equal(t, userId, user.ID) - assert.Equal(t, suite.users["Stan"].Name, user.Name) + + msgColumn := <-events + msgNote := <-events + assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumn.Type) + assert.Equal(t, realtime.BoardEventNotesSync, msgNote.Type) } -func (suite *UserServiceIntegrationTestsuite) Test_Get_NotFound() { +func (suite *SessionServiceIntegrationTestSuite) Test_Exists() { t := suite.T() ctx := context.Background() - userId := uuid.New() + boardId := suite.boards["Read"].id + userId := suite.users["Stan"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { @@ -340,21 +336,19 @@ func (suite *UserServiceIntegrationTestsuite) Test_Get_NotFound() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.Get(ctx, userId) + exists, err := sessionService.Exists(ctx, boardId, userId) - assert.Nil(t, user) - assert.NotNil(t, err) - assert.Equal(t, common.NotFoundError, err) + assert.Nil(t, err) + assert.True(t, exists) } -func (suite *UserServiceIntegrationTestsuite) Test_AvailableForKeyMigration() { +func (suite *SessionServiceIntegrationTestSuite) Test_ModeratorExists() { t := suite.T() ctx := context.Background() - userId := suite.users["Santa"].ID + boardId := suite.boards["Read"].id + userId := suite.users["Stan"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { @@ -369,20 +363,19 @@ func (suite *UserServiceIntegrationTestsuite) Test_AvailableForKeyMigration() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - available, err := userService.IsUserAvailableForKeyMigration(ctx, userId) + exists, err := sessionService.ModeratorSessionExists(ctx, boardId, userId) assert.Nil(t, err) - assert.True(t, available) + assert.True(t, exists) } -func (suite *UserServiceIntegrationTestsuite) Test_SetKeyMigration() { +func (suite *SessionServiceIntegrationTestSuite) Test_IsParticipantBanned() { t := suite.T() ctx := context.Background() - userId := suite.users["Stan"].ID + boardId := suite.boards["Read"].id + userId := suite.users["Bob"] broker, err := realtime.NewNats(suite.natsConnectionString) if err != nil { @@ -397,34 +390,68 @@ func (suite *UserServiceIntegrationTestsuite) Test_SetKeyMigration() { columnService := columns.NewColumnService(columnDatabase, broker, noteService) sessionDatabase := NewSessionDatabase(suite.db) sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - user, err := userService.SetKeyMigration(ctx, userId) + banned, err := sessionService.IsParticipantBanned(ctx, boardId, userId) assert.Nil(t, err) - assert.Equal(t, userId, user.ID) + assert.True(t, banned) } -func (suite *UserServiceIntegrationTestsuite) SeedDatabase(db *bun.DB) { - // test users - suite.users = make(map[string]User, 3) - suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} - suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} - suite.users["Update"] = User{ID: uuid.New(), Name: "UpdateMe", AccountType: common.Anonymous} +func (suite *SessionServiceIntegrationTestSuite) SeedDatabase(db *bun.DB) { + // tests users + suite.users = make(map[string]uuid.UUID, 7) + suite.users["Stan"] = uuid.New() + suite.users["Friend"] = uuid.New() + suite.users["Santa"] = uuid.New() + suite.users["Bob"] = uuid.New() + suite.users["Luke"] = uuid.New() + suite.users["Leia"] = uuid.New() + suite.users["Han"] = uuid.New() // test boards - suite.boards = make(map[string]TestBoard, 1) + suite.boards = make(map[string]TestBoard, 5) + suite.boards["Write"] = TestBoard{id: uuid.New(), name: "Write"} suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} + suite.boards["Read"] = TestBoard{id: uuid.New(), name: "Read"} + suite.boards["ReadFilter"] = TestBoard{id: uuid.New(), name: "ReadFilter"} + suite.boards["UpdateAll"] = TestBoard{id: uuid.New(), name: "UpdateAll"} // test sessions - suite.sessions = make(map[string]BoardSession, 1) - suite.sessions["Update"] = BoardSession{User: suite.users["Update"], Board: suite.boards["Update"].id, Role: common.OwnerRole, Connected: true} - - for _, user := range suite.users { - err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) + suite.sessions = make(map[string]BoardSession, 16) + // test sessions for the write board + suite.sessions["Write"] = BoardSession{ID: suite.users["Han"], Board: suite.boards["Write"].id, Role: common.ParticipantRole} + // test sessions for the update board + suite.sessions["UpdateOwner"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["Update"].id, Role: common.OwnerRole} + suite.sessions["UpdateParticipantModerator"] = BoardSession{ID: suite.users["Luke"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} + suite.sessions["UpdateParticipantOwner"] = BoardSession{ID: suite.users["Leia"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} + suite.sessions["UpdateModeratorOwner"] = BoardSession{ID: suite.users["Han"], Board: suite.boards["Update"].id, Role: common.ParticipantRole} + // test sessions for the read board + suite.sessions["Read1"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["Read"].id, Role: common.OwnerRole} + suite.sessions["Read2"] = BoardSession{ID: suite.users["Friend"], Board: suite.boards["Read"].id, Role: common.ModeratorRole} + suite.sessions["Read3"] = BoardSession{ID: suite.users["Santa"], Board: suite.boards["Read"].id, Role: common.ParticipantRole} + suite.sessions["Read4"] = BoardSession{ID: suite.users["Bob"], Board: suite.boards["Read"].id, Role: common.ParticipantRole, Banned: true} + // test sessions for the read filter board + suite.sessions["ReadFilter1"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["ReadFilter"].id, Role: common.OwnerRole, Ready: true, Connected: true} + suite.sessions["ReadFilter2"] = BoardSession{ID: suite.users["Friend"], Board: suite.boards["ReadFilter"].id, Role: common.ModeratorRole, Ready: true} + suite.sessions["ReadFilter3"] = BoardSession{ID: suite.users["Santa"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true, Connected: true} + suite.sessions["ReadFilter4"] = BoardSession{ID: suite.users["Bob"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true} + // test sessions for the update all board + suite.sessions["UpdateAll1"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["UpdateAll"].id, Role: common.OwnerRole, Connected: true, RaisedHand: true, Ready: false} + suite.sessions["UpdateAll2"] = BoardSession{ID: suite.users["Luke"], Board: suite.boards["UpdateAll"].id, Role: common.ModeratorRole, Connected: true, RaisedHand: false, Ready: true} + suite.sessions["UpdateAll3"] = BoardSession{ID: suite.users["Leia"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: false, Ready: false} + suite.sessions["UpdateAll4"] = BoardSession{ID: suite.users["Han"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: true, Ready: true} + + for name, user := range suite.users { + if name == "Stan" { + err := initialize.InsertUser(db, user, name, string(common.Google)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } else { + err := initialize.InsertUser(db, user, name, string(common.Anonymous)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } } } @@ -436,7 +463,7 @@ func (suite *UserServiceIntegrationTestsuite) SeedDatabase(db *bun.DB) { } for _, session := range suite.sessions { - err := initialize.InsertSession(db, session.User.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) + err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) if err != nil { log.Fatalf("Failed to insert test sessions %s", err) } diff --git a/server/src/sessions/service_sessions_integration_test.go b/server/src/sessions/service_sessions_integration_test.go deleted file mode 100644 index 05de5f81b4..0000000000 --- a/server/src/sessions/service_sessions_integration_test.go +++ /dev/null @@ -1,464 +0,0 @@ -package sessions - -import ( - "context" - "log" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go/modules/nats" - "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/uptrace/bun" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/initialize" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/technical_helper" - "scrumlr.io/server/votings" -) - -type SessionServiceIntegrationTestSuite struct { - suite.Suite - dbContainer *postgres.PostgresContainer - natsContainer *nats.NATSContainer - db *bun.DB - natsConnectionString string - users map[string]User - boards map[string]TestBoard - sessions map[string]BoardSession -} - -func TestSessionServiceIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(SessionServiceIntegrationTestSuite)) -} - -func (suite *SessionServiceIntegrationTestSuite) SetupSuite() { - dbContainer, bun := initialize.StartTestDatabase() - suite.SeedDatabase(bun) - natsContainer, connectionString := initialize.StartTestNats() - - suite.dbContainer = dbContainer - suite.natsContainer = natsContainer - suite.db = bun - suite.natsConnectionString = connectionString -} - -func (suite *SessionServiceIntegrationTestSuite) TearDownSuite() { - initialize.StopTestDatabase(suite.dbContainer) - initialize.StopTestNats(suite.natsContainer) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_Create() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Write"].id - userId := suite.users["Luke"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Create(ctx, boardId, userId) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) - assert.Equal(t, common.ParticipantRole, session.Role) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantCreated, msg.Type) - sessionData, err := technical_helper.Unmarshal[BoardSession](msg.Data) - assert.Nil(t, err) - assert.Equal(t, userId, sessionData.User.ID) - assert.Equal(t, common.ParticipantRole, sessionData.Role) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_Update() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Update"].id - userId := suite.users["Luke"].ID - callerId := suite.users["Stan"].ID - role := common.ModeratorRole - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Update(ctx, BoardSessionUpdateRequest{Caller: callerId, Board: boardId, User: userId, Role: &role}) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) - assert.Equal(t, common.ModeratorRole, session.Role) - - msgSession := <-events - msgColumns := <-events - msgNotes := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msgSession.Type) - assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumns.Type) - assert.Equal(t, realtime.BoardEventNotesSync, msgNotes.Type) - sessionData, err := technical_helper.Unmarshal[BoardSession](msgSession.Data) - assert.Nil(t, err) - assert.Equal(t, userId, session.User.ID) - assert.Equal(t, common.ModeratorRole, sessionData.Role) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_UpdateAll() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["UpdateAll"].id - ready := false - raisedHand := false - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.UpdateAll(ctx, BoardSessionsUpdateRequest{Board: boardId, Ready: &ready, RaisedHand: &raisedHand}) - - assert.Nil(t, err) - assert.Len(t, sessions, 4) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantsUpdated, msg.Type) - sessionData, err := technical_helper.UnmarshalSlice[BoardSession](msg.Data) - assert.Nil(t, err) - assert.Len(t, sessionData, 4) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_UpdateUserBoard() { - -} - -func (suite *SessionServiceIntegrationTestSuite) Test_Get() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Santa"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Get(ctx, boardId, userId) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.User.ID) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_GetAll() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.GetAll(ctx, boardId, BoardSessionFilter{}) - assert.Nil(t, err) - assert.Len(t, sessions, 4) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_GetUserConnectedBoards() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.GetUserConnectedBoards(ctx, userId) - - assert.Nil(t, err) - assert.Len(t, sessions, 2) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_Connect() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Update"].id - userId := suite.users["Luke"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - err = sessionService.Connect(ctx, boardId, userId) - - assert.Nil(t, err) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_Disconnect() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Update"].id - userId := suite.users["Leia"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - err = sessionService.Disconnect(ctx, boardId, userId) - - assert.Nil(t, err) - - msgColumn := <-events - msgNote := <-events - assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumn.Type) - assert.Equal(t, realtime.BoardEventNotesSync, msgNote.Type) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_Exists() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - exists, err := sessionService.Exists(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, exists) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_ModeratorExists() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - exists, err := sessionService.ModeratorSessionExists(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, exists) -} - -func (suite *SessionServiceIntegrationTestSuite) Test_IsParticipantBanned() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Bob"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - banned, err := sessionService.IsParticipantBanned(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, banned) -} - -func (suite *SessionServiceIntegrationTestSuite) SeedDatabase(db *bun.DB) { - // tests users - suite.users = make(map[string]User, 7) - suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} - suite.users["Friend"] = User{ID: uuid.New(), Name: "Friend", AccountType: common.Anonymous} - suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} - suite.users["Bob"] = User{ID: uuid.New(), Name: "Bob", AccountType: common.Anonymous} - suite.users["Luke"] = User{ID: uuid.New(), Name: "Luke", AccountType: common.Anonymous} - suite.users["Leia"] = User{ID: uuid.New(), Name: "Leia", AccountType: common.Anonymous} - suite.users["Han"] = User{ID: uuid.New(), Name: "Han", AccountType: common.Anonymous} - - // test boards - suite.boards = make(map[string]TestBoard, 5) - suite.boards["Write"] = TestBoard{id: uuid.New(), name: "Write"} - suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} - suite.boards["Read"] = TestBoard{id: uuid.New(), name: "Read"} - suite.boards["ReadFilter"] = TestBoard{id: uuid.New(), name: "ReadFilter"} - suite.boards["UpdateAll"] = TestBoard{id: uuid.New(), name: "UpdateAll"} - - // test sessions - suite.sessions = make(map[string]BoardSession, 16) - // test sessions for the write board - suite.sessions["Write"] = BoardSession{User: suite.users["Han"], Board: suite.boards["Write"].id, Role: common.ParticipantRole} - // test sessions for the update board - suite.sessions["UpdateOwner"] = BoardSession{User: suite.users["Stan"], Board: suite.boards["Update"].id, Role: common.OwnerRole} - suite.sessions["UpdateParticipantModerator"] = BoardSession{User: suite.users["Luke"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} - suite.sessions["UpdateParticipantOwner"] = BoardSession{User: suite.users["Leia"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} - suite.sessions["UpdateModeratorOwner"] = BoardSession{User: suite.users["Han"], Board: suite.boards["Update"].id, Role: common.ParticipantRole} - // test sessions for the read board - suite.sessions["Read1"] = BoardSession{User: suite.users["Stan"], Board: suite.boards["Read"].id, Role: common.OwnerRole} - suite.sessions["Read2"] = BoardSession{User: suite.users["Friend"], Board: suite.boards["Read"].id, Role: common.ModeratorRole} - suite.sessions["Read3"] = BoardSession{User: suite.users["Santa"], Board: suite.boards["Read"].id, Role: common.ParticipantRole} - suite.sessions["Read4"] = BoardSession{User: suite.users["Bob"], Board: suite.boards["Read"].id, Role: common.ParticipantRole, Banned: true} - // test sessions for the read filter board - suite.sessions["ReadFilter1"] = BoardSession{User: suite.users["Stan"], Board: suite.boards["ReadFilter"].id, Role: common.OwnerRole, Ready: true, Connected: true} - suite.sessions["ReadFilter2"] = BoardSession{User: suite.users["Friend"], Board: suite.boards["ReadFilter"].id, Role: common.ModeratorRole, Ready: true} - suite.sessions["ReadFilter3"] = BoardSession{User: suite.users["Santa"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true, Connected: true} - suite.sessions["ReadFilter4"] = BoardSession{User: suite.users["Bob"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true} - // test sessions for the update all board - suite.sessions["UpdateAll1"] = BoardSession{User: suite.users["Stan"], Board: suite.boards["UpdateAll"].id, Role: common.OwnerRole, Connected: true, RaisedHand: true, Ready: false} - suite.sessions["UpdateAll2"] = BoardSession{User: suite.users["Luke"], Board: suite.boards["UpdateAll"].id, Role: common.ModeratorRole, Connected: true, RaisedHand: false, Ready: true} - suite.sessions["UpdateAll3"] = BoardSession{User: suite.users["Leia"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: false, Ready: false} - suite.sessions["UpdateAll4"] = BoardSession{User: suite.users["Han"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: true, Ready: true} - - for _, user := range suite.users { - err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } - - for _, board := range suite.boards { - err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) - if err != nil { - log.Fatalf("Failed to insert test board %s", err) - } - } - - for _, session := range suite.sessions { - err := initialize.InsertSession(db, session.User.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) - if err != nil { - log.Fatalf("Failed to insert test sessions %s", err) - } - } -} diff --git a/server/src/users/service_integration_test.go b/server/src/users/service_integration_test.go new file mode 100644 index 0000000000..657cc345ef --- /dev/null +++ b/server/src/users/service_integration_test.go @@ -0,0 +1,449 @@ +package users + +import ( + "context" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go/modules/nats" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/uptrace/bun" + "log" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/initialize" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessions" + "scrumlr.io/server/votings" + "testing" +) + +type TestBoard struct { + id uuid.UUID + name string +} + +type UserServiceIntegrationTestsuite struct { + suite.Suite + dbContainer *postgres.PostgresContainer + natsContainer *nats.NATSContainer + db *bun.DB + natsConnectionString string + users map[string]User + boards map[string]TestBoard + sessions map[string]sessions.BoardSession +} + +func TestUserServiceIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(UserServiceIntegrationTestsuite)) +} + +func (suite *UserServiceIntegrationTestsuite) SetupSuite() { + dbContainer, bun := initialize.StartTestDatabase() + suite.SeedDatabase(bun) + natsContainer, connectionString := initialize.StartTestNats() + + suite.dbContainer = dbContainer + suite.natsContainer = natsContainer + suite.db = bun + suite.natsConnectionString = connectionString +} + +func (suite *UserServiceIntegrationTestsuite) TeardownSuite() { + initialize.StopTestDatabase(suite.dbContainer) + initialize.StopTestNats(suite.natsContainer) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateAnonymous() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAnonymous(ctx, userName) + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Anonymous, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateAppleUser() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAppleUser(ctx, "appleId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Apple, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateAzureadUser() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAzureAdUser(ctx, "azureId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.AzureAd, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateGitHubUser() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateGitHubUser(ctx, "githubId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.GitHub, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateGoogleUser() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateGoogleUser(ctx, "googleId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Google, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateMicrosoft() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateMicrosoftUser(ctx, "microsoftId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Microsoft, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_CreateOIDCUser() { + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateOIDCUser(ctx, "oidcId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.TypeOIDC, user.AccountType) +} + +func (suite *UserServiceIntegrationTestsuite) Test_Update() { + t := suite.T() + ctx := context.Background() + + userId := suite.users["Update"].ID + boardId := suite.boards["Update"].id + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + events := broker.GetBoardChannel(ctx, boardId) + + user, err := userService.Update(ctx, UserUpdateRequest{ID: userId, Name: userName}) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) + assert.Equal(t, userName, user.Name) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) + sessionData := msg.Data.(map[string]interface{}) + assert.True(t, sessionData["connected"].(bool)) + assert.Equal(t, string(common.OwnerRole), sessionData["role"].(string)) +} + +func (suite *UserServiceIntegrationTestsuite) Test_Get() { + t := suite.T() + ctx := context.Background() + + userId := suite.users["Stan"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.Get(ctx, userId) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) + assert.Equal(t, suite.users["Stan"].Name, user.Name) +} + +func (suite *UserServiceIntegrationTestsuite) Test_Get_NotFound() { + t := suite.T() + ctx := context.Background() + + userId := uuid.New() + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.Get(ctx, userId) + + assert.Nil(t, user) + assert.NotNil(t, err) + assert.Equal(t, common.NotFoundError, err) +} + +func (suite *UserServiceIntegrationTestsuite) Test_AvailableForKeyMigration() { + t := suite.T() + ctx := context.Background() + + userId := suite.users["Santa"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + available, err := userService.IsUserAvailableForKeyMigration(ctx, userId) + + assert.Nil(t, err) + assert.True(t, available) +} + +func (suite *UserServiceIntegrationTestsuite) Test_SetKeyMigration() { + t := suite.T() + ctx := context.Background() + + userId := suite.users["Stan"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.SetKeyMigration(ctx, userId) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) +} + +func (suite *UserServiceIntegrationTestsuite) SeedDatabase(db *bun.DB) { + // test users + suite.users = make(map[string]User, 3) + suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} + suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} + suite.users["Update"] = User{ID: uuid.New(), Name: "UpdateMe", AccountType: common.Anonymous} + + // test boards + suite.boards = make(map[string]TestBoard, 1) + suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} + + // test sessions + //suite.sessions = make(map[string]sessions.BoardSession, 1) + //suite.sessions["Update"] = sessions.BoardSession{ID: suite.users["Update"].ID, Board: suite.boards["Update"].id, Role: common.OwnerRole, Connected: true} + + for _, user := range suite.users { + err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } + + for _, board := range suite.boards { + err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) + if err != nil { + log.Fatalf("Failed to insert test board %s", err) + } + } + + for _, session := range suite.sessions { + err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) + if err != nil { + log.Fatalf("Failed to insert test sessions %s", err) + } + } +} diff --git a/server/src/users/service_test.go b/server/src/users/service_test.go index 1e6c4a2b1b..a08976dff8 100644 --- a/server/src/users/service_test.go +++ b/server/src/users/service_test.go @@ -4,11 +4,12 @@ import ( "context" "database/sql" "errors" + "scrumlr.io/server/sessions" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" - mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/mock" "scrumlr.io/server/common" "scrumlr.io/server/realtime" ) @@ -23,7 +24,7 @@ func TestGetUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -43,7 +44,7 @@ func TestGetUser_NotFound(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -65,7 +66,7 @@ func TestGetUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -86,7 +87,7 @@ func TestCreateAnonymusUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -107,7 +108,7 @@ func TestCreateAnonymusUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -127,7 +128,7 @@ func TestCreateAnonymusUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -147,7 +148,7 @@ func TestCreateAnonymusUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -171,7 +172,7 @@ func TestCreateAppleUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -194,7 +195,7 @@ func TestCreateAppleUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -216,7 +217,7 @@ func TestCreateAppleUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -238,7 +239,7 @@ func TestCreateAppleUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -262,7 +263,7 @@ func TestCreateAzureUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -285,7 +286,7 @@ func TestCreateAzureUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -307,7 +308,7 @@ func TestCreateAzureUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -329,7 +330,7 @@ func TestCreateAzureUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -353,7 +354,7 @@ func TestCreateGitHubUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -376,7 +377,7 @@ func TestCreateGitHubUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -398,7 +399,7 @@ func TestCreateGitHubUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -420,7 +421,7 @@ func TestCreateGitHubUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -444,7 +445,7 @@ func TestCreateGoogleUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -467,7 +468,7 @@ func TestCreateGoogleUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -489,7 +490,7 @@ func TestCreateGoogleUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -511,7 +512,7 @@ func TestCreateGoogleUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -535,7 +536,7 @@ func TestCreateMicrosoftUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -558,7 +559,7 @@ func TestCreateMicrosoftUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -580,7 +581,7 @@ func TestCreateMicrosoftUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -602,7 +603,7 @@ func TestCreateMicrosoftUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -626,7 +627,7 @@ func TestCreateOIDCUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -649,7 +650,7 @@ func TestCreateOIDCUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -671,7 +672,7 @@ func TestCreateOIDCUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -693,7 +694,7 @@ func TestCreateOIDCUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -724,18 +725,18 @@ func TestUpdateUser(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) - mockSessionService.EXPECT().GetUserConnectedBoards(mock.Anything, userId). - Return([]*BoardSession{ - {User: user, Board: firstBoardId}, - {User: user, Board: secondBoardId}, + mockUserService := sessions.NewMockSessionService(t) + mockUserService.EXPECT().GetUserConnectedBoards(mock.Anything, userId). + Return([]*sessions.BoardSession{ + {ID: user.ID, Board: firstBoardId}, + {ID: user.ID, Board: secondBoardId}, }, nil) - mockSessionService.EXPECT().Get(mock.Anything, firstBoardId, userId). - Return(&BoardSession{User: user, Board: firstBoardId}, nil) - mockSessionService.EXPECT().Get(mock.Anything, secondBoardId, userId). - Return(&BoardSession{User: user, Board: secondBoardId}, nil) + mockUserService.EXPECT().Get(mock.Anything, firstBoardId, userId). + Return(&sessions.BoardSession{ID: user.ID, Board: firstBoardId}, nil) + mockUserService.EXPECT().Get(mock.Anything, secondBoardId, userId). + Return(&sessions.BoardSession{ID: user.ID, Board: secondBoardId}, nil) - userService := NewUserService(mockUserDatabase, broker, mockSessionService) + userService := NewUserService(mockUserDatabase, broker, mockUserService) updatedUser, err := userService.Update(context.Background(), UserUpdateRequest{ID: userId, Name: name}) @@ -756,7 +757,7 @@ func TestUpdateUser_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -777,7 +778,7 @@ func TestUpdateUser_EmptyUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -798,7 +799,7 @@ func TestUpdateUser_NewLineUsername(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -819,7 +820,7 @@ func TestAvailableForKeyMigration(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -840,7 +841,7 @@ func TestAvailableForKeyMigration_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -861,7 +862,7 @@ func TestSetKeyMigration(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) @@ -882,7 +883,7 @@ func TestSetKeymigration_DatabaseError(t *testing.T) { broker := new(realtime.Broker) broker.Con = mockBroker - mockSessionService := NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) userService := NewUserService(mockUserDatabase, broker, mockSessionService) From e3147b72808cf4157cdf0cc6a2c143176b26ac48 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 26 Sep 2025 11:29:54 +0200 Subject: [PATCH 09/16] fix tests --- server/src/users/service_integration_test.go | 782 +++++++++---------- 1 file changed, 391 insertions(+), 391 deletions(-) diff --git a/server/src/users/service_integration_test.go b/server/src/users/service_integration_test.go index 657cc345ef..68c887cb00 100644 --- a/server/src/users/service_integration_test.go +++ b/server/src/users/service_integration_test.go @@ -1,449 +1,449 @@ package users import ( - "context" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go/modules/nats" - "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/uptrace/bun" - "log" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/initialize" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessions" - "scrumlr.io/server/votings" - "testing" + "context" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go/modules/nats" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/uptrace/bun" + "log" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/initialize" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessions" + "scrumlr.io/server/votings" + "testing" ) type TestBoard struct { - id uuid.UUID - name string + id uuid.UUID + name string } type UserServiceIntegrationTestsuite struct { - suite.Suite - dbContainer *postgres.PostgresContainer - natsContainer *nats.NATSContainer - db *bun.DB - natsConnectionString string - users map[string]User - boards map[string]TestBoard - sessions map[string]sessions.BoardSession + suite.Suite + dbContainer *postgres.PostgresContainer + natsContainer *nats.NATSContainer + db *bun.DB + natsConnectionString string + users map[string]User + boards map[string]TestBoard + sessions map[string]sessions.BoardSession } func TestUserServiceIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(UserServiceIntegrationTestsuite)) + suite.Run(t, new(UserServiceIntegrationTestsuite)) } func (suite *UserServiceIntegrationTestsuite) SetupSuite() { - dbContainer, bun := initialize.StartTestDatabase() - suite.SeedDatabase(bun) - natsContainer, connectionString := initialize.StartTestNats() - - suite.dbContainer = dbContainer - suite.natsContainer = natsContainer - suite.db = bun - suite.natsConnectionString = connectionString + dbContainer, bun := initialize.StartTestDatabase() + suite.SeedDatabase(bun) + natsContainer, connectionString := initialize.StartTestNats() + + suite.dbContainer = dbContainer + suite.natsContainer = natsContainer + suite.db = bun + suite.natsConnectionString = connectionString } func (suite *UserServiceIntegrationTestsuite) TeardownSuite() { - initialize.StopTestDatabase(suite.dbContainer) - initialize.StopTestNats(suite.natsContainer) + initialize.StopTestDatabase(suite.dbContainer) + initialize.StopTestNats(suite.natsContainer) } func (suite *UserServiceIntegrationTestsuite) Test_CreateAnonymous() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateAnonymous(ctx, userName) - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Anonymous, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAnonymous(ctx, userName) + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Anonymous, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateAppleUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateAppleUser(ctx, "appleId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Apple, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAppleUser(ctx, "appleId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Apple, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateAzureadUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateAzureAdUser(ctx, "azureId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.AzureAd, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAzureAdUser(ctx, "azureId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.AzureAd, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateGitHubUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateGitHubUser(ctx, "githubId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.GitHub, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateGitHubUser(ctx, "githubId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.GitHub, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateGoogleUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateGoogleUser(ctx, "googleId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Google, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateGoogleUser(ctx, "googleId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Google, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateMicrosoft() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateMicrosoftUser(ctx, "microsoftId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Microsoft, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateMicrosoftUser(ctx, "microsoftId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Microsoft, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateOIDCUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateOIDCUser(ctx, "oidcId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.TypeOIDC, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateOIDCUser(ctx, "oidcId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.TypeOIDC, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_Update() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Update"].ID - boardId := suite.boards["Update"].id - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - events := broker.GetBoardChannel(ctx, boardId) - - user, err := userService.Update(ctx, UserUpdateRequest{ID: userId, Name: userName}) - - assert.Nil(t, err) - assert.Equal(t, userId, user.ID) - assert.Equal(t, userName, user.Name) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) - sessionData := msg.Data.(map[string]interface{}) - assert.True(t, sessionData["connected"].(bool)) - assert.Equal(t, string(common.OwnerRole), sessionData["role"].(string)) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Update"].ID + boardId := suite.boards["Update"].id + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + events := broker.GetBoardChannel(ctx, boardId) + + user, err := userService.Update(ctx, UserUpdateRequest{ID: userId, Name: userName}) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) + assert.Equal(t, userName, user.Name) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) + sessionData := msg.Data.(map[string]interface{}) + assert.True(t, sessionData["connected"].(bool)) + assert.Equal(t, string(common.OwnerRole), sessionData["role"].(string)) } func (suite *UserServiceIntegrationTestsuite) Test_Get() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.Get(ctx, userId) - - assert.Nil(t, err) - assert.Equal(t, userId, user.ID) - assert.Equal(t, suite.users["Stan"].Name, user.Name) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Stan"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.Get(ctx, userId) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) + assert.Equal(t, suite.users["Stan"].Name, user.Name) } func (suite *UserServiceIntegrationTestsuite) Test_Get_NotFound() { - t := suite.T() - ctx := context.Background() - - userId := uuid.New() - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.Get(ctx, userId) - - assert.Nil(t, user) - assert.NotNil(t, err) - assert.Equal(t, common.NotFoundError, err) + t := suite.T() + ctx := context.Background() + + userId := uuid.New() + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.Get(ctx, userId) + + assert.Nil(t, user) + assert.NotNil(t, err) + assert.Equal(t, common.NotFoundError, err) } func (suite *UserServiceIntegrationTestsuite) Test_AvailableForKeyMigration() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Santa"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - available, err := userService.IsUserAvailableForKeyMigration(ctx, userId) - - assert.Nil(t, err) - assert.True(t, available) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Santa"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + available, err := userService.IsUserAvailableForKeyMigration(ctx, userId) + + assert.Nil(t, err) + assert.True(t, available) } func (suite *UserServiceIntegrationTestsuite) Test_SetKeyMigration() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.SetKeyMigration(ctx, userId) - - assert.Nil(t, err) - assert.Equal(t, userId, user.ID) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Stan"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.SetKeyMigration(ctx, userId) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) } func (suite *UserServiceIntegrationTestsuite) SeedDatabase(db *bun.DB) { - // test users - suite.users = make(map[string]User, 3) - suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} - suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} - suite.users["Update"] = User{ID: uuid.New(), Name: "UpdateMe", AccountType: common.Anonymous} - - // test boards - suite.boards = make(map[string]TestBoard, 1) - suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} - - // test sessions - //suite.sessions = make(map[string]sessions.BoardSession, 1) - //suite.sessions["Update"] = sessions.BoardSession{ID: suite.users["Update"].ID, Board: suite.boards["Update"].id, Role: common.OwnerRole, Connected: true} - - for _, user := range suite.users { - err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } - - for _, board := range suite.boards { - err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) - if err != nil { - log.Fatalf("Failed to insert test board %s", err) - } - } - - for _, session := range suite.sessions { - err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) - if err != nil { - log.Fatalf("Failed to insert test sessions %s", err) - } - } + // test users + suite.users = make(map[string]User, 3) + suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} + suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} + suite.users["Update"] = User{ID: uuid.New(), Name: "UpdateMe", AccountType: common.Anonymous} + + // test boards + suite.boards = make(map[string]TestBoard, 1) + suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} + + // test sessions + suite.sessions = make(map[string]sessions.BoardSession, 1) + suite.sessions["Update"] = sessions.BoardSession{ID: suite.users["Update"].ID, Board: suite.boards["Update"].id, Role: common.OwnerRole, Connected: true} + + for _, user := range suite.users { + err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } + + for _, board := range suite.boards { + err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) + if err != nil { + log.Fatalf("Failed to insert test board %s", err) + } + } + + for _, session := range suite.sessions { + err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) + if err != nil { + log.Fatalf("Failed to insert test sessions %s", err) + } + } } From b4f4bb5599f638be2fe7852701fd6f8de46f59bc Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 26 Sep 2025 12:12:41 +0200 Subject: [PATCH 10/16] fix linter issues --- server/src/api/users.go | 153 ++++++++++++++++++------------------ server/src/feedback/type.go | 32 ++++---- 2 files changed, 94 insertions(+), 91 deletions(-) diff --git a/server/src/api/users.go b/server/src/api/users.go index 395871225d..8d440ecc17 100644 --- a/server/src/api/users.go +++ b/server/src/api/users.go @@ -1,94 +1,97 @@ package api import ( - "github.com/go-chi/chi/v5" - "net/http" - "scrumlr.io/server/users" - - "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" + "github.com/go-chi/chi/v5" + "net/http" + "scrumlr.io/server/users" + + "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" ) //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() + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.get") + defer span.End() - userId := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + 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 - } + 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) + render.Status(r, http.StatusOK) + render.Respond(w, r, user) } func (s *Server) getUserByID(w http.ResponseWriter, r *http.Request) { - //callerId := r.Context().Value(identifiers.UserIdentifier).(uuid.UUID) - - userParam := chi.URLParam(r, "user") - requestedUserId, err := uuid.Parse(userParam) - user, err := s.users.Get(r.Context(), requestedUserId) - if err != nil { - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, user) + //callerId := r.Context().Value(identifiers.UserIdentifier).(uuid.UUID) + + userParam := chi.URLParam(r, "user") + requestedUserId, err := uuid.Parse(userParam) + if err != nil { + common.Throw(w, r, err) + } + user, err := s.users.Get(r.Context(), requestedUserId) + if err != nil { + common.Throw(w, r, err) + return + } + + render.Status(r, http.StatusOK) + render.Respond(w, r, user) } 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) + 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/feedback/type.go b/server/src/feedback/type.go index 4f550a5a1c..f91e2f22c4 100644 --- a/server/src/feedback/type.go +++ b/server/src/feedback/type.go @@ -1,30 +1,30 @@ package feedback import ( - "encoding/json" - "errors" + "encoding/json" + "errors" ) type FeedbackType string const ( - BugReport FeedbackType = "BUG_REPORT" - FeatureRequest FeedbackType = "FEATURE_REQUEST" - Praise FeedbackType = "PRAISE" + BugReport FeedbackType = "BUG_REPORT" + FeatureRequest FeedbackType = "FEATURE_REQUEST" + Praise FeedbackType = "PRAISE" ) func (feedbackType *FeedbackType) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } - unmarshalledFeedbackType := FeedbackType(s) - switch unmarshalledFeedbackType { - case Praise, BugReport, FeatureRequest: - *feedbackType = unmarshalledFeedbackType - return nil - } + unmarshalledFeedbackType := FeedbackType(s) + switch unmarshalledFeedbackType { + case Praise, BugReport, FeatureRequest: + *feedbackType = unmarshalledFeedbackType + return nil + } - return errors.New("Invalid feedback type") + return errors.New("invalid feedback type") } From 83c257dea4bd34fc40239773f285791c732f825d Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 26 Sep 2025 12:24:04 +0200 Subject: [PATCH 11/16] frontend linter --- src/store/features/board/thunks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/features/board/thunks.ts b/src/store/features/board/thunks.ts index 49d9dffd67..e2097cd62a 100644 --- a/src/store/features/board/thunks.ts +++ b/src/store/features/board/thunks.ts @@ -93,7 +93,7 @@ export const permittedBoardAccess = createAsyncThunk< const message: ServerEvent = JSON.parse(evt.data); if (message.type === "INIT") { - const {board, columns, participants, notes, reactions, votes, votings, requests} = message.data; + const {board, columns, notes, reactions, votes, votings, requests} = message.data; const newParticipants = await mapParticipantsWithUsers(message as BoardInitEvent); dispatch( initializeBoard({ From 88e3bda832e76574ab6eafaef9d68236f683a5ce Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Fri, 26 Sep 2025 13:27:17 +0200 Subject: [PATCH 12/16] fix postman tests --- server/api.postman_collection.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/server/api.postman_collection.json b/server/api.postman_collection.json index 7baebef801..1cc5fdb9df 100644 --- a/server/api.postman_collection.json +++ b/server/api.postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "866d0706-8c3f-42a9-a894-37ff2462e1f8", + "_postman_id": "84c3f50c-339d-41ae-8152-29bb900426d1", "name": "scrumlr.io", "description": "This is the documentation for the REST API server of the application [scrumlr.io](https://scrumlr.io). You get in touch with us and send an email to [info@scrumlr.io](https://info@scrumlr.io). The software is [MIT licensed](https://opensource.org/licenses/MIT) so do whatever you want with it. If you want to checkout the progress of our development and take a peek into our backlog you can checkout our [GitHub repository](https://github.com/inovex/scrumlr.io). By the way, this already the third iteration of our server and we're still working on the interface and on further improvements. Since the API is mainly intended for our web client we won't start with API versions at the moment so breaking changes may be incoming. Once it got stable we'll maybe start with that.\n\nIf you're using the postman collection in order to explore the different resources you should also checkout the variables of the collection. Anytime you'll create new resources (e.g. your login or a board) variables will be stored and used for subsequent calls on other resources.\n\nAccess to protected resources will be authorized if a bearer token is sent or it is included in the `jwt` Cookie, which will be automatically set upon login.\n\n## Getting started\n\nLet's try to explain the basic flow of how a new board can will be created and someone tries to join the board as a participant.\n\nFirst you can check whether you are already logged in by a `GET` request on `/user`. See the _User_ section for more information.\n\n1. A user signs into the application (see _Login_ section)\n \n2. The user creates a new board (`POST` on `/boards`, checkout _Boards_ section)\n \n3. Another logged in user tries to join the board (`POST` on `/boards/{id}/participants`, checkout _Participants_ section)\n 1. If the boards access policy is set to `PUBLIC` the participant will be added to the board and afterwards all resources will be available\n \n 2. If the board requires a passphrase and the access policy is set to `BY_PASSPHRASE` a client error will be reported until the user sends the correct passphrase within the payload of the request\n \n 3. If the boards access policy is set to `BY_INVITE` a session request will be created instead and the user will be redirected to the new resource. The board owner now needs to accept or reject the request until the user can continue\n \n\nThese are just the basic steps of how sessions can be created. You can also have a look into the section _Realtime_ to see how you can open websockets and listen to live updates on the data.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "32837949" + "_exporter_id": "33458987" }, "item": [ { @@ -2093,7 +2093,7 @@ " pm.expect(res).to.have.property('participants');", " pm.expect(res.participants).to.be.an('array');", " pm.expect(res.participants.length).to.equal(1);", - " pm.expect(res.participants[0].user.id).to.equal(pm.collectionVariables.get('user_id'));", + " pm.expect(res.participants[0].id).to.equal(pm.collectionVariables.get('user_id'));", "})", "", "pm.test('Check notes', () => {", @@ -5115,8 +5115,8 @@ "});", "", "pm.test(\"Check response content\", () => {", - " pm.expect(res.user).to.be.an(\"object\");", - " pm.expect(res.user.id).to.equal(pm.collectionVariables.get(\"user_id\"));", + " pm.expect(res.id).to.be.a(\"string\");", + " pm.expect(res.id).to.equal(pm.collectionVariables.get(\"user_id\"));", "})" ], "type": "text/javascript", @@ -5156,9 +5156,9 @@ "\r", "pm.test(\"Check response content\", () => {\r", " pm.expect(res).to.be.an(\"array\");\r", - " pm.expect(res[0].user.id).to.equal(pm.collectionVariables.get(\"user_id\"));\r", + " pm.expect(res[0].id).to.equal(pm.collectionVariables.get(\"user_id\"));\r", "\r", - " pm.collectionVariables.set(\"owner_id\", res[0].user.id);\r", + " pm.collectionVariables.set(\"owner_id\", res[0].id);\r", "})" ], "type": "text/javascript", @@ -5198,9 +5198,9 @@ "\r", "pm.test(\"Check response content\", () => {\r", " pm.expect(res).to.be.an(\"array\");\r", - " pm.expect(res[0].user.id).to.equal(pm.collectionVariables.get(\"user_id\"));\r", + " pm.expect(res[0].id).to.equal(pm.collectionVariables.get(\"user_id\"));\r", "\r", - " pm.collectionVariables.set(\"owner_id\", res[0].user.id);\r", + " pm.collectionVariables.set(\"owner_id\", res[0].id);\r", "})" ], "type": "text/javascript", @@ -5261,8 +5261,8 @@ "});", "", "pm.test(\"Check response content\", () => {", - " pm.expect(res.user).to.be.an(\"object\");", - " pm.expect(res.user.id).to.equal(pm.collectionVariables.get(\"owner_id\"));", + " pm.expect(res.id).to.be.a(\"string\");", + " pm.expect(res.id).to.equal(pm.collectionVariables.get(\"owner_id\"));", "})" ], "type": "text/javascript", @@ -5303,7 +5303,7 @@ "", "pm.test(\"Check response content\", () => {", " pm.expect(res).to.be.an(\"array\");", - " pm.expect(res[0].user.id).to.equal(pm.collectionVariables.get(\"user_id\"));", + " pm.expect(res[0].id).to.equal(pm.collectionVariables.get(\"user_id\"));", "", " pm.expect(res[0].ready).to.equal(false);", " pm.expect(res[0].raisedHand).to.equal(false);", @@ -5354,7 +5354,7 @@ "});", "", "pm.test(\"Check response content\", () => {", - " pm.expect(res.user.id).to.equal(pm.collectionVariables.get(\"user_id\"));", + " pm.expect(res.id).to.equal(pm.collectionVariables.get(\"user_id\"));", "", " pm.expect(res.ready).to.equal(true);", " pm.expect(res.raisedHand).to.equal(true);", @@ -5561,4 +5561,4 @@ "value": "{column_template_id}" } ] -} +} \ No newline at end of file From f98448f2228d5f61227c8cdb21f5b4ef23a72338 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 29 Sep 2025 09:51:37 +0200 Subject: [PATCH 13/16] rename session id to userID --- server/src/sessions/dto.go | 4 +- server/src/sessions/service.go | 916 +++++++++--------- .../src/sessions/service_integration_test.go | 792 +++++++-------- server/src/sessions/service_test.go | 16 +- 4 files changed, 864 insertions(+), 864 deletions(-) diff --git a/server/src/sessions/dto.go b/server/src/sessions/dto.go index fad88dc80e..80887b762b 100644 --- a/server/src/sessions/dto.go +++ b/server/src/sessions/dto.go @@ -10,7 +10,7 @@ import ( // BoardSession is the response for all participant requests. type BoardSession struct { - ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"id"` // Flag indicates whether user is online and connected to the board. Connected bool `json:"connected"` @@ -80,7 +80,7 @@ type BoardSessionsUpdateRequest struct { func (b *BoardSession) From(session DatabaseBoardSession) *BoardSession { - b.ID = session.User + b.UserID = session.User b.Connected = session.Connected b.Ready = session.Ready b.RaisedHand = session.RaisedHand diff --git a/server/src/sessions/service.go b/server/src/sessions/service.go index a26c640c2c..7e9c9ebf65 100644 --- a/server/src/sessions/service.go +++ b/server/src/sessions/service.go @@ -1,538 +1,538 @@ 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, boardID, userID uuid.UUID) (*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", boardID.String()), - attribute.String("scrumlr.sessions.service.create.user", userID.String()), - ) - - session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ - Board: boardID, - User: userID, - Role: common.ParticipantRole, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to create board session") - span.RecordError(err) - log.Errorw("unable to create board session", "board", boardID, "user", userID, "error", err) - return nil, err - } - - service.createdSession(ctx, boardID, 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", boardID.String()), + attribute.String("scrumlr.sessions.service.create.user", userID.String()), + ) + + session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ + Board: boardID, + User: userID, + Role: common.ParticipantRole, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to create board session") + span.RecordError(err) + log.Errorw("unable to create board session", "board", boardID, "user", userID, "error", err) + return nil, err + } + + service.createdSession(ctx, boardID, 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 getboard 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 getboard 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 } 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 + 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 update all sessions") - span.RecordError(err) - return nil, err - } - - for _, session := range connectedBoards { - service.updatedSession(ctx, session.Board, session) - } - - return BoardSessions(connectedBoards), err + 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 update all sessions") + span.RecordError(err) + return nil, err + } + + for _, session := range connectedBoards { + service.updatedSession(ctx, session.Board, session) + } + + return BoardSessions(connectedBoards), 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 + 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 } 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 + 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 } 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) - } + 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) 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.ID { - 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_integration_test.go b/server/src/sessions/service_integration_test.go index fcba800788..8067e9f0fc 100644 --- a/server/src/sessions/service_integration_test.go +++ b/server/src/sessions/service_integration_test.go @@ -1,172 +1,172 @@ package sessions import ( - "context" - "log" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go/modules/nats" - "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/uptrace/bun" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/initialize" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/technical_helper" - "scrumlr.io/server/votings" + "context" + "log" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go/modules/nats" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/uptrace/bun" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/initialize" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/technical_helper" + "scrumlr.io/server/votings" ) type SessionServiceIntegrationTestSuite struct { - suite.Suite - dbContainer *postgres.PostgresContainer - natsContainer *nats.NATSContainer - db *bun.DB - natsConnectionString string - users map[string]uuid.UUID - boards map[string]TestBoard - sessions map[string]BoardSession + suite.Suite + dbContainer *postgres.PostgresContainer + natsContainer *nats.NATSContainer + db *bun.DB + natsConnectionString string + users map[string]uuid.UUID + boards map[string]TestBoard + sessions map[string]BoardSession } func TestSessionServiceIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(SessionServiceIntegrationTestSuite)) + suite.Run(t, new(SessionServiceIntegrationTestSuite)) } func (suite *SessionServiceIntegrationTestSuite) SetupSuite() { - dbContainer, bun := initialize.StartTestDatabase() - suite.SeedDatabase(bun) - natsContainer, connectionString := initialize.StartTestNats() - - suite.dbContainer = dbContainer - suite.natsContainer = natsContainer - suite.db = bun - suite.natsConnectionString = connectionString + dbContainer, bun := initialize.StartTestDatabase() + suite.SeedDatabase(bun) + natsContainer, connectionString := initialize.StartTestNats() + + suite.dbContainer = dbContainer + suite.natsContainer = natsContainer + suite.db = bun + suite.natsConnectionString = connectionString } func (suite *SessionServiceIntegrationTestSuite) TearDownSuite() { - initialize.StopTestDatabase(suite.dbContainer) - initialize.StopTestNats(suite.natsContainer) + initialize.StopTestDatabase(suite.dbContainer) + initialize.StopTestNats(suite.natsContainer) } func (suite *SessionServiceIntegrationTestSuite) Test_Create() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Write"].id - userId := suite.users["Luke"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Create(ctx, boardId, userId) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) - assert.Equal(t, common.ParticipantRole, session.Role) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantCreated, msg.Type) - sessionData, err := technical_helper.Unmarshal[BoardSession](msg.Data) - assert.Nil(t, err) - assert.Equal(t, userId, sessionData.ID) - assert.Equal(t, common.ParticipantRole, sessionData.Role) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Write"].id + userId := suite.users["Luke"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + events := broker.GetBoardChannel(ctx, boardId) + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + session, err := sessionService.Create(ctx, boardId, userId) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ParticipantRole, session.Role) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantCreated, msg.Type) + sessionData, err := technical_helper.Unmarshal[BoardSession](msg.Data) + assert.Nil(t, err) + assert.Equal(t, userId, sessionData.UserID) + assert.Equal(t, common.ParticipantRole, sessionData.Role) } func (suite *SessionServiceIntegrationTestSuite) Test_Update() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Update"].id - userId := suite.users["Luke"] - callerId := suite.users["Stan"] - role := common.ModeratorRole - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Update(ctx, BoardSessionUpdateRequest{Caller: callerId, Board: boardId, User: userId, Role: &role}) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) - assert.Equal(t, common.ModeratorRole, session.Role) - - msgSession := <-events - msgColumns := <-events - msgNotes := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msgSession.Type) - assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumns.Type) - assert.Equal(t, realtime.BoardEventNotesSync, msgNotes.Type) - sessionData, err := technical_helper.Unmarshal[BoardSession](msgSession.Data) - assert.Nil(t, err) - assert.Equal(t, userId, session.ID) - assert.Equal(t, common.ModeratorRole, sessionData.Role) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Update"].id + userId := suite.users["Luke"] + callerId := suite.users["Stan"] + role := common.ModeratorRole + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + events := broker.GetBoardChannel(ctx, boardId) + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + session, err := sessionService.Update(ctx, BoardSessionUpdateRequest{Caller: callerId, Board: boardId, User: userId, Role: &role}) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ModeratorRole, session.Role) + + msgSession := <-events + msgColumns := <-events + msgNotes := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msgSession.Type) + assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumns.Type) + assert.Equal(t, realtime.BoardEventNotesSync, msgNotes.Type) + sessionData, err := technical_helper.Unmarshal[BoardSession](msgSession.Data) + assert.Nil(t, err) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ModeratorRole, sessionData.Role) } func (suite *SessionServiceIntegrationTestSuite) Test_UpdateAll() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["UpdateAll"].id - ready := false - raisedHand := false - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.UpdateAll(ctx, BoardSessionsUpdateRequest{Board: boardId, Ready: &ready, RaisedHand: &raisedHand}) - - assert.Nil(t, err) - assert.Len(t, sessions, 4) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantsUpdated, msg.Type) - sessionData, err := technical_helper.UnmarshalSlice[BoardSession](msg.Data) - assert.Nil(t, err) - assert.Len(t, sessionData, 4) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["UpdateAll"].id + ready := false + raisedHand := false + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + events := broker.GetBoardChannel(ctx, boardId) + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + sessions, err := sessionService.UpdateAll(ctx, BoardSessionsUpdateRequest{Board: boardId, Ready: &ready, RaisedHand: &raisedHand}) + + assert.Nil(t, err) + assert.Len(t, sessions, 4) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantsUpdated, msg.Type) + sessionData, err := technical_helper.UnmarshalSlice[BoardSession](msg.Data) + assert.Nil(t, err) + assert.Len(t, sessionData, 4) } func (suite *SessionServiceIntegrationTestSuite) Test_UpdateUserBoard() { @@ -174,298 +174,298 @@ func (suite *SessionServiceIntegrationTestSuite) Test_UpdateUserBoard() { } func (suite *SessionServiceIntegrationTestSuite) Test_Get() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Santa"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Get(ctx, boardId, userId) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Santa"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + session, err := sessionService.Get(ctx, boardId, userId) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) } func (suite *SessionServiceIntegrationTestSuite) Test_GetAll() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.GetAll(ctx, boardId, BoardSessionFilter{}) - assert.Nil(t, err) - assert.Len(t, sessions, 4) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + sessions, err := sessionService.GetAll(ctx, boardId, BoardSessionFilter{}) + assert.Nil(t, err) + assert.Len(t, sessions, 4) } func (suite *SessionServiceIntegrationTestSuite) Test_GetUserConnectedBoards() { - t := suite.T() - ctx := context.Background() + t := suite.T() + ctx := context.Background() - userId := suite.users["Stan"] + userId := suite.users["Stan"] - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - sessions, err := sessionService.GetUserConnectedBoards(ctx, userId) + sessions, err := sessionService.GetUserConnectedBoards(ctx, userId) - assert.Nil(t, err) - assert.Len(t, sessions, 2) + assert.Nil(t, err) + assert.Len(t, sessions, 2) } func (suite *SessionServiceIntegrationTestSuite) Test_Connect() { - t := suite.T() - ctx := context.Background() + t := suite.T() + ctx := context.Background() - boardId := suite.boards["Update"].id - userId := suite.users["Luke"] + boardId := suite.boards["Update"].id + userId := suite.users["Luke"] - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } - events := broker.GetBoardChannel(ctx, boardId) + events := broker.GetBoardChannel(ctx, boardId) - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - err = sessionService.Connect(ctx, boardId, userId) + err = sessionService.Connect(ctx, boardId, userId) - assert.Nil(t, err) + assert.Nil(t, err) - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) } func (suite *SessionServiceIntegrationTestSuite) Test_Disconnect() { - t := suite.T() - ctx := context.Background() + t := suite.T() + ctx := context.Background() - boardId := suite.boards["Update"].id - userId := suite.users["Leia"] + boardId := suite.boards["Update"].id + userId := suite.users["Leia"] - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } - events := broker.GetBoardChannel(ctx, boardId) + events := broker.GetBoardChannel(ctx, boardId) - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - err = sessionService.Disconnect(ctx, boardId, userId) + err = sessionService.Disconnect(ctx, boardId, userId) - assert.Nil(t, err) + assert.Nil(t, err) - msgColumn := <-events - msgNote := <-events - assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumn.Type) - assert.Equal(t, realtime.BoardEventNotesSync, msgNote.Type) + msgColumn := <-events + msgNote := <-events + assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumn.Type) + assert.Equal(t, realtime.BoardEventNotesSync, msgNote.Type) } func (suite *SessionServiceIntegrationTestSuite) Test_Exists() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Stan"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - exists, err := sessionService.Exists(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, exists) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Stan"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + exists, err := sessionService.Exists(ctx, boardId, userId) + + assert.Nil(t, err) + assert.True(t, exists) } func (suite *SessionServiceIntegrationTestSuite) Test_ModeratorExists() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Stan"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - exists, err := sessionService.ModeratorSessionExists(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, exists) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Stan"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + exists, err := sessionService.ModeratorSessionExists(ctx, boardId, userId) + + assert.Nil(t, err) + assert.True(t, exists) } func (suite *SessionServiceIntegrationTestSuite) Test_IsParticipantBanned() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Bob"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - banned, err := sessionService.IsParticipantBanned(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, banned) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Bob"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + banned, err := sessionService.IsParticipantBanned(ctx, boardId, userId) + + assert.Nil(t, err) + assert.True(t, banned) } func (suite *SessionServiceIntegrationTestSuite) SeedDatabase(db *bun.DB) { - // tests users - suite.users = make(map[string]uuid.UUID, 7) - suite.users["Stan"] = uuid.New() - suite.users["Friend"] = uuid.New() - suite.users["Santa"] = uuid.New() - suite.users["Bob"] = uuid.New() - suite.users["Luke"] = uuid.New() - suite.users["Leia"] = uuid.New() - suite.users["Han"] = uuid.New() - - // test boards - suite.boards = make(map[string]TestBoard, 5) - suite.boards["Write"] = TestBoard{id: uuid.New(), name: "Write"} - suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} - suite.boards["Read"] = TestBoard{id: uuid.New(), name: "Read"} - suite.boards["ReadFilter"] = TestBoard{id: uuid.New(), name: "ReadFilter"} - suite.boards["UpdateAll"] = TestBoard{id: uuid.New(), name: "UpdateAll"} - - // test sessions - suite.sessions = make(map[string]BoardSession, 16) - // test sessions for the write board - suite.sessions["Write"] = BoardSession{ID: suite.users["Han"], Board: suite.boards["Write"].id, Role: common.ParticipantRole} - // test sessions for the update board - suite.sessions["UpdateOwner"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["Update"].id, Role: common.OwnerRole} - suite.sessions["UpdateParticipantModerator"] = BoardSession{ID: suite.users["Luke"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} - suite.sessions["UpdateParticipantOwner"] = BoardSession{ID: suite.users["Leia"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} - suite.sessions["UpdateModeratorOwner"] = BoardSession{ID: suite.users["Han"], Board: suite.boards["Update"].id, Role: common.ParticipantRole} - // test sessions for the read board - suite.sessions["Read1"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["Read"].id, Role: common.OwnerRole} - suite.sessions["Read2"] = BoardSession{ID: suite.users["Friend"], Board: suite.boards["Read"].id, Role: common.ModeratorRole} - suite.sessions["Read3"] = BoardSession{ID: suite.users["Santa"], Board: suite.boards["Read"].id, Role: common.ParticipantRole} - suite.sessions["Read4"] = BoardSession{ID: suite.users["Bob"], Board: suite.boards["Read"].id, Role: common.ParticipantRole, Banned: true} - // test sessions for the read filter board - suite.sessions["ReadFilter1"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["ReadFilter"].id, Role: common.OwnerRole, Ready: true, Connected: true} - suite.sessions["ReadFilter2"] = BoardSession{ID: suite.users["Friend"], Board: suite.boards["ReadFilter"].id, Role: common.ModeratorRole, Ready: true} - suite.sessions["ReadFilter3"] = BoardSession{ID: suite.users["Santa"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true, Connected: true} - suite.sessions["ReadFilter4"] = BoardSession{ID: suite.users["Bob"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true} - // test sessions for the update all board - suite.sessions["UpdateAll1"] = BoardSession{ID: suite.users["Stan"], Board: suite.boards["UpdateAll"].id, Role: common.OwnerRole, Connected: true, RaisedHand: true, Ready: false} - suite.sessions["UpdateAll2"] = BoardSession{ID: suite.users["Luke"], Board: suite.boards["UpdateAll"].id, Role: common.ModeratorRole, Connected: true, RaisedHand: false, Ready: true} - suite.sessions["UpdateAll3"] = BoardSession{ID: suite.users["Leia"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: false, Ready: false} - suite.sessions["UpdateAll4"] = BoardSession{ID: suite.users["Han"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: true, Ready: true} - - for name, user := range suite.users { - if name == "Stan" { - err := initialize.InsertUser(db, user, name, string(common.Google)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } else { - err := initialize.InsertUser(db, user, name, string(common.Anonymous)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } - } - - for _, board := range suite.boards { - err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) - if err != nil { - log.Fatalf("Failed to insert test board %s", err) - } - } - - for _, session := range suite.sessions { - err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) - if err != nil { - log.Fatalf("Failed to insert test sessions %s", err) - } - } + // tests users + suite.users = make(map[string]uuid.UUID, 7) + suite.users["Stan"] = uuid.New() + suite.users["Friend"] = uuid.New() + suite.users["Santa"] = uuid.New() + suite.users["Bob"] = uuid.New() + suite.users["Luke"] = uuid.New() + suite.users["Leia"] = uuid.New() + suite.users["Han"] = uuid.New() + + // test boards + suite.boards = make(map[string]TestBoard, 5) + suite.boards["Write"] = TestBoard{id: uuid.New(), name: "Write"} + suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} + suite.boards["Read"] = TestBoard{id: uuid.New(), name: "Read"} + suite.boards["ReadFilter"] = TestBoard{id: uuid.New(), name: "ReadFilter"} + suite.boards["UpdateAll"] = TestBoard{id: uuid.New(), name: "UpdateAll"} + + // test sessions + suite.sessions = make(map[string]BoardSession, 16) + // test sessions for the write board + suite.sessions["Write"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["Write"].id, Role: common.ParticipantRole} + // test sessions for the update board + suite.sessions["UpdateOwner"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["Update"].id, Role: common.OwnerRole} + suite.sessions["UpdateParticipantModerator"] = BoardSession{UserID: suite.users["Luke"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} + suite.sessions["UpdateParticipantOwner"] = BoardSession{UserID: suite.users["Leia"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} + suite.sessions["UpdateModeratorOwner"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["Update"].id, Role: common.ParticipantRole} + // test sessions for the read board + suite.sessions["Read1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["Read"].id, Role: common.OwnerRole} + suite.sessions["Read2"] = BoardSession{UserID: suite.users["Friend"], Board: suite.boards["Read"].id, Role: common.ModeratorRole} + suite.sessions["Read3"] = BoardSession{UserID: suite.users["Santa"], Board: suite.boards["Read"].id, Role: common.ParticipantRole} + suite.sessions["Read4"] = BoardSession{UserID: suite.users["Bob"], Board: suite.boards["Read"].id, Role: common.ParticipantRole, Banned: true} + // test sessions for the read filter board + suite.sessions["ReadFilter1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["ReadFilter"].id, Role: common.OwnerRole, Ready: true, Connected: true} + suite.sessions["ReadFilter2"] = BoardSession{UserID: suite.users["Friend"], Board: suite.boards["ReadFilter"].id, Role: common.ModeratorRole, Ready: true} + suite.sessions["ReadFilter3"] = BoardSession{UserID: suite.users["Santa"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true, Connected: true} + suite.sessions["ReadFilter4"] = BoardSession{UserID: suite.users["Bob"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true} + // test sessions for the update all board + suite.sessions["UpdateAll1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["UpdateAll"].id, Role: common.OwnerRole, Connected: true, RaisedHand: true, Ready: false} + suite.sessions["UpdateAll2"] = BoardSession{UserID: suite.users["Luke"], Board: suite.boards["UpdateAll"].id, Role: common.ModeratorRole, Connected: true, RaisedHand: false, Ready: true} + suite.sessions["UpdateAll3"] = BoardSession{UserID: suite.users["Leia"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: false, Ready: false} + suite.sessions["UpdateAll4"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: true, Ready: true} + + for name, user := range suite.users { + if name == "Stan" { + err := initialize.InsertUser(db, user, name, string(common.Google)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } else { + err := initialize.InsertUser(db, user, name, string(common.Anonymous)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } + } + + for _, board := range suite.boards { + err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) + if err != nil { + log.Fatalf("Failed to insert test board %s", err) + } + } + + for _, session := range suite.sessions { + err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) + if err != nil { + log.Fatalf("Failed to insert test sessions %s", err) + } + } } diff --git a/server/src/sessions/service_test.go b/server/src/sessions/service_test.go index 1171d17782..b97fb91e4b 100644 --- a/server/src/sessions/service_test.go +++ b/server/src/sessions/service_test.go @@ -38,7 +38,7 @@ func TestGetSession(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) + assert.Equal(t, userId, session.UserID) } func TestGetSession_NotFound(t *testing.T) { @@ -116,10 +116,10 @@ func TestGetSessions(t *testing.T) { assert.NotNil(t, boardSessions) assert.Len(t, boardSessions, 2) - assert.Equal(t, firstUserId, boardSessions[0].ID) + assert.Equal(t, firstUserId, boardSessions[0].UserID) assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, secondUserId, boardSessions[1].ID) + assert.Equal(t, secondUserId, boardSessions[1].UserID) assert.Equal(t, boardId, boardSessions[1].Board) } @@ -308,7 +308,7 @@ func TestCreateSession(t *testing.T) { assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) + assert.Equal(t, userId, session.UserID) assert.Equal(t, common.ParticipantRole, session.Role) } @@ -389,7 +389,7 @@ func TestUpdateSession_Role(t *testing.T) { assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) + assert.Equal(t, userId, session.UserID) assert.Equal(t, common.ModeratorRole, session.Role) } @@ -442,7 +442,7 @@ func TestUpdateSession_RaiseHand(t *testing.T) { assert.NotNil(t, session) assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.ID) + assert.Equal(t, userId, session.UserID) assert.Equal(t, raisedHand, session.RaisedHand) } @@ -698,11 +698,11 @@ func TestUpdateAllSessions(t *testing.T) { assert.Len(t, boardSessions, 2) assert.Equal(t, boardId, boardSessions[0].Board) - assert.Equal(t, firstUserId, boardSessions[0].ID) + 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].ID) + assert.Equal(t, secondUserId, boardSessions[1].UserID) assert.Equal(t, ready, boardSessions[1].Ready) } From 2654091cc596802320f86d173e7eee6536844739 Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 29 Sep 2025 10:00:51 +0200 Subject: [PATCH 14/16] rename usage of session id --- server/src/api/boards.go | 1208 ++++++++-------- server/src/api/event_filter.go | 2 +- server/src/api/event_filter_test.go | 1216 ++++++++--------- server/src/boards/service.go | 2 +- .../service_integration_test.go | 2 +- server/src/sessionrequests/service_test.go | 500 +++---- server/src/sessions/service.go | 916 ++++++------- .../src/sessions/service_integration_test.go | 792 +++++------ server/src/users/service.go | 4 +- server/src/users/service_integration_test.go | 782 +++++------ server/src/users/service_test.go | 8 +- 11 files changed, 2716 insertions(+), 2716 deletions(-) diff --git a/server/src/api/boards.go b/server/src/api/boards.go index 031dae7787..b515cf72da 100644 --- a/server/src/api/boards.go +++ b/server/src/api/boards.go @@ -1,658 +1,658 @@ package api import ( - "database/sql" - "encoding/csv" - "errors" - "fmt" - "net/http" - "strconv" + "database/sql" + "encoding/csv" + "errors" + "fmt" + "net/http" + "strconv" - "go.opentelemetry.io/otel/codes" - "scrumlr.io/server/sessions" + "go.opentelemetry.io/otel/codes" + "scrumlr.io/server/sessions" - "scrumlr.io/server/boards" - "scrumlr.io/server/votings" + "scrumlr.io/server/boards" + "scrumlr.io/server/votings" - "scrumlr.io/server/columns" - "scrumlr.io/server/notes" + "scrumlr.io/server/columns" + "scrumlr.io/server/notes" - "scrumlr.io/server/identifiers" + "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" + "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, board, user) - 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 := common.Sha512BySalt(body.Passphrase, *b.Salt) - if encodedPassphrase == *b.Passphrase { - _, err := s.sessions.Create(ctx, board, user) - 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, board, user) + 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 := common.Sha512BySalt(body.Passphrase, *b.Salt) + if encodedPassphrase == *b.Passphrase { + _, err := s.sessions.Create(ctx, board, user) + 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.ID == note.Author { - user, _ := s.users.Get(ctx, session.ID) // 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/event_filter.go b/server/src/api/event_filter.go index 51e2af4f98..ac679dcef6 100644 --- a/server/src/api/event_filter.go +++ b/server/src/api/event_filter.go @@ -199,7 +199,7 @@ func (bs *BoardSubscription) participantUpdated(event *realtime.BoardEvent, isMo if isMod { // Cache the changes of when a participant got updated updatedSessions := technical_helper.MapSlice(bs.boardParticipants, func(boardSession *sessions.BoardSession) *sessions.BoardSession { - if boardSession.ID == participantSession.ID { + if boardSession.UserID == participantSession.UserID { return participantSession } else { return boardSession diff --git a/server/src/api/event_filter_test.go b/server/src/api/event_filter_test.go index b8d32c65a2..14c9c71d7c 100644 --- a/server/src/api/event_filter_test.go +++ b/server/src/api/event_filter_test.go @@ -1,794 +1,794 @@ package api import ( - "math/rand" - "scrumlr.io/server/users" - "testing" - - "scrumlr.io/server/sessions" - - "scrumlr.io/server/boards" - "scrumlr.io/server/common" - "scrumlr.io/server/votings" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "scrumlr.io/server/columns" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessionrequests" - "scrumlr.io/server/technical_helper" + "math/rand" + "scrumlr.io/server/users" + "testing" + + "scrumlr.io/server/sessions" + + "scrumlr.io/server/boards" + "scrumlr.io/server/common" + "scrumlr.io/server/votings" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "scrumlr.io/server/columns" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessionrequests" + "scrumlr.io/server/technical_helper" ) var ( - moderatorUser = users.User{ - ID: uuid.New(), - } - ownerUser = users.User{ - ID: uuid.New(), - } - participantUser = users.User{ - ID: uuid.New(), - AccountType: common.Anonymous, - } - moderatorBoardSession = sessions.BoardSession{ - ID: moderatorUser.ID, - Role: common.ModeratorRole, - } - ownerBoardSession = sessions.BoardSession{ - ID: ownerUser.ID, - Role: common.OwnerRole, - } - participantBoardSession = sessions.BoardSession{ - ID: participantUser.ID, - Role: common.ParticipantRole, - } - boardSessions = []*sessions.BoardSession{ - &participantBoardSession, - &ownerBoardSession, - &moderatorBoardSession, - } - boardSettings = &boards.Board{ - ID: uuid.New(), - AccessPolicy: boards.Public, - ShowAuthors: true, - ShowNotesOfOtherUsers: true, - AllowStacking: true, - } - aSeeableColumn = columns.Column{ - ID: uuid.New(), - Name: "Main Thread", - Color: "backlog-blue", - Visible: true, - Index: 0, - } - aModeratorNote = notes.Note{ - ID: uuid.New(), - Author: moderatorUser.ID, - Text: "Moderator Text", - Position: notes.NotePosition{ - Column: aSeeableColumn.ID, - Stack: uuid.NullUUID{}, - Rank: 1, - }, - } - aParticipantNote = notes.Note{ - ID: uuid.New(), - Author: participantUser.ID, - Text: "User Text", - Position: notes.NotePosition{ - Column: aSeeableColumn.ID, - Stack: uuid.NullUUID{}, - Rank: 0, - }, - } - aHiddenColumn = columns.Column{ - ID: uuid.New(), - Name: "Lean Coffee", - Color: "poker-purple", - Visible: false, - Index: 1, - } - aOwnerNote = notes.Note{ - ID: uuid.New(), - Author: ownerUser.ID, - Text: "Owner Text", - Position: notes.NotePosition{ - Column: aHiddenColumn.ID, - Rank: 1, - Stack: uuid.NullUUID{}, - }, - } - boardSub = &BoardSubscription{ - boardParticipants: []*sessions.BoardSession{&moderatorBoardSession, &ownerBoardSession, &participantBoardSession}, - boardColumns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - boardNotes: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - boardSettings: &boards.Board{ - ShowNotesOfOtherUsers: false, - }, - } - boardEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventBoardUpdated, - Data: boardSettings, - } - columnEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - } - noteEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - } - votingID = uuid.New() - votingData = &votings.VotingUpdated{ - Notes: []votings.Note{ - {ID: aParticipantNote.ID, Author: aParticipantNote.Author, Text: aParticipantNote.Text, Edited: aParticipantNote.Edited, Position: votings.NotePosition{Column: aParticipantNote.Position.Column, Stack: aParticipantNote.Position.Stack, Rank: aParticipantNote.Position.Rank}}, - {ID: aModeratorNote.ID, Author: aModeratorNote.Author, Text: aModeratorNote.Text, Edited: aModeratorNote.Edited, Position: votings.NotePosition{Column: aModeratorNote.Position.Column, Stack: aModeratorNote.Position.Stack, Rank: aModeratorNote.Position.Rank}}, - {ID: aOwnerNote.ID, Author: aOwnerNote.Author, Text: aOwnerNote.Text, Edited: aOwnerNote.Edited, Position: votings.NotePosition{Column: aOwnerNote.Position.Column, Stack: aOwnerNote.Position.Stack, Rank: aOwnerNote.Position.Rank}}, - }, - Voting: &votings.Voting{ - ID: votingID, - VoteLimit: 5, - AllowMultipleVotes: true, - ShowVotesOfOthers: false, - Status: "CLOSED", - VotingResults: &votings.VotingResults{ - Total: 5, - Votes: map[uuid.UUID]votings.VotingResultsPerNote{ - aParticipantNote.ID: { - Total: 2, - Users: nil, - }, - aModeratorNote.ID: { - Total: 1, - Users: nil, - }, - aOwnerNote.ID: { - Total: 2, - Users: nil, - }, - }, - }, - }, - } - votingEvent = &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: votingData, - } - initEvent = InitEvent{ - Type: realtime.BoardEventInit, - Data: boards.FullBoard{ - Board: &boards.Board{}, - Columns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - Notes: []*notes.Note{&aOwnerNote, &aModeratorNote, &aParticipantNote}, - Votings: []*votings.Voting{votingData.Voting}, - Votes: []*votings.Vote{}, - BoardSessions: boardSessions, - BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, - }, - } + moderatorUser = users.User{ + ID: uuid.New(), + } + ownerUser = users.User{ + ID: uuid.New(), + } + participantUser = users.User{ + ID: uuid.New(), + AccountType: common.Anonymous, + } + moderatorBoardSession = sessions.BoardSession{ + UserID: moderatorUser.ID, + Role: common.ModeratorRole, + } + ownerBoardSession = sessions.BoardSession{ + UserID: ownerUser.ID, + Role: common.OwnerRole, + } + participantBoardSession = sessions.BoardSession{ + UserID: participantUser.ID, + Role: common.ParticipantRole, + } + boardSessions = []*sessions.BoardSession{ + &participantBoardSession, + &ownerBoardSession, + &moderatorBoardSession, + } + boardSettings = &boards.Board{ + ID: uuid.New(), + AccessPolicy: boards.Public, + ShowAuthors: true, + ShowNotesOfOtherUsers: true, + AllowStacking: true, + } + aSeeableColumn = columns.Column{ + ID: uuid.New(), + Name: "Main Thread", + Color: "backlog-blue", + Visible: true, + Index: 0, + } + aModeratorNote = notes.Note{ + ID: uuid.New(), + Author: moderatorUser.ID, + Text: "Moderator Text", + Position: notes.NotePosition{ + Column: aSeeableColumn.ID, + Stack: uuid.NullUUID{}, + Rank: 1, + }, + } + aParticipantNote = notes.Note{ + ID: uuid.New(), + Author: participantUser.ID, + Text: "User Text", + Position: notes.NotePosition{ + Column: aSeeableColumn.ID, + Stack: uuid.NullUUID{}, + Rank: 0, + }, + } + aHiddenColumn = columns.Column{ + ID: uuid.New(), + Name: "Lean Coffee", + Color: "poker-purple", + Visible: false, + Index: 1, + } + aOwnerNote = notes.Note{ + ID: uuid.New(), + Author: ownerUser.ID, + Text: "Owner Text", + Position: notes.NotePosition{ + Column: aHiddenColumn.ID, + Rank: 1, + Stack: uuid.NullUUID{}, + }, + } + boardSub = &BoardSubscription{ + boardParticipants: []*sessions.BoardSession{&moderatorBoardSession, &ownerBoardSession, &participantBoardSession}, + boardColumns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + boardNotes: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + boardSettings: &boards.Board{ + ShowNotesOfOtherUsers: false, + }, + } + boardEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventBoardUpdated, + Data: boardSettings, + } + columnEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + } + noteEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + } + votingID = uuid.New() + votingData = &votings.VotingUpdated{ + Notes: []votings.Note{ + {ID: aParticipantNote.ID, Author: aParticipantNote.Author, Text: aParticipantNote.Text, Edited: aParticipantNote.Edited, Position: votings.NotePosition{Column: aParticipantNote.Position.Column, Stack: aParticipantNote.Position.Stack, Rank: aParticipantNote.Position.Rank}}, + {ID: aModeratorNote.ID, Author: aModeratorNote.Author, Text: aModeratorNote.Text, Edited: aModeratorNote.Edited, Position: votings.NotePosition{Column: aModeratorNote.Position.Column, Stack: aModeratorNote.Position.Stack, Rank: aModeratorNote.Position.Rank}}, + {ID: aOwnerNote.ID, Author: aOwnerNote.Author, Text: aOwnerNote.Text, Edited: aOwnerNote.Edited, Position: votings.NotePosition{Column: aOwnerNote.Position.Column, Stack: aOwnerNote.Position.Stack, Rank: aOwnerNote.Position.Rank}}, + }, + Voting: &votings.Voting{ + ID: votingID, + VoteLimit: 5, + AllowMultipleVotes: true, + ShowVotesOfOthers: false, + Status: "CLOSED", + VotingResults: &votings.VotingResults{ + Total: 5, + Votes: map[uuid.UUID]votings.VotingResultsPerNote{ + aParticipantNote.ID: { + Total: 2, + Users: nil, + }, + aModeratorNote.ID: { + Total: 1, + Users: nil, + }, + aOwnerNote.ID: { + Total: 2, + Users: nil, + }, + }, + }, + }, + } + votingEvent = &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: votingData, + } + initEvent = InitEvent{ + Type: realtime.BoardEventInit, + Data: boards.FullBoard{ + Board: &boards.Board{}, + Columns: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + Notes: []*notes.Note{&aOwnerNote, &aModeratorNote, &aParticipantNote}, + Votings: []*votings.Voting{votingData.Voting}, + Votes: []*votings.Vote{}, + BoardSessions: boardSessions, + BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, + }, + } ) func getUserById(id uuid.UUID) users.User { - if ownerUser.ID == id { - return ownerUser - } else if participantUser.ID == id { - return participantUser - } else if moderatorUser.ID == id { - return moderatorUser - } - return users.User{} + if ownerUser.ID == id { + return ownerUser + } else if participantUser.ID == id { + return participantUser + } else if moderatorUser.ID == id { + return moderatorUser + } + return users.User{} } func TestEventFilter(t *testing.T) { - t.Run("TestIsOwnerModerator", testIsOwnerModerator) - t.Run("TestIsModModerator", testIsModModerator) - t.Run("TestIsParticipantModerator", testIsParticipantModerator) - t.Run("TestIsUnknownUuidModerator", testIsUnknownUuidModerator) - t.Run("TestParseBoardSettingsData", testParseBoardSettingsData) - t.Run("TestParseColumnData", testParseColumnData) - t.Run("TestParseNoteData", testParseNoteData) - t.Run("TestParseVotingData", testParseVotingData) - t.Run("TestFilterColumnsAsOwner", testColumnFilterAsOwner) - t.Run("TestFilterColumnsAsModerator", testColumnFilterAsModerator) - t.Run("TestFilterColumnsAsParticipant", testColumnFilterAsParticipant) - t.Run("TestFilterNotesAsOwner", testNoteFilterAsOwner) - t.Run("TestFilterNotesAsModerator", testNoteFilterAsModerator) - t.Run("TestFilterNotesAsParticipant", testNoteFilterAsParticipant) - t.Run("TestFilterVotingUpdatedAsOwner", testFilterVotingUpdatedAsOwner) - t.Run("TestFilterVotingUpdatedAsModerator", testFilterVotingUpdatedAsModerator) - t.Run("TestFilterVotingUpdatedAsParticipant", testFilterVotingUpdatedAsParticipant) - t.Run("TestInitEventAsOwner", testInitFilterAsOwner) - t.Run("TestInitEventAsModerator", testInitFilterAsModerator) - t.Run("TestInitEventAsParticipant", testInitFilterAsParticipant) - t.Run("TestRaiseHandShouldBeUpdatedAfterParticipantUpdated", testRaiseHandShouldBeUpdatedAfterParticipantUpdated) - t.Run("TestParticipantUpdatedShouldHandleError", testParticipantUpdatedShouldHandleError) + t.Run("TestIsOwnerModerator", testIsOwnerModerator) + t.Run("TestIsModModerator", testIsModModerator) + t.Run("TestIsParticipantModerator", testIsParticipantModerator) + t.Run("TestIsUnknownUuidModerator", testIsUnknownUuidModerator) + t.Run("TestParseBoardSettingsData", testParseBoardSettingsData) + t.Run("TestParseColumnData", testParseColumnData) + t.Run("TestParseNoteData", testParseNoteData) + t.Run("TestParseVotingData", testParseVotingData) + t.Run("TestFilterColumnsAsOwner", testColumnFilterAsOwner) + t.Run("TestFilterColumnsAsModerator", testColumnFilterAsModerator) + t.Run("TestFilterColumnsAsParticipant", testColumnFilterAsParticipant) + t.Run("TestFilterNotesAsOwner", testNoteFilterAsOwner) + t.Run("TestFilterNotesAsModerator", testNoteFilterAsModerator) + t.Run("TestFilterNotesAsParticipant", testNoteFilterAsParticipant) + t.Run("TestFilterVotingUpdatedAsOwner", testFilterVotingUpdatedAsOwner) + t.Run("TestFilterVotingUpdatedAsModerator", testFilterVotingUpdatedAsModerator) + t.Run("TestFilterVotingUpdatedAsParticipant", testFilterVotingUpdatedAsParticipant) + t.Run("TestInitEventAsOwner", testInitFilterAsOwner) + t.Run("TestInitEventAsModerator", testInitFilterAsModerator) + t.Run("TestInitEventAsParticipant", testInitFilterAsParticipant) + t.Run("TestRaiseHandShouldBeUpdatedAfterParticipantUpdated", testRaiseHandShouldBeUpdatedAfterParticipantUpdated) + t.Run("TestParticipantUpdatedShouldHandleError", testParticipantUpdatedShouldHandleError) } func testRaiseHandShouldBeUpdatedAfterParticipantUpdated(t *testing.T) { - originalParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { - user := getUserById(session.ID) - return user.AccountType == common.Anonymous - })[0] + originalParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { + user := getUserById(session.UserID) + return user.AccountType == common.Anonymous + })[0] - updateEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: sessions.BoardSession{ - RaisedHand: true, - ID: originalParticipantSession.ID, - Role: common.ParticipantRole, - }, - } + updateEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventParticipantUpdated, + Data: sessions.BoardSession{ + RaisedHand: true, + UserID: originalParticipantSession.UserID, + Role: common.ParticipantRole, + }, + } - isUpdated := boardSub.participantUpdated(updateEvent, true) + isUpdated := boardSub.participantUpdated(updateEvent, true) - updatedParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { - user := getUserById(session.ID) - return user.AccountType == common.Anonymous - })[0] + updatedParticipantSession := technical_helper.Filter(boardSub.boardParticipants, func(session *sessions.BoardSession) bool { + user := getUserById(session.UserID) + return user.AccountType == common.Anonymous + })[0] - assert.Equal(t, true, isUpdated) - assert.Equal(t, false, originalParticipantSession.RaisedHand) - assert.Equal(t, true, updatedParticipantSession.RaisedHand) + assert.Equal(t, true, isUpdated) + assert.Equal(t, false, originalParticipantSession.RaisedHand) + assert.Equal(t, true, updatedParticipantSession.RaisedHand) } func testParticipantUpdatedShouldHandleError(t *testing.T) { - updateEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventParticipantUpdated, - Data: "SHOULD FAIL", - } + updateEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventParticipantUpdated, + Data: "SHOULD FAIL", + } - isUpdated := boardSub.participantUpdated(updateEvent, true) + isUpdated := boardSub.participantUpdated(updateEvent, true) - assert.Equal(t, false, isUpdated) + assert.Equal(t, false, isUpdated) } func testIsModModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(moderatorBoardSession.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(moderatorBoardSession.UserID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.True(t, isMod) - assert.Equal(t, common.ModeratorRole, moderatorBoardSession.Role) + assert.NotNil(t, isMod) + assert.True(t, isMod) + assert.Equal(t, common.ModeratorRole, moderatorBoardSession.Role) } func testIsOwnerModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(ownerBoardSession.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(ownerBoardSession.UserID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.True(t, isMod) - assert.Equal(t, common.OwnerRole, ownerBoardSession.Role) + assert.NotNil(t, isMod) + assert.True(t, isMod) + assert.Equal(t, common.OwnerRole, ownerBoardSession.Role) } func testIsParticipantModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(participantBoardSession.ID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(participantBoardSession.UserID, boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.False(t, isMod) + assert.NotNil(t, isMod) + assert.False(t, isMod) } func testIsUnknownUuidModerator(t *testing.T) { - isMod := sessions.CheckSessionRole(uuid.New(), boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) + isMod := sessions.CheckSessionRole(uuid.New(), boardSessions, []common.SessionRole{common.ModeratorRole, common.OwnerRole}) - assert.NotNil(t, isMod) - assert.False(t, isMod) + assert.NotNil(t, isMod) + assert.False(t, isMod) } func testParseBoardSettingsData(t *testing.T) { - expectedBoardSettings := boardSettings - actualBoardSettings, err := technical_helper.Unmarshal[boards.Board](boardEvent.Data) + expectedBoardSettings := boardSettings + actualBoardSettings, err := technical_helper.Unmarshal[boards.Board](boardEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualBoardSettings) - assert.Equal(t, expectedBoardSettings, actualBoardSettings) + assert.Nil(t, err) + assert.NotNil(t, actualBoardSettings) + assert.Equal(t, expectedBoardSettings, actualBoardSettings) } func testParseColumnData(t *testing.T) { - expectedColumns := []*columns.Column{&aSeeableColumn, &aHiddenColumn} - actualColumns, err := technical_helper.UnmarshalSlice[columns.Column](columnEvent.Data) + expectedColumns := []*columns.Column{&aSeeableColumn, &aHiddenColumn} + actualColumns, err := technical_helper.UnmarshalSlice[columns.Column](columnEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualColumns) - assert.Equal(t, expectedColumns, actualColumns) + assert.Nil(t, err) + assert.NotNil(t, actualColumns) + assert.Equal(t, expectedColumns, actualColumns) } func testParseNoteData(t *testing.T) { - expectedNotes := []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote} - actualNotes, err := technical_helper.UnmarshalSlice[notes.Note](noteEvent.Data) + expectedNotes := []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote} + actualNotes, err := technical_helper.UnmarshalSlice[notes.Note](noteEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualNotes) - assert.Equal(t, expectedNotes, actualNotes) + assert.Nil(t, err) + assert.NotNil(t, actualNotes) + assert.Equal(t, expectedNotes, actualNotes) } func testParseVotingData(t *testing.T) { - expectedVoting := votingData - actualVoting, err := technical_helper.Unmarshal[votings.VotingUpdated](votingEvent.Data) + expectedVoting := votingData + actualVoting, err := technical_helper.Unmarshal[votings.VotingUpdated](votingEvent.Data) - assert.Nil(t, err) - assert.NotNil(t, actualVoting) - assert.Equal(t, expectedVoting, actualVoting) + assert.Nil(t, err) + assert.NotNil(t, actualVoting) + assert.Equal(t, expectedVoting, actualVoting) } func testColumnFilterAsParticipant(t *testing.T) { - expectedColumnEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn}, - } - returnedColumnEvent := boardSub.eventFilter(columnEvent, participantBoardSession.ID) + expectedColumnEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn}, + } + returnedColumnEvent := boardSub.eventFilter(columnEvent, participantBoardSession.UserID) - assert.Equal(t, expectedColumnEvent, returnedColumnEvent) + assert.Equal(t, expectedColumnEvent, returnedColumnEvent) } func testColumnFilterAsOwner(t *testing.T) { - expectedColumnEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - } - returnedColumnEvent := boardSub.eventFilter(columnEvent, ownerBoardSession.ID) + expectedColumnEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + } + returnedColumnEvent := boardSub.eventFilter(columnEvent, ownerBoardSession.UserID) - assert.Equal(t, expectedColumnEvent, returnedColumnEvent) + assert.Equal(t, expectedColumnEvent, returnedColumnEvent) } func testColumnFilterAsModerator(t *testing.T) { - expectedColumnEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventColumnsUpdated, - Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, - } + expectedColumnEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventColumnsUpdated, + Data: []*columns.Column{&aSeeableColumn, &aHiddenColumn}, + } - returnedColumnEvent := boardSub.eventFilter(columnEvent, moderatorBoardSession.ID) + returnedColumnEvent := boardSub.eventFilter(columnEvent, moderatorBoardSession.UserID) - assert.Equal(t, expectedColumnEvent, returnedColumnEvent) + assert.Equal(t, expectedColumnEvent, returnedColumnEvent) } func testNoteFilterAsParticipant(t *testing.T) { - expectedNoteEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: notes.NoteSlice{&aParticipantNote}, - } - returnedNoteEvent := boardSub.eventFilter(noteEvent, participantBoardSession.ID) + expectedNoteEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: notes.NoteSlice{&aParticipantNote}, + } + returnedNoteEvent := boardSub.eventFilter(noteEvent, participantBoardSession.UserID) - assert.Equal(t, expectedNoteEvent, returnedNoteEvent) + assert.Equal(t, expectedNoteEvent, returnedNoteEvent) } func testNoteFilterAsOwner(t *testing.T) { - expectedNoteEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - } - returnedNoteEvent := boardSub.eventFilter(noteEvent, ownerBoardSession.ID) + expectedNoteEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + } + returnedNoteEvent := boardSub.eventFilter(noteEvent, ownerBoardSession.UserID) - assert.Equal(t, expectedNoteEvent, returnedNoteEvent) + assert.Equal(t, expectedNoteEvent, returnedNoteEvent) } func testNoteFilterAsModerator(t *testing.T) { - expectedNoteEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventNotesUpdated, - Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, - } - returnedNoteEvent := boardSub.eventFilter(noteEvent, moderatorBoardSession.ID) + expectedNoteEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventNotesUpdated, + Data: []*notes.Note{&aParticipantNote, &aModeratorNote, &aOwnerNote}, + } + returnedNoteEvent := boardSub.eventFilter(noteEvent, moderatorBoardSession.UserID) - assert.Equal(t, expectedNoteEvent, returnedNoteEvent) + assert.Equal(t, expectedNoteEvent, returnedNoteEvent) } func testFilterVotingUpdatedAsOwner(t *testing.T) { - expectedVotingEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: votingData, - } - returnedVoteEvent := boardSub.eventFilter(votingEvent, ownerBoardSession.ID) + expectedVotingEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: votingData, + } + returnedVoteEvent := boardSub.eventFilter(votingEvent, ownerBoardSession.UserID) - assert.NotNil(t, returnedVoteEvent) - assert.Equal(t, expectedVotingEvent, returnedVoteEvent) + assert.NotNil(t, returnedVoteEvent) + assert.Equal(t, expectedVotingEvent, returnedVoteEvent) } func testFilterVotingUpdatedAsModerator(t *testing.T) { - expectedVotingEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: votingData, - } - returnedVoteEvent := boardSub.eventFilter(votingEvent, moderatorBoardSession.ID) + expectedVotingEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: votingData, + } + returnedVoteEvent := boardSub.eventFilter(votingEvent, moderatorBoardSession.UserID) - assert.NotNil(t, returnedVoteEvent) - assert.Equal(t, expectedVotingEvent, returnedVoteEvent) + assert.NotNil(t, returnedVoteEvent) + assert.Equal(t, expectedVotingEvent, returnedVoteEvent) } func testFilterVotingUpdatedAsParticipant(t *testing.T) { - expectedVoting := &votings.VotingUpdated{ - Notes: []votings.Note{ - { - ID: aParticipantNote.ID, - Author: aParticipantNote.Author, - Text: aParticipantNote.Text, - Edited: aParticipantNote.Edited, - Position: votings.NotePosition{ - Column: aParticipantNote.Position.Column, - Stack: aParticipantNote.Position.Stack, - Rank: aParticipantNote.Position.Rank, - }, - }, - }, - Voting: &votings.Voting{ - ID: votingID, - VoteLimit: 5, - AllowMultipleVotes: true, - ShowVotesOfOthers: false, - Status: "CLOSED", - VotingResults: &votings.VotingResults{ - Total: 2, - Votes: map[uuid.UUID]votings.VotingResultsPerNote{ - aParticipantNote.ID: { - Total: 2, - Users: nil, - }, - }, - }, - }, - } - expectedVotingEvent := &realtime.BoardEvent{ - Type: realtime.BoardEventVotingUpdated, - Data: expectedVoting, - } - returnedVoteEvent := boardSub.eventFilter(votingEvent, participantBoardSession.ID) - - assert.NotNil(t, returnedVoteEvent) - assert.Equal(t, expectedVotingEvent, returnedVoteEvent) + expectedVoting := &votings.VotingUpdated{ + Notes: []votings.Note{ + { + ID: aParticipantNote.ID, + Author: aParticipantNote.Author, + Text: aParticipantNote.Text, + Edited: aParticipantNote.Edited, + Position: votings.NotePosition{ + Column: aParticipantNote.Position.Column, + Stack: aParticipantNote.Position.Stack, + Rank: aParticipantNote.Position.Rank, + }, + }, + }, + Voting: &votings.Voting{ + ID: votingID, + VoteLimit: 5, + AllowMultipleVotes: true, + ShowVotesOfOthers: false, + Status: "CLOSED", + VotingResults: &votings.VotingResults{ + Total: 2, + Votes: map[uuid.UUID]votings.VotingResultsPerNote{ + aParticipantNote.ID: { + Total: 2, + Users: nil, + }, + }, + }, + }, + } + expectedVotingEvent := &realtime.BoardEvent{ + Type: realtime.BoardEventVotingUpdated, + Data: expectedVoting, + } + returnedVoteEvent := boardSub.eventFilter(votingEvent, participantBoardSession.UserID) + + assert.NotNil(t, returnedVoteEvent) + assert.Equal(t, expectedVotingEvent, returnedVoteEvent) } func testInitFilterAsOwner(t *testing.T) { - expectedInitEvent := initEvent - returnedInitEvent := eventInitFilter(initEvent, ownerBoardSession.ID) + expectedInitEvent := initEvent + returnedInitEvent := eventInitFilter(initEvent, ownerBoardSession.UserID) - assert.Equal(t, expectedInitEvent, returnedInitEvent) + assert.Equal(t, expectedInitEvent, returnedInitEvent) } func testInitFilterAsModerator(t *testing.T) { - expectedInitEvent := initEvent - returnedInitEvent := eventInitFilter(initEvent, moderatorBoardSession.ID) + expectedInitEvent := initEvent + returnedInitEvent := eventInitFilter(initEvent, moderatorBoardSession.UserID) - assert.Equal(t, expectedInitEvent, returnedInitEvent) + assert.Equal(t, expectedInitEvent, returnedInitEvent) } func testInitFilterAsParticipant(t *testing.T) { - expectedVoting := votings.Voting{ - ID: votingID, - VoteLimit: 5, - AllowMultipleVotes: true, - ShowVotesOfOthers: false, - Status: "CLOSED", - VotingResults: &votings.VotingResults{ - Total: 2, - Votes: map[uuid.UUID]votings.VotingResultsPerNote{ - aParticipantNote.ID: { - Total: 2, - Users: nil, - }, - }, - }, - } - expectedInitEvent := InitEvent{ - Type: realtime.BoardEventInit, - Data: boards.FullBoard{ - Board: &boards.Board{}, - Columns: []*columns.Column{&aSeeableColumn}, - Notes: []*notes.Note{&aParticipantNote}, - Votings: []*votings.Voting{&expectedVoting}, - Votes: []*votings.Vote{}, - BoardSessions: boardSessions, - BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, - }, - } - returnedInitEvent := eventInitFilter(initEvent, participantBoardSession.ID) - - assert.Equal(t, expectedInitEvent, returnedInitEvent) + expectedVoting := votings.Voting{ + ID: votingID, + VoteLimit: 5, + AllowMultipleVotes: true, + ShowVotesOfOthers: false, + Status: "CLOSED", + VotingResults: &votings.VotingResults{ + Total: 2, + Votes: map[uuid.UUID]votings.VotingResultsPerNote{ + aParticipantNote.ID: { + Total: 2, + Users: nil, + }, + }, + }, + } + expectedInitEvent := InitEvent{ + Type: realtime.BoardEventInit, + Data: boards.FullBoard{ + Board: &boards.Board{}, + Columns: []*columns.Column{&aSeeableColumn}, + Notes: []*notes.Note{&aParticipantNote}, + Votings: []*votings.Voting{&expectedVoting}, + Votes: []*votings.Vote{}, + BoardSessions: boardSessions, + BoardSessionRequests: []*sessionrequests.BoardSessionRequest{}, + }, + } + returnedInitEvent := eventInitFilter(initEvent, participantBoardSession.UserID) + + assert.Equal(t, expectedInitEvent, returnedInitEvent) } func TestShouldFailBecauseOfInvalidBordData(t *testing.T) { - event := buildBoardEvent(buildBoardDto(nil, nil, "lorem ipsum", false), realtime.BoardEventBoardUpdated) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(buildBoardDto(nil, nil, "lorem ipsum", false), realtime.BoardEventBoardUpdated) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.boardUpdated(event, false) + _, success := bordSubscription.boardUpdated(event, false) - assert.False(t, success) + assert.False(t, success) } func TestShouldUpdateBordSubscriptionAsModerator(t *testing.T) { - nameForUpdate := randSeq(10) - descriptionForUpdate := randSeq(10) + nameForUpdate := randSeq(10) + descriptionForUpdate := randSeq(10) - event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.boardUpdated(event, true) + _, success := bordSubscription.boardUpdated(event, true) - assert.Equal(t, nameForUpdate, bordSubscription.boardSettings.Name) - assert.Equal(t, descriptionForUpdate, bordSubscription.boardSettings.Description) - assert.True(t, success) + assert.Equal(t, nameForUpdate, bordSubscription.boardSettings.Name) + assert.Equal(t, descriptionForUpdate, bordSubscription.boardSettings.Description) + assert.True(t, success) } func TestShouldNotUpdateBordSubscriptionWithoutModeratorRights(t *testing.T) { - nameForUpdate := randSeq(10) - descriptionForUpdate := randSeq(10) + nameForUpdate := randSeq(10) + descriptionForUpdate := randSeq(10) - event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, false), realtime.BoardEventBoardUpdated) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.boardUpdated(event, false) + _, success := bordSubscription.boardUpdated(event, false) - assert.Nil(t, bordSubscription.boardSettings.Name) - assert.Nil(t, bordSubscription.boardSettings.Description) - assert.True(t, success) + assert.Nil(t, bordSubscription.boardSettings.Name) + assert.Nil(t, bordSubscription.boardSettings.Description) + assert.True(t, success) } func TestShouldOnlyInsertLatestVotingInInitEventStatusClosed(t *testing.T) { - latestVotingId := uuid.New() - newestVotingId := uuid.New() - clientId := uuid.New() - client := users.User{ - ID: clientId, - } - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ModeratorRole, - ID: client.ID, - }, - }, - Votings: []*votings.Voting{ - buildVoting(latestVotingId, votings.Closed), - buildVoting(newestVotingId, votings.Closed), - }, - Votes: []*votings.Vote{ - buildVote(latestVotingId, uuid.New(), uuid.New()), - buildVote(newestVotingId, uuid.New(), uuid.New()), - }, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) + latestVotingId := uuid.New() + newestVotingId := uuid.New() + clientId := uuid.New() + client := users.User{ + ID: clientId, + } + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ModeratorRole, + UserID: client.ID, + }, + }, + Votings: []*votings.Voting{ + buildVoting(latestVotingId, votings.Closed), + buildVoting(newestVotingId, votings.Closed), + }, + Votes: []*votings.Vote{ + buildVote(latestVotingId, uuid.New(), uuid.New()), + buildVote(newestVotingId, uuid.New(), uuid.New()), + }, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) } func TestShouldOnlyInsertLatestVotingInInitEventStatusOpen(t *testing.T) { - latestVotingId := uuid.New() - newestVotingId := uuid.New() - clientId := uuid.New() - client := users.User{ - ID: clientId, - } - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ModeratorRole, - ID: client.ID, - }, - }, - Votings: []*votings.Voting{ - buildVoting(latestVotingId, votings.Open), - buildVoting(newestVotingId, votings.Closed), - }, - Votes: []*votings.Vote{ - buildVote(latestVotingId, clientId, uuid.New()), - buildVote(newestVotingId, uuid.New(), uuid.New()), - }, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) + latestVotingId := uuid.New() + newestVotingId := uuid.New() + clientId := uuid.New() + client := users.User{ + ID: clientId, + } + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ModeratorRole, + UserID: client.ID, + }, + }, + Votings: []*votings.Voting{ + buildVoting(latestVotingId, votings.Open), + buildVoting(newestVotingId, votings.Closed), + }, + Votes: []*votings.Vote{ + buildVote(latestVotingId, clientId, uuid.New()), + buildVote(newestVotingId, uuid.New(), uuid.New()), + }, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votes[0].Voting) + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) } func TestShouldBeEmptyVotesInInitEventBecauseIdsDiffer(t *testing.T) { - clientId := uuid.New() - latestVotingId := uuid.New() - client := users.User{ - ID: clientId, - } - orgVoting := []*votings.Voting{ - buildVoting(latestVotingId, votings.Open), - buildVoting(uuid.New(), votings.Closed), - } - orgVote := []*votings.Vote{ - buildVote(uuid.New(), uuid.New(), uuid.New()), - buildVote(uuid.New(), uuid.New(), uuid.New()), - } - - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ModeratorRole, - ID: client.ID, - }, - }, - Votings: orgVoting, - Votes: orgVote, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Empty(t, updatedInitEvent.Data.Votes) - assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) + clientId := uuid.New() + latestVotingId := uuid.New() + client := users.User{ + ID: clientId, + } + orgVoting := []*votings.Voting{ + buildVoting(latestVotingId, votings.Open), + buildVoting(uuid.New(), votings.Closed), + } + orgVote := []*votings.Vote{ + buildVote(uuid.New(), uuid.New(), uuid.New()), + buildVote(uuid.New(), uuid.New(), uuid.New()), + } + + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ModeratorRole, + UserID: client.ID, + }, + }, + Votings: orgVoting, + Votes: orgVote, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Empty(t, updatedInitEvent.Data.Votes) + assert.Equal(t, latestVotingId, updatedInitEvent.Data.Votings[0].ID) } func TestShouldCreateNewInitEventBecauseNoModeratorRightsWithVisibleVotes(t *testing.T) { - latestVotingId := uuid.New() - newestVotingId := uuid.New() - noteId := uuid.New() - columnId := uuid.New() - clientId := uuid.New() - client := users.User{ - ID: clientId, - } - nameForUpdate := randSeq(10) - descriptionForUpdate := randSeq(10) - - initEvent := InitEvent{ - Type: "", - Data: boards.FullBoard{ - Columns: []*columns.Column{buildColumn(columnId, true)}, - Board: buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, true), - Notes: []*notes.Note{buildNote(noteId, columnId)}, - BoardSessions: []*sessions.BoardSession{ - { - Role: common.ParticipantRole, - ID: client.ID, - }, - }, - Votings: []*votings.Voting{ - buildVoting(latestVotingId, votings.Open), - buildVoting(newestVotingId, votings.Closed), - }, - Votes: []*votings.Vote{ - buildVote(latestVotingId, clientId, noteId), - buildVote(newestVotingId, uuid.New(), uuid.New()), - }, - }, - } - - updatedInitEvent := eventInitFilter(initEvent, clientId) - - assert.Equal(t, noteId, updatedInitEvent.Data.Votes[0].Note) - assert.Equal(t, clientId, updatedInitEvent.Data.Votes[0].User) + latestVotingId := uuid.New() + newestVotingId := uuid.New() + noteId := uuid.New() + columnId := uuid.New() + clientId := uuid.New() + client := users.User{ + ID: clientId, + } + nameForUpdate := randSeq(10) + descriptionForUpdate := randSeq(10) + + initEvent := InitEvent{ + Type: "", + Data: boards.FullBoard{ + Columns: []*columns.Column{buildColumn(columnId, true)}, + Board: buildBoardDto(nameForUpdate, descriptionForUpdate, boards.Public, true), + Notes: []*notes.Note{buildNote(noteId, columnId)}, + BoardSessions: []*sessions.BoardSession{ + { + Role: common.ParticipantRole, + UserID: client.ID, + }, + }, + Votings: []*votings.Voting{ + buildVoting(latestVotingId, votings.Open), + buildVoting(newestVotingId, votings.Closed), + }, + Votes: []*votings.Vote{ + buildVote(latestVotingId, clientId, noteId), + buildVote(newestVotingId, uuid.New(), uuid.New()), + }, + }, + } + + updatedInitEvent := eventInitFilter(initEvent, clientId) + + assert.Equal(t, noteId, updatedInitEvent.Data.Votes[0].Note) + assert.Equal(t, clientId, updatedInitEvent.Data.Votes[0].User) } func TestShouldFailBecauseOfInvalidVoteData(t *testing.T) { - event := buildBoardEvent(*buildVote(uuid.New(), uuid.New(), uuid.New()), realtime.BoardEventVotesDeleted) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent(*buildVote(uuid.New(), uuid.New(), uuid.New()), realtime.BoardEventVotesDeleted) + bordSubscription := buildBordSubscription(boards.Public) - _, success := bordSubscription.votesDeleted(event, uuid.New()) + _, success := bordSubscription.votesDeleted(event, uuid.New()) - assert.False(t, success) + assert.False(t, success) } func TestShouldReturnEmptyVotesBecauseUserIdNotMatched(t *testing.T) { - event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), uuid.New(), uuid.New())}, realtime.BoardEventVotesDeleted) - bordSubscription := buildBordSubscription(boards.Public) + event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), uuid.New(), uuid.New())}, realtime.BoardEventVotesDeleted) + bordSubscription := buildBordSubscription(boards.Public) - updatedBordEvent, success := bordSubscription.votesDeleted(event, uuid.New()) + updatedBordEvent, success := bordSubscription.votesDeleted(event, uuid.New()) - assert.True(t, success) - assert.Equal(t, 0, len(updatedBordEvent.Data.([]*votings.Vote))) + assert.True(t, success) + assert.Equal(t, 0, len(updatedBordEvent.Data.([]*votings.Vote))) } func TestVotesDeleted(t *testing.T) { - userId := uuid.New() - event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), userId, uuid.New())}, realtime.BoardEventVotesDeleted) - bordSubscription := buildBordSubscription(boards.Public) + userId := uuid.New() + event := buildBoardEvent([]votings.Vote{*buildVote(uuid.New(), userId, uuid.New())}, realtime.BoardEventVotesDeleted) + bordSubscription := buildBordSubscription(boards.Public) - updatedBordEvent, success := bordSubscription.votesDeleted(event, userId) + updatedBordEvent, success := bordSubscription.votesDeleted(event, userId) - assert.True(t, success) - assert.Equal(t, 1, len(updatedBordEvent.Data.([]*votings.Vote))) + assert.True(t, success) + assert.Equal(t, 1, len(updatedBordEvent.Data.([]*votings.Vote))) } func buildNote(id uuid.UUID, columnId uuid.UUID) *notes.Note { - return ¬es.Note{ - ID: id, - Author: uuid.New(), - Text: "lorem in ipsum", - Edited: false, - Position: notes.NotePosition{ - Column: columnId, - Stack: uuid.NullUUID{ - UUID: uuid.New(), - Valid: true, - }, - Rank: 0, - }, - } + return ¬es.Note{ + ID: id, + Author: uuid.New(), + Text: "lorem in ipsum", + Edited: false, + Position: notes.NotePosition{ + Column: columnId, + Stack: uuid.NullUUID{ + UUID: uuid.New(), + Valid: true, + }, + Rank: 0, + }, + } } func buildColumn(id uuid.UUID, visible bool) *columns.Column { - return &columns.Column{ - ID: id, - Visible: visible, - } + return &columns.Column{ + ID: id, + Visible: visible, + } } func buildVote(votingId uuid.UUID, userId uuid.UUID, noteId uuid.UUID) *votings.Vote { - return &votings.Vote{ - Voting: votingId, - User: userId, - Note: noteId, - } + return &votings.Vote{ + Voting: votingId, + User: userId, + Note: noteId, + } } func buildVoting(id uuid.UUID, status votings.VotingStatus) *votings.Voting { - return &votings.Voting{ - ID: id, - Status: status, - } + return &votings.Voting{ + ID: id, + Status: status, + } } func buildBordSubscription(accessPolicy boards.AccessPolicy) BoardSubscription { - return BoardSubscription{ - subscription: nil, - clients: nil, - boardParticipants: nil, - boardSettings: buildBoardDto(nil, nil, accessPolicy, false), - boardColumns: nil, - boardNotes: nil, - boardReactions: nil, - } + return BoardSubscription{ + subscription: nil, + clients: nil, + boardParticipants: nil, + boardSettings: buildBoardDto(nil, nil, accessPolicy, false), + boardColumns: nil, + boardNotes: nil, + boardReactions: nil, + } } func buildBoardEvent(data interface{}, eventType realtime.BoardEventType) *realtime.BoardEvent { - return &realtime.BoardEvent{ - Type: eventType, - Data: data, - } + return &realtime.BoardEvent{ + Type: eventType, + Data: data, + } } func buildBoardDto(name *string, description *string, accessPolicy boards.AccessPolicy, showNotesOfOtherUsers bool) *boards.Board { - return &boards.Board{ - ID: uuid.UUID{}, - Name: name, - Description: description, - AccessPolicy: accessPolicy, - ShowAuthors: false, - ShowNotesOfOtherUsers: showNotesOfOtherUsers, - ShowNoteReactions: false, - AllowStacking: false, - IsLocked: false, - TimerStart: nil, - TimerEnd: nil, - SharedNote: uuid.NullUUID{}, - ShowVoting: uuid.NullUUID{}, - Passphrase: nil, - Salt: nil, - } + return &boards.Board{ + ID: uuid.UUID{}, + Name: name, + Description: description, + AccessPolicy: accessPolicy, + ShowAuthors: false, + ShowNotesOfOtherUsers: showNotesOfOtherUsers, + ShowNoteReactions: false, + AllowStacking: false, + IsLocked: false, + TimerStart: nil, + TimerEnd: nil, + SharedNote: uuid.NullUUID{}, + ShowVoting: uuid.NullUUID{}, + Passphrase: nil, + Salt: nil, + } } func randSeq(n int) *string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } - s := string(b) - return &s + s := string(b) + return &s } diff --git a/server/src/boards/service.go b/server/src/boards/service.go index 4701e894dc..435bcc794d 100644 --- a/server/src/boards/service.go +++ b/server/src/boards/service.go @@ -289,7 +289,7 @@ func (service *Service) BoardOverview(ctx context.Context, boardIDs []uuid.UUID, columnNum := len(boardColumns) dtoBoard := new(Board).From(board) for _, session := range boardSessions { - if session.ID == user { + if session.UserID == user { sessionCreated := session.CreatedAt overviewBoards = append(overviewBoards, &BoardOverview{ Board: dtoBoard, diff --git a/server/src/sessionrequests/service_integration_test.go b/server/src/sessionrequests/service_integration_test.go index 5920f1315e..84d59c80e6 100644 --- a/server/src/sessionrequests/service_integration_test.go +++ b/server/src/sessionrequests/service_integration_test.go @@ -139,7 +139,7 @@ func (suite *SessionRequestServiceIntegrationTestSuite) Test_Update() { sessionData, err := technical_helper.Unmarshal[sessions.BoardSession](sessionMsg.Data) assert.Nil(t, err) - assert.Equal(t, userId, sessionData.ID) + assert.Equal(t, userId, sessionData.UserID) updatedMsg := <-events assert.Equal(t, realtime.BoardEventSessionRequestUpdated, updatedMsg.Type) diff --git a/server/src/sessionrequests/service_test.go b/server/src/sessionrequests/service_test.go index f32d438cf6..6baf24bd2e 100644 --- a/server/src/sessionrequests/service_test.go +++ b/server/src/sessionrequests/service_test.go @@ -1,381 +1,381 @@ package sessionrequests import ( - "context" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "scrumlr.io/server/users" - "testing" - "time" - - "scrumlr.io/server/sessions" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - mock "github.com/stretchr/testify/mock" - "github.com/uptrace/bun" - httpMock "scrumlr.io/server/mocks/net/http" - "scrumlr.io/server/realtime" + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "scrumlr.io/server/users" + "testing" + "time" + + "scrumlr.io/server/sessions" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" + "github.com/uptrace/bun" + httpMock "scrumlr.io/server/mocks/net/http" + "scrumlr.io/server/realtime" ) func TestGetSessionRequest(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSessionRequest{Board: boardId, User: userId, Status: RequestAccepted}, nil) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSessionRequest{Board: boardId, User: userId, Status: RequestAccepted}, nil) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequest, err := service.Get(context.Background(), boardId, userId) + sessionRequest, err := service.Get(context.Background(), boardId, userId) - assert.Nil(t, err) - assert.NotNil(t, sessionRequest) - assert.Equal(t, userId, sessionRequest.User.ID) - assert.Equal(t, RequestAccepted, sessionRequest.Status) + assert.Nil(t, err) + assert.NotNil(t, sessionRequest) + assert.Equal(t, userId, sessionRequest.User.ID) + assert.Equal(t, RequestAccepted, sessionRequest.Status) } func TestGetSessionRequest_NotFound(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "Not found" + boardId := uuid.New() + userId := uuid.New() + dbError := "Not found" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSessionRequest{}, errors.New(dbError)) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Get(mock.Anything, boardId, userId).Return(DatabaseBoardSessionRequest{}, errors.New(dbError)) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequest, err := service.Get(context.Background(), boardId, userId) + sessionRequest, err := service.Get(context.Background(), boardId, userId) - assert.Nil(t, sessionRequest) - assert.NotNil(t, err) - assert.Equal(t, fmt.Sprintf("failed to load board session request: %s", dbError), err.Error()) + assert.Nil(t, sessionRequest) + assert.NotNil(t, err) + assert.Equal(t, fmt.Sprintf("failed to load board session request: %s", dbError), err.Error()) } func TestGetSessionRequests_WithoutQuery(t *testing.T) { - boardId := uuid.New() - firstUserId := uuid.New() - secondUserId := uuid.New() - query := "" + boardId := uuid.New() + firstUserId := uuid.New() + secondUserId := uuid.New() + query := "" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId). - Return([]DatabaseBoardSessionRequest{ - {bun.BaseModel{}, boardId, firstUserId, "Test1", RequestAccepted, time.Now()}, - {bun.BaseModel{}, boardId, secondUserId, "Test2", RequestPending, time.Now()}, - }, nil) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId). + Return([]DatabaseBoardSessionRequest{ + {bun.BaseModel{}, boardId, firstUserId, "Test1", RequestAccepted, time.Now()}, + {bun.BaseModel{}, boardId, secondUserId, "Test2", RequestPending, time.Now()}, + }, nil) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequests, err := service.GetAll(context.Background(), boardId, query) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + sessionRequests, err := service.GetAll(context.Background(), boardId, query) - assert.Nil(t, err) - assert.NotNil(t, sessionRequests) - assert.Len(t, sessionRequests, 2) + assert.Nil(t, err) + assert.NotNil(t, sessionRequests) + assert.Len(t, sessionRequests, 2) - assert.Equal(t, firstUserId, sessionRequests[0].User.ID) - assert.Equal(t, RequestAccepted, sessionRequests[0].Status) + assert.Equal(t, firstUserId, sessionRequests[0].User.ID) + assert.Equal(t, RequestAccepted, sessionRequests[0].Status) - assert.Equal(t, secondUserId, sessionRequests[1].User.ID) - assert.Equal(t, RequestPending, sessionRequests[1].Status) + assert.Equal(t, secondUserId, sessionRequests[1].User.ID) + assert.Equal(t, RequestPending, sessionRequests[1].Status) } func TestListSessionRequests_WithoutQuery_NotFound(t *testing.T) { - boardId := uuid.New() - dbError := "Not found" - query := "" + boardId := uuid.New() + dbError := "Not found" + query := "" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId). - Return([]DatabaseBoardSessionRequest{}, errors.New(dbError)) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId). + Return([]DatabaseBoardSessionRequest{}, errors.New(dbError)) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequest, err := service.GetAll(context.Background(), boardId, query) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + sessionRequest, err := service.GetAll(context.Background(), boardId, query) - assert.Nil(t, sessionRequest) - assert.NotNil(t, err) - assert.Equal(t, fmt.Sprintf("failed to load board session requests: %s", dbError), err.Error()) + assert.Nil(t, sessionRequest) + assert.NotNil(t, err) + assert.Equal(t, fmt.Sprintf("failed to load board session requests: %s", dbError), err.Error()) } func TestListSessionRequests_WithQuery(t *testing.T) { - boardId := uuid.New() - firstUserId := uuid.New() - secondUserId := uuid.New() - query := "PENDING" + boardId := uuid.New() + firstUserId := uuid.New() + secondUserId := uuid.New() + query := "PENDING" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId, []RequestStatus{RequestPending}). - Return([]DatabaseBoardSessionRequest{ - {Board: boardId, User: firstUserId, Name: "Test1", Status: RequestPending, CreatedAt: time.Now()}, - {Board: boardId, User: secondUserId, Name: "Test2", Status: RequestPending, CreatedAt: time.Now()}, - }, nil) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId, []RequestStatus{RequestPending}). + Return([]DatabaseBoardSessionRequest{ + {Board: boardId, User: firstUserId, Name: "Test1", Status: RequestPending, CreatedAt: time.Now()}, + {Board: boardId, User: secondUserId, Name: "Test2", Status: RequestPending, CreatedAt: time.Now()}, + }, nil) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequests, err := service.GetAll(context.Background(), boardId, query) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + sessionRequests, err := service.GetAll(context.Background(), boardId, query) - assert.Nil(t, err) - assert.NotNil(t, sessionRequests) - assert.Len(t, sessionRequests, 2) + assert.Nil(t, err) + assert.NotNil(t, sessionRequests) + assert.Len(t, sessionRequests, 2) - assert.Equal(t, firstUserId, sessionRequests[0].User.ID) - assert.Equal(t, RequestPending, sessionRequests[0].Status) + assert.Equal(t, firstUserId, sessionRequests[0].User.ID) + assert.Equal(t, RequestPending, sessionRequests[0].Status) - assert.Equal(t, secondUserId, sessionRequests[1].User.ID) - assert.Equal(t, RequestPending, sessionRequests[1].Status) + assert.Equal(t, secondUserId, sessionRequests[1].User.ID) + assert.Equal(t, RequestPending, sessionRequests[1].Status) } func TestListSessionRequests_WithQuery_NotFound(t *testing.T) { - boardId := uuid.New() - dbError := "Not found" - query := "ACCEPTED" + boardId := uuid.New() + dbError := "Not found" + query := "ACCEPTED" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId, []RequestStatus{RequestAccepted}). - Return([]DatabaseBoardSessionRequest{}, errors.New(dbError)) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().GetAll(mock.Anything, boardId, []RequestStatus{RequestAccepted}). + Return([]DatabaseBoardSessionRequest{}, errors.New(dbError)) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequests, err := service.GetAll(context.Background(), boardId, query) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + sessionRequests, err := service.GetAll(context.Background(), boardId, query) - assert.Nil(t, sessionRequests) - assert.NotNil(t, err) - assert.Equal(t, fmt.Sprintf("failed to load board session requests: %s", dbError), err.Error()) + assert.Nil(t, sessionRequests) + assert.NotNil(t, err) + assert.Equal(t, fmt.Sprintf("failed to load board session requests: %s", dbError), err.Error()) } func TestListSessionRequests_InvalideQuery(t *testing.T) { - boardId := uuid.New() - query := "INVALIDE" + boardId := uuid.New() + query := "INVALIDE" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - sessionRequests, err := service.GetAll(context.Background(), boardId, query) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + sessionRequests, err := service.GetAll(context.Background(), boardId, query) - assert.Nil(t, sessionRequests) - mockSessionRequestDb.AssertNotCalled(t, "GetBoardSessionRequests") - assert.NotNil(t, err) - assert.Equal(t, "invalid status filter", err.Error()) + assert.Nil(t, sessionRequests) + mockSessionRequestDb.AssertNotCalled(t, "GetBoardSessionRequests") + assert.NotNil(t, err) + assert.Equal(t, "invalid status filter", err.Error()) } func TestCreateSessionRequest(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Create(mock.Anything, DatabaseBoardSessionRequestInsert{Board: boardId, User: userId}). - Return(DatabaseBoardSessionRequest{Board: boardId, User: userId}, nil) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Create(mock.Anything, DatabaseBoardSessionRequestInsert{Board: boardId, User: userId}). + Return(DatabaseBoardSessionRequest{Board: boardId, User: userId}, nil) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - 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 - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - request, err := service.Create(context.Background(), boardId, userId) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + request, err := service.Create(context.Background(), boardId, userId) - assert.NotNil(t, request) - assert.Nil(t, err) - assert.Equal(t, userId, request.User.ID) + assert.NotNil(t, request) + assert.Nil(t, err) + assert.Equal(t, userId, request.User.ID) } func TestCreateSessionRequest_DBError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "Failed to execute" + boardId := uuid.New() + userId := uuid.New() + dbError := "Failed to execute" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Create(mock.Anything, DatabaseBoardSessionRequestInsert{Board: boardId, User: userId}). - Return(DatabaseBoardSessionRequest{}, errors.New(dbError)) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Create(mock.Anything, DatabaseBoardSessionRequestInsert{Board: boardId, User: userId}). + Return(DatabaseBoardSessionRequest{}, errors.New(dbError)) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - request, err := service.Create(context.Background(), boardId, userId) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + request, err := service.Create(context.Background(), boardId, userId) - assert.Nil(t, request) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.Nil(t, request) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestUpdatesessionRequest(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - user := users.User{ - ID: userId, - } - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Update(mock.Anything, DatabaseBoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}). - Return(DatabaseBoardSessionRequest{Board: boardId, User: userId, Status: RequestAccepted}, nil) + user := users.User{ + ID: userId, + } + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Update(mock.Anything, DatabaseBoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}). + Return(DatabaseBoardSessionRequest{Board: boardId, User: userId, Status: RequestAccepted}, nil) - mockSessionService := sessions.NewMockSessionService(t) - mockSessionService.EXPECT().Create(mock.Anything, boardId, userId). - Return(&sessions.BoardSession{Board: boardId, ID: user.ID}, nil) + mockSessionService := sessions.NewMockSessionService(t) + mockSessionService.EXPECT().Create(mock.Anything, boardId, userId). + Return(&sessions.BoardSession{Board: boardId, UserID: user.ID}, 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 - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - request, err := service.Update(context.Background(), BoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + request, err := service.Update(context.Background(), BoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}) - assert.NotNil(t, request) - assert.Nil(t, err) - assert.Equal(t, userId, request.User.ID) + assert.NotNil(t, request) + assert.Nil(t, err) + assert.Equal(t, userId, request.User.ID) } func TestUpdatesessionRequest_DBError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "Failed to execute" + boardId := uuid.New() + userId := uuid.New() + dbError := "Failed to execute" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Update(mock.Anything, DatabaseBoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}). - Return(DatabaseBoardSessionRequest{}, errors.New(dbError)) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Update(mock.Anything, DatabaseBoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}). + Return(DatabaseBoardSessionRequest{}, errors.New(dbError)) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - request, err := service.Update(context.Background(), BoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + request, err := service.Update(context.Background(), BoardSessionRequestUpdate{Board: boardId, User: userId, Status: RequestAccepted}) - assert.Nil(t, request) - assert.NotNil(t, err) - assert.Equal(t, errors.New(dbError), err) + assert.Nil(t, request) + assert.NotNil(t, err) + assert.Equal(t, errors.New(dbError), err) } func TestSessionRequestExists(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() + boardId := uuid.New() + userId := uuid.New() - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Exists(mock.Anything, boardId, userId).Return(true, nil) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Exists(mock.Anything, boardId, userId).Return(true, nil) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - exists, err := service.Exists(context.Background(), boardId, userId) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + exists, err := service.Exists(context.Background(), boardId, userId) - assert.Nil(t, err) - assert.True(t, exists) + assert.Nil(t, err) + assert.True(t, exists) } func TestSessionRequestExists_DbError(t *testing.T) { - boardId := uuid.New() - userId := uuid.New() - dbError := "database error" + boardId := uuid.New() + userId := uuid.New() + dbError := "database error" - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionRequestDb.EXPECT().Exists(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionRequestDb.EXPECT().Exists(mock.Anything, boardId, userId).Return(false, errors.New(dbError)) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) + mockWebSocket := NewMockWebsocket(t) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - exists, err := service.Exists(context.Background(), boardId, userId) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + exists, err := service.Exists(context.Background(), boardId, userId) - assert.NotNil(t, err) - assert.Equal(t, "database error", err.Error()) - assert.False(t, exists) + assert.NotNil(t, err) + assert.Equal(t, "database error", err.Error()) + assert.False(t, exists) } func TestSessionOpenBoardSessionRequestSocket(t *testing.T) { - mockSessionRequestDb := NewMockSessionRequestDatabase(t) - mockSessionService := sessions.NewMockSessionService(t) + mockSessionRequestDb := NewMockSessionRequestDatabase(t) + mockSessionService := sessions.NewMockSessionService(t) - mockBroker := realtime.NewMockClient(t) - broker := new(realtime.Broker) - broker.Con = mockBroker + mockBroker := realtime.NewMockClient(t) + broker := new(realtime.Broker) + broker.Con = mockBroker - mockWebSocket := NewMockWebsocket(t) - mockResponseWriter := httpMock.NewMockResponseWriter(t) - mockRequest := httptest.NewRequest(http.MethodGet, "/test", nil) - mockWebSocket.EXPECT().OpenSocket(mock.Anything, mock.Anything) + mockWebSocket := NewMockWebsocket(t) + mockResponseWriter := httpMock.NewMockResponseWriter(t) + mockRequest := httptest.NewRequest(http.MethodGet, "/test", nil) + mockWebSocket.EXPECT().OpenSocket(mock.Anything, mock.Anything) - service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) - service.OpenSocket(context.Background(), mockResponseWriter, mockRequest) + service := NewSessionRequestService(mockSessionRequestDb, broker, mockWebSocket, mockSessionService) + service.OpenSocket(context.Background(), mockResponseWriter, mockRequest) - mockWebSocket.AssertCalled(t, "OpenSocket", mockResponseWriter, mockRequest) + mockWebSocket.AssertCalled(t, "OpenSocket", mockResponseWriter, mockRequest) } diff --git a/server/src/sessions/service.go b/server/src/sessions/service.go index 7e9c9ebf65..6c364785d9 100644 --- a/server/src/sessions/service.go +++ b/server/src/sessions/service.go @@ -1,538 +1,538 @@ 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, boardID, userID uuid.UUID) (*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", boardID.String()), - attribute.String("scrumlr.sessions.service.create.user", userID.String()), - ) - - session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ - Board: boardID, - User: userID, - Role: common.ParticipantRole, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to create board session") - span.RecordError(err) - log.Errorw("unable to create board session", "board", boardID, "user", userID, "error", err) - return nil, err - } - - service.createdSession(ctx, boardID, 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", boardID.String()), + attribute.String("scrumlr.sessions.service.create.user", userID.String()), + ) + + session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ + Board: boardID, + User: userID, + Role: common.ParticipantRole, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to create board session") + span.RecordError(err) + log.Errorw("unable to create board session", "board", boardID, "user", userID, "error", err) + return nil, err + } + + service.createdSession(ctx, boardID, 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 getboard 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 getboard 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 } 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 + 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 update all sessions") - span.RecordError(err) - return nil, err - } - - for _, session := range connectedBoards { - service.updatedSession(ctx, session.Board, session) - } - - return BoardSessions(connectedBoards), err + 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 update all sessions") + span.RecordError(err) + return nil, err + } + + for _, session := range connectedBoards { + service.updatedSession(ctx, session.Board, session) + } + + return BoardSessions(connectedBoards), 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 + 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 } 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 + 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 } 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) - } + 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) 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_integration_test.go b/server/src/sessions/service_integration_test.go index 8067e9f0fc..95c22d8975 100644 --- a/server/src/sessions/service_integration_test.go +++ b/server/src/sessions/service_integration_test.go @@ -1,172 +1,172 @@ package sessions import ( - "context" - "log" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go/modules/nats" - "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/uptrace/bun" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/initialize" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/technical_helper" - "scrumlr.io/server/votings" + "context" + "log" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go/modules/nats" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/uptrace/bun" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/initialize" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/technical_helper" + "scrumlr.io/server/votings" ) type SessionServiceIntegrationTestSuite struct { - suite.Suite - dbContainer *postgres.PostgresContainer - natsContainer *nats.NATSContainer - db *bun.DB - natsConnectionString string - users map[string]uuid.UUID - boards map[string]TestBoard - sessions map[string]BoardSession + suite.Suite + dbContainer *postgres.PostgresContainer + natsContainer *nats.NATSContainer + db *bun.DB + natsConnectionString string + users map[string]uuid.UUID + boards map[string]TestBoard + sessions map[string]BoardSession } func TestSessionServiceIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(SessionServiceIntegrationTestSuite)) + suite.Run(t, new(SessionServiceIntegrationTestSuite)) } func (suite *SessionServiceIntegrationTestSuite) SetupSuite() { - dbContainer, bun := initialize.StartTestDatabase() - suite.SeedDatabase(bun) - natsContainer, connectionString := initialize.StartTestNats() - - suite.dbContainer = dbContainer - suite.natsContainer = natsContainer - suite.db = bun - suite.natsConnectionString = connectionString + dbContainer, bun := initialize.StartTestDatabase() + suite.SeedDatabase(bun) + natsContainer, connectionString := initialize.StartTestNats() + + suite.dbContainer = dbContainer + suite.natsContainer = natsContainer + suite.db = bun + suite.natsConnectionString = connectionString } func (suite *SessionServiceIntegrationTestSuite) TearDownSuite() { - initialize.StopTestDatabase(suite.dbContainer) - initialize.StopTestNats(suite.natsContainer) + initialize.StopTestDatabase(suite.dbContainer) + initialize.StopTestNats(suite.natsContainer) } func (suite *SessionServiceIntegrationTestSuite) Test_Create() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Write"].id - userId := suite.users["Luke"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Create(ctx, boardId, userId) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) - assert.Equal(t, common.ParticipantRole, session.Role) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantCreated, msg.Type) - sessionData, err := technical_helper.Unmarshal[BoardSession](msg.Data) - assert.Nil(t, err) - assert.Equal(t, userId, sessionData.UserID) - assert.Equal(t, common.ParticipantRole, sessionData.Role) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Write"].id + userId := suite.users["Luke"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + events := broker.GetBoardChannel(ctx, boardId) + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + session, err := sessionService.Create(ctx, boardId, userId) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ParticipantRole, session.Role) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantCreated, msg.Type) + sessionData, err := technical_helper.Unmarshal[BoardSession](msg.Data) + assert.Nil(t, err) + assert.Equal(t, userId, sessionData.UserID) + assert.Equal(t, common.ParticipantRole, sessionData.Role) } func (suite *SessionServiceIntegrationTestSuite) Test_Update() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Update"].id - userId := suite.users["Luke"] - callerId := suite.users["Stan"] - role := common.ModeratorRole - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Update(ctx, BoardSessionUpdateRequest{Caller: callerId, Board: boardId, User: userId, Role: &role}) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) - assert.Equal(t, common.ModeratorRole, session.Role) - - msgSession := <-events - msgColumns := <-events - msgNotes := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msgSession.Type) - assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumns.Type) - assert.Equal(t, realtime.BoardEventNotesSync, msgNotes.Type) - sessionData, err := technical_helper.Unmarshal[BoardSession](msgSession.Data) - assert.Nil(t, err) - assert.Equal(t, userId, session.UserID) - assert.Equal(t, common.ModeratorRole, sessionData.Role) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Update"].id + userId := suite.users["Luke"] + callerId := suite.users["Stan"] + role := common.ModeratorRole + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + events := broker.GetBoardChannel(ctx, boardId) + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + session, err := sessionService.Update(ctx, BoardSessionUpdateRequest{Caller: callerId, Board: boardId, User: userId, Role: &role}) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ModeratorRole, session.Role) + + msgSession := <-events + msgColumns := <-events + msgNotes := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msgSession.Type) + assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumns.Type) + assert.Equal(t, realtime.BoardEventNotesSync, msgNotes.Type) + sessionData, err := technical_helper.Unmarshal[BoardSession](msgSession.Data) + assert.Nil(t, err) + assert.Equal(t, userId, session.UserID) + assert.Equal(t, common.ModeratorRole, sessionData.Role) } func (suite *SessionServiceIntegrationTestSuite) Test_UpdateAll() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["UpdateAll"].id - ready := false - raisedHand := false - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - events := broker.GetBoardChannel(ctx, boardId) - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.UpdateAll(ctx, BoardSessionsUpdateRequest{Board: boardId, Ready: &ready, RaisedHand: &raisedHand}) - - assert.Nil(t, err) - assert.Len(t, sessions, 4) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantsUpdated, msg.Type) - sessionData, err := technical_helper.UnmarshalSlice[BoardSession](msg.Data) - assert.Nil(t, err) - assert.Len(t, sessionData, 4) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["UpdateAll"].id + ready := false + raisedHand := false + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + events := broker.GetBoardChannel(ctx, boardId) + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + sessions, err := sessionService.UpdateAll(ctx, BoardSessionsUpdateRequest{Board: boardId, Ready: &ready, RaisedHand: &raisedHand}) + + assert.Nil(t, err) + assert.Len(t, sessions, 4) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantsUpdated, msg.Type) + sessionData, err := technical_helper.UnmarshalSlice[BoardSession](msg.Data) + assert.Nil(t, err) + assert.Len(t, sessionData, 4) } func (suite *SessionServiceIntegrationTestSuite) Test_UpdateUserBoard() { @@ -174,298 +174,298 @@ func (suite *SessionServiceIntegrationTestSuite) Test_UpdateUserBoard() { } func (suite *SessionServiceIntegrationTestSuite) Test_Get() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Santa"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - session, err := sessionService.Get(ctx, boardId, userId) - - assert.Nil(t, err) - assert.Equal(t, boardId, session.Board) - assert.Equal(t, userId, session.UserID) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Santa"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + session, err := sessionService.Get(ctx, boardId, userId) + + assert.Nil(t, err) + assert.Equal(t, boardId, session.Board) + assert.Equal(t, userId, session.UserID) } func (suite *SessionServiceIntegrationTestSuite) Test_GetAll() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - sessions, err := sessionService.GetAll(ctx, boardId, BoardSessionFilter{}) - assert.Nil(t, err) - assert.Len(t, sessions, 4) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + sessions, err := sessionService.GetAll(ctx, boardId, BoardSessionFilter{}) + assert.Nil(t, err) + assert.Len(t, sessions, 4) } func (suite *SessionServiceIntegrationTestSuite) Test_GetUserConnectedBoards() { - t := suite.T() - ctx := context.Background() + t := suite.T() + ctx := context.Background() - userId := suite.users["Stan"] + userId := suite.users["Stan"] - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - sessions, err := sessionService.GetUserConnectedBoards(ctx, userId) + sessions, err := sessionService.GetUserConnectedBoards(ctx, userId) - assert.Nil(t, err) - assert.Len(t, sessions, 2) + assert.Nil(t, err) + assert.Len(t, sessions, 2) } func (suite *SessionServiceIntegrationTestSuite) Test_Connect() { - t := suite.T() - ctx := context.Background() + t := suite.T() + ctx := context.Background() - boardId := suite.boards["Update"].id - userId := suite.users["Luke"] + boardId := suite.boards["Update"].id + userId := suite.users["Luke"] - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } - events := broker.GetBoardChannel(ctx, boardId) + events := broker.GetBoardChannel(ctx, boardId) - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - err = sessionService.Connect(ctx, boardId, userId) + err = sessionService.Connect(ctx, boardId, userId) - assert.Nil(t, err) + assert.Nil(t, err) - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) } func (suite *SessionServiceIntegrationTestSuite) Test_Disconnect() { - t := suite.T() - ctx := context.Background() + t := suite.T() + ctx := context.Background() - boardId := suite.boards["Update"].id - userId := suite.users["Leia"] + boardId := suite.boards["Update"].id + userId := suite.users["Leia"] - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } - events := broker.GetBoardChannel(ctx, boardId) + events := broker.GetBoardChannel(ctx, boardId) - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - err = sessionService.Disconnect(ctx, boardId, userId) + err = sessionService.Disconnect(ctx, boardId, userId) - assert.Nil(t, err) + assert.Nil(t, err) - msgColumn := <-events - msgNote := <-events - assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumn.Type) - assert.Equal(t, realtime.BoardEventNotesSync, msgNote.Type) + msgColumn := <-events + msgNote := <-events + assert.Equal(t, realtime.BoardEventColumnsUpdated, msgColumn.Type) + assert.Equal(t, realtime.BoardEventNotesSync, msgNote.Type) } func (suite *SessionServiceIntegrationTestSuite) Test_Exists() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Stan"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - exists, err := sessionService.Exists(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, exists) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Stan"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + exists, err := sessionService.Exists(ctx, boardId, userId) + + assert.Nil(t, err) + assert.True(t, exists) } func (suite *SessionServiceIntegrationTestSuite) Test_ModeratorExists() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Stan"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - exists, err := sessionService.ModeratorSessionExists(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, exists) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Stan"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + exists, err := sessionService.ModeratorSessionExists(ctx, boardId, userId) + + assert.Nil(t, err) + assert.True(t, exists) } func (suite *SessionServiceIntegrationTestSuite) Test_IsParticipantBanned() { - t := suite.T() - ctx := context.Background() - - boardId := suite.boards["Read"].id - userId := suite.users["Bob"] - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := NewSessionDatabase(suite.db) - sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) - - banned, err := sessionService.IsParticipantBanned(ctx, boardId, userId) - - assert.Nil(t, err) - assert.True(t, banned) + t := suite.T() + ctx := context.Background() + + boardId := suite.boards["Read"].id + userId := suite.users["Bob"] + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := NewSessionDatabase(suite.db) + sessionService := NewSessionService(sessionDatabase, broker, columnService, noteService) + + banned, err := sessionService.IsParticipantBanned(ctx, boardId, userId) + + assert.Nil(t, err) + assert.True(t, banned) } func (suite *SessionServiceIntegrationTestSuite) SeedDatabase(db *bun.DB) { - // tests users - suite.users = make(map[string]uuid.UUID, 7) - suite.users["Stan"] = uuid.New() - suite.users["Friend"] = uuid.New() - suite.users["Santa"] = uuid.New() - suite.users["Bob"] = uuid.New() - suite.users["Luke"] = uuid.New() - suite.users["Leia"] = uuid.New() - suite.users["Han"] = uuid.New() - - // test boards - suite.boards = make(map[string]TestBoard, 5) - suite.boards["Write"] = TestBoard{id: uuid.New(), name: "Write"} - suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} - suite.boards["Read"] = TestBoard{id: uuid.New(), name: "Read"} - suite.boards["ReadFilter"] = TestBoard{id: uuid.New(), name: "ReadFilter"} - suite.boards["UpdateAll"] = TestBoard{id: uuid.New(), name: "UpdateAll"} - - // test sessions - suite.sessions = make(map[string]BoardSession, 16) - // test sessions for the write board - suite.sessions["Write"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["Write"].id, Role: common.ParticipantRole} - // test sessions for the update board - suite.sessions["UpdateOwner"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["Update"].id, Role: common.OwnerRole} - suite.sessions["UpdateParticipantModerator"] = BoardSession{UserID: suite.users["Luke"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} - suite.sessions["UpdateParticipantOwner"] = BoardSession{UserID: suite.users["Leia"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} - suite.sessions["UpdateModeratorOwner"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["Update"].id, Role: common.ParticipantRole} - // test sessions for the read board - suite.sessions["Read1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["Read"].id, Role: common.OwnerRole} - suite.sessions["Read2"] = BoardSession{UserID: suite.users["Friend"], Board: suite.boards["Read"].id, Role: common.ModeratorRole} - suite.sessions["Read3"] = BoardSession{UserID: suite.users["Santa"], Board: suite.boards["Read"].id, Role: common.ParticipantRole} - suite.sessions["Read4"] = BoardSession{UserID: suite.users["Bob"], Board: suite.boards["Read"].id, Role: common.ParticipantRole, Banned: true} - // test sessions for the read filter board - suite.sessions["ReadFilter1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["ReadFilter"].id, Role: common.OwnerRole, Ready: true, Connected: true} - suite.sessions["ReadFilter2"] = BoardSession{UserID: suite.users["Friend"], Board: suite.boards["ReadFilter"].id, Role: common.ModeratorRole, Ready: true} - suite.sessions["ReadFilter3"] = BoardSession{UserID: suite.users["Santa"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true, Connected: true} - suite.sessions["ReadFilter4"] = BoardSession{UserID: suite.users["Bob"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true} - // test sessions for the update all board - suite.sessions["UpdateAll1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["UpdateAll"].id, Role: common.OwnerRole, Connected: true, RaisedHand: true, Ready: false} - suite.sessions["UpdateAll2"] = BoardSession{UserID: suite.users["Luke"], Board: suite.boards["UpdateAll"].id, Role: common.ModeratorRole, Connected: true, RaisedHand: false, Ready: true} - suite.sessions["UpdateAll3"] = BoardSession{UserID: suite.users["Leia"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: false, Ready: false} - suite.sessions["UpdateAll4"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: true, Ready: true} - - for name, user := range suite.users { - if name == "Stan" { - err := initialize.InsertUser(db, user, name, string(common.Google)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } else { - err := initialize.InsertUser(db, user, name, string(common.Anonymous)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } - } - - for _, board := range suite.boards { - err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) - if err != nil { - log.Fatalf("Failed to insert test board %s", err) - } - } - - for _, session := range suite.sessions { - err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) - if err != nil { - log.Fatalf("Failed to insert test sessions %s", err) - } - } + // tests users + suite.users = make(map[string]uuid.UUID, 7) + suite.users["Stan"] = uuid.New() + suite.users["Friend"] = uuid.New() + suite.users["Santa"] = uuid.New() + suite.users["Bob"] = uuid.New() + suite.users["Luke"] = uuid.New() + suite.users["Leia"] = uuid.New() + suite.users["Han"] = uuid.New() + + // test boards + suite.boards = make(map[string]TestBoard, 5) + suite.boards["Write"] = TestBoard{id: uuid.New(), name: "Write"} + suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} + suite.boards["Read"] = TestBoard{id: uuid.New(), name: "Read"} + suite.boards["ReadFilter"] = TestBoard{id: uuid.New(), name: "ReadFilter"} + suite.boards["UpdateAll"] = TestBoard{id: uuid.New(), name: "UpdateAll"} + + // test sessions + suite.sessions = make(map[string]BoardSession, 16) + // test sessions for the write board + suite.sessions["Write"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["Write"].id, Role: common.ParticipantRole} + // test sessions for the update board + suite.sessions["UpdateOwner"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["Update"].id, Role: common.OwnerRole} + suite.sessions["UpdateParticipantModerator"] = BoardSession{UserID: suite.users["Luke"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} + suite.sessions["UpdateParticipantOwner"] = BoardSession{UserID: suite.users["Leia"], Board: suite.boards["Update"].id, Role: common.ParticipantRole, Connected: true} + suite.sessions["UpdateModeratorOwner"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["Update"].id, Role: common.ParticipantRole} + // test sessions for the read board + suite.sessions["Read1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["Read"].id, Role: common.OwnerRole} + suite.sessions["Read2"] = BoardSession{UserID: suite.users["Friend"], Board: suite.boards["Read"].id, Role: common.ModeratorRole} + suite.sessions["Read3"] = BoardSession{UserID: suite.users["Santa"], Board: suite.boards["Read"].id, Role: common.ParticipantRole} + suite.sessions["Read4"] = BoardSession{UserID: suite.users["Bob"], Board: suite.boards["Read"].id, Role: common.ParticipantRole, Banned: true} + // test sessions for the read filter board + suite.sessions["ReadFilter1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["ReadFilter"].id, Role: common.OwnerRole, Ready: true, Connected: true} + suite.sessions["ReadFilter2"] = BoardSession{UserID: suite.users["Friend"], Board: suite.boards["ReadFilter"].id, Role: common.ModeratorRole, Ready: true} + suite.sessions["ReadFilter3"] = BoardSession{UserID: suite.users["Santa"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true, Connected: true} + suite.sessions["ReadFilter4"] = BoardSession{UserID: suite.users["Bob"], Board: suite.boards["ReadFilter"].id, Role: common.ParticipantRole, RaisedHand: true} + // test sessions for the update all board + suite.sessions["UpdateAll1"] = BoardSession{UserID: suite.users["Stan"], Board: suite.boards["UpdateAll"].id, Role: common.OwnerRole, Connected: true, RaisedHand: true, Ready: false} + suite.sessions["UpdateAll2"] = BoardSession{UserID: suite.users["Luke"], Board: suite.boards["UpdateAll"].id, Role: common.ModeratorRole, Connected: true, RaisedHand: false, Ready: true} + suite.sessions["UpdateAll3"] = BoardSession{UserID: suite.users["Leia"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: false, Ready: false} + suite.sessions["UpdateAll4"] = BoardSession{UserID: suite.users["Han"], Board: suite.boards["UpdateAll"].id, Role: common.ParticipantRole, Connected: true, RaisedHand: true, Ready: true} + + for name, user := range suite.users { + if name == "Stan" { + err := initialize.InsertUser(db, user, name, string(common.Google)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } else { + err := initialize.InsertUser(db, user, name, string(common.Anonymous)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } + } + + for _, board := range suite.boards { + err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) + if err != nil { + log.Fatalf("Failed to insert test board %s", err) + } + } + + for _, session := range suite.sessions { + err := initialize.InsertSession(db, session.UserID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) + if err != nil { + log.Fatalf("Failed to insert test sessions %s", err) + } + } } diff --git a/server/src/users/service.go b/server/src/users/service.go index a904018051..d3cf9344ed 100644 --- a/server/src/users/service.go +++ b/server/src/users/service.go @@ -199,9 +199,9 @@ func (service *Service) updatedUser(ctx context.Context, user DatabaseUser) { } for _, session := range connectedBoards { - userSession, err := service.sessionService.Get(ctx, session.Board, session.ID) + userSession, err := service.sessionService.Get(ctx, session.Board, session.UserID) if err != nil { - logger.Get().Errorw("unable to get board session", "board", userSession.Board, "user", userSession.ID, "err", 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, diff --git a/server/src/users/service_integration_test.go b/server/src/users/service_integration_test.go index 68c887cb00..40489e2a61 100644 --- a/server/src/users/service_integration_test.go +++ b/server/src/users/service_integration_test.go @@ -1,449 +1,449 @@ package users import ( - "context" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go/modules/nats" - "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/uptrace/bun" - "log" - "scrumlr.io/server/columns" - "scrumlr.io/server/common" - "scrumlr.io/server/initialize" - "scrumlr.io/server/notes" - "scrumlr.io/server/realtime" - "scrumlr.io/server/sessions" - "scrumlr.io/server/votings" - "testing" + "context" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go/modules/nats" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/uptrace/bun" + "log" + "scrumlr.io/server/columns" + "scrumlr.io/server/common" + "scrumlr.io/server/initialize" + "scrumlr.io/server/notes" + "scrumlr.io/server/realtime" + "scrumlr.io/server/sessions" + "scrumlr.io/server/votings" + "testing" ) type TestBoard struct { - id uuid.UUID - name string + id uuid.UUID + name string } type UserServiceIntegrationTestsuite struct { - suite.Suite - dbContainer *postgres.PostgresContainer - natsContainer *nats.NATSContainer - db *bun.DB - natsConnectionString string - users map[string]User - boards map[string]TestBoard - sessions map[string]sessions.BoardSession + suite.Suite + dbContainer *postgres.PostgresContainer + natsContainer *nats.NATSContainer + db *bun.DB + natsConnectionString string + users map[string]User + boards map[string]TestBoard + sessions map[string]sessions.BoardSession } func TestUserServiceIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(UserServiceIntegrationTestsuite)) + suite.Run(t, new(UserServiceIntegrationTestsuite)) } func (suite *UserServiceIntegrationTestsuite) SetupSuite() { - dbContainer, bun := initialize.StartTestDatabase() - suite.SeedDatabase(bun) - natsContainer, connectionString := initialize.StartTestNats() - - suite.dbContainer = dbContainer - suite.natsContainer = natsContainer - suite.db = bun - suite.natsConnectionString = connectionString + dbContainer, bun := initialize.StartTestDatabase() + suite.SeedDatabase(bun) + natsContainer, connectionString := initialize.StartTestNats() + + suite.dbContainer = dbContainer + suite.natsContainer = natsContainer + suite.db = bun + suite.natsConnectionString = connectionString } func (suite *UserServiceIntegrationTestsuite) TeardownSuite() { - initialize.StopTestDatabase(suite.dbContainer) - initialize.StopTestNats(suite.natsContainer) + initialize.StopTestDatabase(suite.dbContainer) + initialize.StopTestNats(suite.natsContainer) } func (suite *UserServiceIntegrationTestsuite) Test_CreateAnonymous() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateAnonymous(ctx, userName) - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Anonymous, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAnonymous(ctx, userName) + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Anonymous, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateAppleUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateAppleUser(ctx, "appleId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Apple, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAppleUser(ctx, "appleId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Apple, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateAzureadUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateAzureAdUser(ctx, "azureId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.AzureAd, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateAzureAdUser(ctx, "azureId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.AzureAd, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateGitHubUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateGitHubUser(ctx, "githubId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.GitHub, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateGitHubUser(ctx, "githubId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.GitHub, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateGoogleUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateGoogleUser(ctx, "googleId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Google, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateGoogleUser(ctx, "googleId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Google, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateMicrosoft() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateMicrosoftUser(ctx, "microsoftId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.Microsoft, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateMicrosoftUser(ctx, "microsoftId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.Microsoft, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_CreateOIDCUser() { - t := suite.T() - ctx := context.Background() - - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.CreateOIDCUser(ctx, "oidcId", userName, "") - - assert.Nil(t, err) - assert.Equal(t, userName, user.Name) - assert.Equal(t, common.TypeOIDC, user.AccountType) + t := suite.T() + ctx := context.Background() + + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.CreateOIDCUser(ctx, "oidcId", userName, "") + + assert.Nil(t, err) + assert.Equal(t, userName, user.Name) + assert.Equal(t, common.TypeOIDC, user.AccountType) } func (suite *UserServiceIntegrationTestsuite) Test_Update() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Update"].ID - boardId := suite.boards["Update"].id - userName := "Test User" - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - events := broker.GetBoardChannel(ctx, boardId) - - user, err := userService.Update(ctx, UserUpdateRequest{ID: userId, Name: userName}) - - assert.Nil(t, err) - assert.Equal(t, userId, user.ID) - assert.Equal(t, userName, user.Name) - - msg := <-events - assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) - sessionData := msg.Data.(map[string]interface{}) - assert.True(t, sessionData["connected"].(bool)) - assert.Equal(t, string(common.OwnerRole), sessionData["role"].(string)) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Update"].ID + boardId := suite.boards["Update"].id + userName := "Test User" + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + events := broker.GetBoardChannel(ctx, boardId) + + user, err := userService.Update(ctx, UserUpdateRequest{ID: userId, Name: userName}) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) + assert.Equal(t, userName, user.Name) + + msg := <-events + assert.Equal(t, realtime.BoardEventParticipantUpdated, msg.Type) + sessionData := msg.Data.(map[string]interface{}) + assert.True(t, sessionData["connected"].(bool)) + assert.Equal(t, string(common.OwnerRole), sessionData["role"].(string)) } func (suite *UserServiceIntegrationTestsuite) Test_Get() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.Get(ctx, userId) - - assert.Nil(t, err) - assert.Equal(t, userId, user.ID) - assert.Equal(t, suite.users["Stan"].Name, user.Name) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Stan"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.Get(ctx, userId) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) + assert.Equal(t, suite.users["Stan"].Name, user.Name) } func (suite *UserServiceIntegrationTestsuite) Test_Get_NotFound() { - t := suite.T() - ctx := context.Background() - - userId := uuid.New() - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.Get(ctx, userId) - - assert.Nil(t, user) - assert.NotNil(t, err) - assert.Equal(t, common.NotFoundError, err) + t := suite.T() + ctx := context.Background() + + userId := uuid.New() + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.Get(ctx, userId) + + assert.Nil(t, user) + assert.NotNil(t, err) + assert.Equal(t, common.NotFoundError, err) } func (suite *UserServiceIntegrationTestsuite) Test_AvailableForKeyMigration() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Santa"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - available, err := userService.IsUserAvailableForKeyMigration(ctx, userId) - - assert.Nil(t, err) - assert.True(t, available) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Santa"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + available, err := userService.IsUserAvailableForKeyMigration(ctx, userId) + + assert.Nil(t, err) + assert.True(t, available) } func (suite *UserServiceIntegrationTestsuite) Test_SetKeyMigration() { - t := suite.T() - ctx := context.Background() - - userId := suite.users["Stan"].ID - - broker, err := realtime.NewNats(suite.natsConnectionString) - if err != nil { - log.Fatalf("Faild to connect to nats server %s", err) - } - - voteDatabase := votings.NewVotingDatabase(suite.db) - voteService := votings.NewVotingService(voteDatabase, broker) - noteDatabase := notes.NewNotesDatabase(suite.db) - noteService := notes.NewNotesService(noteDatabase, broker, voteService) - columnDatabase := columns.NewColumnsDatabase(suite.db) - columnService := columns.NewColumnService(columnDatabase, broker, noteService) - sessionDatabase := sessions.NewSessionDatabase(suite.db) - sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) - userDatabase := NewUserDatabase(suite.db) - userService := NewUserService(userDatabase, broker, sessionService) - - user, err := userService.SetKeyMigration(ctx, userId) - - assert.Nil(t, err) - assert.Equal(t, userId, user.ID) + t := suite.T() + ctx := context.Background() + + userId := suite.users["Stan"].ID + + broker, err := realtime.NewNats(suite.natsConnectionString) + if err != nil { + log.Fatalf("Faild to connect to nats server %s", err) + } + + voteDatabase := votings.NewVotingDatabase(suite.db) + voteService := votings.NewVotingService(voteDatabase, broker) + noteDatabase := notes.NewNotesDatabase(suite.db) + noteService := notes.NewNotesService(noteDatabase, broker, voteService) + columnDatabase := columns.NewColumnsDatabase(suite.db) + columnService := columns.NewColumnService(columnDatabase, broker, noteService) + sessionDatabase := sessions.NewSessionDatabase(suite.db) + sessionService := sessions.NewSessionService(sessionDatabase, broker, columnService, noteService) + userDatabase := NewUserDatabase(suite.db) + userService := NewUserService(userDatabase, broker, sessionService) + + user, err := userService.SetKeyMigration(ctx, userId) + + assert.Nil(t, err) + assert.Equal(t, userId, user.ID) } func (suite *UserServiceIntegrationTestsuite) SeedDatabase(db *bun.DB) { - // test users - suite.users = make(map[string]User, 3) - suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} - suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} - suite.users["Update"] = User{ID: uuid.New(), Name: "UpdateMe", AccountType: common.Anonymous} - - // test boards - suite.boards = make(map[string]TestBoard, 1) - suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} - - // test sessions - suite.sessions = make(map[string]sessions.BoardSession, 1) - suite.sessions["Update"] = sessions.BoardSession{ID: suite.users["Update"].ID, Board: suite.boards["Update"].id, Role: common.OwnerRole, Connected: true} - - for _, user := range suite.users { - err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) - if err != nil { - log.Fatalf("Failed to insert test user %s", err) - } - } - - for _, board := range suite.boards { - err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) - if err != nil { - log.Fatalf("Failed to insert test board %s", err) - } - } - - for _, session := range suite.sessions { - err := initialize.InsertSession(db, session.ID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) - if err != nil { - log.Fatalf("Failed to insert test sessions %s", err) - } - } + // test users + suite.users = make(map[string]User, 3) + suite.users["Stan"] = User{ID: uuid.New(), Name: "Stan", AccountType: common.Google} + suite.users["Santa"] = User{ID: uuid.New(), Name: "Santa", AccountType: common.Anonymous} + suite.users["Update"] = User{ID: uuid.New(), Name: "UpdateMe", AccountType: common.Anonymous} + + // test boards + suite.boards = make(map[string]TestBoard, 1) + suite.boards["Update"] = TestBoard{id: uuid.New(), name: "Update"} + + // test sessions + suite.sessions = make(map[string]sessions.BoardSession, 1) + suite.sessions["Update"] = sessions.BoardSession{UserID: suite.users["Update"].ID, Board: suite.boards["Update"].id, Role: common.OwnerRole, Connected: true} + + for _, user := range suite.users { + err := initialize.InsertUser(db, user.ID, user.Name, string(user.AccountType)) + if err != nil { + log.Fatalf("Failed to insert test user %s", err) + } + } + + for _, board := range suite.boards { + err := initialize.InsertBoard(db, board.id, board.name, "", nil, nil, "PUBLIC", true, true, true, true, false) + if err != nil { + log.Fatalf("Failed to insert test board %s", err) + } + } + + for _, session := range suite.sessions { + err := initialize.InsertSession(db, session.UserID, session.Board, string(session.Role), session.Banned, session.Ready, session.Connected, session.RaisedHand) + if err != nil { + log.Fatalf("Failed to insert test sessions %s", err) + } + } } diff --git a/server/src/users/service_test.go b/server/src/users/service_test.go index a08976dff8..620906e43b 100644 --- a/server/src/users/service_test.go +++ b/server/src/users/service_test.go @@ -728,13 +728,13 @@ func TestUpdateUser(t *testing.T) { mockUserService := sessions.NewMockSessionService(t) mockUserService.EXPECT().GetUserConnectedBoards(mock.Anything, userId). Return([]*sessions.BoardSession{ - {ID: user.ID, Board: firstBoardId}, - {ID: user.ID, Board: secondBoardId}, + {UserID: user.ID, Board: firstBoardId}, + {UserID: user.ID, Board: secondBoardId}, }, nil) mockUserService.EXPECT().Get(mock.Anything, firstBoardId, userId). - Return(&sessions.BoardSession{ID: user.ID, Board: firstBoardId}, nil) + Return(&sessions.BoardSession{UserID: user.ID, Board: firstBoardId}, nil) mockUserService.EXPECT().Get(mock.Anything, secondBoardId, userId). - Return(&sessions.BoardSession{ID: user.ID, Board: secondBoardId}, nil) + Return(&sessions.BoardSession{UserID: user.ID, Board: secondBoardId}, nil) userService := NewUserService(mockUserDatabase, broker, mockUserService) From 7663163921e711be4322dab3db3a388c3a2646ec Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 29 Sep 2025 10:19:10 +0200 Subject: [PATCH 15/16] renamings to be consistend with names --- server/src/api/users.go | 164 +++--- server/src/sessions/database.go | 356 ++++++------ server/src/sessions/service.go | 942 ++++++++++++++++---------------- 3 files changed, 735 insertions(+), 727 deletions(-) diff --git a/server/src/api/users.go b/server/src/api/users.go index 8d440ecc17..1c7f16d8a5 100644 --- a/server/src/api/users.go +++ b/server/src/api/users.go @@ -1,97 +1,105 @@ package api import ( - "github.com/go-chi/chi/v5" - "net/http" - "scrumlr.io/server/users" - - "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" + "github.com/go-chi/chi/v5" + "net/http" + "scrumlr.io/server/users" + + "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" ) //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() + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.get") + defer span.End() - userId := ctx.Value(identifiers.UserIdentifier).(uuid.UUID) + 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 - } + 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) + render.Status(r, http.StatusOK) + render.Respond(w, r, user) } func (s *Server) getUserByID(w http.ResponseWriter, r *http.Request) { - //callerId := r.Context().Value(identifiers.UserIdentifier).(uuid.UUID) - - userParam := chi.URLParam(r, "user") - requestedUserId, err := uuid.Parse(userParam) - if err != nil { - common.Throw(w, r, err) - } - user, err := s.users.Get(r.Context(), requestedUserId) - if err != nil { - common.Throw(w, r, err) - return - } - - render.Status(r, http.StatusOK) - render.Respond(w, r, user) + ctx, span := tracer.Start(r.Context(), "scrumlr.users.api.update") + 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) 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) + 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/sessions/database.go b/server/src/sessions/database.go index 1a64ed4d80..1dd1e5b076 100644 --- a/server/src/sessions/database.go +++ b/server/src/sessions/database.go @@ -1,213 +1,213 @@ package sessions import ( - "context" - "errors" + "context" + "errors" - "github.com/google/uuid" - "github.com/uptrace/bun" - "scrumlr.io/server/common" - "scrumlr.io/server/identifiers" + "github.com/google/uuid" + "github.com/uptrace/bun" + "scrumlr.io/server/common" + "scrumlr.io/server/identifiers" ) -type SessionDB struct { - db *bun.DB +type DB struct { + db *bun.DB } func NewSessionDatabase(database *bun.DB) SessionDatabase { - db := new(SessionDB) - db.db = database + db := new(DB) + db.db = database - return db + return db } -func (database *SessionDB) Create(ctx context.Context, boardSession DatabaseBoardSessionInsert) (DatabaseBoardSession, error) { - if boardSession.Role == common.OwnerRole { - return DatabaseBoardSession{}, errors.New("not allowed to create board session with owner role") - } - - var session DatabaseBoardSession - insertQuery := database.db.NewInsert(). - Model(&boardSession). - Returning("*") - - err := database.db.NewSelect(). - With("insertQuery", insertQuery). - Model((*DatabaseBoardSession)(nil)). - ModelTableExpr("\"insertQuery\" AS s"). - ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). - Where("s.board = ?", boardSession.Board). - Where("s.user = ?", boardSession.User). - Join("INNER JOIN users AS u ON u.id = s.user"). - Scan(common.ContextWithValues(ctx, - "Database", database, - "Operation", "INSERT", - identifiers.BoardIdentifier, boardSession.Board, - "Result", &session, - ), &session) - - return session, err +func (database *DB) Create(ctx context.Context, boardSession DatabaseBoardSessionInsert) (DatabaseBoardSession, error) { + if boardSession.Role == common.OwnerRole { + return DatabaseBoardSession{}, errors.New("not allowed to create board session with owner role") + } + + var session DatabaseBoardSession + insertQuery := database.db.NewInsert(). + Model(&boardSession). + Returning("*") + + err := database.db.NewSelect(). + With("insertQuery", insertQuery). + Model((*DatabaseBoardSession)(nil)). + ModelTableExpr("\"insertQuery\" AS s"). + ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). + Where("s.board = ?", boardSession.Board). + Where("s.user = ?", boardSession.User). + Join("INNER JOIN users AS u ON u.id = s.user"). + Scan(common.ContextWithValues(ctx, + "Database", database, + "Operation", "INSERT", + identifiers.BoardIdentifier, boardSession.Board, + "Result", &session, + ), &session) + + return session, err } -func (database *SessionDB) Update(ctx context.Context, update DatabaseBoardSessionUpdate) (DatabaseBoardSession, error) { - updateQuery := database.db.NewUpdate(). - Model(&update) - - if update.Connected != nil { - updateQuery = updateQuery.Column("connected") - } - - if update.Ready != nil { - updateQuery = updateQuery.Column("ready") - } - - if update.ShowHiddenColumns != nil { - updateQuery = updateQuery.Column("show_hidden_columns") - } - - if update.RaisedHand != nil { - updateQuery = updateQuery.Column("raised_hand") - } - - if update.Role != nil { - updateQuery = updateQuery.Column("role") - if *update.Role == common.OwnerRole { - updateQuery.Where("role = ?", common.OwnerRole) - } - } - - if update.Banned != nil { - updateQuery = updateQuery.Column("banned") - } - - updateQuery.Where("\"board\" = ?", update.Board). - Where("\"user\" = ?", update.User). - Returning("*") - - var session DatabaseBoardSession - err := database.db.NewSelect(). - With("updateQuery", updateQuery). - Model((*DatabaseBoardSession)(nil)). - ModelTableExpr("\"updateQuery\" AS s"). - ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). - Where("s.board = ?", update.Board). - Where("s.user = ?", update.User). - Join("INNER JOIN users AS u ON u.id = s.user"). - Scan(common.ContextWithValues(ctx, - "Database", database, - "Operation", "UPDATE", - identifiers.BoardIdentifier, update.Board, - "Result", &session, - ), &session) - - return session, err +func (database *DB) Update(ctx context.Context, update DatabaseBoardSessionUpdate) (DatabaseBoardSession, error) { + updateQuery := database.db.NewUpdate(). + Model(&update) + + if update.Connected != nil { + updateQuery = updateQuery.Column("connected") + } + + if update.Ready != nil { + updateQuery = updateQuery.Column("ready") + } + + if update.ShowHiddenColumns != nil { + updateQuery = updateQuery.Column("show_hidden_columns") + } + + if update.RaisedHand != nil { + updateQuery = updateQuery.Column("raised_hand") + } + + if update.Role != nil { + updateQuery = updateQuery.Column("role") + if *update.Role == common.OwnerRole { + updateQuery.Where("role = ?", common.OwnerRole) + } + } + + if update.Banned != nil { + updateQuery = updateQuery.Column("banned") + } + + updateQuery.Where("\"board\" = ?", update.Board). + Where("\"user\" = ?", update.User). + Returning("*") + + var session DatabaseBoardSession + err := database.db.NewSelect(). + With("updateQuery", updateQuery). + Model((*DatabaseBoardSession)(nil)). + ModelTableExpr("\"updateQuery\" AS s"). + ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). + Where("s.board = ?", update.Board). + Where("s.user = ?", update.User). + Join("INNER JOIN users AS u ON u.id = s.user"). + Scan(common.ContextWithValues(ctx, + "Database", database, + "Operation", "UPDATE", + identifiers.BoardIdentifier, update.Board, + "Result", &session, + ), &session) + + return session, err } -func (database *SessionDB) UpdateAll(ctx context.Context, update DatabaseBoardSessionUpdate) ([]DatabaseBoardSession, error) { - updateQuery := database.db.NewUpdate(). - Model(&update) +func (database *DB) UpdateAll(ctx context.Context, update DatabaseBoardSessionUpdate) ([]DatabaseBoardSession, error) { + updateQuery := database.db.NewUpdate(). + Model(&update) - if update.Ready != nil { - updateQuery = updateQuery.Column("ready") - } + if update.Ready != nil { + updateQuery = updateQuery.Column("ready") + } - if update.RaisedHand != nil { - updateQuery = updateQuery.Column("raised_hand") - } + if update.RaisedHand != nil { + updateQuery = updateQuery.Column("raised_hand") + } - updateQuery.Where("\"board\" = ?", update.Board). - Returning("*") + updateQuery.Where("\"board\" = ?", update.Board). + Returning("*") - var sessions []DatabaseBoardSession - err := database.db.NewSelect(). - With("updateQuery", updateQuery). - Model((*DatabaseBoardSession)(nil)). - ModelTableExpr("\"updateQuery\" AS s"). - ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). - Where("s.board = ?", update.Board). - Join("INNER JOIN users AS u ON u.id = s.user"). - Scan(ctx, &sessions) + var sessions []DatabaseBoardSession + err := database.db.NewSelect(). + With("updateQuery", updateQuery). + Model((*DatabaseBoardSession)(nil)). + ModelTableExpr("\"updateQuery\" AS s"). + ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). + Where("s.board = ?", update.Board). + Join("INNER JOIN users AS u ON u.id = s.user"). + Scan(ctx, &sessions) - return sessions, err + return sessions, err } -func (database *SessionDB) Exists(ctx context.Context, board, user uuid.UUID) (bool, error) { - return database.db.NewSelect(). - Table("board_sessions"). - Where("\"board\" = ?", board). - Where("\"user\" = ?", user). - Exists(ctx) +func (database *DB) Exists(ctx context.Context, board, user uuid.UUID) (bool, error) { + return database.db.NewSelect(). + Table("board_sessions"). + Where("\"board\" = ?", board). + Where("\"user\" = ?", user). + Exists(ctx) } -func (database *SessionDB) ModeratorExists(ctx context.Context, board, user uuid.UUID) (bool, error) { - return database.db.NewSelect(). - Table("board_sessions"). - Where("\"board\" = ?", board). - Where("\"user\" = ?", user). - Where("role <> ?", common.ParticipantRole). - Exists(ctx) +func (database *DB) ModeratorExists(ctx context.Context, board, user uuid.UUID) (bool, error) { + return database.db.NewSelect(). + Table("board_sessions"). + Where("\"board\" = ?", board). + Where("\"user\" = ?", user). + Where("role <> ?", common.ParticipantRole). + Exists(ctx) } -func (database *SessionDB) IsParticipantBanned(ctx context.Context, board, user uuid.UUID) (bool, error) { - return database.db.NewSelect(). - Table("board_sessions"). - Where("\"board\" = ?", board). - Where("\"user\" = ?", user). - Where("\"banned\" = ?", true). - Exists(ctx) +func (database *DB) IsParticipantBanned(ctx context.Context, board, user uuid.UUID) (bool, error) { + return database.db.NewSelect(). + Table("board_sessions"). + Where("\"board\" = ?", board). + Where("\"user\" = ?", user). + Where("\"banned\" = ?", true). + Exists(ctx) } -func (database *SessionDB) Get(ctx context.Context, board, user uuid.UUID) (DatabaseBoardSession, error) { - var session DatabaseBoardSession - err := database.db.NewSelect(). - TableExpr("board_sessions AS s"). - ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). - Where("s.board = ?", board). - Where("s.user = ?", user). - Join("INNER JOIN users AS u ON u.id = s.user"). - Scan(ctx, &session) - - return session, err +func (database *DB) Get(ctx context.Context, board, user uuid.UUID) (DatabaseBoardSession, error) { + var session DatabaseBoardSession + err := database.db.NewSelect(). + TableExpr("board_sessions AS s"). + ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). + Where("s.board = ?", board). + Where("s.user = ?", user). + Join("INNER JOIN users AS u ON u.id = s.user"). + Scan(ctx, &session) + + return session, err } -func (database *SessionDB) GetAll(ctx context.Context, board uuid.UUID, filter ...BoardSessionFilter) ([]DatabaseBoardSession, error) { - query := database.db.NewSelect(). - TableExpr("board_sessions AS s"). - ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). - Where("s.board = ?", board). - Join("INNER JOIN users AS u ON u.id = s.user") - - if len(filter) > 0 { - f := filter[0] - if f.Ready != nil { - query = query.Where("s.ready = ?", *f.Ready) - } - if f.RaisedHand != nil { - query = query.Where("s.raised_hand = ?", *f.RaisedHand) - } - if f.Connected != nil { - query = query.Where("s.connected = ?", *f.Connected) - } - if f.Role != nil { - query = query.Where("s.role = ?", *f.Role) - } - } - - var sessions []DatabaseBoardSession - err := query.Scan(ctx, &sessions) - return sessions, err +func (database *DB) GetAll(ctx context.Context, board uuid.UUID, filter ...BoardSessionFilter) ([]DatabaseBoardSession, error) { + query := database.db.NewSelect(). + TableExpr("board_sessions AS s"). + ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). + Where("s.board = ?", board). + Join("INNER JOIN users AS u ON u.id = s.user") + + if len(filter) > 0 { + f := filter[0] + if f.Ready != nil { + query = query.Where("s.ready = ?", *f.Ready) + } + if f.RaisedHand != nil { + query = query.Where("s.raised_hand = ?", *f.RaisedHand) + } + if f.Connected != nil { + query = query.Where("s.connected = ?", *f.Connected) + } + if f.Role != nil { + query = query.Where("s.role = ?", *f.Role) + } + } + + var sessions []DatabaseBoardSession + err := query.Scan(ctx, &sessions) + return sessions, err } // Gets all board sessions of a single user who he is currently connected to -func (database *SessionDB) GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]DatabaseBoardSession, error) { - var sessions []DatabaseBoardSession - err := database.db.NewSelect(). - TableExpr("board_sessions AS s"). - ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). - Where("s.user = ?", user). - Where("s.connected"). - Join("INNER JOIN users AS u ON u.id = s.user"). - Scan(ctx, &sessions) - - return sessions, err +func (database *DB) GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]DatabaseBoardSession, error) { + var sessions []DatabaseBoardSession + err := database.db.NewSelect(). + TableExpr("board_sessions AS s"). + ColumnExpr("s.board, s.user, u.avatar, u.name, u.account_type, s.connected, s.show_hidden_columns, s.ready, s.raised_hand, s.role, s.banned"). + Where("s.user = ?", user). + Where("s.connected"). + Join("INNER JOIN users AS u ON u.id = s.user"). + Scan(ctx, &sessions) + + return sessions, err } diff --git a/server/src/sessions/service.go b/server/src/sessions/service.go index 6c364785d9..ee144944dc 100644 --- a/server/src/sessions/service.go +++ b/server/src/sessions/service.go @@ -1,538 +1,538 @@ 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 +type Service struct { + 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(Service) + service.database = db + service.realtime = rt + service.columnService = columnService + service.noteService = noteService - return service + return service } -func (service *BoardSessionService) Create(ctx context.Context, boardID, userID uuid.UUID) (*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", boardID.String()), - attribute.String("scrumlr.sessions.service.create.user", userID.String()), - ) - - session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ - Board: boardID, - User: userID, - Role: common.ParticipantRole, - }) - - if err != nil { - span.SetStatus(codes.Error, "failed to create board session") - span.RecordError(err) - log.Errorw("unable to create board session", "board", boardID, "user", userID, "error", err) - return nil, err - } - - service.createdSession(ctx, boardID, session) - - sessionCreatedCounter.Add(ctx, 1) - return new(BoardSession).From(session), err +func (service *Service) Create(ctx context.Context, boardID, userID uuid.UUID) (*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", boardID.String()), + attribute.String("scrumlr.sessions.service.create.user", userID.String()), + ) + + session, err := service.database.Create(ctx, DatabaseBoardSessionInsert{ + Board: boardID, + User: userID, + Role: common.ParticipantRole, + }) + + if err != nil { + span.SetStatus(codes.Error, "failed to create board session") + span.RecordError(err) + log.Errorw("unable to create board session", "board", boardID, "user", userID, "error", err) + return nil, err + } + + service.createdSession(ctx, boardID, 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 getboard 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 +func (service *Service) 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 getboard 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 } -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 *Service) 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() +func (service *Service) 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()), - ) + 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 update all sessions") - span.RecordError(err) - return nil, err - } + connectedBoards, err := service.database.GetUserConnectedBoards(ctx, body.User) + if err != nil { + span.SetStatus(codes.Error, "failed to update all sessions") + span.RecordError(err) + return nil, err + } - for _, session := range connectedBoards { - service.updatedSession(ctx, session.Board, session) - } + for _, session := range connectedBoards { + service.updatedSession(ctx, session.Board, session) + } - return BoardSessions(connectedBoards), err + return BoardSessions(connectedBoards), 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 +func (service *Service) 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 } -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() +func (service *Service) GetAll(ctx context.Context, boardID uuid.UUID, filter BoardSessionFilter) ([]*BoardSession, error) { + 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() +func (service *Service) GetUserConnectedBoards(ctx context.Context, user uuid.UUID) ([]*BoardSession, error) { + 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 +func (service *Service) 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 } -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 +func (service *Service) 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 } -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() +func (service *Service) Exists(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { + 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() +func (service *Service) ModeratorSessionExists(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { + 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() +func (service *Service) IsParticipantBanned(ctx context.Context, boardID, userID uuid.UUID) (bool, error) { + 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 +func (service *Service) 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 } -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) - } +func (service *Service) 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) + } } -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 *Service) 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) 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) - } +func (service *Service) 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) + } } 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 } From 391a6da5f6fab3121d2a48322b7c91b2df6e358c Mon Sep 17 00:00:00 2001 From: Mateo Ivankovic Date: Mon, 29 Sep 2025 10:25:27 +0200 Subject: [PATCH 16/16] add postman test --- server/api.postman_collection.json | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/server/api.postman_collection.json b/server/api.postman_collection.json index 1cc5fdb9df..4f660d3ba9 100644 --- a/server/api.postman_collection.json +++ b/server/api.postman_collection.json @@ -482,6 +482,52 @@ "body": "{\n \"id\": \"34fc390b-8abe-458b-8a1e-9a918dec4b48\",\n \"name\": \"Jane Doe\"\n}" } ] + }, + { + "name": "Get user by ID", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const res = pm.response.json();", + "pm.test(\"Successful GET request\", () => {", + " pm.expect(pm.response).to.have.status(200);", + "});", + "", + "pm.test(\"Check id is included\", () => {", + " pm.expect(res.id).to.exist;", + "});", + "", + "pm.test(\"Check name is included\", () => {", + " pm.expect(res.name).to.exist;", + "});", + "", + "pm.test(\"Check identical id\", () => {", + " pm.expect(res.id).to.equal(pm.collectionVariables.get(\"user_id\"));", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/user/{{user_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "user", + "{{user_id}}" + ] + } + }, + "response": [] } ], "description": "These resources can update or read user information of the currently authenticated user. Since the `jwt` Cookie, which holds the user session, is set to HTTP only and therefore cannot be read from your web application you can use these methods to check which is user is actively using the application.",