* feat(metadata): add original-language preferences * fix(settings): show metadata exceptions immediately * docs(settings): add metadata language screenshot * fix(settings): make language exceptions responsive * fix(settings): standardize language display names
153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
package access
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/settingscontract"
|
|
"github.com/Silo-Server/silo-server/internal/settingskeys"
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
)
|
|
|
|
// settingStoreStub only answers the one method resolution reaches; the
|
|
// embedded nil interface panics on anything else, which is the point — this
|
|
// path must not touch the rest of the store.
|
|
type settingStoreStub struct {
|
|
userstore.UserStore
|
|
rows []userstore.SettingValue
|
|
err error
|
|
}
|
|
|
|
func (s settingStoreStub) ListSettingValuesForResolution(
|
|
context.Context, userstore.SettingResolutionQuery,
|
|
) ([]userstore.SettingValue, error) {
|
|
return s.rows, s.err
|
|
}
|
|
|
|
type capturingLogHandler struct {
|
|
mu sync.Mutex
|
|
records []slog.Record
|
|
}
|
|
|
|
func (h *capturingLogHandler) Enabled(context.Context, slog.Level) bool { return true }
|
|
func (h *capturingLogHandler) Handle(_ context.Context, r slog.Record) error {
|
|
h.mu.Lock()
|
|
h.records = append(h.records, r)
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
func (h *capturingLogHandler) WithAttrs([]slog.Attr) slog.Handler { return h }
|
|
func (h *capturingLogHandler) WithGroup(string) slog.Handler { return h }
|
|
|
|
func (h *capturingLogHandler) snapshot() []slog.Record {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return append([]slog.Record(nil), h.records...)
|
|
}
|
|
|
|
func captureLogs(t *testing.T) *capturingLogHandler {
|
|
t.Helper()
|
|
handler := &capturingLogHandler{}
|
|
prev := slog.Default()
|
|
slog.SetDefault(slog.New(handler))
|
|
t.Cleanup(func() { slog.SetDefault(prev) })
|
|
return handler
|
|
}
|
|
|
|
// TestPreferredMetadataLanguageLogsStoreFailures pins the operator signal: the
|
|
// value deliberately degrades to "" on a store failure, but before the cutover
|
|
// it rode on the already-loaded profile row where a load failure was a hard
|
|
// error. A silent degrade would make transient pool exhaustion — or a
|
|
// persistently broken query path — indistinguishable from "no preference".
|
|
func TestPreferredMetadataLanguageLogsStoreFailures(t *testing.T) {
|
|
handler := captureLogs(t)
|
|
|
|
store := settingStoreStub{err: errors.New("connection pool exhausted")}
|
|
if got := PreferredMetadataLanguage(context.Background(), store, "profile-1"); got != "" {
|
|
t.Fatalf("degraded value = %q, want \"\"", got)
|
|
}
|
|
|
|
records := handler.snapshot()
|
|
if len(records) == 0 {
|
|
t.Fatal("a store failure resolved to the default with no log output")
|
|
}
|
|
record := records[0]
|
|
if record.Level < slog.LevelWarn {
|
|
t.Errorf("logged at %v, want at least WARN", record.Level)
|
|
}
|
|
var loggedError, loggedProfile bool
|
|
record.Attrs(func(a slog.Attr) bool {
|
|
switch a.Key {
|
|
case "error":
|
|
loggedError = strings.Contains(a.Value.String(), "connection pool exhausted")
|
|
case "profile_id":
|
|
loggedProfile = a.Value.String() == "profile-1"
|
|
}
|
|
return true
|
|
})
|
|
if !loggedError {
|
|
t.Errorf("log %q does not carry the store error", record.Message)
|
|
}
|
|
if !loggedProfile {
|
|
t.Errorf("log %q does not name the profile", record.Message)
|
|
}
|
|
}
|
|
|
|
// TestPreferredMetadataLanguageStaysQuietWhenNothingIsStored: the healthy
|
|
// no-preference answer must not spam the log.
|
|
func TestPreferredMetadataLanguageStaysQuietWhenNothingIsStored(t *testing.T) {
|
|
handler := captureLogs(t)
|
|
|
|
if got := PreferredMetadataLanguage(context.Background(), settingStoreStub{}, "profile-1"); got != "" {
|
|
t.Fatalf("no-preference value = %q, want \"\"", got)
|
|
}
|
|
if records := handler.snapshot(); len(records) != 0 {
|
|
t.Errorf("healthy resolution logged %d records, want none", len(records))
|
|
}
|
|
}
|
|
|
|
func TestResolveViewerPreferencesIncludesMetadataLanguageOverrides(t *testing.T) {
|
|
store := settingStoreStub{rows: []userstore.SettingValue{
|
|
{
|
|
SettingIdentity: userstore.SettingIdentity{
|
|
Key: settingskeys.UiDisabledLibraryIds, Scope: settingscontract.ScopeProfile,
|
|
ProfileID: "profile-1",
|
|
},
|
|
Value: json.RawMessage(`[]`),
|
|
},
|
|
{
|
|
SettingIdentity: userstore.SettingIdentity{
|
|
Key: settingskeys.CatalogMetadataLanguage, Scope: settingscontract.ScopeProfile,
|
|
ProfileID: "profile-1",
|
|
},
|
|
Value: json.RawMessage(`"en"`),
|
|
},
|
|
{
|
|
SettingIdentity: userstore.SettingIdentity{
|
|
Key: settingskeys.CatalogMetadataLanguageOverrides, Scope: settingscontract.ScopeProfile,
|
|
ProfileID: "profile-1",
|
|
},
|
|
Value: json.RawMessage(`{"nor":"x-silo-original","JA":"de"}`),
|
|
},
|
|
}}
|
|
|
|
preferences := ResolveViewerPreferences(context.Background(), store, "profile-1")
|
|
if preferences.PreferredMetadataLanguage != "en" {
|
|
t.Fatalf("fallback language = %q, want en", preferences.PreferredMetadataLanguage)
|
|
}
|
|
want := map[string]string{"no": OriginalMetadataLanguage, "ja": "de"}
|
|
if len(preferences.MetadataLanguageOverrides) != len(want) {
|
|
t.Fatalf("metadata overrides = %#v, want %#v", preferences.MetadataLanguageOverrides, want)
|
|
}
|
|
for source, target := range want {
|
|
if preferences.MetadataLanguageOverrides[source] != target {
|
|
t.Errorf("metadata override %q = %q, want %q", source, preferences.MetadataLanguageOverrides[source], target)
|
|
}
|
|
}
|
|
}
|