diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py index 895c13ab40..8abbee3fc3 100644 --- a/src/pretix/plugins/wallet/api.py +++ b/src/pretix/plugins/wallet/api.py @@ -49,7 +49,7 @@ class WalletLayoutSerializer(I18nAwareModelSerializer): style = platform_styles[data["style"]] layout = PassLayout(style=style, layout=data["layout"]) - context = {"placeholders": {k: {"content": v['content']} for k,v in get_layout_variables(self.context['event']).items()}} + context = {"placeholders": get_layout_variables(self.context['event'])} layout.validate(context=context) return data diff --git a/src/pretix/plugins/wallet/styles/apple.py b/src/pretix/plugins/wallet/styles/apple.py index 20ce2fc458..37abad3594 100644 --- a/src/pretix/plugins/wallet/styles/apple.py +++ b/src/pretix/plugins/wallet/styles/apple.py @@ -1,14 +1,12 @@ from .base import ( - FieldEntry, FieldEntryType, - FieldContentType, ImageFieldGroup, PlaceholderFieldGroup, + PredefinedFieldGroup, TextFieldGroup, WalletPlatform, PassStyle, PlaceholderFieldEntry, - CustomFieldEntry, ) from django.utils.translation import gettext as _ from i18nfield.strings import LazyI18nString @@ -17,10 +15,10 @@ import hashlib import zipfile import cryptography import cryptography.hazmat.primitives.serialization.pkcs7 -# from cryptography import x509 -# from cryptography.hazmat.primitives import hashes, serialization -# from cryptography.hazmat.primitives.serialization import pkcs7 import json +from django.contrib.staticfiles import finders + + class ApplePlatform(WalletPlatform): identifier = "apple" @@ -36,32 +34,39 @@ class StringResource: self.entries = {} self.locales = set(locales) - def add_entry(self, key: str, value: LazyI18nString): # TODO: replace LazyI18nString with dict or handle strings where data == "" + def add_entry(self, key: str, value: LazyI18nString): if key in self.entries: raise ValueError(f"{key} already exists in this StringResource") self.entries[key] = value - if isinstance(value.data, dict): - self.locales |= value.data.keys() def escape(self, string): - return string.translate(str.maketrans({"\"": "\\\"", "\r": "\\r", "\n": "\\n", "\\": "\\\\"})) + return string.translate( + str.maketrans({'"': '\\"', "\r": "\\r", "\n": "\\n", "\\": "\\\\"}) + ) + def generate_resource(self, language): output = "" for key, entry in self.entries.items(): - output += f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n' + output += ( + f'"{self.escape(key)}" = "{self.escape(entry.localize(language))}";\n' + ) return output.strip() - + def generate(self): return {language: self.generate_resource(language) for language in self.locales} - class SignedZipFile: - """ Generates a zip-file with manifest and signature as apple expects a pkpass file to be """ + """Generates a zip-file with manifest and signature as apple expects a pkpass file to be""" + def __init__(self, ca_certificate, certificate, key, password): - self.ca_certificate = cryptography.x509.load_pem_x509_certificate(ca_certificate) + self.ca_certificate = cryptography.x509.load_pem_x509_certificate( + ca_certificate + ) self.certificate = cryptography.x509.load_pem_x509_certificate(certificate) - self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key(key, password) + self.key = cryptography.hazmat.primitives.serialization.load_pem_private_key( + key, password + ) self.password = password self.file = io.BytesIO() @@ -107,22 +112,45 @@ class SignedZipFile: class AppleWalletStyle(PassStyle): platform = ApplePlatform - def generate_pass_json(self, layout, context): - pass_json = {} + def pass_content(self, layout, context, strings): + raise NotImplementedError() + + def generate_pass_json(self, layout, context, strings): + def add_from_context(key): + value = context.get(key) + if not value: + raise ValueError(f"{key} must be set to a truthy value") + return value + + pass_json = { + "formatVersion": 1, + "description": add_from_context("description"), + "organizationName": add_from_context("organizationName"), + "passTypeIdentifier": add_from_context("passTypeIdentifier"), + "teamIdentifier": add_from_context("teamIdentifier"), + "serialNumber": add_from_context("serialNumber"), + **self.pass_content(layout, context, strings), + } return pass_json - + def generate(self, layout, context): - for key in ["certificate", "key", "wwdr_certificate", "password"]: + for key in ["ca_certificate", "certificate", "key", "password", "locales"]: if key not in context: raise ValueError(f"{key} missing from context") pkpass = SignedZipFile( + context["ca_certificate"], context["certificate"], context["key"], - context["wwdr_certificate"], context["password"], ) + strings = StringResource(locales=context['locales']) + + pass_json = self.generate_pass_json(layout, context, strings) + print(pass_json) + pkpass.add_file( + "icon.png", open(finders.find("pretix_passbook/icon.png"), "rb").read() + ) - pass_json = self.generate_pass_json() pkpass.add_file("pass.json", json.dumps(pass_json)) return pkpass.finish() @@ -134,7 +162,7 @@ class AppleWalletEventTicket(AppleWalletStyle): ImageFieldGroup( identifier="logo", name=_("Logo"), - min_entries=1, + min_entries=0, max_entries=1, labels=False, default_entries=[ @@ -166,3 +194,63 @@ class AppleWalletEventTicket(AppleWalletStyle): TextFieldGroup(identifier="back", name=_("Back")), ] # preview_image = "apple/event_ticket.svg" + + def get_pass_fields(self, layout, context): + fields = {} + for group in self.fieldgroups: + if isinstance(group, PredefinedFieldGroup): + pass + elif isinstance(group, PlaceholderFieldGroup): + group_fields = [] + if group.identifier in layout["fieldgroups"]: + for field in layout["fieldgroups"][group.identifier]["entries"]: + field_entry = {} + if group.labels: + field_entry["label"] = LazyI18nString(field["label"]) + if field["type"] == FieldEntryType.PLACEHOLDER.value: + placeholder = ( + context.get("placeholders") + .get(group.content_type.value, {}) + .get(field["content"]) + ) + if placeholder: + placeholder_value = placeholder["evaluate"]( + *context.get("evaluation_context", []) + ) + if placeholder_value: + field_entry["value"] = placeholder_value + elif field["type"] == FieldEntryType.TEXT.value: + placeholder_value = LazyI18nString(field["content"]) + elif field["type"] == FieldEntryType.IMAGE.value: + raise NotImplementedError( + "Image placeholders not implemented" + ) + if "value" in field_entry and field_entry["value"]: + group_fields.append(field_entry) + if group.min_entries and len(group_fields) < group.min_entries: + raise ValueError( + f"Group {group.identifier} needs at least {group.min_entries} entries, but only {len(group_fields)} were provided" + ) + fields[group.identifier] = group_fields[: group.max_entries] + else: + raise ValueError("Unknown field group") + return fields + + def convert_fields(self, strings, fields): + converted = [] + for i,f in enumerate(fields): + converted_field = {**f, "key": f"primary-{i}"} + if "label" in converted_field and isinstance(converted_field['label'], LazyI18nString): + strings.add_entry(f"primary-{i}-label", converted_field['label']) + converted_field['label'] = f"primary-{i}-label" + + converted.append(converted_field) + return converted + + def pass_content(self, layout, context, strings): + fields = self.get_pass_fields(layout, context) + return { + "eventTicket": { + "primaryFields": self.convert_fields(strings, fields['primary']) + } + } diff --git a/src/pretix/plugins/wallet/styles/base.py b/src/pretix/plugins/wallet/styles/base.py index ff8e05358d..ebcc347f96 100644 --- a/src/pretix/plugins/wallet/styles/base.py +++ b/src/pretix/plugins/wallet/styles/base.py @@ -293,5 +293,6 @@ class PassLayout: raise ValidationError("Invalid layout: {}".format(str(e))) def generate(self, context): + # TODO: how to handle nonexisting placeholders here? self.validate(context) return self.style.generate(self.layout, context) \ No newline at end of file diff --git a/src/pretix/plugins/wallet/ticketoutput.py b/src/pretix/plugins/wallet/ticketoutput.py index a0dad0bdc6..620015a57b 100644 --- a/src/pretix/plugins/wallet/ticketoutput.py +++ b/src/pretix/plugins/wallet/ticketoutput.py @@ -26,6 +26,11 @@ from pretix.base.models import Event from pretix.base.settings import SettingsSandbox from django.template.loader import render_to_string +from .styles import AVAILABLE_STYLES_DICT + +from .models import WalletLayout +from .views import get_layout_variables + logger = logging.getLogger("pretix.plugins.wallet") @@ -67,5 +72,60 @@ class AppleWalletTicketOutput(WalletOutput): verbose_name = _("Apple") download_button_text = "Add to Apple Wallet" + def generate(self, op): + order = op.order + event = order.event + filename = "{}-{}.pkpass".format(order.event.slug, order.code) + + # layout = self.override_layout_signal.send_chained( + # order.event, 'layout', orderposition=op, layout=self.layout_map.get( + # (op.item_id, self.override_channel or order.sales_channel.identifier), + # self.layout_map.get( + # (op.item_id, 'web'), + # self.default_layout + # ) + # ) + # ) + layout = WalletLayout.objects.get(pk=1) + + ticket = str(op.item.name) + if op.variation: + ticket += " - " + str(op.variation) + + serialNumber = "%s-%s-%s-%d" % ( + order.event.organizer.slug, + order.event.slug, + order.code, + op.pk, + ) + + context = { + "placeholders": get_layout_variables(op.order.event), + "evaluation_context": [op, order, order.event], + "ca_certificate": open( + "/Users/engelhardt/code/tmp/wallet/apple/ca_cert.pem", "rb" + ).read(), + "certificate": open( + "/Users/engelhardt/code/tmp/wallet/apple/cert.pem", "rb" + ).read(), + "key": open( + "/Users/engelhardt/code/tmp/wallet/apple/secret_key.pem", "rb" + ).read(), + "password": None, + "description": _("Ticket for {event} ({product})").format( # TODO: i18n + event=event.name, product=ticket + ), + "organizationName": event.organizer.name, + "passTypeIdentifier": "pass.test.test", + "teamIdentifier": "TEST123456", + "serialNumber": serialNumber, + "locales": event.settings.locales + } + assert layout.platform == "apple" + data = AVAILABLE_STYLES_DICT[layout.platform][layout.style].generate( + layout.layout, context + ) + return filename, "application/vnd.apple.pkpass", data + OUTPUTS = [WalletSettingsHolder, GoogleWalletTicketOutput, AppleWalletTicketOutput] diff --git a/src/pretix/plugins/wallet/views.py b/src/pretix/plugins/wallet/views.py index 5e40a2c311..9000425bd6 100644 --- a/src/pretix/plugins/wallet/views.py +++ b/src/pretix/plugins/wallet/views.py @@ -15,19 +15,20 @@ from .styles import AVAILABLE_STYLES, AVAILABLE_PLATFORMS def get_layout_variables(event): return { - "text": { - varname: {"label": var["label"], "editor_sample": var["editor_sample"]} - for varname, var in get_variables(event).items() - }, - "image": { - varname: {"label": var["label"]} - for varname, var in get_images(event).items() - } + "text": get_variables(event), + "image": get_images(event) | {"poweredby": {"label": _("pretix-Logo")}}, # TODO: image upload } + def get_editor_variables(event): - return {t: {vid: {"label": v.get("label"), "editor_sample": v.get("editor_sample")} for vid,v in vs.items()} for t,vs in get_layout_variables(event).items()} + return { + t: { + vid: {"label": v.get("label"), "editor_sample": v.get("editor_sample")} + for vid, v in vs.items() + } + for t, vs in get_layout_variables(event).items() + } # TODO: should this even be a list view? @@ -65,7 +66,10 @@ class LayoutEditorView(DetailView): style.identifier: style.asdict() for style in self.get_platform_styles() } context["variables"] = get_editor_variables(self.request.event) - context['locales'] = {l: dict(settings.LANGUAGES).get(l, l) for l in self.request.event.settings.get('locales')} + context["locales"] = { + l: dict(settings.LANGUAGES).get(l, l) + for l in self.request.event.settings.get("locales") + } return context diff --git a/src/tests/plugins/wallet/test_apple.py b/src/tests/plugins/wallet/test_apple.py new file mode 100644 index 0000000000..1b0d44e257 --- /dev/null +++ b/src/tests/plugins/wallet/test_apple.py @@ -0,0 +1,186 @@ +from pretix.plugins.wallet.styles.apple import SignedZipFile, StringResource, AppleWalletEventTicket +from django.utils.translation import gettext as _ +import pytest +from i18nfield.strings import LazyI18nString +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization, hashes +from cryptography import x509 +import datetime +import io +import zipfile +import json +import jsonschema + + +@pytest.fixture +def pkpass_context(): + key_pw = b"TESTPW" + now = datetime.datetime.now() + ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + ca_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")]) + ) + .issuer_name( + x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "ROOT Inc.")]) + ) + .public_key(ca_key.public_key()) + .serial_number(1) + .not_valid_before(now) + .not_valid_after(now + datetime.timedelta(days=365)) + .sign(ca_key, hashes.SHA256()) + ) + + key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "UID=pass.test.test")] + ) + ) + .issuer_name( + x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")]) + ) + .public_key(key.public_key()) + .serial_number(2) + .not_valid_before(now) + .not_valid_after(now + datetime.timedelta(days=365)) + .sign(ca_key, hashes.SHA256()) + ) + + ca_cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) + cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) + key_pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.BestAvailableEncryption(key_pw), + ) + return { + "ca_certificate": ca_cert_pem, + "certificate": cert_pem, + "key": key_pem, + "password": key_pw, + } + + +def test_signed_zip(pkpass_context): + pkpass = SignedZipFile(**pkpass_context) + generated_pass = pkpass.finish() + + with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file: + assert set(zip_file.namelist()) == {"manifest.json", "signature"} + with zip_file.open("manifest.json") as f: + manifest = json.load(f) + assert manifest == {} + + with zip_file.open("signature") as f: + signature = f.read() + + assert signature + + pkpass = SignedZipFile(**pkpass_context) + pkpass.add_file("test", b"test content") + generated_pass = pkpass.finish() + + with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file: + assert set(zip_file.namelist()) == {"test", "manifest.json", "signature"} + with zip_file.open("manifest.json") as f: + manifest = json.load(f) + assert manifest == {"test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"} + + with zip_file.open("signature") as f: + signature = f.read() + + assert signature + + pkpass = SignedZipFile(**pkpass_context) + pkpass.add_file("test/test", "test content") + generated_pass = pkpass.finish() + + with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file: + assert set(zip_file.namelist()) == {"test/test", "manifest.json", "signature"} + with zip_file.open("manifest.json") as f: + manifest = json.load(f) + assert manifest == {"test/test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"} + + with zip_file.open("signature") as f: + signature = f.read() + + assert signature + + +def test_stringresource_minimal(): + resource = StringResource(locales=["de", "en"]) + resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"})) + stringfiles = resource.generate() + + assert stringfiles.keys() == {"de", "en"} + assert stringfiles["de"] == '"TEST" = "test-de";' + assert stringfiles["en"] == '"TEST" = "test-en";' + + +@pytest.mark.parametrize( + "input,output", + [ + ['te"st', 'te\\"st'], + ["te\rst", "te\\rst"], + ["te\nst", "te\\nst"], + ["te\r\nst", "te\\r\\nst"], + ["te\r\nst", "te\\r\\nst"], + ["te\\st", "te\\\\st"], + ], +) +def test_stringresource_escaping(input, output): + resource = StringResource(locales=["en"]) + resource.add_entry("TEST", LazyI18nString({"en": input})) + stringfiles = resource.generate() + + assert stringfiles.keys() == {"en"} + assert stringfiles["en"] == f'"TEST" = "{output}";' + + resource = StringResource(locales=["en"]) + resource.add_entry(input, LazyI18nString({"en": "test"})) + stringfiles = resource.generate() + + assert stringfiles.keys() == {"en"} + assert stringfiles["en"] == f'"{output}" = "test";' + + + +def test_stringresource_additional_locale(): + resource = StringResource(locales=["de", "en", "fr"]) + resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"})) + stringfiles = resource.generate() + + assert stringfiles.keys() == {"de", "en", "fr"} + assert stringfiles["de"] == '"TEST" = "test-de";' + assert stringfiles["en"] == '"TEST" = "test-en";' + assert stringfiles["fr"] == '"TEST" = "test-en";' + +def test_generate_pass_json(): + context = { + "placeholders": { + "text": {"test_placeholder": {"evaluate": lambda: "test placeholder"}} + }, + "description": "Ticket for Test", + "organizationName": "TestOrg", + "serialNumber": "1", + "passTypeIdentifier": "pass.test.test", + "teamIdentifier": "ABCDEF123456" + } + layout = {"fieldgroups": {"primary": {"entries": [{"type": "placeholder", "label": "test", "content": "test_placeholder"}, {"type": "text", "label": {"de":"test-de", "en": "test-en"}, "content": "test content"}]}}} + style = AppleWalletEventTicket() + schema = style.layout_schema(context) + jsonschema.validate(schema, layout) + + result = style.generate_pass_json(layout, context) + + required_fields = ["description", "formatVersion", "organizationName", "passTypeIdentifier", "serialNumber", "teamIdentifier"] + for field in required_fields: + assert field in result + + assert result['formatVersion'] == 1 + + breakpoint() \ No newline at end of file diff --git a/src/tests/plugins/wallet/test_wallet.py b/src/tests/plugins/wallet/test_wallet.py index fa27e90ce5..8be7e7cd95 100644 --- a/src/tests/plugins/wallet/test_wallet.py +++ b/src/tests/plugins/wallet/test_wallet.py @@ -1,5 +1,6 @@ from pretix.plugins.wallet.styles.base import ( PassStyle, + PredefinedFieldGroup, WalletPlatform, PlaceholderFieldGroup, FieldContentType, @@ -7,7 +8,6 @@ from pretix.plugins.wallet.styles.base import ( FieldGroupType, FieldEntryType, ) -from pretix.plugins.wallet.styles.apple import SignedZipFile, StringResource from django.utils.translation import gettext as _ import jsonschema import pytest @@ -65,9 +65,9 @@ class TicketTestStyle(PassStyle): for group in self.fieldgroups: if group.identifier in layout["fieldgroups"]: output += f"Group: {group.name}\n" - if group.type == FieldGroupType.PREDEFINED: + if isinstance(group, PredefinedFieldGroup): output += "PREDEFINED\n" - else: + elif isinstance(group, PlaceholderFieldGroup): for field in layout["fieldgroups"][group.identifier]["entries"]: if group.labels: label = LazyI18nString(field["label"]) @@ -89,6 +89,8 @@ class TicketTestStyle(PassStyle): elif field["type"] == FieldEntryType.IMAGE.value: output += f"{field['content']}" output += "\n" + else: + raise ValueError("Unknown field group") output += "\n" return output @@ -332,151 +334,3 @@ def test_layout_generate(layout_context): == "Generated Pass: Test Wallet Style Ticket\n\nGroup: Text 1\ntest-en: test placeholder\ntest: test content\n\n" ) - -@pytest.fixture -def pkpass_context(): - key_pw = b"TESTPW" - now = datetime.datetime.now() - ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096) - ca_cert = ( - x509.CertificateBuilder() - .subject_name( - x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")]) - ) - .issuer_name( - x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "ROOT Inc.")]) - ) - .public_key(ca_key.public_key()) - .serial_number(1) - .not_valid_before(now) - .not_valid_after(now + datetime.timedelta(days=365)) - .sign(ca_key, hashes.SHA256()) - ) - - key = rsa.generate_private_key(public_exponent=65537, key_size=4096) - cert = ( - x509.CertificateBuilder() - .subject_name( - x509.Name( - [x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "UID=pass.test.test")] - ) - ) - .issuer_name( - x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, "TTDR")]) - ) - .public_key(key.public_key()) - .serial_number(2) - .not_valid_before(now) - .not_valid_after(now + datetime.timedelta(days=365)) - .sign(ca_key, hashes.SHA256()) - ) - - ca_cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) - cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) - key_pem = key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(key_pw), - ) - return { - "ca_certificate": ca_cert_pem, - "certificate": cert_pem, - "key": key_pem, - "password": key_pw, - } - - -def test_signed_zip(pkpass_context): - pkpass = SignedZipFile(**pkpass_context) - generated_pass = pkpass.finish() - - with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file: - assert set(zip_file.namelist()) == {"manifest.json", "signature"} - with zip_file.open("manifest.json") as f: - manifest = json.load(f) - assert manifest == {} - - with zip_file.open("signature") as f: - signature = f.read() - - assert signature - - pkpass = SignedZipFile(**pkpass_context) - pkpass.add_file("test", b"test content") - generated_pass = pkpass.finish() - - with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file: - assert set(zip_file.namelist()) == {"test", "manifest.json", "signature"} - with zip_file.open("manifest.json") as f: - manifest = json.load(f) - assert manifest == {"test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"} - - with zip_file.open("signature") as f: - signature = f.read() - - assert signature - - pkpass = SignedZipFile(**pkpass_context) - pkpass.add_file("test/test", "test content") - generated_pass = pkpass.finish() - - with zipfile.ZipFile(io.BytesIO(generated_pass), "r") as zip_file: - assert set(zip_file.namelist()) == {"test/test", "manifest.json", "signature"} - with zip_file.open("manifest.json") as f: - manifest = json.load(f) - assert manifest == {"test/test": "1eebdf4fdc9fc7bf283031b93f9aef3338de9052"} - - with zip_file.open("signature") as f: - signature = f.read() - - assert signature - - -def test_stringresource_minimal(): - resource = StringResource(locales=["de", "en"]) - resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"})) - stringfiles = resource.generate() - - assert stringfiles.keys() == {"de", "en"} - assert stringfiles["de"] == '"TEST" = "test-de";' - assert stringfiles["en"] == '"TEST" = "test-en";' - - -@pytest.mark.parametrize( - "input,output", - [ - ['te"st', 'te\\"st'], - ["te\rst", "te\\rst"], - ["te\nst", "te\\nst"], - ["te\r\nst", "te\\r\\nst"], - ["te\r\nst", "te\\r\\nst"], - ["te\\st", "te\\\\st"], - ], -) -def test_stringresource_escaping(input, output): - resource = StringResource(locales=["en"]) - resource.add_entry("TEST", LazyI18nString({"en": input})) - stringfiles = resource.generate() - - assert stringfiles.keys() == {"en"} - assert stringfiles["en"] == f'"TEST" = "{output}";' - - resource = StringResource(locales=["en"]) - resource.add_entry(input, LazyI18nString({"en": "test"})) - stringfiles = resource.generate() - - assert stringfiles.keys() == {"en"} - assert stringfiles["en"] == f'"{output}" = "test";' - - - -def test_stringresource_additional_locale(): - resource = StringResource(locales=["de", "en", "fr"]) - resource.add_entry("TEST", LazyI18nString({"de": "test-de", "en": "test-en"})) - stringfiles = resource.generate() - - assert stringfiles.keys() == {"de", "en", "fr"} - assert stringfiles["de"] == '"TEST" = "test-de";' - assert stringfiles["en"] == '"TEST" = "test-en";' - assert stringfiles["fr"] == '"TEST" = "test-en";' -