mirror of https://github.com/usememos/memos.git
466 lines
15 KiB
Go
466 lines
15 KiB
Go
package test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
colorpb "google.golang.org/genproto/googleapis/type/color"
|
|
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
|
|
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
|
)
|
|
|
|
func TestGetInstanceProfile(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("GetInstanceProfile returns instance profile", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Call GetInstanceProfile directly
|
|
req := &v1pb.GetInstanceProfileRequest{}
|
|
resp, err := ts.Service.GetInstanceProfile(ctx, req)
|
|
|
|
// Verify response
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
|
|
// Verify the response contains expected data
|
|
require.Equal(t, "test-1.0.0", resp.Version)
|
|
require.True(t, resp.Demo)
|
|
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
|
|
|
|
// Instance should not be initialized since no admin users are created
|
|
require.Nil(t, resp.Admin)
|
|
})
|
|
|
|
t.Run("GetInstanceProfile with initialized instance", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Create a host user in the store
|
|
hostUser, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, hostUser)
|
|
|
|
// Call GetInstanceProfile directly
|
|
req := &v1pb.GetInstanceProfileRequest{}
|
|
resp, err := ts.Service.GetInstanceProfile(ctx, req)
|
|
|
|
// Verify response
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
|
|
// Verify the response contains expected data with initialized flag
|
|
require.Equal(t, "test-1.0.0", resp.Version)
|
|
require.True(t, resp.Demo)
|
|
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
|
|
|
|
// Instance should be initialized since an admin user exists
|
|
require.NotNil(t, resp.Admin)
|
|
require.Equal(t, hostUser.Username, resp.Admin.Username)
|
|
})
|
|
}
|
|
|
|
func TestGetInstanceProfile_Concurrency(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("Concurrent access to service", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Create a host user
|
|
_, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
|
|
// Make concurrent requests
|
|
numGoroutines := 10
|
|
results := make(chan *v1pb.InstanceProfile, numGoroutines)
|
|
errors := make(chan error, numGoroutines)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
req := &v1pb.GetInstanceProfileRequest{}
|
|
resp, err := ts.Service.GetInstanceProfile(ctx, req)
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
results <- resp
|
|
}()
|
|
}
|
|
|
|
// Collect all results
|
|
for i := 0; i < numGoroutines; i++ {
|
|
select {
|
|
case err := <-errors:
|
|
t.Fatalf("Goroutine returned error: %v", err)
|
|
case resp := <-results:
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, "test-1.0.0", resp.Version)
|
|
require.True(t, resp.Demo)
|
|
require.Equal(t, "http://localhost:8080", resp.InstanceUrl)
|
|
require.NotNil(t, resp.Admin)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetInstanceSetting(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("GetInstanceSetting - general setting", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Call GetInstanceSetting for general setting
|
|
req := &v1pb.GetInstanceSettingRequest{
|
|
Name: "instance/settings/GENERAL",
|
|
}
|
|
resp, err := ts.Service.GetInstanceSetting(ctx, req)
|
|
|
|
// Verify response
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, "instance/settings/GENERAL", resp.Name)
|
|
|
|
// The general setting should have a general_setting field
|
|
generalSetting := resp.GetGeneralSetting()
|
|
require.NotNil(t, generalSetting)
|
|
|
|
// General setting should have default values
|
|
require.False(t, generalSetting.DisallowUserRegistration)
|
|
require.False(t, generalSetting.DisallowPasswordAuth)
|
|
require.Empty(t, generalSetting.AdditionalScript)
|
|
})
|
|
|
|
t.Run("GetInstanceSetting - storage setting", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Create a host user for storage setting access
|
|
hostUser, err := ts.CreateHostUser(ctx, "testhost")
|
|
require.NoError(t, err)
|
|
|
|
// Add user to context
|
|
userCtx := ts.CreateUserContext(ctx, hostUser.ID)
|
|
|
|
// Call GetInstanceSetting for storage setting
|
|
req := &v1pb.GetInstanceSettingRequest{
|
|
Name: "instance/settings/STORAGE",
|
|
}
|
|
resp, err := ts.Service.GetInstanceSetting(userCtx, req)
|
|
|
|
// Verify response
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, "instance/settings/STORAGE", resp.Name)
|
|
|
|
// The storage setting should have a storage_setting field
|
|
storageSetting := resp.GetStorageSetting()
|
|
require.NotNil(t, storageSetting)
|
|
})
|
|
|
|
t.Run("GetInstanceSetting - memo related setting", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Call GetInstanceSetting for memo related setting
|
|
req := &v1pb.GetInstanceSettingRequest{
|
|
Name: "instance/settings/MEMO_RELATED",
|
|
}
|
|
resp, err := ts.Service.GetInstanceSetting(ctx, req)
|
|
|
|
// Verify response
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, "instance/settings/MEMO_RELATED", resp.Name)
|
|
|
|
// The memo related setting should have a memo_related_setting field
|
|
memoRelatedSetting := resp.GetMemoRelatedSetting()
|
|
require.NotNil(t, memoRelatedSetting)
|
|
})
|
|
|
|
t.Run("GetInstanceSetting - tags setting", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
req := &v1pb.GetInstanceSettingRequest{
|
|
Name: "instance/settings/TAGS",
|
|
}
|
|
resp, err := ts.Service.GetInstanceSetting(ctx, req)
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, "instance/settings/TAGS", resp.Name)
|
|
require.NotNil(t, resp.GetTagsSetting())
|
|
require.Empty(t, resp.GetTagsSetting().GetTags())
|
|
})
|
|
|
|
t.Run("GetInstanceSetting - notification setting requires admin", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
admin, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
adminCtx := ts.CreateUserContext(ctx, admin.ID)
|
|
|
|
regularUser, err := ts.CreateRegularUser(ctx, "user")
|
|
require.NoError(t, err)
|
|
userCtx := ts.CreateUserContext(ctx, regularUser.ID)
|
|
|
|
req := &v1pb.GetInstanceSettingRequest{Name: "instance/settings/NOTIFICATION"}
|
|
|
|
// Unauthenticated request must be rejected.
|
|
_, err = ts.Service.GetInstanceSetting(ctx, req)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "not authenticated")
|
|
|
|
// Non-admin request must be rejected.
|
|
_, err = ts.Service.GetInstanceSetting(userCtx, req)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "permission denied")
|
|
|
|
// Admin request succeeds and does NOT expose SmtpPassword.
|
|
resp, err := ts.Service.GetInstanceSetting(adminCtx, req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, "instance/settings/NOTIFICATION", resp.Name)
|
|
require.NotNil(t, resp.GetNotificationSetting())
|
|
require.Empty(t, resp.GetNotificationSetting().GetEmail().GetSmtpPassword(),
|
|
"SmtpPassword must never be returned in responses")
|
|
})
|
|
|
|
t.Run("GetInstanceSetting - invalid setting name", func(t *testing.T) {
|
|
// Create test service for this specific test
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
// Call GetInstanceSetting with invalid name
|
|
req := &v1pb.GetInstanceSettingRequest{
|
|
Name: "invalid/setting/name",
|
|
}
|
|
_, err := ts.Service.GetInstanceSetting(ctx, req)
|
|
|
|
// Should return an error
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid instance setting name")
|
|
})
|
|
}
|
|
|
|
func TestUpdateInstanceSetting(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("UpdateInstanceSetting - tags setting", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
hostUser, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
|
|
resp, err := ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: &v1pb.InstanceSetting{
|
|
Name: "instance/settings/TAGS",
|
|
Value: &v1pb.InstanceSetting_TagsSetting_{
|
|
TagsSetting: &v1pb.InstanceSetting_TagsSetting{
|
|
Tags: map[string]*v1pb.InstanceSetting_TagMetadata{
|
|
"bug": {
|
|
BackgroundColor: &colorpb.Color{
|
|
Red: 0.9,
|
|
Green: 0.1,
|
|
Blue: 0.1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"tags"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp.GetTagsSetting())
|
|
require.Contains(t, resp.GetTagsSetting().GetTags(), "bug")
|
|
})
|
|
|
|
t.Run("UpdateInstanceSetting - invalid tags color", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
hostUser, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
|
|
_, err = ts.Service.UpdateInstanceSetting(ts.CreateUserContext(ctx, hostUser.ID), &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: &v1pb.InstanceSetting{
|
|
Name: "instance/settings/TAGS",
|
|
Value: &v1pb.InstanceSetting_TagsSetting_{
|
|
TagsSetting: &v1pb.InstanceSetting_TagsSetting{
|
|
Tags: map[string]*v1pb.InstanceSetting_TagMetadata{
|
|
"bug": {
|
|
BackgroundColor: &colorpb.Color{
|
|
Red: 1.2,
|
|
Green: 0.1,
|
|
Blue: 0.1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid instance setting")
|
|
})
|
|
|
|
t.Run("UpdateInstanceSetting - notification setting password is write-only", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
hostUser, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
adminCtx := ts.CreateUserContext(ctx, hostUser.ID)
|
|
|
|
// Save notification setting with a password.
|
|
resp, err := ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: &v1pb.InstanceSetting{
|
|
Name: "instance/settings/NOTIFICATION",
|
|
Value: &v1pb.InstanceSetting_NotificationSetting_{
|
|
NotificationSetting: &v1pb.InstanceSetting_NotificationSetting{
|
|
Email: &v1pb.InstanceSetting_NotificationSetting_EmailSetting{
|
|
Enabled: true,
|
|
SmtpHost: "smtp.example.com",
|
|
SmtpPort: 587,
|
|
SmtpUsername: "bot@example.com",
|
|
SmtpPassword: "secret",
|
|
FromEmail: "bot@example.com",
|
|
FromName: "Memos Bot",
|
|
ReplyTo: "support@example.com",
|
|
UseTls: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"notification_setting"}},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, resp.GetNotificationSetting().GetEmail().GetEnabled())
|
|
require.Equal(t, "smtp.example.com", resp.GetNotificationSetting().GetEmail().GetSmtpHost())
|
|
// Password must not be returned even in the update response.
|
|
require.Empty(t, resp.GetNotificationSetting().GetEmail().GetSmtpPassword(),
|
|
"SmtpPassword must never be returned in responses")
|
|
})
|
|
|
|
t.Run("UpdateInstanceSetting - empty password preserves existing credential", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
hostUser, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
adminCtx := ts.CreateUserContext(ctx, hostUser.ID)
|
|
|
|
notificationSetting := &v1pb.InstanceSetting{
|
|
Name: "instance/settings/NOTIFICATION",
|
|
Value: &v1pb.InstanceSetting_NotificationSetting_{
|
|
NotificationSetting: &v1pb.InstanceSetting_NotificationSetting{
|
|
Email: &v1pb.InstanceSetting_NotificationSetting_EmailSetting{
|
|
Enabled: true,
|
|
SmtpHost: "smtp.example.com",
|
|
SmtpPort: 587,
|
|
SmtpUsername: "bot@example.com",
|
|
SmtpPassword: "original-password",
|
|
FromEmail: "bot@example.com",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// First save with a real password.
|
|
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: notificationSetting,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Second update with an empty password (simulating a UI that doesn't re-send the secret).
|
|
notificationSetting.GetNotificationSetting().GetEmail().SmtpPassword = ""
|
|
notificationSetting.GetNotificationSetting().GetEmail().SmtpHost = "smtp2.example.com"
|
|
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: notificationSetting,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The stored setting should have preserved the original password.
|
|
stored, err := ts.Store.GetInstanceNotificationSetting(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "original-password", stored.GetEmail().GetSmtpPassword(),
|
|
"existing SmtpPassword must be preserved when an empty value is sent")
|
|
require.Equal(t, "smtp2.example.com", stored.GetEmail().GetSmtpHost())
|
|
})
|
|
|
|
t.Run("UpdateInstanceSetting - S3 secret is write-only and preserved on empty", func(t *testing.T) {
|
|
ts := NewTestService(t)
|
|
defer ts.Cleanup()
|
|
|
|
hostUser, err := ts.CreateHostUser(ctx, "admin")
|
|
require.NoError(t, err)
|
|
adminCtx := ts.CreateUserContext(ctx, hostUser.ID)
|
|
|
|
// Save storage setting with a real secret.
|
|
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: &v1pb.InstanceSetting{
|
|
Name: "instance/settings/STORAGE",
|
|
Value: &v1pb.InstanceSetting_StorageSetting_{
|
|
StorageSetting: &v1pb.InstanceSetting_StorageSetting{
|
|
S3Config: &v1pb.InstanceSetting_StorageSetting_S3Config{
|
|
AccessKeyId: "AKID",
|
|
AccessKeySecret: "super-secret",
|
|
Endpoint: "s3.example.com",
|
|
Region: "us-east-1",
|
|
Bucket: "memos",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Read back: secret must not be returned.
|
|
resp, err := ts.Service.GetInstanceSetting(adminCtx, &v1pb.GetInstanceSettingRequest{
|
|
Name: "instance/settings/STORAGE",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Empty(t, resp.GetStorageSetting().GetS3Config().GetAccessKeySecret(),
|
|
"AccessKeySecret must never be returned in responses")
|
|
|
|
// Update with empty secret; original must be preserved in the store.
|
|
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
|
Setting: &v1pb.InstanceSetting{
|
|
Name: "instance/settings/STORAGE",
|
|
Value: &v1pb.InstanceSetting_StorageSetting_{
|
|
StorageSetting: &v1pb.InstanceSetting_StorageSetting{
|
|
S3Config: &v1pb.InstanceSetting_StorageSetting_S3Config{
|
|
AccessKeyId: "AKID",
|
|
AccessKeySecret: "", // omitted / not changed
|
|
Endpoint: "s3-v2.example.com",
|
|
Region: "us-east-1",
|
|
Bucket: "memos",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
stored, err := ts.Store.GetInstanceStorageSetting(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "super-secret", stored.GetS3Config().GetAccessKeySecret(),
|
|
"existing AccessKeySecret must be preserved when an empty value is sent")
|
|
require.Equal(t, "s3-v2.example.com", stored.GetS3Config().GetEndpoint())
|
|
})
|
|
}
|