mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-21 02:28:12 +08:00
This splits the backend into two parts: the local backend for desktop cellxgene and the AWS backend for hosted cellxgene. The local backend is in local_server while the hosted remains in server. The general idea is to copy everything from server to local_server, pull unneeded stuff out of local_server, and keep server as-is for this PR. Not touching server means all the infra and deployment code will continue working just as it did before so we can make those changes incrementally.
324 lines
14 KiB
Python
324 lines
14 KiB
Python
import os
|
|
import unittest
|
|
from unittest import mock
|
|
from unittest.mock import patch
|
|
|
|
from server.common.config.base_config import BaseConfig
|
|
from server.common.utils.utils import find_available_port
|
|
from server.test import PROJECT_ROOT, FIXTURES_ROOT
|
|
|
|
import requests
|
|
|
|
from server.common.config.app_config import AppConfig
|
|
from server.common.errors import ConfigurationError
|
|
from server.test import test_server
|
|
from server.test.unit.common.config import ConfigTests
|
|
|
|
|
|
def mockenv(**envvars):
|
|
return mock.patch.dict(os.environ, envvars)
|
|
|
|
|
|
class TestServerConfig(ConfigTests):
|
|
def setUp(self):
|
|
self.config_file_name = f"{unittest.TestCase.id(self).split('.')[-1]}.yml"
|
|
self.config = AppConfig()
|
|
self.config.update_server_config(app__flask_secret_key="secret")
|
|
self.config.update_server_config(multi_dataset__dataroot=FIXTURES_ROOT)
|
|
self.server_config = self.config.server_config
|
|
self.config.complete_config()
|
|
|
|
message_list = []
|
|
|
|
def noop(message):
|
|
message_list.append(message)
|
|
|
|
messagefn = noop
|
|
self.context = dict(messagefn=messagefn, messages=message_list)
|
|
|
|
def get_config(self, **kwargs):
|
|
file_name = self.custom_app_config(
|
|
dataroot=f"{FIXTURES_ROOT}", config_file_name=self.config_file_name, **kwargs
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
return config
|
|
|
|
def test_init_raises_error_if_default_config_is_invalid(self):
|
|
invalid_config = self.get_config(port="not_valid")
|
|
with self.assertRaises(ConfigurationError):
|
|
invalid_config.complete_config()
|
|
|
|
@patch("server.common.config.server_config.BaseConfig.validate_correct_type_of_configuration_attribute")
|
|
def test_complete_config_checks_all_attr(self, mock_check_attrs):
|
|
mock_check_attrs.side_effect = BaseConfig.validate_correct_type_of_configuration_attribute()
|
|
self.server_config.complete_config(self.context)
|
|
self.assertEqual(mock_check_attrs.call_count, 41)
|
|
|
|
def test_handle_app__throws_error_if_port_doesnt_exist(self):
|
|
config = self.get_config(port=99999999)
|
|
with self.assertRaises(ConfigurationError):
|
|
config.server_config.handle_app(self.context)
|
|
|
|
@patch("server.common.config.server_config.discover_s3_region_name")
|
|
def test_handle_data_locator_works_for_default_types(self, mock_discover_region_name):
|
|
mock_discover_region_name.return_value = None
|
|
# Default config
|
|
self.assertEqual(self.config.server_config.data_locator__s3__region_name, None)
|
|
# hard coded
|
|
config = self.get_config()
|
|
self.assertEqual(config.server_config.data_locator__s3__region_name, "us-east-1")
|
|
# incorrectly formatted
|
|
dataroot = {
|
|
"d1": {"base_url": "set1", "dataroot": "/path/to/set1_datasets/"},
|
|
"d2": {"base_url": "set2/subdir", "dataroot": "s3://shouldnt/work"},
|
|
}
|
|
file_name = self.custom_app_config(
|
|
dataroot=dataroot, config_file_name=self.config_file_name, data_locater_region_name="true"
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
with self.assertRaises(ConfigurationError):
|
|
config.server_config.handle_data_locator()
|
|
|
|
@patch("server.common.config.server_config.discover_s3_region_name")
|
|
def test_handle_data_locator_can_read_from_dataroot(self, mock_discover_region_name):
|
|
mock_discover_region_name.return_value = "us-west-2"
|
|
dataroot = {
|
|
"d1": {"base_url": "set1", "dataroot": "/path/to/set1_datasets/"},
|
|
"d2": {"base_url": "set2/subdir", "dataroot": "s3://hosted-cellxgene-dev"},
|
|
}
|
|
file_name = self.custom_app_config(
|
|
dataroot=dataroot, config_file_name=self.config_file_name, data_locater_region_name="true"
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
config.server_config.handle_data_locator()
|
|
self.assertEqual(config.server_config.data_locator__s3__region_name, "us-west-2")
|
|
mock_discover_region_name.assert_called_once_with("s3://hosted-cellxgene-dev")
|
|
|
|
def test_handle_app___can_use_envar_port(self):
|
|
config = self.get_config(port=24)
|
|
self.assertEqual(config.server_config.app__port, 24)
|
|
|
|
# Note if the port is set in the config file it will NOT be overwritten by a different envvar
|
|
os.environ["CXG_SERVER_PORT"] = "4008"
|
|
self.config = AppConfig()
|
|
self.config.update_server_config(app__flask_secret_key="secret")
|
|
self.config.server_config.handle_app(self.context)
|
|
self.assertEqual(self.config.server_config.app__port, 4008)
|
|
del os.environ["CXG_SERVER_PORT"]
|
|
|
|
def test_handle_app__can_get_secret_key_from_envvar_or_config_file_with_envvar_given_preference(self):
|
|
config = self.get_config(flask_secret_key="KEY_FROM_FILE")
|
|
self.assertEqual(config.server_config.app__flask_secret_key, "KEY_FROM_FILE")
|
|
|
|
os.environ["CXG_SECRET_KEY"] = "KEY_FROM_ENV"
|
|
config.external_config.handle_environment(self.context)
|
|
self.assertEqual(config.server_config.app__flask_secret_key, "KEY_FROM_ENV")
|
|
|
|
def test_handle_app__sets_web_base_url(self):
|
|
config = self.get_config(web_base_url="anything.com")
|
|
self.assertEqual(config.server_config.app__web_base_url, "anything.com")
|
|
|
|
def test_handle_auth__gets_client_secret_from_envvars_or_config_with_envvars_given_preference(self):
|
|
config = self.get_config(client_secret="KEY_FROM_FILE")
|
|
config.server_config.handle_authentication()
|
|
self.assertEqual(config.server_config.authentication__params_oauth__client_secret, "KEY_FROM_FILE")
|
|
|
|
os.environ["CXG_OAUTH_CLIENT_SECRET"] = "KEY_FROM_ENV"
|
|
config.external_config.handle_environment(self.context)
|
|
|
|
self.assertEqual(config.server_config.authentication__params_oauth__client_secret, "KEY_FROM_ENV")
|
|
|
|
def test_handle_data_source__errors_when_passed_zero_or_two_dataroots(self):
|
|
file_name = self.custom_app_config(
|
|
dataroot=f"{FIXTURES_ROOT}",
|
|
config_file_name="two_data_roots.yml",
|
|
dataset_datapath=f"{FIXTURES_ROOT}/pbmc3k-CSC-gz.h5ad",
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
with self.assertRaises(ConfigurationError):
|
|
config.server_config.handle_data_source()
|
|
|
|
file_name = self.custom_app_config(config_file_name="zero_roots.yml")
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
with self.assertRaises(ConfigurationError):
|
|
config.server_config.handle_data_source()
|
|
|
|
def test_get_api_base_url_works(self):
|
|
|
|
# test the api_base_url feature, and that it can contain a path
|
|
config = AppConfig()
|
|
backend_port = find_available_port("localhost", 10000)
|
|
config.update_server_config(
|
|
app__flask_secret_key="secret",
|
|
app__api_base_url=f"http://localhost:{backend_port}/additional/path",
|
|
multi_dataset__dataroot=f"{PROJECT_ROOT}/example-dataset",
|
|
)
|
|
|
|
config.complete_config()
|
|
|
|
with test_server(["-p", str(backend_port)], app_config=config) as server:
|
|
session = requests.Session()
|
|
self.assertEqual(server, f"http://localhost:{backend_port}")
|
|
response = session.get(f"{server}/additional/path/d/pbmc3k.h5ad/api/v0.2/config")
|
|
self.assertEqual(response.status_code, 200)
|
|
data_config = response.json()
|
|
self.assertEqual(data_config["config"]["displayNames"]["dataset"], "pbmc3k")
|
|
|
|
# test the health check at the correct url
|
|
response = session.get(f"{server}/additional/path/health")
|
|
assert response.json()["status"] == "pass"
|
|
|
|
def test_get_web_base_url_works(self):
|
|
config = self.get_config(web_base_url="www.thisisawebsite.com")
|
|
web_base_url = config.server_config.get_web_base_url()
|
|
self.assertEqual(web_base_url, "www.thisisawebsite.com")
|
|
|
|
config = self.get_config(web_base_url="local", port=12)
|
|
web_base_url = config.server_config.get_web_base_url()
|
|
self.assertEqual(web_base_url, "http://localhost:12")
|
|
|
|
config = self.get_config(web_base_url="www.thisisawebsite.com/")
|
|
web_base_url = config.server_config.get_web_base_url()
|
|
self.assertEqual(web_base_url, "www.thisisawebsite.com")
|
|
|
|
config = self.get_config(api_base_url="www.api_base.com/")
|
|
web_base_url = config.server_config.get_web_base_url()
|
|
self.assertEqual(web_base_url, "www.api_base.com")
|
|
|
|
def test_config_for_single_dataset(self):
|
|
file_name = self.custom_app_config(
|
|
config_file_name="single_dataset.yml", dataset_datapath=f"{FIXTURES_ROOT}/pbmc3k.cxg"
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
config.server_config.handle_single_dataset(self.context)
|
|
self.assertIsNotNone(config.server_config.matrix_data_cache_manager)
|
|
|
|
file_name = self.custom_app_config(
|
|
config_file_name="single_dataset_with_about.yml",
|
|
about="www.cziscience.com",
|
|
dataset_datapath=f"{FIXTURES_ROOT}/pbmc3k.cxg",
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(file_name)
|
|
with self.assertRaises(ConfigurationError):
|
|
config.server_config.handle_single_dataset(self.context)
|
|
|
|
def test_multi_dataset_raises_error_for_illegal_routes(self):
|
|
# test for illegal url_dataroots
|
|
for illegal in ("../b", "!$*", "\\n", "", "(bad)"):
|
|
self.config.update_server_config(
|
|
multi_dataset__dataroot={"tag": {"base_url": illegal, "dataroot": f"{PROJECT_ROOT}/example-dataset"}}
|
|
)
|
|
with self.assertRaises(ConfigurationError):
|
|
self.config.complete_config()
|
|
|
|
def test_multidataset_works_for_legal_routes(self):
|
|
# test for legal url_dataroots
|
|
for legal in ("d", "this.is-okay_", "a/b"):
|
|
self.config.update_server_config(
|
|
multi_dataset__dataroot={"tag": {"base_url": legal, "dataroot": f"{PROJECT_ROOT}/example-dataset"}}
|
|
)
|
|
self.config.complete_config()
|
|
|
|
def test_mulitdatasets_work_e2e(self):
|
|
# test that multi dataroots work end to end
|
|
self.config.update_server_config(
|
|
multi_dataset__dataroot=dict(
|
|
s1=dict(dataroot=f"{PROJECT_ROOT}/example-dataset", base_url="set1/1/2"),
|
|
s2=dict(dataroot=f"{FIXTURES_ROOT}", base_url="set2"),
|
|
s3=dict(dataroot=f"{FIXTURES_ROOT}", base_url="set3"),
|
|
)
|
|
)
|
|
|
|
# Change this default to test if the dataroot overrides below work.
|
|
self.config.update_default_dataset_config(app__about_legal_tos="tos_default.html")
|
|
|
|
# specialize the configs for set1
|
|
self.config.add_dataroot_config(
|
|
"s1", user_annotations__enable=False, diffexp__enable=True, app__about_legal_tos="tos_set1.html"
|
|
)
|
|
|
|
# specialize the configs for set2
|
|
self.config.add_dataroot_config(
|
|
"s2", user_annotations__enable=True, diffexp__enable=False, app__about_legal_tos="tos_set2.html"
|
|
)
|
|
|
|
# no specializations for set3 (they get the default dataset config)
|
|
self.config.complete_config()
|
|
|
|
with test_server(app_config=self.config) as server:
|
|
session = requests.Session()
|
|
|
|
response = session.get(f"{server}/set1/1/2/pbmc3k.h5ad/api/v0.2/config")
|
|
data_config = response.json()
|
|
assert data_config["config"]["displayNames"]["dataset"] == "pbmc3k"
|
|
assert data_config["config"]["parameters"]["annotations"] is False
|
|
assert data_config["config"]["parameters"]["disable-diffexp"] is False
|
|
assert data_config["config"]["parameters"]["about_legal_tos"] == "tos_set1.html"
|
|
|
|
response = session.get(f"{server}/set2/pbmc3k.cxg/api/v0.2/config")
|
|
data_config = response.json()
|
|
assert data_config["config"]["displayNames"]["dataset"] == "pbmc3k"
|
|
assert data_config["config"]["parameters"]["annotations"] is True
|
|
assert data_config["config"]["parameters"]["about_legal_tos"] == "tos_set2.html"
|
|
|
|
response = session.get(f"{server}/set3/pbmc3k.cxg/api/v0.2/config")
|
|
data_config = response.json()
|
|
assert data_config["config"]["displayNames"]["dataset"] == "pbmc3k"
|
|
assert data_config["config"]["parameters"]["annotations"] is True
|
|
assert data_config["config"]["parameters"]["disable-diffexp"] is False
|
|
assert data_config["config"]["parameters"]["about_legal_tos"] == "tos_default.html"
|
|
|
|
response = session.get(f"{server}/health")
|
|
assert response.json()["status"] == "pass"
|
|
|
|
# access a dataset (no slash)
|
|
response = session.get(f"{server}/set2/pbmc3k.cxg")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
# access a dataset (with slash)
|
|
response = session.get(f"{server}/set2/pbmc3k.cxg/")
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@patch("server.common.config.server_config.diffexp_tiledb.set_config")
|
|
def test_handle_diffexp(self, mock_tiledb_config):
|
|
custom_config_file = self.custom_app_config(
|
|
dataroot=f"{FIXTURES_ROOT}",
|
|
cpu_multiplier=3,
|
|
diffexp_max_workers=1,
|
|
target_workunit=4,
|
|
config_file_name=self.config_file_name,
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(custom_config_file)
|
|
config.server_config.handle_diffexp()
|
|
# called with the min of diffexp_max_workers and cpus*cpu_multiplier
|
|
mock_tiledb_config.assert_called_once_with(1, 4)
|
|
|
|
@patch("server.data_cxg.cxg_adaptor.CxgAdaptor.set_tiledb_context")
|
|
def test_handle_adaptor(self, mock_tiledb_context):
|
|
custom_config = self.custom_app_config(
|
|
dataroot=f"{FIXTURES_ROOT}", cxg_tile_cache_size=10, cxg_num_reader_threads=2
|
|
)
|
|
config = AppConfig()
|
|
config.update_from_config_file(custom_config)
|
|
config.server_config.handle_adaptor()
|
|
mock_tiledb_context.assert_called_once_with(
|
|
{"sm.tile_cache_size": 10, "sm.num_reader_threads": 2, "vfs.s3.region": "us-east-1"}
|
|
)
|
|
|
|
def test_test_auth_only_in_insecure(self):
|
|
|
|
config = self.get_config(auth_type="test")
|
|
with self.assertRaises(ConfigurationError):
|
|
config.complete_config()
|
|
|
|
config.update_server_config(authentication__insecure_test_environment=True)
|
|
config.complete_config()
|