mirror of https://github.com/usememos/memos
				
				
				
			refactor(api): migrate inbox functionality to user notifications
- Remove standalone InboxService and move functionality to UserService - Rename inbox to user notifications for better API consistency - Add ListUserNotifications, UpdateUserNotification, DeleteUserNotification methods - Update frontend components to use new notification endpoints - Update store layer to support new notification model 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>pull/5075/merge
							parent
							
								
									e915e3a46b
								
							
						
					
					
						commit
						bc1550e926
					
				@ -1,149 +0,0 @@
 | 
			
		||||
syntax = "proto3";
 | 
			
		||||
 | 
			
		||||
package memos.api.v1;
 | 
			
		||||
 | 
			
		||||
import "google/api/annotations.proto";
 | 
			
		||||
import "google/api/client.proto";
 | 
			
		||||
import "google/api/field_behavior.proto";
 | 
			
		||||
import "google/api/resource.proto";
 | 
			
		||||
import "google/protobuf/empty.proto";
 | 
			
		||||
import "google/protobuf/field_mask.proto";
 | 
			
		||||
import "google/protobuf/timestamp.proto";
 | 
			
		||||
 | 
			
		||||
option go_package = "gen/api/v1";
 | 
			
		||||
 | 
			
		||||
service InboxService {
 | 
			
		||||
  // ListInboxes lists inboxes for a user.
 | 
			
		||||
  rpc ListInboxes(ListInboxesRequest) returns (ListInboxesResponse) {
 | 
			
		||||
    option (google.api.http) = {get: "/api/v1/{parent=users/*}/inboxes"};
 | 
			
		||||
    option (google.api.method_signature) = "parent";
 | 
			
		||||
  }
 | 
			
		||||
  // UpdateInbox updates an inbox.
 | 
			
		||||
  rpc UpdateInbox(UpdateInboxRequest) returns (Inbox) {
 | 
			
		||||
    option (google.api.http) = {
 | 
			
		||||
      patch: "/api/v1/{inbox.name=inboxes/*}"
 | 
			
		||||
      body: "inbox"
 | 
			
		||||
    };
 | 
			
		||||
    option (google.api.method_signature) = "inbox,update_mask";
 | 
			
		||||
  }
 | 
			
		||||
  // DeleteInbox deletes an inbox.
 | 
			
		||||
  rpc DeleteInbox(DeleteInboxRequest) returns (google.protobuf.Empty) {
 | 
			
		||||
    option (google.api.http) = {delete: "/api/v1/{name=inboxes/*}"};
 | 
			
		||||
    option (google.api.method_signature) = "name";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Inbox {
 | 
			
		||||
  option (google.api.resource) = {
 | 
			
		||||
    type: "memos.api.v1/Inbox"
 | 
			
		||||
    pattern: "inboxes/{inbox}"
 | 
			
		||||
    name_field: "name"
 | 
			
		||||
    singular: "inbox"
 | 
			
		||||
    plural: "inboxes"
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // The resource name of the inbox.
 | 
			
		||||
  // Format: inboxes/{inbox}
 | 
			
		||||
  string name = 1 [(google.api.field_behavior) = IDENTIFIER];
 | 
			
		||||
 | 
			
		||||
  // The sender of the inbox notification.
 | 
			
		||||
  // Format: users/{user}
 | 
			
		||||
  string sender = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
 | 
			
		||||
 | 
			
		||||
  // The receiver of the inbox notification.
 | 
			
		||||
  // Format: users/{user}
 | 
			
		||||
  string receiver = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
 | 
			
		||||
 | 
			
		||||
  // The status of the inbox notification.
 | 
			
		||||
  Status status = 4 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
 | 
			
		||||
  // Output only. The creation timestamp.
 | 
			
		||||
  google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
 | 
			
		||||
 | 
			
		||||
  // The type of the inbox notification.
 | 
			
		||||
  Type type = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
 | 
			
		||||
 | 
			
		||||
  // Optional. The activity ID associated with this inbox notification.
 | 
			
		||||
  optional int32 activity_id = 7 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
 | 
			
		||||
  // Status enumeration for inbox notifications.
 | 
			
		||||
  enum Status {
 | 
			
		||||
    // Unspecified status.
 | 
			
		||||
    STATUS_UNSPECIFIED = 0;
 | 
			
		||||
    // The notification is unread.
 | 
			
		||||
    UNREAD = 1;
 | 
			
		||||
    // The notification is archived.
 | 
			
		||||
    ARCHIVED = 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Type enumeration for inbox notifications.
 | 
			
		||||
  enum Type {
 | 
			
		||||
    // Unspecified type.
 | 
			
		||||
    TYPE_UNSPECIFIED = 0;
 | 
			
		||||
    // Memo comment notification.
 | 
			
		||||
    MEMO_COMMENT = 1;
 | 
			
		||||
    // Version update notification.
 | 
			
		||||
    VERSION_UPDATE = 2;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListInboxesRequest {
 | 
			
		||||
  // Required. The parent resource whose inboxes will be listed.
 | 
			
		||||
  // Format: users/{user}
 | 
			
		||||
  string parent = 1 [
 | 
			
		||||
    (google.api.field_behavior) = REQUIRED,
 | 
			
		||||
    (google.api.resource_reference) = {type: "memos.api.v1/User"}
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  // Optional. The maximum number of inboxes to return.
 | 
			
		||||
  // The service may return fewer than this value.
 | 
			
		||||
  // If unspecified, at most 50 inboxes will be returned.
 | 
			
		||||
  // The maximum value is 1000; values above 1000 will be coerced to 1000.
 | 
			
		||||
  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
 | 
			
		||||
  // Optional. A page token, received from a previous `ListInboxes` call.
 | 
			
		||||
  // Provide this to retrieve the subsequent page.
 | 
			
		||||
  string page_token = 3 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
 | 
			
		||||
  // Optional. Filter to apply to the list results.
 | 
			
		||||
  // Example: "status=UNREAD" or "type=MEMO_COMMENT"
 | 
			
		||||
  // Supported operators: =, !=
 | 
			
		||||
  // Supported fields: status, type, sender, create_time
 | 
			
		||||
  string filter = 4 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
 | 
			
		||||
  // Optional. The order to sort results by.
 | 
			
		||||
  // Example: "create_time desc" or "status asc"
 | 
			
		||||
  string order_by = 5 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListInboxesResponse {
 | 
			
		||||
  // The list of inboxes.
 | 
			
		||||
  repeated Inbox inboxes = 1;
 | 
			
		||||
 | 
			
		||||
  // A token that can be sent as `page_token` to retrieve the next page.
 | 
			
		||||
  // If this field is omitted, there are no subsequent pages.
 | 
			
		||||
  string next_page_token = 2;
 | 
			
		||||
 | 
			
		||||
  // The total count of inboxes (may be approximate).
 | 
			
		||||
  int32 total_size = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message UpdateInboxRequest {
 | 
			
		||||
  // Required. The inbox to update.
 | 
			
		||||
  Inbox inbox = 1 [(google.api.field_behavior) = REQUIRED];
 | 
			
		||||
 | 
			
		||||
  // Required. The list of fields to update.
 | 
			
		||||
  google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];
 | 
			
		||||
 | 
			
		||||
  // Optional. If set to true, allows updating missing fields.
 | 
			
		||||
  bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message DeleteInboxRequest {
 | 
			
		||||
  // Required. The resource name of the inbox to delete.
 | 
			
		||||
  // Format: inboxes/{inbox}
 | 
			
		||||
  string name = 1 [
 | 
			
		||||
    (google.api.field_behavior) = REQUIRED,
 | 
			
		||||
    (google.api.resource_reference) = {type: "memos.api.v1/Inbox"}
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
@ -1,224 +0,0 @@
 | 
			
		||||
package v1
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"google.golang.org/grpc/codes"
 | 
			
		||||
	"google.golang.org/grpc/status"
 | 
			
		||||
	"google.golang.org/protobuf/types/known/emptypb"
 | 
			
		||||
	"google.golang.org/protobuf/types/known/timestamppb"
 | 
			
		||||
 | 
			
		||||
	v1pb "github.com/usememos/memos/proto/gen/api/v1"
 | 
			
		||||
	"github.com/usememos/memos/store"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxesRequest) (*v1pb.ListInboxesResponse, error) {
 | 
			
		||||
	// Extract user ID from parent resource name
 | 
			
		||||
	userID, err := ExtractUserIDFromName(request.Parent)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.InvalidArgument, "invalid parent name %q: %v", request.Parent, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get current user for authorization
 | 
			
		||||
	currentUser, err := s.GetCurrentUser(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to get current user")
 | 
			
		||||
	}
 | 
			
		||||
	if currentUser == nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if current user can access the requested user's inboxes
 | 
			
		||||
	if currentUser.ID != userID {
 | 
			
		||||
		// Only allow hosts and admins to access other users' inboxes
 | 
			
		||||
		if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
 | 
			
		||||
			return nil, status.Errorf(codes.PermissionDenied, "cannot access inboxes for user %q", request.Parent)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var limit, offset int
 | 
			
		||||
	if request.PageToken != "" {
 | 
			
		||||
		var pageToken v1pb.PageToken
 | 
			
		||||
		if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil {
 | 
			
		||||
			return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		limit = int(pageToken.Limit)
 | 
			
		||||
		offset = int(pageToken.Offset)
 | 
			
		||||
	} else {
 | 
			
		||||
		limit = int(request.PageSize)
 | 
			
		||||
	}
 | 
			
		||||
	if limit <= 0 {
 | 
			
		||||
		limit = DefaultPageSize
 | 
			
		||||
	}
 | 
			
		||||
	if limit > MaxPageSize {
 | 
			
		||||
		limit = MaxPageSize
 | 
			
		||||
	}
 | 
			
		||||
	limitPlusOne := limit + 1
 | 
			
		||||
 | 
			
		||||
	findInbox := &store.FindInbox{
 | 
			
		||||
		ReceiverID: &userID,
 | 
			
		||||
		Limit:      &limitPlusOne,
 | 
			
		||||
		Offset:     &offset,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	inboxes, err := s.Store.ListInboxes(ctx, findInbox)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	inboxMessages := []*v1pb.Inbox{}
 | 
			
		||||
	nextPageToken := ""
 | 
			
		||||
	if len(inboxes) == limitPlusOne {
 | 
			
		||||
		inboxes = inboxes[:limit]
 | 
			
		||||
		nextPageToken, err = getPageToken(limit, offset+limit)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, status.Errorf(codes.Internal, "failed to get next page token: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for _, inbox := range inboxes {
 | 
			
		||||
		inboxMessage := convertInboxFromStore(inbox)
 | 
			
		||||
		if inboxMessage.Type == v1pb.Inbox_TYPE_UNSPECIFIED {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		inboxMessages = append(inboxMessages, inboxMessage)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := &v1pb.ListInboxesResponse{
 | 
			
		||||
		Inboxes:       inboxMessages,
 | 
			
		||||
		NextPageToken: nextPageToken,
 | 
			
		||||
		TotalSize:     int32(len(inboxMessages)), // For now, use actual returned count
 | 
			
		||||
	}
 | 
			
		||||
	return response, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInboxRequest) (*v1pb.Inbox, error) {
 | 
			
		||||
	if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
 | 
			
		||||
		return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	inboxID, err := ExtractInboxIDFromName(request.Inbox.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Inbox.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get current user for authorization
 | 
			
		||||
	currentUser, err := s.GetCurrentUser(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to get current user")
 | 
			
		||||
	}
 | 
			
		||||
	if currentUser == nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the existing inbox to verify ownership
 | 
			
		||||
	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
 | 
			
		||||
		ID: &inboxID,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if len(inboxes) == 0 {
 | 
			
		||||
		return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Inbox.Name)
 | 
			
		||||
	}
 | 
			
		||||
	existingInbox := inboxes[0]
 | 
			
		||||
 | 
			
		||||
	// Check if current user can update this inbox (must be the receiver)
 | 
			
		||||
	if currentUser.ID != existingInbox.ReceiverID {
 | 
			
		||||
		return nil, status.Errorf(codes.PermissionDenied, "cannot update inbox for another user")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update := &store.UpdateInbox{
 | 
			
		||||
		ID: inboxID,
 | 
			
		||||
	}
 | 
			
		||||
	for _, field := range request.UpdateMask.Paths {
 | 
			
		||||
		if field == "status" {
 | 
			
		||||
			if request.Inbox.Status == v1pb.Inbox_STATUS_UNSPECIFIED {
 | 
			
		||||
				return nil, status.Errorf(codes.InvalidArgument, "status cannot be unspecified")
 | 
			
		||||
			}
 | 
			
		||||
			update.Status = convertInboxStatusToStore(request.Inbox.Status)
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil, status.Errorf(codes.InvalidArgument, "unsupported field in update mask: %q", field)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	inbox, err := s.Store.UpdateInbox(ctx, update)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return convertInboxFromStore(inbox), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *APIV1Service) DeleteInbox(ctx context.Context, request *v1pb.DeleteInboxRequest) (*emptypb.Empty, error) {
 | 
			
		||||
	inboxID, err := ExtractInboxIDFromName(request.Name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get current user for authorization
 | 
			
		||||
	currentUser, err := s.GetCurrentUser(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to get current user")
 | 
			
		||||
	}
 | 
			
		||||
	if currentUser == nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the existing inbox to verify ownership
 | 
			
		||||
	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
 | 
			
		||||
		ID: &inboxID,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if len(inboxes) == 0 {
 | 
			
		||||
		return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Name)
 | 
			
		||||
	}
 | 
			
		||||
	existingInbox := inboxes[0]
 | 
			
		||||
 | 
			
		||||
	// Check if current user can delete this inbox (must be the receiver)
 | 
			
		||||
	if currentUser.ID != existingInbox.ReceiverID {
 | 
			
		||||
		return nil, status.Errorf(codes.PermissionDenied, "cannot delete inbox for another user")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{
 | 
			
		||||
		ID: inboxID,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return nil, status.Errorf(codes.Internal, "failed to delete inbox: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return &emptypb.Empty{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertInboxFromStore(inbox *store.Inbox) *v1pb.Inbox {
 | 
			
		||||
	return &v1pb.Inbox{
 | 
			
		||||
		Name:       fmt.Sprintf("%s%d", InboxNamePrefix, inbox.ID),
 | 
			
		||||
		Sender:     fmt.Sprintf("%s%d", UserNamePrefix, inbox.SenderID),
 | 
			
		||||
		Receiver:   fmt.Sprintf("%s%d", UserNamePrefix, inbox.ReceiverID),
 | 
			
		||||
		Status:     convertInboxStatusFromStore(inbox.Status),
 | 
			
		||||
		CreateTime: timestamppb.New(time.Unix(inbox.CreatedTs, 0)),
 | 
			
		||||
		Type:       v1pb.Inbox_Type(inbox.Message.Type),
 | 
			
		||||
		ActivityId: inbox.Message.ActivityId,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertInboxStatusFromStore(status store.InboxStatus) v1pb.Inbox_Status {
 | 
			
		||||
	switch status {
 | 
			
		||||
	case store.UNREAD:
 | 
			
		||||
		return v1pb.Inbox_UNREAD
 | 
			
		||||
	case store.ARCHIVED:
 | 
			
		||||
		return v1pb.Inbox_ARCHIVED
 | 
			
		||||
	default:
 | 
			
		||||
		return v1pb.Inbox_STATUS_UNSPECIFIED
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertInboxStatusToStore(status v1pb.Inbox_Status) store.InboxStatus {
 | 
			
		||||
	switch status {
 | 
			
		||||
	case v1pb.Inbox_ARCHIVED:
 | 
			
		||||
		return store.ARCHIVED
 | 
			
		||||
	default:
 | 
			
		||||
		return store.UNREAD
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,559 +0,0 @@
 | 
			
		||||
package test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
	"google.golang.org/grpc/codes"
 | 
			
		||||
	"google.golang.org/grpc/status"
 | 
			
		||||
	"google.golang.org/protobuf/types/known/fieldmaskpb"
 | 
			
		||||
 | 
			
		||||
	v1pb "github.com/usememos/memos/proto/gen/api/v1"
 | 
			
		||||
	storepb "github.com/usememos/memos/proto/gen/store"
 | 
			
		||||
	"github.com/usememos/memos/store"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestListInboxes(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	t.Run("ListInboxes success", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		// List inboxes (should be empty initially)
 | 
			
		||||
		req := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent: fmt.Sprintf("users/%d", user.ID),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp, err := ts.Service.ListInboxes(userCtx, req)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotNil(t, resp)
 | 
			
		||||
		require.Empty(t, resp.Inboxes)
 | 
			
		||||
		require.Equal(t, int32(0), resp.TotalSize)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("ListInboxes with pagination", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create some inbox entries
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		for i := 0; i < 3; i++ {
 | 
			
		||||
			_, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
				SenderID:   systemBotID,
 | 
			
		||||
				ReceiverID: user.ID,
 | 
			
		||||
				Status:     store.UNREAD,
 | 
			
		||||
				Message: &storepb.InboxMessage{
 | 
			
		||||
					Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		// List inboxes with page size limit
 | 
			
		||||
		req := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent:   fmt.Sprintf("users/%d", user.ID),
 | 
			
		||||
			PageSize: 2,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp, err := ts.Service.ListInboxes(userCtx, req)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotNil(t, resp)
 | 
			
		||||
		require.Equal(t, 2, len(resp.Inboxes))
 | 
			
		||||
		require.NotEmpty(t, resp.NextPageToken)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("ListInboxes permission denied for different user", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create two users
 | 
			
		||||
		user1, err := ts.CreateRegularUser(ctx, "user1")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		user2, err := ts.CreateRegularUser(ctx, "user2")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user1 context but try to list user2's inboxes
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user1.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent: fmt.Sprintf("users/%d", user2.ID),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.ListInboxes(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "cannot access inboxes")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("ListInboxes host can access other users' inboxes", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a host user and a regular user
 | 
			
		||||
		hostUser, err := ts.CreateHostUser(ctx, "hostuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		regularUser, err := ts.CreateRegularUser(ctx, "regularuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox for the regular user
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		_, err = ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: regularUser.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set host user context and try to list regular user's inboxes
 | 
			
		||||
		hostCtx := ts.CreateUserContext(ctx, hostUser.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent: fmt.Sprintf("users/%d", regularUser.ID),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp, err := ts.Service.ListInboxes(hostCtx, req)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotNil(t, resp)
 | 
			
		||||
		require.Equal(t, 1, len(resp.Inboxes))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("ListInboxes invalid parent format", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent: "invalid-parent-format",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.ListInboxes(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid parent name")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("ListInboxes unauthenticated", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent: "users/1",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := ts.Service.ListInboxes(ctx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "user not authenticated")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUpdateInbox(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	t.Run("UpdateInbox success", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox entry
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: user.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		// Update inbox status
 | 
			
		||||
		req := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
			UpdateMask: &fieldmaskpb.FieldMask{
 | 
			
		||||
				Paths: []string{"status"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resp, err := ts.Service.UpdateInbox(userCtx, req)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotNil(t, resp)
 | 
			
		||||
		require.Equal(t, v1pb.Inbox_ARCHIVED, resp.Status)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UpdateInbox permission denied for different user", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create two users
 | 
			
		||||
		user1, err := ts.CreateRegularUser(ctx, "user1")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		user2, err := ts.CreateRegularUser(ctx, "user2")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox entry for user2
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: user2.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user1 context but try to update user2's inbox
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user1.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
			UpdateMask: &fieldmaskpb.FieldMask{
 | 
			
		||||
				Paths: []string{"status"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.UpdateInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "cannot update inbox")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UpdateInbox missing update mask", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   "inboxes/1",
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.UpdateInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "update mask is required")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UpdateInbox invalid name format", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   "invalid-inbox-name",
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
			UpdateMask: &fieldmaskpb.FieldMask{
 | 
			
		||||
				Paths: []string{"status"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.UpdateInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid inbox name")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UpdateInbox not found", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   "inboxes/99999", // Non-existent inbox
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
			UpdateMask: &fieldmaskpb.FieldMask{
 | 
			
		||||
				Paths: []string{"status"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.UpdateInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		st, ok := status.FromError(err)
 | 
			
		||||
		require.True(t, ok)
 | 
			
		||||
		require.Equal(t, codes.NotFound, st.Code())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UpdateInbox unsupported field", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox entry
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: user.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
			UpdateMask: &fieldmaskpb.FieldMask{
 | 
			
		||||
				Paths: []string{"unsupported_field"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.UpdateInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "unsupported field")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeleteInbox(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	t.Run("DeleteInbox success", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox entry
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: user.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		// Delete inbox
 | 
			
		||||
		req := &v1pb.DeleteInboxRequest{
 | 
			
		||||
			Name: fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.DeleteInbox(userCtx, req)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Verify inbox is deleted
 | 
			
		||||
		inboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{
 | 
			
		||||
			ReceiverID: &user.ID,
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, 0, len(inboxes))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("DeleteInbox permission denied for different user", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create two users
 | 
			
		||||
		user1, err := ts.CreateRegularUser(ctx, "user1")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		user2, err := ts.CreateRegularUser(ctx, "user2")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox entry for user2
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: user2.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user1 context but try to delete user2's inbox
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user1.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.DeleteInboxRequest{
 | 
			
		||||
			Name: fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.DeleteInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "cannot delete inbox")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("DeleteInbox invalid name format", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.DeleteInboxRequest{
 | 
			
		||||
			Name: "invalid-inbox-name",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.DeleteInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Contains(t, err.Error(), "invalid inbox name")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("DeleteInbox not found", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		req := &v1pb.DeleteInboxRequest{
 | 
			
		||||
			Name: "inboxes/99999", // Non-existent inbox
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = ts.Service.DeleteInbox(userCtx, req)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		st, ok := status.FromError(err)
 | 
			
		||||
		require.True(t, ok)
 | 
			
		||||
		require.Equal(t, codes.NotFound, st.Code())
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInboxCRUDComplete(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	t.Run("Complete CRUD lifecycle", func(t *testing.T) {
 | 
			
		||||
		ts := NewTestService(t)
 | 
			
		||||
		defer ts.Cleanup()
 | 
			
		||||
 | 
			
		||||
		// Create a user
 | 
			
		||||
		user, err := ts.CreateRegularUser(ctx, "testuser")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Create an inbox entry directly in store
 | 
			
		||||
		const systemBotID int32 = 0
 | 
			
		||||
		inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{
 | 
			
		||||
			SenderID:   systemBotID,
 | 
			
		||||
			ReceiverID: user.ID,
 | 
			
		||||
			Status:     store.UNREAD,
 | 
			
		||||
			Message: &storepb.InboxMessage{
 | 
			
		||||
				Type: storepb.InboxMessage_MEMO_COMMENT,
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Set user context
 | 
			
		||||
		userCtx := ts.CreateUserContext(ctx, user.ID)
 | 
			
		||||
 | 
			
		||||
		// 1. List inboxes - should have 1
 | 
			
		||||
		listReq := &v1pb.ListInboxesRequest{
 | 
			
		||||
			Parent: fmt.Sprintf("users/%d", user.ID),
 | 
			
		||||
		}
 | 
			
		||||
		listResp, err := ts.Service.ListInboxes(userCtx, listReq)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, 1, len(listResp.Inboxes))
 | 
			
		||||
		require.Equal(t, v1pb.Inbox_UNREAD, listResp.Inboxes[0].Status)
 | 
			
		||||
 | 
			
		||||
		// 2. Update inbox status to ARCHIVED
 | 
			
		||||
		updateReq := &v1pb.UpdateInboxRequest{
 | 
			
		||||
			Inbox: &v1pb.Inbox{
 | 
			
		||||
				Name:   fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
				Status: v1pb.Inbox_ARCHIVED,
 | 
			
		||||
			},
 | 
			
		||||
			UpdateMask: &fieldmaskpb.FieldMask{
 | 
			
		||||
				Paths: []string{"status"},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		updateResp, err := ts.Service.UpdateInbox(userCtx, updateReq)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, v1pb.Inbox_ARCHIVED, updateResp.Status)
 | 
			
		||||
 | 
			
		||||
		// 3. List inboxes again - should still have 1 but ARCHIVED
 | 
			
		||||
		listResp, err = ts.Service.ListInboxes(userCtx, listReq)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, 1, len(listResp.Inboxes))
 | 
			
		||||
		require.Equal(t, v1pb.Inbox_ARCHIVED, listResp.Inboxes[0].Status)
 | 
			
		||||
 | 
			
		||||
		// 4. Delete inbox
 | 
			
		||||
		deleteReq := &v1pb.DeleteInboxRequest{
 | 
			
		||||
			Name: fmt.Sprintf("inboxes/%d", inbox.ID),
 | 
			
		||||
		}
 | 
			
		||||
		_, err = ts.Service.DeleteInbox(userCtx, deleteReq)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// 5. List inboxes - should be empty
 | 
			
		||||
		listResp, err = ts.Service.ListInboxes(userCtx, listReq)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, 0, len(listResp.Inboxes))
 | 
			
		||||
		require.Equal(t, int32(0), listResp.TotalSize)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								
					Loading…
					
					
				
		Reference in New Issue