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
760c1643
Commit
760c1643
authored
Jun 17, 2025
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: add server tests
parent
f6e5da44
Changes
11
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1824 additions
and
109 deletions
+1824
-109
claude.prompt.md
.github/prompts/claude.prompt.md
+5
-0
CLAUDE.md
CLAUDE.md
+1
-20
idp_service.go
server/router/api/v1/idp_service.go
+11
-1
shortcut_service.go
server/router/api/v1/shortcut_service.go
+7
-1
idp_service_test.go
server/router/api/v1/test/idp_service_test.go
+520
-0
shortcut_service_test.go
server/router/api/v1/test/shortcut_service_test.go
+819
-0
test_helper.go
server/router/api/v1/test/test_helper.go
+6
-6
webhook_service_test.go
server/router/api/v1/test/webhook_service_test.go
+408
-0
workspace_service_test.go
server/router/api/v1/test/workspace_service_test.go
+0
-80
test_auth.go
server/router/api/v1/test_auth.go
+19
-0
webhook_service.go
server/router/api/v1/webhook_service.go
+28
-1
No files found.
.github/prompts/claude.prompt.md
0 → 100644
View file @
760c1643
---
mode
:
agent
---
Please follow
`./CLAUDE.md`
for the basic structure and development guidelines of the Memos project.
CLAUDE.md
View file @
760c1643
...
@@ -215,23 +215,4 @@ FROM alpine:latest AS production
...
@@ -215,23 +215,4 @@ FROM alpine:latest AS production
1.
**Lint Checking**
: All linters must pass
1.
**Lint Checking**
: All linters must pass
2.
**Test Coverage**
: New code should include tests
2.
**Test Coverage**
: New code should include tests
3.
**Documentation**
: Update relevant documentation
3.
**Documentation**
: Update relevant documentation
4.
**AIP Compliance**
: New APIs should follow AIP standards
4.
**AIP Compliance**
: New APIs should follow
[
AIP
](
https://google.aip.dev/
)
standards
## Future Considerations
### Planned Improvements
-
**Additional Service Tests**
: Expand test coverage to all services
-
**API Versioning**
: Support for multiple API versions
-
**Enhanced Metrics**
: Better observability and monitoring
-
**Plugin System**
: Extensible architecture for custom features
### Technical Debt
-
**Legacy API Cleanup**
: Remove deprecated endpoints
-
**Performance Optimization**
: Database query optimization
-
**Security Hardening**
: Enhanced security measures
---
_This documentation reflects the current state of the Memos project as of June 2025, including recent AIP compliance refactoring and comprehensive testing infrastructure._
server/router/api/v1/idp_service.go
View file @
760c1643
...
@@ -18,7 +18,7 @@ func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb
...
@@ -18,7 +18,7 @@ func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
}
if
currentUser
.
Role
!=
store
.
RoleHost
{
if
currentUser
==
nil
||
currentUser
.
Role
!=
store
.
RoleHost
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
}
...
@@ -97,6 +97,16 @@ func (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb
...
@@ -97,6 +97,16 @@ func (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid identity provider name: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"invalid identity provider name: %v"
,
err
)
}
}
// Check if the identity provider exists before trying to delete it
identityProvider
,
err
:=
s
.
Store
.
GetIdentityProvider
(
ctx
,
&
store
.
FindIdentityProvider
{
ID
:
&
id
})
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to check identity provider existence: %v"
,
err
)
}
if
identityProvider
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"identity provider not found"
)
}
if
err
:=
s
.
Store
.
DeleteIdentityProvider
(
ctx
,
&
store
.
DeleteIdentityProvider
{
ID
:
id
});
err
!=
nil
{
if
err
:=
s
.
Store
.
DeleteIdentityProvider
(
ctx
,
&
store
.
DeleteIdentityProvider
{
ID
:
id
});
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to delete identity provider, error: %+v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to delete identity provider, error: %+v"
,
err
)
}
}
...
...
server/router/api/v1/shortcut
s
_service.go
→
server/router/api/v1/shortcut_service.go
View file @
760c1643
...
@@ -290,17 +290,23 @@ func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteS
...
@@ -290,17 +290,23 @@ func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteS
return
nil
,
err
return
nil
,
err
}
}
if
userSetting
==
nil
{
if
userSetting
==
nil
{
return
&
emptypb
.
Empty
{},
nil
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"shortcut not found"
)
}
}
shortcutsUserSetting
:=
userSetting
.
GetShortcuts
()
shortcutsUserSetting
:=
userSetting
.
GetShortcuts
()
shortcuts
:=
shortcutsUserSetting
.
GetShortcuts
()
shortcuts
:=
shortcutsUserSetting
.
GetShortcuts
()
newShortcuts
:=
make
([]
*
storepb
.
ShortcutsUserSetting_Shortcut
,
0
,
len
(
shortcuts
))
newShortcuts
:=
make
([]
*
storepb
.
ShortcutsUserSetting_Shortcut
,
0
,
len
(
shortcuts
))
found
:=
false
for
_
,
shortcut
:=
range
shortcuts
{
for
_
,
shortcut
:=
range
shortcuts
{
if
shortcut
.
GetId
()
!=
shortcutID
{
if
shortcut
.
GetId
()
!=
shortcutID
{
newShortcuts
=
append
(
newShortcuts
,
shortcut
)
newShortcuts
=
append
(
newShortcuts
,
shortcut
)
}
else
{
found
=
true
}
}
}
}
if
!
found
{
return
nil
,
status
.
Errorf
(
codes
.
NotFound
,
"shortcut not found"
)
}
shortcutsUserSetting
.
Shortcuts
=
newShortcuts
shortcutsUserSetting
.
Shortcuts
=
newShortcuts
userSetting
.
Value
=
&
storepb
.
UserSetting_Shortcuts
{
userSetting
.
Value
=
&
storepb
.
UserSetting_Shortcuts
{
Shortcuts
:
shortcutsUserSetting
,
Shortcuts
:
shortcutsUserSetting
,
...
...
server/router/api/v1/test/idp_service_test.go
0 → 100644
View file @
760c1643
This diff is collapsed.
Click to expand it.
server/router/api/v1/test/shortcut_service_test.go
0 → 100644
View file @
760c1643
This diff is collapsed.
Click to expand it.
server/router/api/v1/test_helper.go
→
server/router/api/v1/test
/test
_helper.go
View file @
760c1643
...
@@ -5,13 +5,14 @@ import (
...
@@ -5,13 +5,14 @@ import (
"testing"
"testing"
"github.com/usememos/memos/internal/profile"
"github.com/usememos/memos/internal/profile"
apiv1
"github.com/usememos/memos/server/router/api/v1"
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
teststore
"github.com/usememos/memos/store/test"
teststore
"github.com/usememos/memos/store/test"
)
)
// TestService holds the test service setup for API v1 services.
// TestService holds the test service setup for API v1 services.
type
TestService
struct
{
type
TestService
struct
{
Service
*
APIV1Service
Service
*
apiv1
.
APIV1Service
Store
*
store
.
Store
Store
*
store
.
Store
Profile
*
profile
.
Profile
Profile
*
profile
.
Profile
Secret
string
Secret
string
...
@@ -35,7 +36,7 @@ func NewTestService(t *testing.T) *TestService {
...
@@ -35,7 +36,7 @@ func NewTestService(t *testing.T) *TestService {
// Create APIV1Service with nil grpcServer since we're testing direct calls
// Create APIV1Service with nil grpcServer since we're testing direct calls
secret
:=
"test-secret"
secret
:=
"test-secret"
service
:=
&
APIV1Service
{
service
:=
&
apiv1
.
APIV1Service
{
Secret
:
secret
,
Secret
:
secret
,
Profile
:
testProfile
,
Profile
:
testProfile
,
Store
:
testStore
,
Store
:
testStore
,
...
@@ -52,8 +53,7 @@ func NewTestService(t *testing.T) *TestService {
...
@@ -52,8 +53,7 @@ func NewTestService(t *testing.T) *TestService {
// Cleanup clears caches and closes resources after test.
// Cleanup clears caches and closes resources after test.
func
(
ts
*
TestService
)
Cleanup
()
{
func
(
ts
*
TestService
)
Cleanup
()
{
ts
.
Store
.
Close
()
ts
.
Store
.
Close
()
// Clear the global owner cache for test isolation
// Note: Owner cache is package-level in parent package, cannot clear from test package
ownerCache
=
nil
}
}
// CreateHostUser creates a host user for testing.
// CreateHostUser creates a host user for testing.
...
@@ -76,6 +76,6 @@ func (ts *TestService) CreateRegularUser(ctx context.Context, username string) (
...
@@ -76,6 +76,6 @@ func (ts *TestService) CreateRegularUser(ctx context.Context, username string) (
// CreateUserContext creates a context with the given username for authentication.
// CreateUserContext creates a context with the given username for authentication.
func
(
ts
*
TestService
)
CreateUserContext
(
ctx
context
.
Context
,
username
string
)
context
.
Context
{
func
(
ts
*
TestService
)
CreateUserContext
(
ctx
context
.
Context
,
username
string
)
context
.
Context
{
_
=
ts
// Silence unused receiver warning - method is part of TestService interfac
e
// Use the real context key from the parent packag
e
return
context
.
WithValue
(
ctx
,
ContextKey
(
0
),
username
)
// usernameContextKey = 0
return
apiv1
.
CreateTestUserContext
(
ctx
,
username
)
}
}
server/router/api/v1/test/webhook_service_test.go
0 → 100644
View file @
760c1643
This diff is collapsed.
Click to expand it.
server/router/api/v1/workspace_service_test.go
→
server/router/api/v1/
test/
workspace_service_test.go
View file @
760c1643
...
@@ -64,86 +64,6 @@ func TestGetWorkspaceProfile(t *testing.T) {
...
@@ -64,86 +64,6 @@ func TestGetWorkspaceProfile(t *testing.T) {
})
})
}
}
func
TestGetWorkspaceProfile_ErrorCases
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
t
.
Run
(
"Service handles multiple calls correctly"
,
func
(
t
*
testing
.
T
)
{
// Create test service for this specific test
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
// Make multiple calls to ensure consistency
for
i
:=
0
;
i
<
5
;
i
++
{
req
:=
&
v1pb
.
GetWorkspaceProfileRequest
{}
resp
,
err
:=
ts
.
Service
.
GetWorkspaceProfile
(
ctx
,
req
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
resp
)
require
.
Equal
(
t
,
"test-1.0.0"
,
resp
.
Version
)
require
.
Equal
(
t
,
"dev"
,
resp
.
Mode
)
require
.
Equal
(
t
,
"http://localhost:8080"
,
resp
.
InstanceUrl
)
require
.
Empty
(
t
,
resp
.
Owner
)
}
})
t
.
Run
(
"Multiple users, only host is returned as owner"
,
func
(
t
*
testing
.
T
)
{
// Create test service for this specific test
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
// Create a regular user first
_
,
err
:=
ts
.
CreateRegularUser
(
ctx
,
"user1"
)
require
.
NoError
(
t
,
err
)
// Create another regular user
_
,
err
=
ts
.
CreateRegularUser
(
ctx
,
"user2"
)
require
.
NoError
(
t
,
err
)
// Create a host user
hostUser
,
err
:=
ts
.
CreateHostUser
(
ctx
,
"admin"
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
hostUser
)
// Call GetWorkspaceProfile
req
:=
&
v1pb
.
GetWorkspaceProfileRequest
{}
resp
,
err
:=
ts
.
Service
.
GetWorkspaceProfile
(
ctx
,
req
)
// Verify response
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
resp
)
// Should return the host user as owner, not any of the regular users
expectedOwnerName
:=
fmt
.
Sprintf
(
"users/%d"
,
hostUser
.
ID
)
require
.
Equal
(
t
,
expectedOwnerName
,
resp
.
Owner
)
})
t
.
Run
(
"Cache behavior - owner cached after first lookup"
,
func
(
t
*
testing
.
T
)
{
// Create test service for this specific test
ts
:=
NewTestService
(
t
)
defer
ts
.
Cleanup
()
// Create a host user
hostUser
,
err
:=
ts
.
CreateHostUser
(
ctx
,
"admin"
)
require
.
NoError
(
t
,
err
)
expectedOwnerName
:=
fmt
.
Sprintf
(
"users/%d"
,
hostUser
.
ID
)
// First call should query the database
req
:=
&
v1pb
.
GetWorkspaceProfileRequest
{}
resp1
,
err
:=
ts
.
Service
.
GetWorkspaceProfile
(
ctx
,
req
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
expectedOwnerName
,
resp1
.
Owner
)
// Create another host user (this shouldn't change the result due to caching)
_
,
err
=
ts
.
CreateHostUser
(
ctx
,
"admin2"
)
require
.
NoError
(
t
,
err
)
// Second call should return cached result (first host user)
resp2
,
err
:=
ts
.
Service
.
GetWorkspaceProfile
(
ctx
,
req
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
expectedOwnerName
,
resp2
.
Owner
)
// Should still be the first host user
})
}
func
TestGetWorkspaceProfile_Concurrency
(
t
*
testing
.
T
)
{
func
TestGetWorkspaceProfile_Concurrency
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
...
...
server/router/api/v1/test_auth.go
0 → 100644
View file @
760c1643
package
v1
import
(
"context"
"github.com/usememos/memos/store"
)
// CreateTestUserContext creates a context with username for testing purposes
// This function is only intended for use in tests
func
CreateTestUserContext
(
ctx
context
.
Context
,
username
string
)
context
.
Context
{
return
context
.
WithValue
(
ctx
,
usernameContextKey
,
username
)
}
// CreateTestUserContextWithUser creates a context and ensures the user exists for testing
// This function is only intended for use in tests
func
CreateTestUserContextWithUser
(
ctx
context
.
Context
,
s
*
APIV1Service
,
user
*
store
.
User
)
context
.
Context
{
return
context
.
WithValue
(
ctx
,
usernameContextKey
,
user
.
Username
)
}
server/router/api/v1/webhook_service.go
View file @
760c1643
...
@@ -21,8 +21,23 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe
...
@@ -21,8 +21,23 @@ func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWe
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"user not authenticated"
)
}
// TODO: Handle webhook_id, validate_only, and request_id fields
// Only host users can create webhooks
if
!
isSuperUser
(
currentUser
)
{
return
nil
,
status
.
Errorf
(
codes
.
PermissionDenied
,
"permission denied"
)
}
// Validate required fields
if
request
.
Webhook
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"webhook is required"
)
}
if
strings
.
TrimSpace
(
request
.
Webhook
.
Url
)
==
""
{
return
nil
,
status
.
Errorf
(
codes
.
InvalidArgument
,
"webhook URL is required"
)
}
// TODO: Handle webhook_id, validate_only, and request_id fields
if
request
.
ValidateOnly
{
if
request
.
ValidateOnly
{
// Perform validation checks without actually creating the webhook
// Perform validation checks without actually creating the webhook
return
&
v1pb
.
Webhook
{
return
&
v1pb
.
Webhook
{
...
@@ -49,6 +64,9 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *v1pb.ListWebhooksReq
...
@@ -49,6 +64,9 @@ func (s *APIV1Service) ListWebhooks(ctx context.Context, _ *v1pb.ListWebhooksReq
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"user not authenticated"
)
}
// TODO: Implement proper filtering, ordering, and pagination
// TODO: Implement proper filtering, ordering, and pagination
// For now, list webhooks for the current user
// For now, list webhooks for the current user
...
@@ -79,6 +97,9 @@ func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookR
...
@@ -79,6 +97,9 @@ func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookR
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"user not authenticated"
)
}
webhook
,
err
:=
s
.
Store
.
GetWebhook
(
ctx
,
&
store
.
FindWebhook
{
webhook
,
err
:=
s
.
Store
.
GetWebhook
(
ctx
,
&
store
.
FindWebhook
{
ID
:
&
webhookID
,
ID
:
&
webhookID
,
...
@@ -112,6 +133,9 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe
...
@@ -112,6 +133,9 @@ func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWe
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"user not authenticated"
)
}
// Check if webhook exists and user has permission
// Check if webhook exists and user has permission
existingWebhook
,
err
:=
s
.
Store
.
GetWebhook
(
ctx
,
&
store
.
FindWebhook
{
existingWebhook
,
err
:=
s
.
Store
.
GetWebhook
(
ctx
,
&
store
.
FindWebhook
{
...
@@ -160,6 +184,9 @@ func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWe
...
@@ -160,6 +184,9 @@ func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWe
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get user: %v"
,
err
)
}
}
if
currentUser
==
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Unauthenticated
,
"user not authenticated"
)
}
// Check if webhook exists and user has permission
// Check if webhook exists and user has permission
webhook
,
err
:=
s
.
Store
.
GetWebhook
(
ctx
,
&
store
.
FindWebhook
{
webhook
,
err
:=
s
.
Store
.
GetWebhook
(
ctx
,
&
store
.
FindWebhook
{
...
...
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