mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:51:43 +00:00
parent
326a9061c2
commit
a44d145255
@ -729,6 +729,17 @@ type Mutation {
|
||||
"""
|
||||
value: Boolean!
|
||||
): FeatureFlagOverride!
|
||||
|
||||
"""
|
||||
Overwrites and saves the temporary settings for the current user.
|
||||
If temporary settings for the user do not exist, they are created.
|
||||
"""
|
||||
overwriteTemporarySettings(
|
||||
"""
|
||||
The new temporary settings for the current user, as a JSON string.
|
||||
"""
|
||||
contents: String!
|
||||
): EmptyResponse!
|
||||
}
|
||||
|
||||
"""
|
||||
@ -1386,6 +1397,11 @@ type Query {
|
||||
Retrieve the values of all feature flags for the current user
|
||||
"""
|
||||
viewerFeatureFlags: [EvaluatedFeatureFlag!]!
|
||||
|
||||
"""
|
||||
Retrieves the temporary settings for the current user.
|
||||
"""
|
||||
temporarySettings: TemporarySettings!
|
||||
}
|
||||
|
||||
"""
|
||||
@ -6387,3 +6403,13 @@ type ExecutionLogEntry {
|
||||
"""
|
||||
durationMilliseconds: Int
|
||||
}
|
||||
|
||||
"""
|
||||
Temporary settings for a user.
|
||||
"""
|
||||
type TemporarySettings {
|
||||
"""
|
||||
A JSON string representing the temporary settings.
|
||||
"""
|
||||
contents: String!
|
||||
}
|
||||
|
||||
48
cmd/frontend/graphqlbackend/temporary_settings.go
Normal file
48
cmd/frontend/graphqlbackend/temporary_settings.go
Normal file
@ -0,0 +1,48 @@
|
||||
package graphqlbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
|
||||
ts "github.com/sourcegraph/sourcegraph/internal/temporarysettings"
|
||||
)
|
||||
|
||||
type TemporarySettingsResolver struct {
|
||||
db dbutil.DB
|
||||
inner *ts.TemporarySettings
|
||||
}
|
||||
|
||||
func (r *schemaResolver) TemporarySettings(ctx context.Context) (*TemporarySettingsResolver, error) {
|
||||
user, err := CurrentUser(ctx, r.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New("not authenticated")
|
||||
}
|
||||
|
||||
temporarySettings, err := database.TemporarySettings(r.db).GetTemporarySettings(ctx, user.DatabaseID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TemporarySettingsResolver{db: r.db, inner: temporarySettings}, nil
|
||||
}
|
||||
|
||||
func (t *TemporarySettingsResolver) Contents() string {
|
||||
return t.inner.Contents
|
||||
}
|
||||
|
||||
func (r *schemaResolver) OverwriteTemporarySettings(ctx context.Context, args struct{ Contents string }) (*EmptyResponse, error) {
|
||||
user, err := CurrentUser(ctx, r.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New("not authenticated")
|
||||
}
|
||||
|
||||
return &EmptyResponse{}, database.TemporarySettings(r.db).UpsertTemporarySettings(ctx, user.DatabaseID(), args.Contents)
|
||||
}
|
||||
169
cmd/frontend/graphqlbackend/temporary_settings_test.go
Normal file
169
cmd/frontend/graphqlbackend/temporary_settings_test.go
Normal file
@ -0,0 +1,169 @@
|
||||
package graphqlbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
gqlerrors "github.com/graph-gophers/graphql-go/errors"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
ts "github.com/sourcegraph/sourcegraph/internal/temporarysettings"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
)
|
||||
|
||||
func TestTemporarySettingsNotSignedIn(t *testing.T) {
|
||||
resetMocks()
|
||||
|
||||
database.Mocks.Users.GetByCurrentAuthUser = func(context.Context) (*types.User, error) {
|
||||
return nil, database.ErrNoCurrentUser
|
||||
}
|
||||
|
||||
calledGetTemporarySettings := false
|
||||
database.Mocks.TemporarySettings.GetTemporarySettings = func(ctx context.Context, userID int32) (*ts.TemporarySettings, error) {
|
||||
calledGetTemporarySettings = true
|
||||
return &ts.TemporarySettings{Contents: "{\"search.collapsedSidebarSections\": {\"types\": false}}"}, nil
|
||||
}
|
||||
|
||||
wantErr := errors.New("not authenticated")
|
||||
|
||||
RunTests(t, []*Test{
|
||||
{
|
||||
Schema: mustParseGraphQLSchema(t),
|
||||
Query: `
|
||||
query {
|
||||
temporarySettings {
|
||||
contents
|
||||
}
|
||||
}
|
||||
`,
|
||||
ExpectedResult: "null",
|
||||
ExpectedErrors: []*gqlerrors.QueryError{
|
||||
{
|
||||
Path: []interface{}{"temporarySettings"},
|
||||
Message: wantErr.Error(),
|
||||
ResolverError: wantErr,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if calledGetTemporarySettings {
|
||||
t.Fatal("should not call GetTemporarySettings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemporarySettings(t *testing.T) {
|
||||
resetMocks()
|
||||
|
||||
database.Mocks.Users.GetByCurrentAuthUser = func(context.Context) (*types.User, error) {
|
||||
return &types.User{ID: 1, SiteAdmin: false}, nil
|
||||
}
|
||||
|
||||
calledGetTemporarySettings := false
|
||||
database.Mocks.TemporarySettings.GetTemporarySettings = func(ctx context.Context, userID int32) (*ts.TemporarySettings, error) {
|
||||
calledGetTemporarySettings = true
|
||||
return &ts.TemporarySettings{Contents: "{\"search.collapsedSidebarSections\": {\"types\": false}}"}, nil
|
||||
}
|
||||
|
||||
RunTests(t, []*Test{
|
||||
{
|
||||
Schema: mustParseGraphQLSchema(t),
|
||||
Query: `
|
||||
query {
|
||||
temporarySettings {
|
||||
contents
|
||||
}
|
||||
}
|
||||
`,
|
||||
ExpectedResult: `
|
||||
{
|
||||
"temporarySettings": {
|
||||
"contents": "{\"search.collapsedSidebarSections\": {\"types\": false}}"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
})
|
||||
|
||||
if !calledGetTemporarySettings {
|
||||
t.Fatal("should call GetTemporarySettings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverwriteTemporarySettingsNotSignedIn(t *testing.T) {
|
||||
resetMocks()
|
||||
|
||||
database.Mocks.Users.GetByCurrentAuthUser = func(context.Context) (*types.User, error) {
|
||||
return nil, database.ErrNoCurrentUser
|
||||
}
|
||||
|
||||
calledUpsertTemporarySettings := false
|
||||
database.Mocks.TemporarySettings.UpsertTemporarySettings = func(ctx context.Context, userID int32, contents string) error {
|
||||
calledUpsertTemporarySettings = true
|
||||
return nil
|
||||
}
|
||||
|
||||
wantErr := errors.New("not authenticated")
|
||||
|
||||
RunTests(t, []*Test{
|
||||
{
|
||||
Schema: mustParseGraphQLSchema(t),
|
||||
Query: `
|
||||
mutation ModifyTemporarySettings {
|
||||
overwriteTemporarySettings(
|
||||
contents: "{\"search.collapsedSidebarSections\": []}"
|
||||
) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`,
|
||||
ExpectedResult: "null",
|
||||
ExpectedErrors: []*gqlerrors.QueryError{
|
||||
{
|
||||
Path: []interface{}{"overwriteTemporarySettings"},
|
||||
Message: wantErr.Error(),
|
||||
ResolverError: wantErr,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if calledUpsertTemporarySettings {
|
||||
t.Fatal("should not call UpsertTemporarySettings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverwriteTemporarySettings(t *testing.T) {
|
||||
resetMocks()
|
||||
|
||||
database.Mocks.Users.GetByCurrentAuthUser = func(context.Context) (*types.User, error) {
|
||||
return &types.User{ID: 1, SiteAdmin: false}, nil
|
||||
}
|
||||
|
||||
calledUpsertTemporarySettings := false
|
||||
database.Mocks.TemporarySettings.UpsertTemporarySettings = func(ctx context.Context, userID int32, contents string) error {
|
||||
calledUpsertTemporarySettings = true
|
||||
return nil
|
||||
}
|
||||
|
||||
RunTests(t, []*Test{
|
||||
{
|
||||
Schema: mustParseGraphQLSchema(t),
|
||||
Query: `
|
||||
mutation ModifyTemporarySettings {
|
||||
overwriteTemporarySettings(
|
||||
contents: "{\"search.collapsedSidebarSections\": []}"
|
||||
) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`,
|
||||
ExpectedResult: "{\"overwriteTemporarySettings\":{\"alwaysNil\":null}}",
|
||||
},
|
||||
})
|
||||
|
||||
if !calledUpsertTemporarySettings {
|
||||
t.Fatal("should call UpsertTemporarySettings")
|
||||
}
|
||||
}
|
||||
@ -29,4 +29,6 @@ type MockStores struct {
|
||||
Authz MockAuthz
|
||||
|
||||
EventLogs MockEventLogs
|
||||
|
||||
TemporarySettings MockTemporarySettings
|
||||
}
|
||||
|
||||
63
internal/database/temporary_settings.go
Normal file
63
internal/database/temporary_settings.go
Normal file
@ -0,0 +1,63 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
"github.com/keegancsmith/sqlf"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
|
||||
ts "github.com/sourcegraph/sourcegraph/internal/temporarysettings"
|
||||
)
|
||||
|
||||
type TemporarySettingsStore struct {
|
||||
*basestore.Store
|
||||
}
|
||||
|
||||
func TemporarySettings(db dbutil.DB) *TemporarySettingsStore {
|
||||
return &TemporarySettingsStore{Store: basestore.NewWithDB(db, sql.TxOptions{})}
|
||||
}
|
||||
|
||||
func (f *TemporarySettingsStore) GetTemporarySettings(ctx context.Context, userID int32) (*ts.TemporarySettings, error) {
|
||||
if Mocks.TemporarySettings.GetTemporarySettings != nil {
|
||||
return Mocks.TemporarySettings.GetTemporarySettings(ctx, userID)
|
||||
}
|
||||
|
||||
const getTemporarySettingsQuery = `
|
||||
SELECT contents
|
||||
FROM temporary_settings
|
||||
WHERE user_id = %s
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
var contents string
|
||||
err := f.QueryRow(ctx, sqlf.Sprintf(getTemporarySettingsQuery, userID)).Scan(&contents)
|
||||
|
||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||
// No settings are saved for this user yet, return an empty settings object.
|
||||
contents = "{}"
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ts.TemporarySettings{Contents: contents}, nil
|
||||
}
|
||||
|
||||
func (f *TemporarySettingsStore) UpsertTemporarySettings(ctx context.Context, userID int32, contents string) error {
|
||||
if Mocks.TemporarySettings.UpsertTemporarySettings != nil {
|
||||
return Mocks.TemporarySettings.UpsertTemporarySettings(ctx, userID, contents)
|
||||
}
|
||||
|
||||
const upsertTemporarySettingsQuery = `
|
||||
INSERT INTO temporary_settings (user_id, contents)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
contents = %s,
|
||||
updated_at = now();
|
||||
`
|
||||
|
||||
return f.Exec(ctx, sqlf.Sprintf(upsertTemporarySettingsQuery, userID, contents, contents))
|
||||
}
|
||||
12
internal/database/temporary_settings_mock.go
Normal file
12
internal/database/temporary_settings_mock.go
Normal file
@ -0,0 +1,12 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ts "github.com/sourcegraph/sourcegraph/internal/temporarysettings"
|
||||
)
|
||||
|
||||
type MockTemporarySettings struct {
|
||||
GetTemporarySettings func(ctx context.Context, userID int32) (*ts.TemporarySettings, error)
|
||||
UpsertTemporarySettings func(ctx context.Context, userID int32, contents string) error
|
||||
}
|
||||
102
internal/database/temporary_settings_test.go
Normal file
102
internal/database/temporary_settings_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
|
||||
ts "github.com/sourcegraph/sourcegraph/internal/temporarysettings"
|
||||
)
|
||||
|
||||
func TestTemporarySettingsStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("GetEmpty", testGetEmpty)
|
||||
t.Run("InsertAndGet", testInsertAndGet)
|
||||
t.Run("UpdateAndGet", testUpdateAndGet)
|
||||
t.Run("InsertWithInvalidData", testInsertWithInvalidData)
|
||||
}
|
||||
|
||||
func testGetEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
temporarySettingsStore := TemporarySettings(dbtest.NewDB(t, ""))
|
||||
|
||||
ctx := actor.WithInternalActor(context.Background())
|
||||
|
||||
expected := ts.TemporarySettings{Contents: "{}"}
|
||||
|
||||
res, err := temporarySettingsStore.GetTemporarySettings(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, res, &expected)
|
||||
}
|
||||
|
||||
func testInsertAndGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := dbtest.NewDB(t, "")
|
||||
usersStore := Users(db)
|
||||
temporarySettingsStore := TemporarySettings(db)
|
||||
|
||||
ctx := actor.WithInternalActor(context.Background())
|
||||
|
||||
contents := "{\"search.collapsedSidebarSections\": {}}"
|
||||
|
||||
user, err := usersStore.Create(ctx, NewUser{Username: "u", Password: "p"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = temporarySettingsStore.UpsertTemporarySettings(ctx, user.ID, contents)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := temporarySettingsStore.GetTemporarySettings(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := ts.TemporarySettings{Contents: contents}
|
||||
require.Equal(t, res, &expected)
|
||||
}
|
||||
|
||||
func testUpdateAndGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := dbtest.NewDB(t, "")
|
||||
usersStore := Users(db)
|
||||
temporarySettingsStore := TemporarySettings(db)
|
||||
|
||||
ctx := actor.WithInternalActor(context.Background())
|
||||
|
||||
contents := "{\"search.collapsedSidebarSections\": {}}"
|
||||
|
||||
user, err := usersStore.Create(ctx, NewUser{Username: "u", Password: "p"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = temporarySettingsStore.UpsertTemporarySettings(ctx, user.ID, contents)
|
||||
require.NoError(t, err)
|
||||
|
||||
contents2 := "{\"search.collapsedSidebarSections\": {\"types\": false}}"
|
||||
|
||||
err = temporarySettingsStore.UpsertTemporarySettings(ctx, user.ID, contents2)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := temporarySettingsStore.GetTemporarySettings(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := ts.TemporarySettings{Contents: contents2}
|
||||
require.Equal(t, res, &expected)
|
||||
}
|
||||
|
||||
func testInsertWithInvalidData(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := dbtest.NewDB(t, "")
|
||||
usersStore := Users(db)
|
||||
temporarySettingsStore := TemporarySettings(db)
|
||||
|
||||
ctx := actor.WithInternalActor(context.Background())
|
||||
|
||||
contents := "{\"search.collapsedSidebarSections\": {}"
|
||||
|
||||
user, err := usersStore.Create(ctx, NewUser{Username: "u", Password: "p"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = temporarySettingsStore.UpsertTemporarySettings(ctx, user.ID, contents)
|
||||
require.EqualError(t, err, "ERROR: invalid input syntax for type json (SQLSTATE 22P02)")
|
||||
}
|
||||
5
internal/temporarysettings/temporary_settings.go
Normal file
5
internal/temporarysettings/temporary_settings.go
Normal file
@ -0,0 +1,5 @@
|
||||
package temporarysettings
|
||||
|
||||
type TemporarySettings struct {
|
||||
Contents string
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user