refactor: remove legacy spaces and migrate lists to standalone resources with direct sharing
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
177b69234c
commit
0d6c603e9c
9 files changed
+616
-950
No files matched your search
+122
-185
@@ -63,6 +63,7 @@ func (s *Server) Router() http.Handler {
|
||||
r.Post("/api/v1/webhooks/ingest", s.handleWebhookIngest)
|
||||
r.Get("/api/v1/ws", s.handleWebSocket)
|
||||
r.Get("/api/v1/ws/spaces/{spaceId}", s.handleWebSocket)
|
||||
r.Get("/api/v1/ws/lists/{listId}", s.handleWebSocket)
|
||||
|
||||
// Authenticated API Routes
|
||||
r.Group(func(r chi.Router) {
|
||||
@@ -125,18 +126,14 @@ func (s *Server) Router() http.Handler {
|
||||
r.Post("/api/v1/documents/{id}/lock/heartbeat", s.handleHeartbeatDocLock)
|
||||
r.Delete("/api/v1/documents/{id}/lock", s.handleUnlockDoc)
|
||||
|
||||
// Shared Spaces & Realtime Lists
|
||||
r.Get("/api/v1/spaces", s.handleGetSpaces)
|
||||
r.Post("/api/v1/spaces", s.handleCreateSpace)
|
||||
r.Post("/api/v1/spaces/join", s.handleJoinSpace)
|
||||
r.Get("/api/v1/spaces/{id}/shares", s.handleGetSpaceShares)
|
||||
r.Post("/api/v1/spaces/{id}/members", s.handleAddSpaceMember)
|
||||
r.Delete("/api/v1/spaces/{id}/members/{userId}", s.handleRemoveSpaceMember)
|
||||
r.Get("/api/v1/spaces/{id}/lists", s.handleGetSpaceLists)
|
||||
r.Post("/api/v1/spaces/{id}/lists", s.handleCreateList)
|
||||
|
||||
// Shared Lists & Realtime Collaboration
|
||||
r.Get("/api/v1/lists", s.handleGetAllLists)
|
||||
r.Post("/api/v1/lists", s.handleCreateList)
|
||||
r.Get("/api/v1/lists/{id}", s.handleGetListByID)
|
||||
r.Delete("/api/v1/lists/{id}", s.handleDeleteList)
|
||||
r.Get("/api/v1/lists/{id}/shares", s.handleGetListShares)
|
||||
r.Post("/api/v1/lists/{id}/shares", s.handleCreateListShare)
|
||||
r.Delete("/api/v1/lists/{id}/shares/{shareId}", s.handleDeleteListShare)
|
||||
r.Post("/api/v1/lists/{id}/items", s.handleCreateListItem)
|
||||
r.Patch("/api/v1/lists/{id}/items/{itemId}", s.handleToggleListItem)
|
||||
r.Delete("/api/v1/lists/{id}/items/{itemId}", s.handleDeleteListItem)
|
||||
@@ -584,8 +581,8 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
||||
if converted, convErr := s.store.UpdateThoughtTriage(userID, thought.ID, "keep", target, targetListID, dueDate, taskListID); convErr == nil && converted != nil {
|
||||
thought = converted
|
||||
if target == "list_item" && targetListID != "" {
|
||||
if lst, lErr := s.store.GetListByID(targetListID); lErr == nil && lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, lst.ID, "ITEM_CREATED", lst)
|
||||
if lst, lErr := s.store.GetListByID(userID, targetListID); lErr == nil && lst != nil {
|
||||
s.wsHub.BroadcastListEvent(lst.ID, "ITEM_CREATED", lst)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -661,8 +658,8 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// If converted to a list item, broadcast via WebSocket to the space
|
||||
if convertTo == "list_item" && targetListID != "" {
|
||||
if lst, err := s.store.GetListByID(targetListID); err == nil && lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, lst.ID, "ITEM_CREATED", lst)
|
||||
if lst, err := s.store.GetListByID(userID, targetListID); err == nil && lst != nil {
|
||||
s.wsHub.BroadcastListEvent(lst.ID, "ITEM_CREATED", lst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,151 +1070,7 @@ func (s *Server) handleUnlockDoc(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"unlocked": released})
|
||||
}
|
||||
|
||||
// --- SPACES & REALTIME LISTS ---
|
||||
|
||||
func (s *Server) handleGetSpaces(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
spaces, err := s.store.GetSpacesForUser(userID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, spaces)
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
sp, err := s.store.CreateSpace(userID, req.Name, req.Icon, req.Description, false)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, sp)
|
||||
}
|
||||
|
||||
func (s *Server) handleJoinSpace(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
InviteCode string `json:"inviteCode"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
sp, err := s.store.JoinSpaceByInviteCode(userID, req.InviteCode)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastToUser(userID, "SPACE_JOINED", map[string]interface{}{"spaceId": sp.ID})
|
||||
s.wsHub.Broadcast(sp.ID, "", "MEMBER_ADDED", map[string]interface{}{"spaceId": sp.ID, "userId": userID})
|
||||
|
||||
writeJSON(w, http.StatusOK, sp)
|
||||
}
|
||||
|
||||
func (s *Server) handleAddSpaceMember(w http.ResponseWriter, r *http.Request) {
|
||||
currentUserID := auth.GetUserID(r.Context())
|
||||
spaceID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
TeamID string `json:"teamId"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Role != "owner" && req.Role != "editor" && req.Role != "viewer" {
|
||||
req.Role = "editor"
|
||||
}
|
||||
|
||||
if req.TeamID != "" {
|
||||
share, err := s.store.CreateSpaceTeamShare(currentUserID, spaceID, req.TeamID, req.Role)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
members, _ := s.store.GetTeamMembers(req.TeamID)
|
||||
for _, tm := range members {
|
||||
s.wsHub.BroadcastToUser(tm.UserID, "SPACE_JOINED", map[string]interface{}{"spaceId": spaceID})
|
||||
}
|
||||
s.wsHub.Broadcast(spaceID, "", "SPACE_SHARED", share)
|
||||
writeJSON(w, http.StatusCreated, share)
|
||||
return
|
||||
}
|
||||
|
||||
target := req.UserID
|
||||
if target == "" {
|
||||
target = req.Email
|
||||
}
|
||||
if target == "" {
|
||||
writeError(w, http.StatusBadRequest, "Benutzer-ID, Benutzername, E-Mail oder Team erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
member, err := s.store.AddSpaceMember(currentUserID, spaceID, target, req.Role)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastToUser(member.UserID, "SPACE_JOINED", map[string]interface{}{"spaceId": spaceID})
|
||||
s.wsHub.Broadcast(spaceID, "", "MEMBER_ADDED", member)
|
||||
|
||||
writeJSON(w, http.StatusCreated, member)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetSpaceShares(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID := chi.URLParam(r, "id")
|
||||
shares, err := s.store.GetSpaceShares(spaceID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, shares)
|
||||
}
|
||||
|
||||
func (s *Server) handleRemoveSpaceMember(w http.ResponseWriter, r *http.Request) {
|
||||
currentUserID := auth.GetUserID(r.Context())
|
||||
spaceID := chi.URLParam(r, "id")
|
||||
targetUserID := chi.URLParam(r, "userId")
|
||||
|
||||
// If prefixed with sm_, strip it for individual space member removal
|
||||
cleanTargetID := strings.TrimPrefix(targetUserID, "sm_")
|
||||
|
||||
if err := s.store.RemoveSpaceMember(currentUserID, spaceID, cleanTargetID); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastToUser(cleanTargetID, "SPACE_LEFT", map[string]interface{}{"spaceId": spaceID})
|
||||
s.wsHub.Broadcast(spaceID, "", "MEMBER_REMOVED", map[string]interface{}{"spaceId": spaceID, "userId": cleanTargetID})
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetSpaceLists(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID := chi.URLParam(r, "id")
|
||||
lists, err := s.store.GetListsForSpace(spaceID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, lists)
|
||||
}
|
||||
// --- REALTIME LISTS & SHARING ---
|
||||
|
||||
func (s *Server) handleGetAllLists(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
@@ -1229,8 +1082,19 @@ func (s *Server) handleGetAllLists(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, lists)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetListByID(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
listID := chi.URLParam(r, "id")
|
||||
lst, err := s.store.GetListByID(userID, listID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "Liste nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, lst)
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID := chi.URLParam(r, "id")
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
@@ -1241,28 +1105,96 @@ func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
lst, err := s.store.CreateList(spaceID, req.Title, req.Icon, req.ListType)
|
||||
lst, err := s.store.CreateList(userID, req.Title, req.Icon, req.ListType)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.Broadcast(spaceID, lst.ID, "LIST_CREATED", lst)
|
||||
s.wsHub.BroadcastListEvent(lst.ID, "LIST_CREATED", lst)
|
||||
writeJSON(w, http.StatusCreated, lst)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteList(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
listID := chi.URLParam(r, "id")
|
||||
lst, _ := s.store.GetListByID(listID)
|
||||
if err := s.store.DeleteList(listID); err != nil {
|
||||
if err := s.store.DeleteList(userID, listID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, listID, "LIST_DELETED", map[string]string{"listId": listID})
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(listID, "LIST_DELETED", map[string]string{"listId": listID})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetListShares(w http.ResponseWriter, r *http.Request) {
|
||||
listID := chi.URLParam(r, "id")
|
||||
shares, err := s.store.GetListShares(listID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, shares)
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateListShare(w http.ResponseWriter, r *http.Request) {
|
||||
currentUserID := auth.GetUserID(r.Context())
|
||||
listID := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
TargetType string `json:"targetType"` // "user" or "team"
|
||||
TargetID string `json:"targetId"` // userId/email or teamId
|
||||
Role string `json:"role"` // "editor" or "viewer"
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
targetType := strings.ToLower(strings.TrimSpace(req.TargetType))
|
||||
if targetType != "team" {
|
||||
targetType = "user"
|
||||
}
|
||||
|
||||
role := strings.ToLower(strings.TrimSpace(req.Role))
|
||||
if role != "viewer" && role != "editor" {
|
||||
role = "editor"
|
||||
}
|
||||
|
||||
share, err := s.store.AddListShare(currentUserID, listID, targetType, req.TargetID, role)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if targetType == "team" {
|
||||
members, _ := s.store.GetTeamMembers(share.TargetID)
|
||||
for _, tm := range members {
|
||||
s.wsHub.BroadcastToUser(tm.UserID, "LIST_SHARED", share)
|
||||
}
|
||||
} else {
|
||||
s.wsHub.BroadcastToUser(share.TargetID, "LIST_SHARED", share)
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(listID, "LIST_SHARED", share)
|
||||
|
||||
writeJSON(w, http.StatusCreated, share)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteListShare(w http.ResponseWriter, r *http.Request) {
|
||||
currentUserID := auth.GetUserID(r.Context())
|
||||
listID := chi.URLParam(r, "id")
|
||||
shareID := chi.URLParam(r, "shareId")
|
||||
|
||||
if err := s.store.RemoveListShare(currentUserID, listID, shareID); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastListEvent(listID, "LIST_UNSHARED", map[string]string{
|
||||
"listId": listID,
|
||||
"shareId": shareID,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateListItem(w http.ResponseWriter, r *http.Request) {
|
||||
listID := chi.URLParam(r, "id")
|
||||
var req struct {
|
||||
@@ -1281,9 +1213,7 @@ func (s *Server) handleCreateListItem(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, listID, "ITEM_CREATED", item)
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(listID, "ITEM_CREATED", item)
|
||||
writeJSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
@@ -1301,7 +1231,7 @@ func (s *Server) handleToggleListItem(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
user, _ := s.store.GetUserByID(userID)
|
||||
userName := "Familie"
|
||||
userName := "Mitglied"
|
||||
if user != nil {
|
||||
userName = user.DisplayName
|
||||
}
|
||||
@@ -1312,9 +1242,7 @@ func (s *Server) handleToggleListItem(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, listID, "ITEM_TOGGLED", item)
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(listID, "ITEM_TOGGLED", item)
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
@@ -1328,12 +1256,10 @@ func (s *Server) handleDeleteListItem(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, listID, "ITEM_DELETED", map[string]string{
|
||||
"itemId": itemID,
|
||||
"listId": listID,
|
||||
})
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(listID, "ITEM_DELETED", map[string]string{
|
||||
"itemId": itemID,
|
||||
"listId": listID,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
@@ -1345,14 +1271,25 @@ func (s *Server) handleClearCompletedItems(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||||
s.wsHub.Broadcast(lst.SpaceID, listID, "LIST_CLEARED", lst)
|
||||
}
|
||||
s.wsHub.BroadcastListEvent(listID, "LIST_CLEARED", map[string]interface{}{
|
||||
"listId": listID,
|
||||
"removed": removed,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]int64{"removed": removed})
|
||||
}
|
||||
|
||||
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID := chi.URLParam(r, "spaceId")
|
||||
roomID := chi.URLParam(r, "listId")
|
||||
if roomID == "" {
|
||||
roomID = chi.URLParam(r, "spaceId")
|
||||
}
|
||||
if roomID == "" {
|
||||
roomID = r.URL.Query().Get("listId")
|
||||
}
|
||||
if roomID == "" {
|
||||
roomID = r.URL.Query().Get("spaceId")
|
||||
}
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
if authHdr := r.Header.Get("Authorization"); strings.HasPrefix(authHdr, "Bearer ") {
|
||||
@@ -1365,7 +1302,7 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
userID = claims.UserID
|
||||
}
|
||||
}
|
||||
s.wsHub.ServeWS(w, r, spaceID, userID)
|
||||
s.wsHub.ServeWS(w, r, roomID, userID)
|
||||
}
|
||||
|
||||
// --- API KEYS & WEBHOOK INGESTION ---
|
||||
|
||||
+39
-41
@@ -117,7 +117,6 @@ func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
|
||||
"/api/v1/tasks",
|
||||
"/api/v1/organizations",
|
||||
"/api/v1/teams",
|
||||
"/api/v1/spaces",
|
||||
"/api/v1/lists",
|
||||
}
|
||||
for _, ep := range arrayEndpoints {
|
||||
@@ -190,7 +189,6 @@ func TestConcurrentInitialLoadAndListsNoDeadlock(t *testing.T) {
|
||||
"/api/v1/auth/me",
|
||||
"/api/v1/triage/deck",
|
||||
"/api/v1/lists",
|
||||
"/api/v1/spaces",
|
||||
"/api/v1/tasks",
|
||||
"/api/v1/documents",
|
||||
}
|
||||
@@ -557,104 +555,104 @@ func TestSpaceMembersDirectInvitationAPI(t *testing.T) {
|
||||
t.Fatalf("failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
// 1. Admin creates a space
|
||||
createSpaceBody := []byte(`{"name":"Einkauf WG","icon":"🛒","description":"Gemeinsame Einkaufsliste"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/spaces", bytes.NewReader(createSpaceBody))
|
||||
// 1. Admin creates a list
|
||||
createListBody := []byte(`{"title":"Einkauf WG","icon":"🛒","listType":"shopping"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/lists", bytes.NewReader(createListBody))
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created from /api/v1/spaces, got %d: %s", rec.Code, rec.Body.String())
|
||||
t.Fatalf("expected 201 Created from /api/v1/lists, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var createdSpace domain.Space
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &createdSpace)
|
||||
if createdSpace.ID == "" {
|
||||
t.Fatalf("expected created space with ID, got %+v", createdSpace)
|
||||
var createdList domain.SharedList
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &createdList)
|
||||
if createdList.ID == "" {
|
||||
t.Fatalf("expected created list with ID, got %+v", createdList)
|
||||
}
|
||||
|
||||
// 2. Admin invites normalUser by username "user"
|
||||
addMemberBody := []byte(`{"userId":"user","role":"editor"}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/spaces/"+createdSpace.ID+"/members", bytes.NewReader(addMemberBody))
|
||||
// 2. Admin shares list with normalUser by username "user"
|
||||
addShareBody := []byte(`{"targetType":"user","targetId":"user","role":"editor"}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/lists/"+createdList.ID+"/shares", bytes.NewReader(addShareBody))
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created from add space member, got %d: %s", rec.Code, rec.Body.String())
|
||||
t.Fatalf("expected 201 Created from add list share, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var newMember domain.SpaceMember
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &newMember)
|
||||
if newMember.UserID != normalUser.ID {
|
||||
t.Fatalf("expected member userId %s, got %s", normalUser.ID, newMember.UserID)
|
||||
var newShare domain.ListShare
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &newShare)
|
||||
if newShare.TargetID != normalUser.ID {
|
||||
t.Fatalf("expected share targetId %s, got %s", normalUser.ID, newShare.TargetID)
|
||||
}
|
||||
|
||||
// 3. User checks their spaces; the shared space must be listed
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/spaces", nil)
|
||||
// 3. User checks their lists; the shared list must be listed
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/lists", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+userToken)
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK for user spaces, got %d", rec.Code)
|
||||
t.Fatalf("expected 200 OK for user lists, got %d", rec.Code)
|
||||
}
|
||||
var userSpaces []domain.Space
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &userSpaces)
|
||||
var userLists []domain.SharedList
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &userLists)
|
||||
found := false
|
||||
for _, s := range userSpaces {
|
||||
if s.ID == createdSpace.ID {
|
||||
for _, l := range userLists {
|
||||
if l.ID == createdList.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected space %s to be visible to user, got %+v", createdSpace.ID, userSpaces)
|
||||
t.Fatalf("expected list %s to be visible to user, got %+v", createdList.ID, userLists)
|
||||
}
|
||||
|
||||
// 4. Admin removes user from space
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/spaces/"+createdSpace.ID+"/members/"+normalUser.ID, nil)
|
||||
// 4. Admin removes user share from list
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/lists/"+createdList.ID+"/shares/"+newShare.ID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK for remove member, got %d: %s", rec.Code, rec.Body.String())
|
||||
t.Fatalf("expected 200 OK for remove share, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// 5. Test adding a team to a space
|
||||
// 5. Test sharing list with a team
|
||||
team, err := store.CreateTeam("", "WG Team", "Haushalt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create team: %v", err)
|
||||
}
|
||||
_ = store.AddTeamMember(team.ID, normalUser.ID, "member")
|
||||
|
||||
addTeamBody := []byte(`{"teamId":"` + team.ID + `","role":"editor"}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/spaces/"+createdSpace.ID+"/members", bytes.NewReader(addTeamBody))
|
||||
addTeamBody := []byte(`{"targetType":"team","targetId":"` + team.ID + `","role":"editor"}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/lists/"+createdList.ID+"/shares", bytes.NewReader(addTeamBody))
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusCreated && rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 201 or 200 for add space team, got %d: %s", rec.Code, rec.Body.String())
|
||||
t.Fatalf("expected 201 or 200 for add list team share, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// User should see the space again because their team was added
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/spaces", nil)
|
||||
// User should see the list again because their team was added
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/lists", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+userToken)
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK for user spaces after team add, got %d", rec.Code)
|
||||
t.Fatalf("expected 200 OK for user lists after team share add, got %d", rec.Code)
|
||||
}
|
||||
userSpaces = nil
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &userSpaces)
|
||||
userLists = nil
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &userLists)
|
||||
found = false
|
||||
for _, s := range userSpaces {
|
||||
if s.ID == createdSpace.ID {
|
||||
for _, l := range userLists {
|
||||
if l.ID == createdList.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected space %s to be visible to user via team, got %+v", createdSpace.ID, userSpaces)
|
||||
t.Fatalf("expected list %s to be visible to user via team share, got %+v", createdList.ID, userLists)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user