0.29.1原版
This commit is contained in:
@@ -0,0 +1,945 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"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"
|
||||
storepb "github.com/usememos/memos/proto/gen/store"
|
||||
)
|
||||
|
||||
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.Equal(t, "test-commit", resp.Commit)
|
||||
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.Equal(t, "test-commit", resp.Commit)
|
||||
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.Equal(t, "test-commit", resp.Commit)
|
||||
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 - AI setting requires authenticated user", 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/AI"}
|
||||
|
||||
_, err = ts.Service.GetInstanceSetting(ctx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "not authenticated")
|
||||
|
||||
resp, err := ts.Service.GetInstanceSetting(userCtx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.GetAiSetting())
|
||||
require.Empty(t, resp.GetAiSetting().GetProviders())
|
||||
|
||||
resp, err = ts.Service.GetInstanceSetting(adminCtx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.GetAiSetting())
|
||||
require.Empty(t, resp.GetAiSetting().GetProviders())
|
||||
})
|
||||
|
||||
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 TestBatchGetInstanceSettings(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("BatchGetInstanceSettings - returns settings in request order", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
resp, err := ts.Service.BatchGetInstanceSettings(ctx, &v1pb.BatchGetInstanceSettingsRequest{
|
||||
Names: []string{
|
||||
"instance/settings/TAGS",
|
||||
"instance/settings/GENERAL",
|
||||
"instance/settings/MEMO_RELATED",
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, resp.Settings, 3)
|
||||
require.Equal(t, "instance/settings/TAGS", resp.Settings[0].Name)
|
||||
require.NotNil(t, resp.Settings[0].GetTagsSetting())
|
||||
require.Equal(t, "instance/settings/GENERAL", resp.Settings[1].Name)
|
||||
require.NotNil(t, resp.Settings[1].GetGeneralSetting())
|
||||
require.Equal(t, "instance/settings/MEMO_RELATED", resp.Settings[2].Name)
|
||||
require.NotNil(t, resp.Settings[2].GetMemoRelatedSetting())
|
||||
})
|
||||
|
||||
t.Run("BatchGetInstanceSettings - admin-only setting requires admin", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
regularUser, err := ts.CreateRegularUser(ctx, "batch-user")
|
||||
require.NoError(t, err)
|
||||
userCtx := ts.CreateUserContext(ctx, regularUser.ID)
|
||||
|
||||
_, err = ts.Service.BatchGetInstanceSettings(userCtx, &v1pb.BatchGetInstanceSettingsRequest{
|
||||
Names: []string{"instance/settings/GENERAL", "instance/settings/NOTIFICATION"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
|
||||
admin, err := ts.CreateHostUser(ctx, "batch-admin")
|
||||
require.NoError(t, err)
|
||||
adminCtx := ts.CreateUserContext(ctx, admin.ID)
|
||||
|
||||
resp, err := ts.Service.BatchGetInstanceSettings(adminCtx, &v1pb.BatchGetInstanceSettingsRequest{
|
||||
Names: []string{"instance/settings/GENERAL", "instance/settings/NOTIFICATION"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Settings, 2)
|
||||
require.NotNil(t, resp.Settings[1].GetNotificationSetting())
|
||||
})
|
||||
|
||||
t.Run("BatchGetInstanceSettings - invalid setting name", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
_, err := ts.Service.BatchGetInstanceSettings(ctx, &v1pb.BatchGetInstanceSettingsRequest{
|
||||
Names: []string{"instance/settings/GENERAL", "invalid/setting/name"},
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid instance setting name")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestInstanceEmailSettingAuthorization(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
admin, err := ts.CreateHostUser(ctx, "email-test-admin")
|
||||
require.NoError(t, err)
|
||||
adminCtx := ts.CreateUserContext(ctx, admin.ID)
|
||||
|
||||
regularUser, err := ts.CreateRegularUser(ctx, "email-test-user")
|
||||
require.NoError(t, err)
|
||||
userCtx := ts.CreateUserContext(ctx, regularUser.ID)
|
||||
|
||||
req := &v1pb.TestInstanceEmailSettingRequest{}
|
||||
|
||||
_, err = ts.Service.TestInstanceEmailSetting(ctx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "not authenticated")
|
||||
|
||||
_, err = ts.Service.TestInstanceEmailSetting(userCtx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
|
||||
_, err = ts.Service.TestInstanceEmailSetting(adminCtx, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid notification email setting")
|
||||
}
|
||||
|
||||
func TestTestInstanceEmailSettingRequiresPasswordWhenSMTPIdentityChanges(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
admin, err := ts.CreateHostUser(ctx, "email-test-identity-admin")
|
||||
require.NoError(t, err)
|
||||
adminCtx := ts.CreateUserContext(ctx, admin.ID)
|
||||
|
||||
_, err = ts.Store.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{
|
||||
Key: storepb.InstanceSettingKey_NOTIFICATION,
|
||||
Value: &storepb.InstanceSetting_NotificationSetting{
|
||||
NotificationSetting: &storepb.InstanceNotificationSetting{
|
||||
Email: &storepb.InstanceNotificationSetting_EmailSetting{
|
||||
Enabled: true,
|
||||
SmtpHost: "smtp.example.com",
|
||||
SmtpPort: 587,
|
||||
SmtpUsername: "bot@example.com",
|
||||
SmtpPassword: "stored-password",
|
||||
FromEmail: "bot@example.com",
|
||||
UseTls: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ts.Service.TestInstanceEmailSetting(adminCtx, &v1pb.TestInstanceEmailSettingRequest{
|
||||
Email: &v1pb.InstanceSetting_NotificationSetting_EmailSetting{
|
||||
Enabled: true,
|
||||
SmtpHost: "attacker.example.com",
|
||||
SmtpPort: 587,
|
||||
SmtpUsername: "bot@example.com",
|
||||
SmtpPassword: "",
|
||||
FromEmail: "bot@example.com",
|
||||
UseTls: true,
|
||||
},
|
||||
RecipientEmail: admin.Email,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "smtp password is required")
|
||||
}
|
||||
|
||||
func TestUpdateInstanceSetting(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("UpdateInstanceSetting - AI setting requires admin", func(t *testing.T) {
|
||||
ts := NewTestService(t)
|
||||
defer ts.Cleanup()
|
||||
|
||||
regularUser, err := ts.CreateRegularUser(ctx, "user")
|
||||
require.NoError(t, err)
|
||||
userCtx := ts.CreateUserContext(ctx, regularUser.ID)
|
||||
|
||||
setting := &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "sk-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(ctx, &v1pb.UpdateInstanceSettingRequest{Setting: setting})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "not authenticated")
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(userCtx, &v1pb.UpdateInstanceSettingRequest{Setting: setting})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "permission denied")
|
||||
})
|
||||
|
||||
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 - tags setting without color", 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{
|
||||
"spoiler": {
|
||||
BlurContent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.GetTagsSetting())
|
||||
require.Contains(t, resp.GetTagsSetting().GetTags(), "spoiler")
|
||||
require.Nil(t, resp.GetTagsSetting().GetTags()["spoiler"].GetBackgroundColor())
|
||||
require.True(t, resp.GetTagsSetting().GetTags()["spoiler"].GetBlurContent())
|
||||
})
|
||||
|
||||
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().FromName = "Updated Bot"
|
||||
_, 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, "Updated Bot", stored.GetEmail().GetFromName())
|
||||
})
|
||||
|
||||
t.Run("UpdateInstanceSetting - empty password rejected when SMTP identity changes", 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",
|
||||
UseTls: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: notificationSetting,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
notificationSetting.GetNotificationSetting().GetEmail().SmtpPassword = ""
|
||||
notificationSetting.GetNotificationSetting().GetEmail().SmtpHost = "smtp2.example.com"
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: notificationSetting,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "smtp password is required")
|
||||
})
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
t.Run("UpdateInstanceSetting - AI provider keys are 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)
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "sk-original",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := ts.Service.GetInstanceSetting(adminCtx, &v1pb.GetInstanceSettingRequest{
|
||||
Name: "instance/settings/AI",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.GetAiSetting().GetProviders(), 1)
|
||||
provider := resp.GetAiSetting().GetProviders()[0]
|
||||
require.Empty(t, provider.GetApiKey(), "AI provider API key must never be returned in responses")
|
||||
require.True(t, provider.GetApiKeySet())
|
||||
require.Equal(t, "sk-o...inal", provider.GetApiKeyHint())
|
||||
require.Equal(t, "https://api.openai.com/v1", provider.GetEndpoint())
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI primary",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
stored, err := ts.Store.GetInstanceAISetting(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stored.GetProviders(), 1)
|
||||
require.Equal(t, "sk-original", stored.GetProviders()[0].GetApiKey(),
|
||||
"existing AI provider API key must be preserved when an empty value is sent")
|
||||
require.Equal(t, "OpenAI primary", stored.GetProviders()[0].GetTitle())
|
||||
})
|
||||
|
||||
t.Run("UpdateInstanceSetting - transcription provider_id must reference an existing provider", 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)
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "sk-test",
|
||||
},
|
||||
},
|
||||
Transcription: &v1pb.InstanceSetting_TranscriptionConfig{
|
||||
ProviderId: "does-not-exist",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "transcription provider_id")
|
||||
})
|
||||
|
||||
t.Run("UpdateInstanceSetting - transcription strings are length-capped", 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)
|
||||
|
||||
base := &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "sk-test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
oversizedModel := strings.Repeat("a", 257)
|
||||
base.GetAiSetting().Transcription = &v1pb.InstanceSetting_TranscriptionConfig{
|
||||
ProviderId: "openai-main",
|
||||
Model: oversizedModel,
|
||||
}
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{Setting: base})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "transcription model")
|
||||
|
||||
oversizedLanguage := strings.Repeat("a", 33)
|
||||
base.GetAiSetting().Transcription = &v1pb.InstanceSetting_TranscriptionConfig{
|
||||
ProviderId: "openai-main",
|
||||
Language: oversizedLanguage,
|
||||
}
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{Setting: base})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "transcription language")
|
||||
|
||||
oversizedPrompt := strings.Repeat("a", 4097)
|
||||
base.GetAiSetting().Transcription = &v1pb.InstanceSetting_TranscriptionConfig{
|
||||
ProviderId: "openai-main",
|
||||
Prompt: oversizedPrompt,
|
||||
}
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{Setting: base})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "transcription prompt")
|
||||
})
|
||||
|
||||
t.Run("UpdateInstanceSetting - transcription is preserved when omitted on update", 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)
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "sk-test",
|
||||
},
|
||||
},
|
||||
Transcription: &v1pb.InstanceSetting_TranscriptionConfig{
|
||||
ProviderId: "openai-main",
|
||||
Model: "whisper-1",
|
||||
Language: "en",
|
||||
Prompt: "names: Alice",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ts.Service.UpdateInstanceSetting(adminCtx, &v1pb.UpdateInstanceSettingRequest{
|
||||
Setting: &v1pb.InstanceSetting{
|
||||
Name: "instance/settings/AI",
|
||||
Value: &v1pb.InstanceSetting_AiSetting{
|
||||
AiSetting: &v1pb.InstanceSetting_AISetting{
|
||||
Providers: []*v1pb.InstanceSetting_AIProviderConfig{
|
||||
{
|
||||
Id: "openai-main",
|
||||
Title: "OpenAI",
|
||||
Type: v1pb.InstanceSetting_OPENAI,
|
||||
ApiKey: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
stored, err := ts.Store.GetInstanceAISetting(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, stored.GetTranscription())
|
||||
require.Equal(t, "openai-main", stored.GetTranscription().GetProviderId())
|
||||
require.Equal(t, "whisper-1", stored.GetTranscription().GetModel())
|
||||
require.Equal(t, "en", stored.GetTranscription().GetLanguage())
|
||||
require.Equal(t, "names: Alice", stored.GetTranscription().GetPrompt())
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user