Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update all languages in Weblate, but only offer supported ones in securedrop-admin sdconfig #6557

Merged
merged 4 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions admin/securedrop_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
MAX_NAMESERVERS = 3
LIST_SPLIT_RE = re.compile(r"\s*,\s*|\s+")

I18N_CONF = "securedrop/i18n.json"
I18N_DEFAULT_LOCALES = {"en_US"}


class FingerprintException(Exception):
pass
Expand Down Expand Up @@ -204,24 +207,27 @@ def __init__(self, appdir: str) -> None:
self.translation_dir = os.path.realpath(os.path.join(appdir, "translations"))

def get_translations(self) -> Set[str]:
translations = set(["en_US"])
translations = I18N_DEFAULT_LOCALES
for dirname in os.listdir(self.translation_dir):
if dirname != "messages.pot":
translations.add(dirname)
return translations

class ValidateLocales(Validator):
def __init__(self, basedir: str) -> None:
self.basedir = basedir
def __init__(self, basedir: str, supported: Set[str]) -> None:
present = SiteConfig.Locales(basedir).get_translations()
self.available = present & supported

super(SiteConfig.ValidateLocales, self).__init__()

def validate(self, document: Document) -> bool:
desired = document.text.split()
existing = SiteConfig.Locales(self.basedir).get_translations()
missing = set(desired) - set(existing)
missing = set(desired) - self.available
if not missing:
return True
raise ValidationError(message="The following locales do not exist " + " ".join(missing))
raise ValidationError(
message="The following locales are not available " + " ".join(missing)
)

class ValidateOSSECUsername(Validator):
def validate(self, document: Document) -> bool:
Expand Down Expand Up @@ -268,8 +274,14 @@ def __init__(self, args: argparse.Namespace) -> None:
# Hold runtime configuration before save, to support
# referencing other responses during validation
self._config_in_progress = {} # type: Dict
translations = SiteConfig.Locales(self.args.app_path).get_translations()
translations_as_str = " ".join(translations)

supported_locales = I18N_DEFAULT_LOCALES.copy()
i18n_conf_path = os.path.join(args.root, I18N_CONF)
if os.path.exists(i18n_conf_path):
with open(i18n_conf_path) as i18n_conf_file:
i18n_conf = json.load(i18n_conf_file)
supported_locales.update(set(i18n_conf["supported_locales"].keys()))
locale_validator = SiteConfig.ValidateLocales(self.args.app_path, supported_locales)

self.desc = [
(
Expand Down Expand Up @@ -503,8 +515,8 @@ def __init__(self, args: argparse.Namespace) -> None:
[],
list,
"Space separated list of additional locales to support "
"(" + translations_as_str + ")",
SiteConfig.ValidateLocales(self.args.app_path),
"(" + " ".join(sorted(list(locale_validator.available))) + ")",
locale_validator,
str.split,
lambda config: True,
),
Expand Down
16 changes: 16 additions & 0 deletions admin/tests/files/securedrop/i18n.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"supported_locales": {
"de_DE": {
"name": "German",
"desktop": "de_DE"
},
"es_ES": {
"name": "Spanish",
"desktop": "es_ES"
},
"fr_FR": {
"name": "French",
"desktop": "fr"
}
}
}
3 changes: 3 additions & 0 deletions admin/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def setup_function(function):
for name in ["de_DE", "es_ES", "fr_FR", "pt_BR"]:
dircmd = "mkdir -p {0}/securedrop/translations/{1}".format(SD_DIR, name)
subprocess.check_call(dircmd.split())
subprocess.check_call(
"cp {0}/files/securedrop/i18n.json {1}/securedrop".format(CURRENT_DIR, SD_DIR).split()
)


def teardown_function(function):
Expand Down
80 changes: 59 additions & 21 deletions admin/tests/test_securedrop-admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,13 +480,16 @@ def test_exit_codes(self, tmpdir):


class TestSiteConfig(object):
def test_exists(self):
def test_exists(self, tmpdir):
args = argparse.Namespace(
site_config="DOES_NOT_EXIST", ansible_path=".", app_path=dirname(__file__)
site_config="DOES_NOT_EXIST",
ansible_path=".",
app_path=dirname(__file__),
root=tmpdir,
)
assert not securedrop_admin.SiteConfig(args).exists()
args = argparse.Namespace(
site_config=__file__, ansible_path=".", app_path=dirname(__file__)
site_config=__file__, ansible_path=".", app_path=dirname(__file__), root=tmpdir
)
assert securedrop_admin.SiteConfig(args).exists()

Expand Down Expand Up @@ -623,9 +626,12 @@ def test_validate_optional_fingerprint(self):
assert validator.validate(Document("012345678901234567890123456789ABCDEFABCD"))
assert validator.validate(Document(""))

def test_sanitize_fingerprint(self):
def test_sanitize_fingerprint(self, tmpdir):
args = argparse.Namespace(
site_config="DOES_NOT_EXIST", ansible_path=".", app_path=dirname(__file__)
site_config="DOES_NOT_EXIST",
ansible_path=".",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
assert "ABC" == site_config.sanitize_fingerprint(" A bc")
Expand All @@ -643,7 +649,9 @@ def test_locales(self):
assert "fr_FR" in translations

def test_validate_locales(self):
validator = securedrop_admin.SiteConfig.ValidateLocales(dirname(__file__))
validator = securedrop_admin.SiteConfig.ValidateLocales(
dirname(__file__), {"en_US", "fr_FR"}
)
assert validator.validate(Document("en_US fr_FR "))
with pytest.raises(ValidationError) as e:
validator.validate(Document("BAD"))
Expand All @@ -652,7 +660,10 @@ def test_validate_locales(self):
def test_save(self, tmpdir):
site_config_path = join(str(tmpdir), "site_config")
args = argparse.Namespace(
site_config=site_config_path, ansible_path=".", app_path=dirname(__file__)
site_config=site_config_path,
ansible_path=".",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
site_config.config = {"var1": "val1", "var2": "val2"}
Expand All @@ -665,9 +676,12 @@ def test_save(self, tmpdir):
)
assert expected == io.open(site_config_path).read()

def test_validate_gpg_key(self, caplog):
def test_validate_gpg_key(self, tmpdir, caplog):
args = argparse.Namespace(
site_config="INVALID", ansible_path="tests/files", app_path=dirname(__file__)
site_config="INVALID",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
good_config = {
"securedrop_app_gpg_public_key": "test_journalist_key.pub",
Expand All @@ -689,9 +703,12 @@ def test_validate_gpg_key(self, caplog):
site_config.validate_gpg_keys()
assert "FAIL does not match" in str(e)

def test_journalist_alert_email(self):
def test_journalist_alert_email(self, tmpdir):
args = argparse.Namespace(
site_config="INVALID", ansible_path="tests/files", app_path=dirname(__file__)
site_config="INVALID",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
site_config.config = {
Expand Down Expand Up @@ -723,6 +740,7 @@ def test_update_config(self, mock_save, mock_validate_input):
site_config="tests/files/site-specific",
ansible_path="tests/files",
app_path=dirname(__file__),
root="tests/files",
)
site_config = securedrop_admin.SiteConfig(args)

Expand All @@ -736,19 +754,23 @@ def test_update_config(self, mock_save, mock_validate_input):
def test_update_config_no_site_specific(self, validate_gpg_keys, mock_validate_input, tmpdir):
site_config_path = join(str(tmpdir), "site_config")
args = argparse.Namespace(
site_config=site_config_path, ansible_path=".", app_path=dirname(__file__)
site_config=site_config_path,
ansible_path=".",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
assert site_config.load_and_update_config()
mock_validate_input.assert_called()
validate_gpg_keys.assert_called_once()
assert exists(site_config_path)

def test_load_and_update_config(self):
def test_load_and_update_config(self, tmpdir):
args = argparse.Namespace(
site_config="tests/files/site-specific",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
with mock.patch("securedrop_admin.SiteConfig.update_config"):
Expand All @@ -759,14 +781,18 @@ def test_load_and_update_config(self):
site_config="tests/files/site-specific-missing-entries",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
with mock.patch("securedrop_admin.SiteConfig.update_config"):
site_config.load_and_update_config()
assert site_config.config != {}

args = argparse.Namespace(
site_config="UNKNOWN", ansible_path="tests/files", app_path=dirname(__file__)
site_config="UNKNOWN",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
with mock.patch("securedrop_admin.SiteConfig.update_config"):
Expand Down Expand Up @@ -797,7 +823,7 @@ def verify_prompt_boolean(self, site_config, desc):
assert site_config.user_prompt_config_one(desc, "YES") is True
assert site_config.user_prompt_config_one(desc, "NO") is False

def test_desc_conditional(self):
def test_desc_conditional(self, tmpdir):
"""Ensure that conditional prompts behave correctly.

Prompts which depend on another question should only be
Expand Down Expand Up @@ -827,6 +853,7 @@ def test_desc_conditional(self):
site_config="tests/files/site-specific",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
site_config.desc = questions
Expand Down Expand Up @@ -904,9 +931,12 @@ def verify_prompt_securedrop_supported_locales(self, site_config, desc):
with pytest.raises(ValidationError):
site_config.user_prompt_config_one(desc, "wrong")

def test_user_prompt_config_one(self):
def test_user_prompt_config_one(self, tmpdir):
args = argparse.Namespace(
site_config="UNKNOWN", ansible_path="tests/files", app_path=dirname(__file__)
site_config="UNKNOWN",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)

Expand All @@ -922,9 +952,12 @@ def auto_prompt(prompt, default, **kwargs):
print("checking " + method)
getattr(self, method)(site_config, desc)

def test_validated_input(self):
def test_validated_input(self, tmpdir):
args = argparse.Namespace(
site_config="UNKNOWN", ansible_path="tests/files", app_path=dirname(__file__)
site_config="UNKNOWN",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)

Expand All @@ -941,17 +974,21 @@ def auto_prompt(prompt, default, **kwargs):
assert "a b" == site_config.validated_input("", ["a", "b"], lambda: True, None)
assert "{}" == site_config.validated_input("", {}, lambda: True, None)

def test_load(self, caplog):
def test_load(self, tmpdir, caplog):
args = argparse.Namespace(
site_config="tests/files/site-specific",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
assert "app_hostname" in site_config.load()

args = argparse.Namespace(
site_config="UNKNOWN", ansible_path="tests/files", app_path=dirname(__file__)
site_config="UNKNOWN",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
with pytest.raises(IOError) as e:
Expand All @@ -963,6 +1000,7 @@ def test_load(self, caplog):
site_config="tests/files/corrupted",
ansible_path="tests/files",
app_path=dirname(__file__),
root=tmpdir,
)
site_config = securedrop_admin.SiteConfig(args)
with pytest.raises(yaml.YAMLError) as e:
Expand Down
Loading