refactor(sqllab): migrate share queries via kv by permalink

This commit is contained in:
justinpark 2024-06-10 12:57:38 -07:00
parent fc9bc175e6
commit 836a60ccc2
18 changed files with 589 additions and 23 deletions

View File

@ -1185,6 +1185,26 @@ export function persistEditorHeight(queryEditor, northPercent, southPercent) {
};
}
export function popPermalink(key) {
return function (dispatch) {
return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/${key}` })
.then(({ json }) =>
dispatch(
addQueryEditor({
name: json.name ? json.name : t('Shared query'),
dbId: json.dbId ? parseInt(json.dbId, 10) : null,
catalog: json.catalog ? json.catalog : null,
schema: json.schema ? json.schema : null,
autorun: json.autorun ? json.autorun : false,
sql: json.sql ? json.sql : 'SELECT ...',
templateParams: json.templateParams,
}),
),
)
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
};
}
export function popStoredQuery(urlId) {
return function (dispatch) {
return SupersetClient.get({ endpoint: `/kv/${urlId}` })

View File

@ -23,7 +23,7 @@ import fetchMock from 'fetch-mock';
import * as uiCore from '@superset-ui/core';
import { Provider } from 'react-redux';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import { render, screen, act } from '@testing-library/react';
import { render, screen, act, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import * as utils from 'src/utils/common';
@ -92,20 +92,24 @@ const standardProviderWithUnsaved: FC = ({ children }) => (
);
describe('ShareSqlLabQuery', () => {
const storeQueryUrl = 'glob:*/kv/store/';
const storeQueryMockId = '123';
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
const storeQueryMockId = 'ci39c3';
beforeEach(async () => {
fetchMock.post(storeQueryUrl, () => ({ id: storeQueryMockId }), {
overwriteRoutes: true,
});
fetchMock.post(
storeQueryUrl,
() => ({ key: storeQueryMockId, url: `/p/${storeQueryMockId}` }),
{
overwriteRoutes: true,
},
);
fetchMock.resetHistory();
jest.clearAllMocks();
});
afterAll(fetchMock.reset);
describe('via /kv/store', () => {
describe('via permalink api', () => {
beforeAll(() => {
isFeatureEnabledMock = jest
.spyOn(uiCore, 'isFeatureEnabled')
@ -124,11 +128,13 @@ describe('ShareSqlLabQuery', () => {
});
const button = screen.getByRole('button');
const { id, remoteId, ...expected } = mockQueryEditor;
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
userEvent.click(button);
expect(storeQuerySpy.mock.calls).toHaveLength(1);
expect(storeQuerySpy).toBeCalledWith(expected);
storeQuerySpy.mockRestore();
await waitFor(() =>
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
);
expect(
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
).toEqual(expected);
});
it('calls storeQuery() with unsaved changes', async () => {
@ -139,11 +145,13 @@ describe('ShareSqlLabQuery', () => {
});
const button = screen.getByRole('button');
const { id, ...expected } = unsavedQueryEditor;
const storeQuerySpy = jest.spyOn(utils, 'storeQuery');
userEvent.click(button);
expect(storeQuerySpy.mock.calls).toHaveLength(1);
expect(storeQuerySpy).toBeCalledWith(expected);
storeQuerySpy.mockRestore();
await waitFor(() =>
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
);
expect(
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
).toEqual(expected);
});
});

View File

@ -23,12 +23,12 @@ import {
useTheme,
isFeatureEnabled,
getClientErrorObject,
SupersetClient,
} from '@superset-ui/core';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import withToasts from 'src/components/MessageToasts/withToasts';
import CopyToClipboard from 'src/components/CopyToClipboard';
import { storeQuery } from 'src/utils/common';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
interface ShareSqlLabQueryProps {
@ -63,12 +63,16 @@ const ShareSqlLabQuery = ({
'templateParams',
]);
const getCopyUrlForKvStore = (callback: Function) => {
const getCopyUrlForPermalink = (callback: Function) => {
const sharedQuery = { dbId, name, schema, autorun, sql, templateParams };
return storeQuery(sharedQuery)
.then(shortUrl => {
callback(shortUrl);
return SupersetClient.post({
endpoint: '/api/v1/sqllab/permalink',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sharedQuery),
})
.then(({ json }) => {
callback(json.url);
})
.catch(response => {
getClientErrorObject(response).then(() => {
@ -92,7 +96,7 @@ const ShareSqlLabQuery = ({
};
const getCopyUrl = (callback: Function) => {
if (isFeatureEnabled(FeatureFlag.ShareQueriesViaKvStore)) {
return getCopyUrlForKvStore(callback);
return getCopyUrlForPermalink(callback);
}
return getCopyUrlForSavedQuery(callback);
};

View File

@ -71,6 +71,27 @@ describe('componentDidMount', () => {
'/sqllab',
);
});
test('should handle permalink', async () => {
const key = '9sadkfl';
fetchMock.get(`glob:*/api/v1/sqllab/permalink/${key}`, {
label: 'test permalink',
sql: 'SELECT * FROM test_table',
dbId: 1,
});
uriStub.mockReturnValue({ p: key });
setup(store);
await waitFor(() =>
expect(
fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
'/sqllab',
);
fetchMock.reset();
});
test('should handle savedQueryId', () => {
uriStub.mockReturnValue({ savedQueryId: 1 });
setup(store);

View File

@ -79,6 +79,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
const {
id,
name,
p,
sql,
savedQueryId,
datasourceKey,
@ -97,8 +98,10 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
} as Record<string, string>;
// Popping a new tab based on the querystring
if (id || sql || savedQueryId || datasourceKey || queryId) {
if (id) {
if (p || id || sql || savedQueryId || datasourceKey || queryId) {
if (p) {
this.props.actions.popPermalink(p);
} else if (id) {
this.props.actions.popStoredQuery(id);
} else if (savedQueryId) {
this.props.actions.popSavedQuery(savedQueryId);

View File

@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,35 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from abc import ABC
from superset.commands.base import BaseCommand
from superset.key_value.shared_entries import get_permalink_salt
from superset.key_value.types import (
KeyValueResource,
MarshmallowKeyValueCodec,
SharedKey,
)
from superset.sqllab.permalink.schemas import SqlLabPermalinkSchema
class BaseSqlLabPermalinkCommand(BaseCommand, ABC):
resource: KeyValueResource = KeyValueResource.SQLLAB_PERMALINK
codec = MarshmallowKeyValueCodec(SqlLabPermalinkSchema())
@property
def salt(self) -> str:
return get_permalink_salt(SharedKey.SQLLAB_PERMALINK_SALT)

View File

@ -0,0 +1,49 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from typing import Any
from superset.commands.key_value.create import CreateKeyValueCommand
from superset.commands.sql_lab.permalink.base import BaseSqlLabPermalinkCommand
from superset.key_value.exceptions import KeyValueCodecEncodeException
from superset.key_value.utils import encode_permalink_key
from superset.sqllab.permalink.exceptions import SqlLabPermalinkCreateFailedError
logger = logging.getLogger(__name__)
class CreateSqlLabPermalinkCommand(BaseSqlLabPermalinkCommand):
def __init__(self, state: dict[str, Any]):
self._properties = state.copy()
def run(self) -> str:
self.validate()
try:
command = CreateKeyValueCommand(
resource=self.resource,
value=self._properties,
codec=self.codec,
)
key = command.run()
if key.id is None:
raise SqlLabPermalinkCreateFailedError("Unexpected missing key id")
return encode_permalink_key(key=key.id, salt=self.salt)
except KeyValueCodecEncodeException as ex:
raise SqlLabPermalinkCreateFailedError(str(ex)) from ex
def validate(self) -> None:
pass

View File

@ -0,0 +1,58 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from typing import Optional
from superset.commands.key_value.get import GetKeyValueCommand
from superset.commands.sql_lab.permalink.base import BaseSqlLabPermalinkCommand
from superset.key_value.exceptions import (
KeyValueCodecDecodeException,
KeyValueGetFailedError,
KeyValueParseKeyError,
)
from superset.key_value.utils import decode_permalink_id
from superset.sqllab.permalink.exceptions import SqlLabPermalinkGetFailedError
from superset.sqllab.permalink.types import SqlLabPermalinkValue
logger = logging.getLogger(__name__)
class GetSqlLabPermalinkCommand(BaseSqlLabPermalinkCommand):
def __init__(self, key: str):
self.key = key
def run(self) -> Optional[SqlLabPermalinkValue]:
self.validate()
try:
key = decode_permalink_id(self.key, salt=self.salt)
value: Optional[SqlLabPermalinkValue] = GetKeyValueCommand(
resource=self.resource,
key=key,
codec=self.codec,
).run()
if value:
return value
return None
except (
KeyValueCodecDecodeException,
KeyValueGetFailedError,
KeyValueParseKeyError,
) as ex:
raise SqlLabPermalinkGetFailedError(message=ex.message) from ex
def validate(self) -> None:
pass

View File

@ -154,6 +154,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
from superset.row_level_security.api import RLSRestApi
from superset.security.api import SecurityRestApi
from superset.sqllab.api import SqlLabRestApi
from superset.sqllab.permalink.api import SqlLabPermalinkRestApi
from superset.tags.api import TagRestApi
from superset.views.alerts import AlertView, ReportView
from superset.views.all_entities import TaggedObjectsModelView
@ -221,6 +222,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_api(SavedQueryRestApi)
appbuilder.add_api(TagRestApi)
appbuilder.add_api(SqlLabRestApi)
appbuilder.add_api(SqlLabPermalinkRestApi)
#
# Setup regular views
#

View File

@ -50,11 +50,13 @@ class KeyValueResource(StrEnum):
EXPLORE_PERMALINK = "explore_permalink"
METASTORE_CACHE = "superset_metastore_cache"
LOCK = "lock"
SQLLAB_PERMALINK = "sqllab_permalink"
class SharedKey(StrEnum):
DASHBOARD_PERMALINK_SALT = "dashboard_permalink_salt"
EXPLORE_PERMALINK_SALT = "explore_permalink_salt"
SQLLAB_PERMALINK_SALT = "sqllab_permalink_salt"
class KeyValueCodec(ABC):

View File

@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,144 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import logging
from flask import request, Response
from flask_appbuilder.api import expose, protect, safe
from marshmallow import ValidationError
from superset.commands.sql_lab.permalink.create import CreateSqlLabPermalinkCommand
from superset.commands.sql_lab.permalink.get import GetSqlLabPermalinkCommand
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.extensions import event_logger
from superset.key_value.exceptions import KeyValueAccessDeniedError
from superset.sqllab.permalink.exceptions import SqlLabPermalinkInvalidStateError
from superset.sqllab.permalink.schemas import SqlLabPermalinkSchema
from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics
logger = logging.getLogger(__name__)
class SqlLabPermalinkRestApi(BaseSupersetApi):
add_model_schema = SqlLabPermalinkSchema()
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
allow_browser_login = True
class_permission_name = "SqlLabPermalinkRestApi"
resource_name = "sqllab"
openapi_spec_tag = "SQL Lab Permanent Link"
openapi_spec_component_schemas = (SqlLabPermalinkSchema,)
@expose("/permalink", methods=("POST",))
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
log_to_statsd=False,
)
@requires_json
def post(self) -> Response:
"""Create a new permanent link for SQL Lab editor
---
post:
summary: Create a new permanent link
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExplorePermalinkStateSchema'
responses:
201:
description: The permanent link was stored successfully.
content:
application/json:
schema:
type: object
properties:
key:
type: string
description: The key to retrieve the permanent link data.
url:
type: string
description: permanent link.
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
422:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
"""
try:
state = self.add_model_schema.load(request.json)
key = CreateSqlLabPermalinkCommand(state=state).run()
http_origin = request.headers.environ.get("HTTP_ORIGIN")
url = f"{http_origin}/sqllab?p={key}"
return self.response(201, key=key, url=url)
except ValidationError as ex:
return self.response(400, message=ex.messages)
except KeyValueAccessDeniedError as ex:
return self.response(403, message=str(ex))
@expose("/permalink/<string:key>", methods=("GET",))
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get",
log_to_statsd=False,
)
def get(self, key: str) -> Response:
"""Get chart's permanent link state.
---
get:
summary: Get chart's permanent link state
parameters:
- in: path
schema:
type: string
name: key
responses:
200:
description: Returns the stored form_data.
content:
application/json:
schema:
type: object
properties:
state:
type: object
description: The stored state
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
404:
$ref: '#/components/responses/404'
422:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
"""
try:
value = GetSqlLabPermalinkCommand(key=key).run()
if not value:
return self.response_404()
return self.response(200, **value)
except SqlLabPermalinkInvalidStateError as ex:
return self.response(400, message=str(ex))

View File

@ -0,0 +1,31 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from flask_babel import lazy_gettext as _
from superset.commands.exceptions import CommandException, CreateFailedError
class SqlLabPermalinkInvalidStateError(CreateFailedError):
message = _("Invalid state.")
class SqlLabPermalinkCreateFailedError(CreateFailedError):
message = _("An error occurred while creating the copy link.")
class SqlLabPermalinkGetFailedError(CommandException):
message = _("An error occurred while accessing the copy link.")

View File

@ -0,0 +1,46 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from marshmallow import fields, Schema
class SqlLabPermalinkSchema(Schema):
autorun = fields.Boolean()
dbId = fields.Integer(
required=True,
allow_none=False,
metadata={"description": "The id of the database"},
)
name = fields.String(
required=True,
allow_none=False,
metadata={"description": "The label of the editor tab"},
)
schema = fields.String(
required=False,
allow_none=True,
metadata={"description": "The schema name of the query"},
)
sql = fields.String(
required=True,
allow_none=False,
metadata={"description": "SQL query text"},
)
templateParams = fields.String(
required=False,
allow_none=True,
metadata={"description": "stringfied JSON string for template parameters"},
)

View File

@ -0,0 +1,26 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Optional, TypedDict
class SqlLabPermalinkValue(TypedDict):
dbId: int
name: str
schema: Optional[str]
sql: str
autorun: bool
templateParams: Optional[str]

View File

@ -0,0 +1,16 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

View File

@ -0,0 +1,69 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import json
from superset import db
from superset.key_value.models import KeyValueEntry
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.constants import (
GAMMA_SQLLAB_USERNAME,
)
class TestSqlLabPermalinkApi(SupersetTestCase):
data = {
"dbId": 1,
"name": "Untitled Query 1",
"schema": "main",
"sql": "SELECT 'foo' as bar",
"autorun": False,
"templateParams": '{"param1": "value1"}',
}
def test_post(self):
self.login(GAMMA_SQLLAB_USERNAME)
resp = self.client.post("api/v1/sqllab/permalink", json=self.data)
assert resp.status_code == 201
data = resp.json
key = data["key"]
url = data["url"]
assert key in url
db.session.query(KeyValueEntry).filter_by(id=key).delete()
db.session.commit()
def test_post_access_denied(self):
resp = self.client.post("api/v1/sqllab/permalink", json=self.data)
assert resp.status_code == 401
def test_post_invalid_schema(self):
self.login(GAMMA_SQLLAB_USERNAME)
resp = self.client.post(
"api/v1/sqllab/permalink", json={"name": "Untitled Query 1", "sql": "Test"}
)
assert resp.status_code == 400
def test_get(self):
self.login(GAMMA_SQLLAB_USERNAME)
resp = self.client.post("api/v1/sqllab/permalink", json=self.data)
data = resp.json
key = data["key"]
resp = self.client.get(f"api/v1/sqllab/permalink/{key}")
assert resp.status_code == 200
result = json.loads(resp.data.decode("utf-8"))
assert result == self.data
db.session.query(KeyValueEntry).filter_by(id=key).delete()
db.session.commit()