You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
memos/server/router/api/v1/test/inbox_service_test.go

560 lines
14 KiB
Go

package v1
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.Username)
// 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.Username)
// 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.Username)
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.Username)
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.Username)
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.Username)
// 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.Username)
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.Username)
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.Username)
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.Username)
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.Username)
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.Username)
// 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.Username)
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.Username)
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.Username)
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.Username)
// 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)
})
}