From 9064069cf37176ed6a5f3b48267c9204f3186570 Mon Sep 17 00:00:00 2001 From: Kara Engelhardt Date: Wed, 15 Apr 2026 18:59:32 +0200 Subject: [PATCH] WIP: i18n editor, start apple wallet generation --- src/pretix/plugins/wallet/api.py | 2 +- .../wallet/components/input/i18ninput.vue | 11 +- .../wallet/components/input/select.vue | 4 +- .../components/placeholder-field-settings.vue | 49 +- .../wallet/components/text-content.vue | 16 +- .../static/pretixplugins/wallet/index.d.ts | 12 +- src/pretix/plugins/wallet/styles/apple.py | 124 ++++- src/pretix/plugins/wallet/styles/base.py | 46 +- src/pretix/plugins/wallet/views.py | 5 +- src/tests/plugins/wallet/test_wallet.py | 482 ++++++++++++++++++ 10 files changed, 689 insertions(+), 62 deletions(-) create mode 100644 src/tests/plugins/wallet/test_wallet.py diff --git a/src/pretix/plugins/wallet/api.py b/src/pretix/plugins/wallet/api.py index 568970f3c..895c13ab4 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: list(v.keys()) for k,v in get_layout_variables(self.context['event']).items()}} + context = {"placeholders": {k: {"content": v['content']} for k,v in get_layout_variables(self.context['event']).items()}} layout.validate(context=context) return data diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/i18ninput.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/i18ninput.vue index cc98f6c00..1115fdebe 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/i18ninput.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/i18ninput.vue @@ -1,15 +1,15 @@ diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue index be41e492d..b84dee798 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/input/select.vue @@ -6,7 +6,7 @@ defineOptions({ }) const props = defineProps<{ - label: string + label?: string choices: Array<[string, string]> errors?: string[] }>() @@ -24,7 +24,7 @@ watchEffect(() => { + \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue index defc8b6a7..19004d724 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/text-content.vue @@ -1,12 +1,13 @@ diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts index 4bf28557c..e5d1590de 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/index.d.ts @@ -5,7 +5,7 @@ type BaseFieldGroupDefinition = { required: boolean; } -type FieldGroupDefinition = PlaceholderFieldGroupDefinition | PredefinedFieldGroup; +type FieldGroupDefinition = PlaceholderFieldGroupDefinition | PredefinedFieldGroupDefinition; type PlaceholderFieldGroupDefinition = BaseFieldGroupDefinition & { type: 'placeholder'; @@ -24,13 +24,19 @@ type I18nString = string | Record type FieldContentType = 'text' | 'image'; -type FieldEntry = { - type: 'placeholder' | FieldContentType; +type PlaceholderFieldEntry = { + type: 'placeholder'; label?: I18nString; content?: string; } +type ContentFieldEntry = { + type: FieldContentType; + label?: I18nString; + content?: I18nString; +} +type FieldEntry = PlaceholderFieldEntry | ContentFieldEntry; type Style = { identifier: string; diff --git a/src/pretix/plugins/wallet/styles/apple.py b/src/pretix/plugins/wallet/styles/apple.py index 372032f63..20ce2fc45 100644 --- a/src/pretix/plugins/wallet/styles/apple.py +++ b/src/pretix/plugins/wallet/styles/apple.py @@ -1,25 +1,132 @@ from .base import ( FieldEntry, - FieldEntryContentType, + FieldEntryType, FieldContentType, ImageFieldGroup, PlaceholderFieldGroup, TextFieldGroup, WalletPlatform, PassStyle, + PlaceholderFieldEntry, + CustomFieldEntry, ) from django.utils.translation import gettext as _ from i18nfield.strings import LazyI18nString - +import io +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 class ApplePlatform(WalletPlatform): identifier = "apple" name = _("Apple") +class StringResource: + # mapping string in default event locale -> LazyI18nString + entries: dict[str, LazyI18nString] + locales: set[str] + + def __init__(self, locales): + self.entries = {} + self.locales = set(locales) + + def add_entry(self, key: str, value: LazyI18nString): # TODO: replace LazyI18nString with dict or handle strings where data == "" + 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", "\\": "\\\\"})) + def generate_resource(self, language): + output = "" + for key, entry in self.entries.items(): + 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 """ + def __init__(self, ca_certificate, certificate, key, password): + 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.password = password + + self.file = io.BytesIO() + self.zip_file = zipfile.ZipFile(self.file, "w") + self.manifest = {} + + def sign(self, data: bytes): + return ( + cryptography.hazmat.primitives.serialization.pkcs7.PKCS7SignatureBuilder() + .set_data(data) + .add_signer( + self.certificate, + self.key, + cryptography.hazmat.primitives.hashes.SHA256(), + ) + .add_certificate(self.ca_certificate) + .sign( + cryptography.hazmat.primitives.serialization.Encoding.DER, + [ + cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.Binary, + cryptography.hazmat.primitives.serialization.pkcs7.PKCS7Options.DetachedSignature, + ], + ) + ) + + def finish(self): + manifest = json.dumps(self.manifest).encode() + signature = self.sign(manifest) + self.add_file("manifest.json", manifest) + self.add_file("signature", signature) + self.zip_file.close() + return self.file.getvalue() + + def add_file(self, filename: str, content: str | bytes): + if isinstance(content, str): + content = content.encode() + + with self.zip_file.open(filename, "w") as f: + f.write(content) + self.manifest[filename] = hashlib.sha1(content).hexdigest() + + class AppleWalletStyle(PassStyle): platform = ApplePlatform + def generate_pass_json(self, layout, context): + pass_json = {} + return pass_json + + def generate(self, layout, context): + for key in ["certificate", "key", "wwdr_certificate", "password"]: + if key not in context: + raise ValueError(f"{key} missing from context") + pkpass = SignedZipFile( + context["certificate"], + context["key"], + context["wwdr_certificate"], + context["password"], + ) + + pass_json = self.generate_pass_json() + pkpass.add_file("pass.json", json.dumps(pass_json)) + return pkpass.finish() + + class AppleWalletEventTicket(AppleWalletStyle): identifier = "event_1" name = _("Event Ticket Layout 1") @@ -31,10 +138,8 @@ class AppleWalletEventTicket(AppleWalletStyle): max_entries=1, labels=False, default_entries=[ - FieldEntry( - type=FieldEntryContentType.IMAGE, - label=LazyI18nString("logo"), - content="event:image", + PlaceholderFieldEntry( + content="poweredby", ) ], ), @@ -44,13 +149,12 @@ class AppleWalletEventTicket(AppleWalletStyle): min_entries=1, max_entries=1, default_entries=[ - FieldEntry( - type=FieldEntryContentType.PLACEHOLDER, + PlaceholderFieldEntry( label=LazyI18nString({"de": "Tickettyp", "en": "Ticket type"}), content="item", ) ], # TODO: support Lazyi18nproxy here - description=_("These fields appear prominently featured on the pass.") + description=_("These fields appear prominently featured on the pass."), ), TextFieldGroup( identifier="secondary", name=_("Secondary"), max_entries=4 @@ -62,5 +166,3 @@ class AppleWalletEventTicket(AppleWalletStyle): TextFieldGroup(identifier="back", name=_("Back")), ] # preview_image = "apple/event_ticket.svg" - - diff --git a/src/pretix/plugins/wallet/styles/base.py b/src/pretix/plugins/wallet/styles/base.py index 5063e4f61..ff8e05358 100644 --- a/src/pretix/plugins/wallet/styles/base.py +++ b/src/pretix/plugins/wallet/styles/base.py @@ -1,6 +1,7 @@ import enum from i18nfield.strings import LazyI18nString import jsonschema +from django.core.exceptions import ValidationError class WalletPlatform: identifier: str @@ -47,19 +48,19 @@ class FieldContentType(enum.Enum): TEXT = "text" -class FieldEntryContentType(enum.Enum): +class FieldEntryType(enum.Enum): IMAGE = "image" TEXT = "text" PLACEHOLDER = "placeholder" -class FieldEntry: - type: FieldEntryContentType +class FieldEntry[T]: + type: FieldEntryType label: LazyI18nString | None - content: str + content: T def __init__( - self, type: FieldEntryContentType, label: LazyI18nString | None, content: str + self, type: FieldEntryType, content: T, label: LazyI18nString | None = None ): self.type = type self.label = label @@ -68,6 +69,27 @@ class FieldEntry: def asdict(self) -> dict: return {"type": self.type.value, "content": self.content, "label": self.label.data if self.label else None} +class PlaceholderFieldEntry(FieldEntry[str]): + type = FieldEntryType.PLACEHOLDER + label: LazyI18nString | None + content: str + + def __init__( + self, content: str, label: LazyI18nString | None = None + ): + self.label = label + self.content = content + + +class CustomFieldEntry(FieldEntry[LazyI18nString]): + type: FieldEntryType + label: LazyI18nString | None + content: LazyI18nString + + def asdict(self) -> dict: + return {"type": self.type.value, "content": self.content.data, "label": self.label.data if self.label else None} + + class PredefinedFieldGroup(FieldGroup): type = FieldGroupType.PREDEFINED @@ -126,13 +148,13 @@ class PlaceholderFieldGroup(FieldGroup): remaining_fields: list["FieldGroup"], context: dict, ): - placeholders = context.get("placeholders", {}).get(self.content_type.value, []) + placeholders = list(context.get("placeholders", {}).get(self.content_type.value, {}).keys()) return { "type": "object", "properties": { "entries": self.entries_schema(placeholders=placeholders), "overflow": { - "oneOf": [ + "anyOf": [ {"type": "null"}, { "type": "string", @@ -158,7 +180,7 @@ class PlaceholderFieldGroup(FieldGroup): "type": "array", "items": { "type": "object", - "oneOf": [ + "anyOf": [ { "properties": { **baseprops, @@ -170,7 +192,7 @@ class PlaceholderFieldGroup(FieldGroup): "properties": { **baseprops, "type": {"const": self.content_type.value}, - "content": {"type": "string"}, + "content": {"$ref": "#/$defs/I18nString"}, } }, ], @@ -252,6 +274,8 @@ class PassStyle: return schema + def generate(self, layout, context): + raise NotImplementedError() class PassLayout: style: PassStyle @@ -267,3 +291,7 @@ class PassLayout: jsonschema.validate(self.layout, schema) except jsonschema.ValidationError as e: raise ValidationError("Invalid layout: {}".format(str(e))) + + def generate(self, context): + self.validate(context) + return self.style.generate(self.layout, context) \ No newline at end of file diff --git a/src/pretix/plugins/wallet/views.py b/src/pretix/plugins/wallet/views.py index eaf54de7c..5e40a2c31 100644 --- a/src/pretix/plugins/wallet/views.py +++ b/src/pretix/plugins/wallet/views.py @@ -26,6 +26,9 @@ def get_layout_variables(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()} + # TODO: should this even be a list view? class LayoutListView(EventPermissionRequiredMixin, ListView): @@ -61,7 +64,7 @@ class LayoutEditorView(DetailView): context["styles"] = { style.identifier: style.asdict() for style in self.get_platform_styles() } - context["variables"] = get_layout_variables(self.request.event) + 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')} return context diff --git a/src/tests/plugins/wallet/test_wallet.py b/src/tests/plugins/wallet/test_wallet.py new file mode 100644 index 000000000..fa27e90ce --- /dev/null +++ b/src/tests/plugins/wallet/test_wallet.py @@ -0,0 +1,482 @@ +from pretix.plugins.wallet.styles.base import ( + PassStyle, + WalletPlatform, + PlaceholderFieldGroup, + FieldContentType, + PassLayout, + FieldGroupType, + FieldEntryType, +) +from pretix.plugins.wallet.styles.apple import SignedZipFile, StringResource +from django.utils.translation import gettext as _ +import jsonschema +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 + + +class WalletTestPlatform(WalletPlatform): + identifier = "test_platform" + name = _("Test Wallet Platform") + + +class MinimalTestStyle(PassStyle): + platform = WalletTestPlatform + identifier = "test_style" + name = _("Test Wallet Style") + fieldgroups = [] + + +class TicketTestStyle(PassStyle): + platform = WalletTestPlatform + identifier = "test_ticket" + name = _("Test Wallet Style Ticket") + fieldgroups = [ + PlaceholderFieldGroup( + identifier="text1", + name=_("Text 1"), + content_type=FieldContentType.TEXT, + required=True, + ), + PlaceholderFieldGroup( + identifier="text2", + name=_("Text 2"), + content_type=FieldContentType.TEXT, + required=False, + labels=False, + ), + PlaceholderFieldGroup( + identifier="image1", + name=_("Image 1"), + content_type=FieldContentType.IMAGE, + required=False, + labels=False, + ), + ] + + def generate(self, layout, context): + output = f"Generated Pass: {self.name}\n\n" + for group in self.fieldgroups: + if group.identifier in layout["fieldgroups"]: + output += f"Group: {group.name}\n" + if group.type == FieldGroupType.PREDEFINED: + output += "PREDEFINED\n" + else: + for field in layout["fieldgroups"][group.identifier]["entries"]: + if group.labels: + label = LazyI18nString(field["label"]) + output += f"{label}: " + if field["type"] == FieldEntryType.PLACEHOLDER.value: + placeholder = ( + context.get("placeholders") + .get(group.content_type.value, {}) + .get(field["content"]) + ) + if placeholder: + output += placeholder["evaluate"]( + *context.get("evaluation_context", []) + ) + else: + output += f"UNKNOWN: {field['content']}" + elif field["type"] == FieldEntryType.TEXT.value: + output += str(LazyI18nString(field["content"])) + elif field["type"] == FieldEntryType.IMAGE.value: + output += f"{field['content']}" + output += "\n" + output += "\n" + return output + + +@pytest.fixture +def layout_context(): + return { + "placeholders": { + "text": {"test_placeholder": {"evaluate": lambda: "test placeholder"}} + } + } + + +def test_schema_generation_minimal(): + style = MinimalTestStyle() + context = {} + schema = style.layout_schema(context) + assert isinstance(schema, dict) + assert "properties" in schema + assert "fieldgroups" in schema["properties"] + + jsonschema.validate({}, schema) + jsonschema.validate({"fieldgroups": {}}, schema) + + +def test_schema_ticket_generation(layout_context): + style = TicketTestStyle() + schema = style.layout_schema(layout_context) + assert isinstance(schema, dict) + assert "properties" in schema + assert "fieldgroups" in schema["properties"] + + +@pytest.mark.parametrize( + "layout", + [ + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": "test", + "content": "test_placeholder", + } + ] + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": {"de": "test-de", "en": "test-en"}, + "content": "test_placeholder", + } + ] + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + {"type": "text", "label": "test", "content": "test content"} + ] + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": {"de": "test-de", "en": "test-en"}, + "content": "test_placeholder", + }, + {"type": "text", "label": "test", "content": "test content"}, + ] + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": {"de": "test-de", "en": "test-en"}, + "content": "test_placeholder", + }, + {"type": "text", "label": "test", "content": "test content"}, + ], + "overflow": "text2", + } + } + }, + ], +) +def test_schema_ticket_valid(layout_context, layout): + style = TicketTestStyle() + schema = style.layout_schema(layout_context) + + jsonschema.validate(layout, schema) + + +@pytest.mark.parametrize( + "layout", + [ + {}, + {"fieldgroups": {}}, + {"fieldgroups": {"text1": {}}}, + {"fieldgroups": {"text1": {"entries": []}}}, + {"fieldgroups": {"text1": {"overflow": "test"}}}, + { + "fieldgroups": { + "text1": { + "entries": [{"type": "placeholder", "content": "test_placeholder"}] + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": [], + "content": "test_placeholder", + } + ] + } + } + }, + { + "fieldgroups": { + "text1": {"entries": [{"type": "text", "content": "test content"}]} + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": "test", + "content": "test_placeholder", + } + ], + "overflow": "invalid_group", + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": "test", + "content": "test_placeholder", + } + ], + "overflow": "image1", + } + } + }, + { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": "test", + "content": "test_placeholder", + } + ], + }, + "text2": { + "entries": [ + { + "type": "placeholder", + "label": "test", + "content": "test_placeholder", + } + ], + "overflow": "text1", + }, + } + }, + ], +) +def test_schema_ticket_invalid(layout_context, layout): + style = TicketTestStyle() + schema = style.layout_schema(layout_context) + + with pytest.raises(jsonschema.ValidationError): + jsonschema.validate(layout, schema) + + +def test_style_representation(): + style = TicketTestStyle() + style_dict = style.asdict() + assert style_dict["platform"] == "test_platform" + assert style_dict["identifier"] == "test_ticket" + assert style_dict["name"] == _("Test Wallet Style Ticket") + + assert style_dict["fieldgroups"][0]["identifier"] == "text1" + assert style_dict["fieldgroups"][0]["name"] == "Text 1" + assert style_dict["fieldgroups"][0]["content_type"] == "text" + assert style_dict["fieldgroups"][0]["labels"] == True + assert style_dict["fieldgroups"][0]["required"] == True + + +def test_layout_generate(layout_context): + style = TicketTestStyle() + layout = { + "fieldgroups": { + "text1": { + "entries": [ + { + "type": "placeholder", + "label": {"de": "test-de", "en": "test-en"}, + "content": "test_placeholder", + }, + {"type": "text", "label": "test", "content": "test content"}, + ], + "overflow": "text2", + } + } + } + + pass_layout = PassLayout(style, layout) + generated_pass = pass_layout.generate(layout_context) + + assert ( + generated_pass + == "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";' +