temporary settings: graphql backend (#24295)

Fixes #23724
This commit is contained in:
Juliana Peña 2021-08-26 07:57:22 -07:00 committed by GitHub
parent 326a9061c2
commit a44d145255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 427 additions and 0 deletions

View File

@ -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!
}

View 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)
}

View 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")
}
}

View File

@ -29,4 +29,6 @@ type MockStores struct {
Authz MockAuthz
EventLogs MockEventLogs
TemporarySettings MockTemporarySettings
}

View 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))
}

View 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
}

View 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)")
}

View File

@ -0,0 +1,5 @@
package temporarysettings
type TemporarySettings struct {
Contents string
}