Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
25feef3a
Unverified
Commit
25feef3a
authored
Apr 06, 2026
by
boojack
Committed by
GitHub
Apr 06, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(api): tolerate missing related users in memo conversions (#5809)
parent
87d411bc
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
392 additions
and
29 deletions
+392
-29
memo_service.go
server/router/api/v1/memo_service.go
+21
-0
memo_service_converter.go
server/router/api/v1/memo_service_converter.go
+97
-8
memo_share_service.go
server/router/api/v1/memo_share_service.go
+4
-0
reaction_service.go
server/router/api/v1/reaction_service.go
+13
-21
memo_service_test.go
server/router/api/v1/test/memo_service_test.go
+120
-0
memo_share_service_test.go
server/router/api/v1/test/memo_share_service_test.go
+48
-0
reaction_service_test.go
server/router/api/v1/test/reaction_service_test.go
+40
-0
user_notification_test.go
server/router/api/v1/test/user_notification_test.go
+40
-0
user_service.go
server/router/api/v1/user_service.go
+9
-0
No files found.
server/router/api/v1/memo_service.go
View file @
25feef3a
...
@@ -2,6 +2,7 @@ package v1
...
@@ -2,6 +2,7 @@ package v1
import
(
import
(
"context"
"context"
stderrors
"errors"
"fmt"
"fmt"
"log/slog"
"log/slog"
"strings"
"strings"
...
@@ -312,6 +313,14 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
...
@@ -312,6 +313,14 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq
memoMessage
,
err
:=
s
.
convertMemoFromStoreWithCreators
(
ctx
,
memo
,
reactions
,
attachments
,
relations
,
creatorMap
)
memoMessage
,
err
:=
s
.
convertMemoFromStoreWithCreators
(
ctx
,
memo
,
reactions
,
attachments
,
relations
,
creatorMap
)
if
err
!=
nil
{
if
err
!=
nil
{
if
stderrors
.
Is
(
err
,
errMemoCreatorNotFound
)
{
slog
.
Warn
(
"Skipping memo with missing creator"
,
slog
.
Int64
(
"memo_id"
,
int64
(
memo
.
ID
)),
slog
.
String
(
"memo_uid"
,
memo
.
UID
),
slog
.
Int64
(
"creator_id"
,
int64
(
memo
.
CreatorID
)),
)
continue
}
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
...
@@ -384,6 +393,9 @@ func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest
...
@@ -384,6 +393,9 @@ func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest
}
}
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
)
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
)
if
err
!=
nil
{
if
err
!=
nil
{
if
stderrors
.
Is
(
err
,
errMemoCreatorNotFound
)
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"memo creator not found"
)
}
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
return
memoMessage
,
nil
return
memoMessage
,
nil
...
@@ -767,6 +779,15 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
...
@@ -767,6 +779,15 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
memoMessage
,
err
:=
s
.
convertMemoFromStoreWithCreators
(
ctx
,
m
,
reactions
,
attachments
,
relations
,
creatorMap
)
memoMessage
,
err
:=
s
.
convertMemoFromStoreWithCreators
(
ctx
,
m
,
reactions
,
attachments
,
relations
,
creatorMap
)
if
err
!=
nil
{
if
err
!=
nil
{
if
stderrors
.
Is
(
err
,
errMemoCreatorNotFound
)
{
slog
.
Warn
(
"Skipping memo comment with missing creator"
,
slog
.
Int64
(
"memo_id"
,
int64
(
m
.
ID
)),
slog
.
String
(
"memo_uid"
,
m
.
UID
),
slog
.
Int64
(
"creator_id"
,
int64
(
m
.
CreatorID
)),
slog
.
String
(
"parent_name"
,
request
.
Name
),
)
continue
}
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
memosResponse
=
append
(
memosResponse
,
memoMessage
)
memosResponse
=
append
(
memosResponse
,
memoMessage
)
...
...
server/router/api/v1/memo_service_converter.go
View file @
25feef3a
...
@@ -2,7 +2,9 @@ package v1
...
@@ -2,7 +2,9 @@ package v1
import
(
import
(
"context"
"context"
stderrors
"errors"
"fmt"
"fmt"
"log/slog"
"time"
"time"
"github.com/pkg/errors"
"github.com/pkg/errors"
...
@@ -13,6 +15,11 @@ import (
...
@@ -13,6 +15,11 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
var
(
errMemoCreatorNotFound
=
stderrors
.
New
(
"memo creator not found"
)
errReactionCreatorNotFound
=
stderrors
.
New
(
"reaction creator not found"
)
)
func
(
s
*
APIV1Service
)
convertMemoFromStore
(
ctx
context
.
Context
,
memo
*
store
.
Memo
,
reactions
[]
*
store
.
Reaction
,
attachments
[]
*
store
.
Attachment
,
relations
[]
*
v1pb
.
MemoRelation
)
(
*
v1pb
.
Memo
,
error
)
{
func
(
s
*
APIV1Service
)
convertMemoFromStore
(
ctx
context
.
Context
,
memo
*
store
.
Memo
,
reactions
[]
*
store
.
Reaction
,
attachments
[]
*
store
.
Attachment
,
relations
[]
*
v1pb
.
MemoRelation
)
(
*
v1pb
.
Memo
,
error
)
{
creatorMap
,
err
:=
s
.
listUsersByID
(
ctx
,
[]
int32
{
memo
.
CreatorID
})
creatorMap
,
err
:=
s
.
listUsersByID
(
ctx
,
[]
int32
{
memo
.
CreatorID
})
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -34,7 +41,7 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem
...
@@ -34,7 +41,7 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem
name
:=
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
memo
.
UID
)
name
:=
fmt
.
Sprintf
(
"%s%s"
,
MemoNamePrefix
,
memo
.
UID
)
creator
:=
creatorMap
[
memo
.
CreatorID
]
creator
:=
creatorMap
[
memo
.
CreatorID
]
if
creator
==
nil
{
if
creator
==
nil
{
return
nil
,
err
ors
.
New
(
"memo creator not found"
)
return
nil
,
err
MemoCreatorNotFound
}
}
memoMessage
:=
&
v1pb
.
Memo
{
memoMessage
:=
&
v1pb
.
Memo
{
Name
:
name
,
Name
:
name
,
...
@@ -58,13 +65,9 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem
...
@@ -58,13 +65,9 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem
memoMessage
.
Parent
=
&
parentName
memoMessage
.
Parent
=
&
parentName
}
}
memoMessage
.
Reactions
=
[]
*
v1pb
.
Reaction
{}
memoMessage
.
Reactions
,
err
=
s
.
convertReactionsFromStoreWithCreators
(
ctx
,
reactions
,
creatorMap
)
for
_
,
reaction
:=
range
reactions
{
reactionResponse
,
err
:=
s
.
convertReactionFromStore
(
ctx
,
reaction
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert reaction"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert reactions"
)
}
memoMessage
.
Reactions
=
append
(
memoMessage
.
Reactions
,
reactionResponse
)
}
}
if
relations
!=
nil
{
if
relations
!=
nil
{
...
@@ -88,6 +91,92 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem
...
@@ -88,6 +91,92 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem
return
memoMessage
,
nil
return
memoMessage
,
nil
}
}
func
(
s
*
APIV1Service
)
listUsersByIDWithExisting
(
ctx
context
.
Context
,
userIDs
[]
int32
,
existing
map
[
int32
]
*
store
.
User
)
(
map
[
int32
]
*
store
.
User
,
error
)
{
usersByID
:=
make
(
map
[
int32
]
*
store
.
User
,
len
(
existing
)
+
len
(
userIDs
))
for
userID
,
user
:=
range
existing
{
if
user
!=
nil
{
usersByID
[
userID
]
=
user
}
}
missingUserIDs
:=
make
([]
int32
,
0
,
len
(
userIDs
))
seenMissingUserIDs
:=
make
(
map
[
int32
]
struct
{},
len
(
userIDs
))
for
_
,
userID
:=
range
userIDs
{
if
_
,
ok
:=
usersByID
[
userID
];
ok
{
continue
}
if
_
,
ok
:=
seenMissingUserIDs
[
userID
];
ok
{
continue
}
seenMissingUserIDs
[
userID
]
=
struct
{}{}
missingUserIDs
=
append
(
missingUserIDs
,
userID
)
}
if
len
(
missingUserIDs
)
==
0
{
return
usersByID
,
nil
}
missingUsersByID
,
err
:=
s
.
listUsersByID
(
ctx
,
missingUserIDs
)
if
err
!=
nil
{
return
nil
,
err
}
for
userID
,
user
:=
range
missingUsersByID
{
if
user
!=
nil
{
usersByID
[
userID
]
=
user
}
}
return
usersByID
,
nil
}
func
(
s
*
APIV1Service
)
convertReactionsFromStoreWithCreators
(
ctx
context
.
Context
,
reactions
[]
*
store
.
Reaction
,
creatorMap
map
[
int32
]
*
store
.
User
)
([]
*
v1pb
.
Reaction
,
error
)
{
if
len
(
reactions
)
==
0
{
return
[]
*
v1pb
.
Reaction
{},
nil
}
creatorIDs
:=
make
([]
int32
,
0
,
len
(
reactions
))
for
_
,
reaction
:=
range
reactions
{
creatorIDs
=
append
(
creatorIDs
,
reaction
.
CreatorID
)
}
creatorsByID
,
err
:=
s
.
listUsersByIDWithExisting
(
ctx
,
creatorIDs
,
creatorMap
)
if
err
!=
nil
{
return
nil
,
err
}
reactionMessages
:=
make
([]
*
v1pb
.
Reaction
,
0
,
len
(
reactions
))
for
_
,
reaction
:=
range
reactions
{
reactionMessage
,
err
:=
convertReactionFromStoreWithCreators
(
reaction
,
creatorsByID
)
if
err
!=
nil
{
if
stderrors
.
Is
(
err
,
errReactionCreatorNotFound
)
{
slog
.
Warn
(
"Skipping reaction with missing creator"
,
slog
.
Int64
(
"reaction_id"
,
int64
(
reaction
.
ID
)),
slog
.
Int64
(
"creator_id"
,
int64
(
reaction
.
CreatorID
)),
slog
.
String
(
"content_id"
,
reaction
.
ContentID
),
)
continue
}
return
nil
,
err
}
reactionMessages
=
append
(
reactionMessages
,
reactionMessage
)
}
return
reactionMessages
,
nil
}
func
convertReactionFromStoreWithCreators
(
reaction
*
store
.
Reaction
,
creatorsByID
map
[
int32
]
*
store
.
User
)
(
*
v1pb
.
Reaction
,
error
)
{
creator
:=
creatorsByID
[
reaction
.
CreatorID
]
if
creator
==
nil
{
return
nil
,
errReactionCreatorNotFound
}
reactionUID
:=
fmt
.
Sprintf
(
"%d"
,
reaction
.
ID
)
return
&
v1pb
.
Reaction
{
Name
:
fmt
.
Sprintf
(
"%s/%s%s"
,
reaction
.
ContentID
,
ReactionNamePrefix
,
reactionUID
),
Creator
:
BuildUserName
(
creator
.
Username
),
ContentId
:
reaction
.
ContentID
,
ReactionType
:
reaction
.
ReactionType
,
CreateTime
:
timestamppb
.
New
(
time
.
Unix
(
reaction
.
CreatedTs
,
0
)),
},
nil
}
// batchConvertMemoRelations batch-loads relations for a list of memos and returns
// batchConvertMemoRelations batch-loads relations for a list of memos and returns
// a map from memo ID to its converted relations. This avoids N+1 queries when listing memos.
// a map from memo ID to its converted relations. This avoids N+1 queries when listing memos.
func
(
s
*
APIV1Service
)
batchConvertMemoRelations
(
ctx
context
.
Context
,
memos
[]
*
store
.
Memo
)
(
map
[
int32
][]
*
v1pb
.
MemoRelation
,
error
)
{
func
(
s
*
APIV1Service
)
batchConvertMemoRelations
(
ctx
context
.
Context
,
memos
[]
*
store
.
Memo
)
(
map
[
int32
][]
*
v1pb
.
MemoRelation
,
error
)
{
...
...
server/router/api/v1/memo_share_service.go
View file @
25feef3a
...
@@ -2,6 +2,7 @@ package v1
...
@@ -2,6 +2,7 @@ package v1
import
(
import
(
"context"
"context"
stderrors
"errors"
"fmt"
"fmt"
"time"
"time"
...
@@ -182,6 +183,9 @@ func (s *APIV1Service) GetMemoByShare(ctx context.Context, request *v1pb.GetMemo
...
@@ -182,6 +183,9 @@ func (s *APIV1Service) GetMemoByShare(ctx context.Context, request *v1pb.GetMemo
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
[
memo
.
ID
])
memoMessage
,
err
:=
s
.
convertMemoFromStore
(
ctx
,
memo
,
reactions
,
attachments
,
relations
[
memo
.
ID
])
if
err
!=
nil
{
if
err
!=
nil
{
if
stderrors
.
Is
(
err
,
errMemoCreatorNotFound
)
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"not found"
)
}
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to convert memo"
)
}
}
return
memoMessage
,
nil
return
memoMessage
,
nil
...
...
server/router/api/v1/reaction_service.go
View file @
25feef3a
...
@@ -2,13 +2,11 @@ package v1
...
@@ -2,13 +2,11 @@ package v1
import
(
import
(
"context"
"context"
"fmt"
"log/slog"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
v1pb
"github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
...
@@ -52,12 +50,9 @@ func (s *APIV1Service) ListMemoReactions(ctx context.Context, request *v1pb.List
...
@@ -52,12 +50,9 @@ func (s *APIV1Service) ListMemoReactions(ctx context.Context, request *v1pb.List
response
:=
&
v1pb
.
ListMemoReactionsResponse
{
response
:=
&
v1pb
.
ListMemoReactionsResponse
{
Reactions
:
[]
*
v1pb
.
Reaction
{},
Reactions
:
[]
*
v1pb
.
Reaction
{},
}
}
for
_
,
reaction
:=
range
reactions
{
response
.
Reactions
,
err
=
s
.
convertReactionsFromStoreWithCreators
(
ctx
,
reactions
,
nil
)
reactionMessage
,
err
:=
s
.
convertReactionFromStore
(
ctx
,
reaction
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to convert reaction"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to convert reactions"
)
}
response
.
Reactions
=
append
(
response
.
Reactions
,
reactionMessage
)
}
}
return
response
,
nil
return
response
,
nil
}
}
...
@@ -169,21 +164,18 @@ func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.Del
...
@@ -169,21 +164,18 @@ func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.Del
}
}
func
(
s
*
APIV1Service
)
convertReactionFromStore
(
ctx
context
.
Context
,
reaction
*
store
.
Reaction
)
(
*
v1pb
.
Reaction
,
error
)
{
func
(
s
*
APIV1Service
)
convertReactionFromStore
(
ctx
context
.
Context
,
reaction
*
store
.
Reaction
)
(
*
v1pb
.
Reaction
,
error
)
{
reactionUID
:=
fmt
.
Sprintf
(
"%d"
,
reaction
.
ID
)
creatorsByID
,
err
:=
s
.
listUsersByIDWithExisting
(
ctx
,
[]
int32
{
reaction
.
CreatorID
},
nil
)
creator
,
err
:=
s
.
Store
.
GetUser
(
ctx
,
&
store
.
FindUser
{
ID
:
&
reaction
.
CreatorID
})
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get reaction creator"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get reaction creator"
)
}
}
if
creator
==
nil
{
reactionMessage
,
err
:=
convertReactionFromStoreWithCreators
(
reaction
,
creatorsByID
)
if
err
!=
nil
{
slog
.
Warn
(
"Failed to convert reaction with missing creator"
,
slog
.
Int64
(
"reaction_id"
,
int64
(
reaction
.
ID
)),
slog
.
Int64
(
"creator_id"
,
int64
(
reaction
.
CreatorID
)),
slog
.
String
(
"content_id"
,
reaction
.
ContentID
),
)
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"reaction creator not found"
)
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"reaction creator not found"
)
}
}
// Generate nested resource name: memos/{memo}/reactions/{reaction}
return
reactionMessage
,
nil
// reaction.ContentID already contains "memos/{memo}"
return
&
v1pb
.
Reaction
{
Name
:
fmt
.
Sprintf
(
"%s/%s%s"
,
reaction
.
ContentID
,
ReactionNamePrefix
,
reactionUID
),
Creator
:
BuildUserName
(
creator
.
Username
),
ContentId
:
reaction
.
ContentID
,
ReactionType
:
reaction
.
ReactionType
,
CreateTime
:
timestamppb
.
New
(
time
.
Unix
(
reaction
.
CreatedTs
,
0
)),
},
nil
}
}
server/router/api/v1/test/memo_service_test.go
View file @
25feef3a
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/timestamppb"
apiv1
"github.com/usememos/memos/proto/gen/api/v1"
apiv1
"github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
)
)
func
TestListMemos
(
t
*
testing
.
T
)
{
func
TestListMemos
(
t
*
testing
.
T
)
{
...
@@ -253,6 +254,125 @@ func TestListMemos(t *testing.T) {
...
@@ -253,6 +254,125 @@ func TestListMemos(t *testing.T) {
require
.
Equal
(
t
,
"👍"
,
userTwoReaction
.
ReactionType
)
require
.
Equal
(
t
,
"👍"
,
userTwoReaction
.
ReactionType
)
}
}
func
TestListMemosSkipsReactionsWithMissingCreators
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
owner
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"memo-owner"
)
require
.
NoError
(
t
,
err
)
ownerCtx
:=
ts
.
CreateUserContext
(
ctx
,
owner
.
ID
)
reactor
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"memo-reactor"
)
require
.
NoError
(
t
,
err
)
reactorCtx
:=
ts
.
CreateUserContext
(
ctx
,
reactor
.
ID
)
memo
,
err
:=
ts
.
Service
.
CreateMemo
(
ownerCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"memo with orphan reaction"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
UpsertMemoReaction
(
reactorCtx
,
&
apiv1
.
UpsertMemoReactionRequest
{
Name
:
memo
.
Name
,
Reaction
:
&
apiv1
.
Reaction
{
ContentId
:
memo
.
Name
,
ReactionType
:
"👍"
,
},
})
require
.
NoError
(
t
,
err
)
err
=
ts
.
Store
.
DeleteUser
(
ctx
,
&
store
.
DeleteUser
{
ID
:
reactor
.
ID
})
require
.
NoError
(
t
,
err
)
resp
,
err
:=
ts
.
Service
.
ListMemos
(
ownerCtx
,
&
apiv1
.
ListMemosRequest
{
PageSize
:
10
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
resp
.
Memos
,
1
)
require
.
Equal
(
t
,
memo
.
Name
,
resp
.
Memos
[
0
]
.
Name
)
require
.
Empty
(
t
,
resp
.
Memos
[
0
]
.
Reactions
)
}
func
TestListMemosSkipsMemosWithMissingCreators
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
owner
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"memo-visible-owner"
)
require
.
NoError
(
t
,
err
)
ownerCtx
:=
ts
.
CreateUserContext
(
ctx
,
owner
.
ID
)
orphanCreator
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"memo-orphan-creator"
)
require
.
NoError
(
t
,
err
)
orphanCtx
:=
ts
.
CreateUserContext
(
ctx
,
orphanCreator
.
ID
)
ownerMemo
,
err
:=
ts
.
Service
.
CreateMemo
(
ownerCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"owner memo"
,
Visibility
:
apiv1
.
Visibility_PRIVATE
,
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
CreateMemo
(
orphanCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"orphan memo"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
err
=
ts
.
Store
.
DeleteUser
(
ctx
,
&
store
.
DeleteUser
{
ID
:
orphanCreator
.
ID
})
require
.
NoError
(
t
,
err
)
resp
,
err
:=
ts
.
Service
.
ListMemos
(
ownerCtx
,
&
apiv1
.
ListMemosRequest
{
PageSize
:
10
})
require
.
NoError
(
t
,
err
)
require
.
Len
(
t
,
resp
.
Memos
,
1
)
require
.
Equal
(
t
,
ownerMemo
.
Name
,
resp
.
Memos
[
0
]
.
Name
)
}
func
TestListMemoCommentsSkipsCommentsWithMissingCreators
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
owner
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"comment-owner"
)
require
.
NoError
(
t
,
err
)
ownerCtx
:=
ts
.
CreateUserContext
(
ctx
,
owner
.
ID
)
commenter
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"comment-orphan"
)
require
.
NoError
(
t
,
err
)
commenterCtx
:=
ts
.
CreateUserContext
(
ctx
,
commenter
.
ID
)
memo
,
err
:=
ts
.
Service
.
CreateMemo
(
ownerCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"memo with comment"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
CreateMemoComment
(
commenterCtx
,
&
apiv1
.
CreateMemoCommentRequest
{
Name
:
memo
.
Name
,
Comment
:
&
apiv1
.
Memo
{
Content
:
"comment to orphan"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
err
=
ts
.
Store
.
DeleteUser
(
ctx
,
&
store
.
DeleteUser
{
ID
:
commenter
.
ID
})
require
.
NoError
(
t
,
err
)
resp
,
err
:=
ts
.
Service
.
ListMemoComments
(
ownerCtx
,
&
apiv1
.
ListMemoCommentsRequest
{
Name
:
memo
.
Name
})
require
.
NoError
(
t
,
err
)
require
.
Empty
(
t
,
resp
.
Memos
)
}
// TestCreateMemoWithCustomTimestamps tests that custom timestamps can be set when creating memos and comments.
// TestCreateMemoWithCustomTimestamps tests that custom timestamps can be set when creating memos and comments.
// This addresses issue #5483: https://github.com/usememos/memos/issues/5483
// This addresses issue #5483: https://github.com/usememos/memos/issues/5483
func
TestCreateMemoWithCustomTimestamps
(
t
*
testing
.
T
)
{
func
TestCreateMemoWithCustomTimestamps
(
t
*
testing
.
T
)
{
...
...
server/router/api/v1/test/memo_share_service_test.go
View file @
25feef3a
...
@@ -110,6 +110,54 @@ func TestGetMemoByShare_IncludesReactions(t *testing.T) {
...
@@ -110,6 +110,54 @@ func TestGetMemoByShare_IncludesReactions(t *testing.T) {
require
.
Equal
(
t
,
memo
.
Name
,
sharedMemo
.
Reactions
[
0
]
.
ContentId
)
require
.
Equal
(
t
,
memo
.
Name
,
sharedMemo
.
Reactions
[
0
]
.
ContentId
)
}
}
func
TestGetMemoByShare_SkipsReactionsWithMissingCreators
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
owner
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"share-owner"
)
require
.
NoError
(
t
,
err
)
ownerCtx
:=
ts
.
CreateUserContext
(
ctx
,
owner
.
ID
)
reactor
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"share-reaction-orphan"
)
require
.
NoError
(
t
,
err
)
reactorCtx
:=
ts
.
CreateUserContext
(
ctx
,
reactor
.
ID
)
memo
,
err
:=
ts
.
Service
.
CreateMemo
(
ownerCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"memo with orphan share reaction"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
UpsertMemoReaction
(
reactorCtx
,
&
apiv1
.
UpsertMemoReactionRequest
{
Name
:
memo
.
Name
,
Reaction
:
&
apiv1
.
Reaction
{
ContentId
:
memo
.
Name
,
ReactionType
:
"👍"
,
},
})
require
.
NoError
(
t
,
err
)
share
,
err
:=
ts
.
Service
.
CreateMemoShare
(
ownerCtx
,
&
apiv1
.
CreateMemoShareRequest
{
Parent
:
memo
.
Name
,
MemoShare
:
&
apiv1
.
MemoShare
{},
})
require
.
NoError
(
t
,
err
)
err
=
ts
.
Store
.
DeleteUser
(
ctx
,
&
store
.
DeleteUser
{
ID
:
reactor
.
ID
})
require
.
NoError
(
t
,
err
)
shareToken
:=
share
.
Name
[
strings
.
LastIndex
(
share
.
Name
,
"/"
)
+
1
:
]
sharedMemo
,
err
:=
ts
.
Service
.
GetMemoByShare
(
ctx
,
&
apiv1
.
GetMemoByShareRequest
{
ShareId
:
shareToken
,
})
require
.
NoError
(
t
,
err
)
require
.
Empty
(
t
,
sharedMemo
.
Reactions
)
}
func
TestGetMemoByShare_ReturnsNotFoundForUnknownShare
(
t
*
testing
.
T
)
{
func
TestGetMemoByShare_ReturnsNotFoundForUnknownShare
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
...
...
server/router/api/v1/test/reaction_service_test.go
View file @
25feef3a
...
@@ -7,6 +7,7 @@ import (
...
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
apiv1
"github.com/usememos/memos/proto/gen/api/v1"
apiv1
"github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
)
)
func
TestDeleteMemoReaction
(
t
*
testing
.
T
)
{
func
TestDeleteMemoReaction
(
t
*
testing
.
T
)
{
...
@@ -193,3 +194,42 @@ func TestDeleteMemoReaction(t *testing.T) {
...
@@ -193,3 +194,42 @@ func TestDeleteMemoReaction(t *testing.T) {
require
.
NotContains
(
t
,
err
.
Error
(),
"not found"
)
require
.
NotContains
(
t
,
err
.
Error
(),
"not found"
)
})
})
}
}
func
TestListMemoReactionsSkipsMissingCreators
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
owner
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"reaction-owner"
)
require
.
NoError
(
t
,
err
)
ownerCtx
:=
ts
.
CreateUserContext
(
ctx
,
owner
.
ID
)
reactor
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"reaction-orphan"
)
require
.
NoError
(
t
,
err
)
reactorCtx
:=
ts
.
CreateUserContext
(
ctx
,
reactor
.
ID
)
memo
,
err
:=
ts
.
Service
.
CreateMemo
(
ownerCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"reaction list memo"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
UpsertMemoReaction
(
reactorCtx
,
&
apiv1
.
UpsertMemoReactionRequest
{
Name
:
memo
.
Name
,
Reaction
:
&
apiv1
.
Reaction
{
ContentId
:
memo
.
Name
,
ReactionType
:
"🔥"
,
},
})
require
.
NoError
(
t
,
err
)
err
=
ts
.
Store
.
DeleteUser
(
ctx
,
&
store
.
DeleteUser
{
ID
:
reactor
.
ID
})
require
.
NoError
(
t
,
err
)
resp
,
err
:=
ts
.
Service
.
ListMemoReactions
(
ctx
,
&
apiv1
.
ListMemoReactionsRequest
{
Name
:
memo
.
Name
})
require
.
NoError
(
t
,
err
)
require
.
Empty
(
t
,
resp
.
Reactions
)
}
server/router/api/v1/test/user_notification_test.go
View file @
25feef3a
...
@@ -144,6 +144,46 @@ func TestListUserNotificationsOmitsPayloadWhenMemosDeleted(t *testing.T) {
...
@@ -144,6 +144,46 @@ func TestListUserNotificationsOmitsPayloadWhenMemosDeleted(t *testing.T) {
require
.
Nil
(
t
,
resp
.
Notifications
[
0
]
.
GetMemoComment
())
require
.
Nil
(
t
,
resp
.
Notifications
[
0
]
.
GetMemoComment
())
}
}
func
TestListUserNotificationsSkipsNotificationsWithMissingUsers
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
owner
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"notification-owner"
)
require
.
NoError
(
t
,
err
)
ownerCtx
:=
ts
.
CreateUserContext
(
ctx
,
owner
.
ID
)
commenter
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"notification-orphan"
)
require
.
NoError
(
t
,
err
)
commenterCtx
:=
ts
.
CreateUserContext
(
ctx
,
commenter
.
ID
)
memo
,
err
:=
ts
.
Service
.
CreateMemo
(
ownerCtx
,
&
apiv1
.
CreateMemoRequest
{
Memo
:
&
apiv1
.
Memo
{
Content
:
"Base memo"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
_
,
err
=
ts
.
Service
.
CreateMemoComment
(
commenterCtx
,
&
apiv1
.
CreateMemoCommentRequest
{
Name
:
memo
.
Name
,
Comment
:
&
apiv1
.
Memo
{
Content
:
"Comment content"
,
Visibility
:
apiv1
.
Visibility_PUBLIC
,
},
})
require
.
NoError
(
t
,
err
)
err
=
ts
.
Store
.
DeleteUser
(
ctx
,
&
store
.
DeleteUser
{
ID
:
commenter
.
ID
})
require
.
NoError
(
t
,
err
)
resp
,
err
:=
ts
.
Service
.
ListUserNotifications
(
ownerCtx
,
&
apiv1
.
ListUserNotificationsRequest
{
Parent
:
fmt
.
Sprintf
(
"users/%s"
,
owner
.
Username
),
})
require
.
NoError
(
t
,
err
)
require
.
Empty
(
t
,
resp
.
Notifications
)
}
func
TestListUserNotificationsRejectsNumericParent
(
t
*
testing
.
T
)
{
func
TestListUserNotificationsRejectsNumericParent
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
ts
:=
NewTestService
(
t
)
ts
:=
NewTestService
(
t
)
...
...
server/router/api/v1/user_service.go
View file @
25feef3a
...
@@ -5,6 +5,7 @@ import (
...
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/rand"
"encoding/hex"
"encoding/hex"
"fmt"
"fmt"
"log/slog"
"regexp"
"regexp"
"strconv"
"strconv"
"strings"
"strings"
...
@@ -1293,6 +1294,14 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
...
@@ -1293,6 +1294,14 @@ func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.
for
_
,
inbox
:=
range
inboxes
{
for
_
,
inbox
:=
range
inboxes
{
notification
,
err
:=
s
.
convertInboxToUserNotificationWithUsers
(
ctx
,
inbox
,
usersByID
)
notification
,
err
:=
s
.
convertInboxToUserNotificationWithUsers
(
ctx
,
inbox
,
usersByID
)
if
err
!=
nil
{
if
err
!=
nil
{
if
status
.
Code
(
err
)
==
codes
.
NotFound
{
slog
.
Warn
(
"Skipping notification with missing user"
,
slog
.
Int64
(
"notification_id"
,
int64
(
inbox
.
ID
)),
slog
.
Int64
(
"receiver_id"
,
int64
(
inbox
.
ReceiverID
)),
slog
.
Int64
(
"sender_id"
,
int64
(
inbox
.
SenderID
)),
)
continue
}
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to convert inbox: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to convert inbox: %v"
,
err
)
}
}
notifications
=
append
(
notifications
,
notification
)
notifications
=
append
(
notifications
,
notification
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment