diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php
index 87d8213f5c0..e87fee19f1b 100644
--- a/lib/private/legacy/OC_App.php
+++ b/lib/private/legacy/OC_App.php
@@ -470,9 +470,8 @@ class OC_App {
}
}
- $info['license'] ??= $info['licence'];
+ $info['license'] = $info['licence'];
$info['version'] = $appManager->getAppVersion($app);
- $info['license'] ??= $info['licence'];
$appList[] = $info;
}
}
diff --git a/tests/data/app/appinfo-attributes-once.json b/tests/data/app/appinfo-attributes-once.json
new file mode 100644
index 00000000000..3ecc803733d
--- /dev/null
+++ b/tests/data/app/appinfo-attributes-once.json
@@ -0,0 +1,162 @@
+{
+ "id": "attributes_once",
+ "name": {
+ "@attributes": {
+ "lang": "en"
+ },
+ "@value": "Attributes Once"
+ },
+ "summary": {
+ "@attributes": {
+ "lang": "en"
+ },
+ "@value": "Single occurrence with attributes set on allowed elements."
+ },
+ "description": {
+ "@attributes": {
+ "lang": "en"
+ },
+ "@value": "Fixture that sets attributes where allowed (e.g., lang, type, min-version, for)."
+ },
+ "version": "1.2.3",
+ "licence": "agpl",
+ "author": {
+ "@attributes": {
+ "homepage": "http://example.com",
+ "mail": "jane@example.com"
+ },
+ "@value": "Jane Doe"
+ },
+ "types": [
+ "filesystem"
+ ],
+ "documentation": {
+ "user": "https://example.test/attributes-once/user"
+ },
+ "category": [
+ "tools"
+ ],
+ "website": "https://example.test/attributes-once",
+ "bugs": "https://example.com/issues",
+ "repository": {
+ "@attributes": {
+ "type": "git"
+ },
+ "@value": "https://example.test/attributes-once.git"
+ },
+ "screenshot": {
+ "@attributes": {
+ "small-thumbnail": "https://example.test/attributes-once-small.png"
+ },
+ "@value": "https://example.test/attributes-once.png"
+ },
+ "dependencies": {
+ "php": {
+ "@attributes": {
+ "min-version": "8.2"
+ }
+ },
+ "database": {
+ "@attributes": {
+ "min-version": "2.0"
+ },
+ "@value": "pgsql"
+ },
+ "lib": {
+ "@attributes": {
+ "min-version": "1.5"
+ },
+ "@value": "curl"
+ },
+ "owncloud": {
+ "@attributes": {
+ "min-version": "1.0",
+ "max-version": "2.0"
+ }
+ },
+ "nextcloud": {
+ "@attributes": {
+ "min-version": "30.0",
+ "max-version": "31.0"
+ }
+ },
+ "backend": [
+ "caldav"
+ ]
+ },
+ "background-jobs": {
+ "job": "OCA\\AttributesOnce\\BackgroundJob\\Job"
+ },
+ "repair-steps": {
+ "install": {
+ "step": "OCA\\AttributesOnce\\RepairStep\\Install"
+ },
+ "pre-migration": [],
+ "post-migration": [],
+ "live-migration": [],
+ "uninstall": []
+ },
+ "commands": {
+ "command": "OCA\\AttributesOnce\\Command\\Run"
+ },
+ "settings": {
+ "admin": [
+ "OCA\\AttributesOnce\\Settings\\Admin"
+ ],
+ "admin-section": [],
+ "personal": [],
+ "personal-section": []
+ },
+ "activity": {
+ "providers": {
+ "provider": "OCA\\AttributesOnce\\Activity\\Provider"
+ },
+ "filters": [],
+ "settings": []
+ },
+ "navigations": {
+ "navigation": [
+ {
+ "@attributes": {
+ "role": "admin"
+ },
+ "name": "Attributes",
+ "route": "attributes.once.route",
+ "icon": "attributes-once.svg",
+ "order": "5"
+ }
+ ]
+ },
+ "collaboration": {
+ "plugins": {
+ "@attributes": {
+ "type": "collaborator-search"
+ },
+ "@value": "OCA\\AttributesOnce\\Collaboration\\Plugin"
+ }
+ },
+ "sabre": {
+ "plugins": {
+ "plugin": "OCA\\AttributesOnce\\Sabre\\Plugin"
+ }
+ },
+ "trash": {
+ "backend": {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\AttributesOnce\\Trash\\Backend"
+ }
+ },
+ "versions": {
+ "backend": {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\AttributesOnce\\Versions\\Backend"
+ }
+ },
+ "remote": [],
+ "public": [],
+ "two-factor-providers": []
+}
diff --git a/tests/data/app/appinfo-attributes-once.json.license b/tests/data/app/appinfo-attributes-once.json.license
new file mode 100644
index 00000000000..8c5ffd52494
--- /dev/null
+++ b/tests/data/app/appinfo-attributes-once.json.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/tests/data/app/appinfo-attributes-once.xml b/tests/data/app/appinfo-attributes-once.xml
new file mode 100644
index 00000000000..d3c246240ae
--- /dev/null
+++ b/tests/data/app/appinfo-attributes-once.xml
@@ -0,0 +1,77 @@
+
+
+
+ attributes_once
+ Attributes Once
+ Single occurrence with attributes set on allowed elements.
+ Fixture that sets attributes where allowed (e.g., lang, type, min-version, for).
+ 1.2.3
+ agpl
+ Jane Doe
+
+
+
+
+ https://example.test/attributes-once/user
+
+ tools
+ https://example.test/attributes-once
+ https://example.com/issues
+ https://example.test/attributes-once.git
+ https://example.test/attributes-once.png
+
+
+ pgsql
+ curl
+
+
+ caldav
+
+
+ OCA\AttributesOnce\BackgroundJob\Job
+
+
+
+ OCA\AttributesOnce\RepairStep\Install
+
+
+
+ OCA\AttributesOnce\Command\Run
+
+
+ OCA\AttributesOnce\Settings\Admin
+
+
+
+ OCA\AttributesOnce\Activity\Provider
+
+
+
+
+ Attributes
+ attributes.once.route
+ attributes-once.svg
+ 5
+
+
+
+
+ OCA\AttributesOnce\Collaboration\Plugin
+
+
+
+
+ OCA\AttributesOnce\Sabre\Plugin
+
+
+
+ OCA\AttributesOnce\Trash\Backend
+
+
+ OCA\AttributesOnce\Versions\Backend
+
+
diff --git a/tests/data/app/appinfo-multi-once.json b/tests/data/app/appinfo-multi-once.json
new file mode 100644
index 00000000000..0145060ebe7
--- /dev/null
+++ b/tests/data/app/appinfo-multi-once.json
@@ -0,0 +1,192 @@
+{
+ "id": "multi_once",
+ "name": "Multi Once",
+ "summary": "Every repeatable element is used exactly once.",
+ "description": "Fixture that exercises the single-item normalization path.",
+ "version": "1.0.0",
+ "licence": "agpl",
+ "author": [
+ "Jane Doe"
+ ],
+ "types": [
+ "filesystem",
+ "logging"
+ ],
+ "documentation": {
+ "user": "https://example.test/multi-once/user",
+ "admin": "https://example.test/multi-once/admin",
+ "developer": "https://example.test/multi-once/developer"
+ },
+ "category": [
+ "monitoring"
+ ],
+ "website": "https://example.test/multi-once",
+ "discussion": "https://example.test/multi-once/discussion",
+ "bugs": "https://example.test/multi-once/issues",
+ "repository": "https://example.test/multi-once.git",
+ "screenshot": [
+ "https://example.test/multi-once.png"
+ ],
+ "donation": "https://example.test/donate",
+ "dependencies": {
+ "database": "sqlite",
+ "command": "awk",
+ "lib": {
+ "@attributes": {
+ "min-version": "1.0"
+ },
+ "@value": "curl"
+ },
+ "nextcloud": {
+ "@attributes": {
+ "min-version": "30.0",
+ "max-version": "31.0"
+ }
+ },
+ "architecture": "x86_64",
+ "backend": [
+ "caldav"
+ ]
+ },
+ "background-jobs": {
+ "job": "OCA\\MultiOnce\\BackgroundJob\\Cleanup"
+ },
+ "repair-steps": {
+ "pre-migration": {
+ "step": "OCA\\MultiOnce\\RepairStep\\PreMigration"
+ },
+ "post-migration": {
+ "step": "OCA\\MultiOnce\\RepairStep\\PostMigration"
+ },
+ "live-migration": {
+ "step": "OCA\\MultiOnce\\RepairStep\\LiveMigration"
+ },
+ "install": {
+ "step": "OCA\\MultiOnce\\RepairStep\\Install"
+ },
+ "uninstall": {
+ "step": "OCA\\MultiOnce\\RepairStep\\Uninstall"
+ }
+ },
+ "two-factor-providers": {
+ "provider": "OCA\\MultiOnce\\TwoFactor\\Provider"
+ },
+ "commands": {
+ "command": "OCA\\MultiOnce\\Command\\Migrate"
+ },
+ "settings": {
+ "admin": [
+ "OCA\\MultiOnce\\Settings\\Admin"
+ ],
+ "admin-section": [
+ "OCA\\MultiOnce\\Settings\\AdminSection"
+ ],
+ "personal": [
+ "OCA\\MultiOnce\\Settings\\Personal"
+ ],
+ "personal-section": [
+ "OCA\\MultiOnce\\Settings\\PersonalSection"
+ ],
+ "admin-delegation": [
+ "OCA\\MultiOnce\\Settings\\AdminDelegation"
+ ],
+ "admin-delegation-section": [
+ "OCA\\MultiOnce\\Settings\\AdminDelegationSection"
+ ]
+ },
+ "activity": {
+ "settings": {
+ "setting": "OCA\\MultiOnce\\Activity\\Setting"
+ },
+ "filters": {
+ "filter": "OCA\\MultiOnce\\Activity\\Filter"
+ },
+ "providers": {
+ "provider": "OCA\\MultiOnce\\Activity\\Provider"
+ }
+ },
+ "dashboard": {
+ "widget": "OCA\\MultiOnce\\Dashboard\\Widget"
+ },
+ "fulltextsearch": {
+ "platform": "OCA\\MultiOnce\\Search\\Platform",
+ "provider": "OCA\\MultiOnce\\Search\\Provider"
+ },
+ "navigations": {
+ "navigation": [
+ {
+ "name": "Multi Once",
+ "route": "multi.once.route",
+ "icon": "multi-once.svg",
+ "order": "1"
+ }
+ ]
+ },
+ "contactsmenu": {
+ "provider": "OCA\\MultiOnce\\ContactsMenu\\Provider"
+ },
+ "collaboration": {
+ "plugins": {
+ "@attributes": {
+ "type": "collaborator-search"
+ },
+ "@value": "OCA\\MultiOnce\\Collaboration\\Plugin"
+ }
+ },
+ "openmetrics": {
+ "exporter": [
+ "OCA\\MultiOnce\\OpenMetrics\\Exporter"
+ ]
+ },
+ "sabre": {
+ "collections": {
+ "collection": "OCA\\MultiOnce\\Sabre\\Collection"
+ },
+ "plugins": {
+ "plugin": "OCA\\MultiOnce\\Sabre\\Plugin"
+ },
+ "address-book-plugins": {
+ "plugin": "OCA\\MultiOnce\\Sabre\\AddressBookPlugin"
+ },
+ "calendar-plugins": {
+ "plugin": "OCA\\MultiOnce\\Sabre\\CalendarPlugin"
+ }
+ },
+ "trash": {
+ "backend": {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\MultiOnce\\Trash\\Backend"
+ }
+ },
+ "versions": {
+ "backend": {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\MultiOnce\\Versions\\Backend"
+ }
+ },
+ "external-app": {
+ "docker-install": {
+ "registry": "registry.example.test",
+ "image": "multi-once",
+ "image-tag": "1.0.0"
+ },
+ "scopes": {
+ "value": "scope-one"
+ },
+ "system": "true",
+ "environment-variables": {
+ "variable": {
+ "name": "MULTI_ONCE_ONE",
+ "display-name": "Multi Once One",
+ "description": "First variable",
+ "default": "one"
+ }
+ }
+ },
+ "remote": [],
+ "public": []
+}
diff --git a/tests/data/app/appinfo-multi-once.json.license b/tests/data/app/appinfo-multi-once.json.license
new file mode 100644
index 00000000000..5e8eced1a6a
--- /dev/null
+++ b/tests/data/app/appinfo-multi-once.json.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
\ No newline at end of file
diff --git a/tests/data/app/appinfo-multi-once.xml b/tests/data/app/appinfo-multi-once.xml
new file mode 100644
index 00000000000..3c14664a073
--- /dev/null
+++ b/tests/data/app/appinfo-multi-once.xml
@@ -0,0 +1,149 @@
+
+
+
+ multi_once
+ Multi Once
+ Every repeatable element is used exactly once.
+ Fixture that exercises the single-item normalization path.
+ 1.0.0
+ agpl
+ Jane Doe
+
+
+
+
+
+ https://example.test/multi-once/user
+ https://example.test/multi-once/admin
+ https://example.test/multi-once/developer
+
+ monitoring
+ https://example.test/multi-once
+ https://example.test/multi-once/discussion
+ https://example.test/multi-once/issues
+ https://example.test/multi-once.git
+ https://example.test/multi-once.png
+ https://example.test/donate
+
+ sqlite
+ awk
+ curl
+
+ x86_64
+ caldav
+
+
+ OCA\MultiOnce\BackgroundJob\Cleanup
+
+
+
+ OCA\MultiOnce\RepairStep\PreMigration
+
+
+ OCA\MultiOnce\RepairStep\PostMigration
+
+
+ OCA\MultiOnce\RepairStep\LiveMigration
+
+
+ OCA\MultiOnce\RepairStep\Install
+
+
+ OCA\MultiOnce\RepairStep\Uninstall
+
+
+
+ OCA\MultiOnce\TwoFactor\Provider
+
+
+ OCA\MultiOnce\Command\Migrate
+
+
+ OCA\MultiOnce\Settings\Admin
+ OCA\MultiOnce\Settings\AdminSection
+ OCA\MultiOnce\Settings\Personal
+ OCA\MultiOnce\Settings\PersonalSection
+ OCA\MultiOnce\Settings\AdminDelegation
+ OCA\MultiOnce\Settings\AdminDelegationSection
+
+
+
+ OCA\MultiOnce\Activity\Setting
+
+
+ OCA\MultiOnce\Activity\Filter
+
+
+ OCA\MultiOnce\Activity\Provider
+
+
+
+ OCA\MultiOnce\Dashboard\Widget
+
+
+ OCA\MultiOnce\Search\Platform
+ OCA\MultiOnce\Search\Provider
+
+
+
+ Multi Once
+ multi.once.route
+ multi-once.svg
+ 1
+
+
+
+ OCA\MultiOnce\ContactsMenu\Provider
+
+
+
+ OCA\MultiOnce\Collaboration\Plugin
+
+
+
+ OCA\MultiOnce\OpenMetrics\Exporter
+
+
+
+ OCA\MultiOnce\Sabre\Collection
+
+
+ OCA\MultiOnce\Sabre\Plugin
+
+
+ OCA\MultiOnce\Sabre\AddressBookPlugin
+
+
+ OCA\MultiOnce\Sabre\CalendarPlugin
+
+
+
+ OCA\MultiOnce\Trash\Backend
+
+
+ OCA\MultiOnce\Versions\Backend
+
+
+
+ registry.example.test
+ multi-once
+ 1.0.0
+
+
+ scope-one
+
+ true
+
+
+ MULTI_ONCE_ONE
+ Multi Once One
+ First variable
+ one
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/app/appinfo-multi-twice.json b/tests/data/app/appinfo-multi-twice.json
new file mode 100644
index 00000000000..114cab05f6c
--- /dev/null
+++ b/tests/data/app/appinfo-multi-twice.json
@@ -0,0 +1,325 @@
+{
+ "id": "multi_twice",
+ "name": "Multi Twice",
+ "summary": "Every repeatable element is used exactly twice.",
+ "description": "Fixture that exercises the list normalization path.",
+ "version": "1.0.0",
+ "licence": [
+ "agpl",
+ "mit"
+ ],
+ "author": [
+ "Jane Doe",
+ "John Doe"
+ ],
+ "types": [
+ "filesystem",
+ "logging"
+ ],
+ "documentation": {
+ "user": "https://example.test/multi-twice/user",
+ "admin": "https://example.test/multi-twice/admin",
+ "developer": "https://example.test/multi-twice/developer"
+ },
+ "category": [
+ "monitoring",
+ "social"
+ ],
+ "website": "https://example.test/multi-twice",
+ "discussion": "https://example.test/multi-twice/discussion",
+ "bugs": "https://example.test/multi-twice/issues",
+ "repository": {
+ "@attributes": {
+ "type": "git"
+ },
+ "@value": "https://example.test/multi-twice.git"
+ },
+ "screenshot": [
+ "https://example.test/multi-twice-1.png",
+ "https://example.test/multi-twice-2.png"
+ ],
+ "donation": [
+ "https://example.test/donate/1",
+ "https://example.test/donate/2"
+ ],
+ "dependencies": {
+ "php": {
+ "@attributes": {
+ "min-version": "8.2"
+ }
+ },
+ "database": [
+ {
+ "@attributes": {
+ "min-version": "1.0"
+ },
+ "@value": "sqlite"
+ },
+ {
+ "@attributes": {
+ "min-version": "1.0"
+ },
+ "@value": "mysql"
+ }
+ ],
+ "command": [
+ "awk",
+ "grep"
+ ],
+ "lib": [
+ {
+ "@attributes": {
+ "min-version": "1.0"
+ },
+ "@value": "curl"
+ },
+ {
+ "@attributes": {
+ "min-version": "1.0"
+ },
+ "@value": "intl"
+ }
+ ],
+ "owncloud": {
+ "@attributes": {
+ "min-version": "1.0",
+ "max-version": "2.0"
+ }
+ },
+ "nextcloud": {
+ "@attributes": {
+ "min-version": "30.0",
+ "max-version": "31.0"
+ }
+ },
+ "architecture": [
+ "x86_64",
+ "aarch64"
+ ],
+ "backend": [
+ "caldav",
+ "caldav"
+ ]
+ },
+ "background-jobs": [
+ "OCA\\MultiTwice\\BackgroundJob\\CleanupOne",
+ "OCA\\MultiTwice\\BackgroundJob\\CleanupTwo"
+ ],
+ "repair-steps": {
+ "pre-migration": [
+ "OCA\\MultiTwice\\RepairStep\\PreMigrationOne",
+ "OCA\\MultiTwice\\RepairStep\\PreMigrationTwo"
+ ],
+ "post-migration": [
+ "OCA\\MultiTwice\\RepairStep\\PostMigrationOne",
+ "OCA\\MultiTwice\\RepairStep\\PostMigrationTwo"
+ ],
+ "live-migration": [
+ "OCA\\MultiTwice\\RepairStep\\LiveMigrationOne",
+ "OCA\\MultiTwice\\RepairStep\\LiveMigrationTwo"
+ ],
+ "install": [
+ "OCA\\MultiTwice\\RepairStep\\InstallOne",
+ "OCA\\MultiTwice\\RepairStep\\InstallTwo"
+ ],
+ "uninstall": [
+ "OCA\\MultiTwice\\RepairStep\\UninstallOne",
+ "OCA\\MultiTwice\\RepairStep\\UninstallTwo"
+ ]
+ },
+ "two-factor-providers": [
+ "OCA\\MultiTwice\\TwoFactor\\ProviderOne",
+ "OCA\\MultiTwice\\TwoFactor\\ProviderTwo"
+ ],
+ "commands": [
+ "OCA\\MultiTwice\\Command\\MigrateOne",
+ "OCA\\MultiTwice\\Command\\MigrateTwo"
+ ],
+ "settings": {
+ "admin": [
+ "OCA\\MultiTwice\\Settings\\AdminOne",
+ "OCA\\MultiTwice\\Settings\\AdminTwo"
+ ],
+ "admin-section": [
+ "OCA\\MultiTwice\\Settings\\AdminSectionOne",
+ "OCA\\MultiTwice\\Settings\\AdminSectionTwo"
+ ],
+ "personal": [
+ "OCA\\MultiTwice\\Settings\\PersonalOne",
+ "OCA\\MultiTwice\\Settings\\PersonalTwo"
+ ],
+ "personal-section": [
+ "OCA\\MultiTwice\\Settings\\PersonalSectionOne",
+ "OCA\\MultiTwice\\Settings\\PersonalSectionTwo"
+ ],
+ "admin-delegation": [
+ "OCA\\MultiTwice\\Settings\\AdminDelegationOne",
+ "OCA\\MultiTwice\\Settings\\AdminDelegationTwo"
+ ],
+ "admin-delegation-section": [
+ "OCA\\MultiTwice\\Settings\\AdminDelegationSectionOne",
+ "OCA\\MultiTwice\\Settings\\AdminDelegationSectionTwo"
+ ]
+ },
+ "activity": {
+ "settings": [
+ "OCA\\MultiTwice\\Activity\\SettingOne",
+ "OCA\\MultiTwice\\Activity\\SettingTwo"
+ ],
+ "filters": [
+ "OCA\\MultiTwice\\Activity\\FilterOne",
+ "OCA\\MultiTwice\\Activity\\FilterTwo"
+ ],
+ "providers": [
+ "OCA\\MultiTwice\\Activity\\ProviderOne",
+ "OCA\\MultiTwice\\Activity\\ProviderTwo"
+ ]
+ },
+ "dashboard": {
+ "widget": [
+ "OCA\\MultiTwice\\Dashboard\\WidgetOne",
+ "OCA\\MultiTwice\\Dashboard\\WidgetTwo"
+ ]
+ },
+ "fulltextsearch": {
+ "platform": [
+ "OCA\\MultiTwice\\Search\\PlatformOne",
+ "OCA\\MultiTwice\\Search\\PlatformTwo"
+ ],
+ "provider": [
+ "OCA\\MultiTwice\\Search\\ProviderOne",
+ "OCA\\MultiTwice\\Search\\ProviderTwo"
+ ]
+ },
+ "navigations": {
+ "navigation": [
+ {
+ "name": "Multi Twice One",
+ "route": "multi.twice.one",
+ "icon": "multi-twice-1.svg",
+ "order": "1"
+ },
+ {
+ "name": "Multi Twice Two",
+ "route": "multi.twice.two",
+ "icon": "multi-twice-2.svg",
+ "order": "2"
+ }
+ ]
+ },
+ "contactsmenu": {
+ "provider": "OCA\\MultiTwice\\ContactsMenu\\Provider"
+ },
+ "collaboration": {
+ "plugins": [
+ {
+ "@attributes": {
+ "type": "collaborator-search"
+ },
+ "@value": "OCA\\MultiTwice\\Collaboration\\PluginOne"
+ },
+ {
+ "@attributes": {
+ "type": "autocomplete-sort"
+ },
+ "@value": "OCA\\MultiTwice\\Collaboration\\PluginTwo"
+ }
+ ]
+ },
+ "openmetrics": {
+ "exporter": [
+ "OCA\\MultiTwice\\OpenMetrics\\ExporterOne",
+ "OCA\\MultiTwice\\OpenMetrics\\ExporterTwo"
+ ]
+ },
+ "sabre": {
+ "collections": {
+ "collection": [
+ "OCA\\MultiTwice\\Sabre\\CollectionOne",
+ "OCA\\MultiTwice\\Sabre\\CollectionTwo"
+ ]
+ },
+ "plugins": {
+ "plugin": [
+ "OCA\\MultiTwice\\Sabre\\PluginOne",
+ "OCA\\MultiTwice\\Sabre\\PluginTwo"
+ ]
+ },
+ "address-book-plugins": {
+ "plugin": [
+ "OCA\\MultiTwice\\Sabre\\AddressBookPluginOne",
+ "OCA\\MultiTwice\\Sabre\\AddressBookPluginTwo"
+ ]
+ },
+ "calendar-plugins": {
+ "plugin": [
+ "OCA\\MultiTwice\\Sabre\\CalendarPluginOne",
+ "OCA\\MultiTwice\\Sabre\\CalendarPluginTwo"
+ ]
+ }
+ },
+ "trash": {
+ "backend": [
+ {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\MultiTwice\\Trash\\BackendOne"
+ },
+ {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\MultiTwice\\Trash\\BackendTwo"
+ }
+ ]
+ },
+ "versions": {
+ "backend": [
+ {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\MultiTwice\\Versions\\BackendOne"
+ },
+ {
+ "@attributes": {
+ "for": "files"
+ },
+ "@value": "OCA\\MultiTwice\\Versions\\BackendTwo"
+ }
+ ]
+ },
+ "external-app": {
+ "docker-install": {
+ "registry": "registry.example.test",
+ "image": "multi-twice",
+ "image-tag": "2.0.0"
+ },
+ "scopes": {
+ "value": [
+ "scope-one",
+ "scope-two"
+ ]
+ },
+ "system": "true",
+ "environment-variables": {
+ "variable": [
+ {
+ "name": "MULTI_TWICE_ONE",
+ "display-name": "Multi Twice One",
+ "description": "First variable",
+ "default": "one"
+ },
+ {
+ "name": "MULTI_TWICE_TWO",
+ "display-name": "Multi Twice Two",
+ "description": "Second variable",
+ "default": "two"
+ }
+ ]
+ }
+ },
+ "remote": [],
+ "public": []
+}
diff --git a/tests/data/app/appinfo-multi-twice.json.license b/tests/data/app/appinfo-multi-twice.json.license
new file mode 100644
index 00000000000..5e8eced1a6a
--- /dev/null
+++ b/tests/data/app/appinfo-multi-twice.json.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
\ No newline at end of file
diff --git a/tests/data/app/appinfo-multi-twice.xml b/tests/data/app/appinfo-multi-twice.xml
new file mode 100644
index 00000000000..911e8342815
--- /dev/null
+++ b/tests/data/app/appinfo-multi-twice.xml
@@ -0,0 +1,202 @@
+
+
+
+ multi_twice
+ Multi Twice
+ Every repeatable element is used exactly twice.
+ Fixture that exercises the list normalization path.
+ 1.0.0
+ agpl
+ mit
+ Jane Doe
+ John Doe
+
+
+
+
+
+ https://example.test/multi-twice/user
+ https://example.test/multi-twice/admin
+ https://example.test/multi-twice/developer
+
+ monitoring
+ social
+ https://example.test/multi-twice
+ https://example.test/multi-twice/discussion
+ https://example.test/multi-twice/issues
+ https://example.test/multi-twice.git
+ https://example.test/multi-twice-1.png
+ https://example.test/multi-twice-2.png
+ https://example.test/donate/1
+ https://example.test/donate/2
+
+
+ sqlite
+ mysql
+ awk
+ grep
+ curl
+ intl
+
+
+ x86_64
+ aarch64
+ caldav
+ caldav
+
+
+ OCA\MultiTwice\BackgroundJob\CleanupOne
+ OCA\MultiTwice\BackgroundJob\CleanupTwo
+
+
+
+ OCA\MultiTwice\RepairStep\PreMigrationOne
+ OCA\MultiTwice\RepairStep\PreMigrationTwo
+
+
+ OCA\MultiTwice\RepairStep\PostMigrationOne
+ OCA\MultiTwice\RepairStep\PostMigrationTwo
+
+
+ OCA\MultiTwice\RepairStep\LiveMigrationOne
+ OCA\MultiTwice\RepairStep\LiveMigrationTwo
+
+
+ OCA\MultiTwice\RepairStep\InstallOne
+ OCA\MultiTwice\RepairStep\InstallTwo
+
+
+ OCA\MultiTwice\RepairStep\UninstallOne
+ OCA\MultiTwice\RepairStep\UninstallTwo
+
+
+
+ OCA\MultiTwice\TwoFactor\ProviderOne
+ OCA\MultiTwice\TwoFactor\ProviderTwo
+
+
+ OCA\MultiTwice\Command\MigrateOne
+ OCA\MultiTwice\Command\MigrateTwo
+
+
+ OCA\MultiTwice\Settings\AdminOne
+ OCA\MultiTwice\Settings\AdminTwo
+ OCA\MultiTwice\Settings\AdminSectionOne
+ OCA\MultiTwice\Settings\AdminSectionTwo
+ OCA\MultiTwice\Settings\PersonalOne
+ OCA\MultiTwice\Settings\PersonalTwo
+ OCA\MultiTwice\Settings\PersonalSectionOne
+ OCA\MultiTwice\Settings\PersonalSectionTwo
+ OCA\MultiTwice\Settings\AdminDelegationOne
+ OCA\MultiTwice\Settings\AdminDelegationTwo
+ OCA\MultiTwice\Settings\AdminDelegationSectionOne
+ OCA\MultiTwice\Settings\AdminDelegationSectionTwo
+
+
+
+ OCA\MultiTwice\Activity\SettingOne
+ OCA\MultiTwice\Activity\SettingTwo
+
+
+ OCA\MultiTwice\Activity\FilterOne
+ OCA\MultiTwice\Activity\FilterTwo
+
+
+ OCA\MultiTwice\Activity\ProviderOne
+ OCA\MultiTwice\Activity\ProviderTwo
+
+
+
+ OCA\MultiTwice\Dashboard\WidgetOne
+ OCA\MultiTwice\Dashboard\WidgetTwo
+
+
+ OCA\MultiTwice\Search\PlatformOne
+ OCA\MultiTwice\Search\PlatformTwo
+ OCA\MultiTwice\Search\ProviderOne
+ OCA\MultiTwice\Search\ProviderTwo
+
+
+
+ Multi Twice One
+ multi.twice.one
+ multi-twice-1.svg
+ 1
+
+
+ Multi Twice Two
+ multi.twice.two
+ multi-twice-2.svg
+ 2
+
+
+
+ OCA\MultiTwice\ContactsMenu\Provider
+
+
+
+ OCA\MultiTwice\Collaboration\PluginOne
+ OCA\MultiTwice\Collaboration\PluginTwo
+
+
+
+ OCA\MultiTwice\OpenMetrics\ExporterOne
+ OCA\MultiTwice\OpenMetrics\ExporterTwo
+
+
+
+ OCA\MultiTwice\Sabre\CollectionOne
+ OCA\MultiTwice\Sabre\CollectionTwo
+
+
+ OCA\MultiTwice\Sabre\PluginOne
+ OCA\MultiTwice\Sabre\PluginTwo
+
+
+ OCA\MultiTwice\Sabre\AddressBookPluginOne
+ OCA\MultiTwice\Sabre\AddressBookPluginTwo
+
+
+ OCA\MultiTwice\Sabre\CalendarPluginOne
+ OCA\MultiTwice\Sabre\CalendarPluginTwo
+
+
+
+ OCA\MultiTwice\Trash\BackendOne
+ OCA\MultiTwice\Trash\BackendTwo
+
+
+ OCA\MultiTwice\Versions\BackendOne
+ OCA\MultiTwice\Versions\BackendTwo
+
+
+
+ registry.example.test
+ multi-twice
+ 2.0.0
+
+
+ scope-one
+ scope-two
+
+ true
+
+
+ MULTI_TWICE_ONE
+ Multi Twice One
+ First variable
+ one
+
+
+ MULTI_TWICE_TWO
+ Multi Twice Two
+ Second variable
+ two
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/app/description-multi-lang.xml b/tests/data/app/description-multi-lang.xml
deleted file mode 100644
index be1dd616a99..00000000000
--- a/tests/data/app/description-multi-lang.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- files_encryption
- Server-side Encryption
- English
- German
- AGPL
-
diff --git a/tests/data/app/description-single-lang.xml b/tests/data/app/description-single-lang.xml
deleted file mode 100644
index 36fb2aacbe2..00000000000
--- a/tests/data/app/description-single-lang.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- files_encryption
- Server-side Encryption
- English
- AGPL
-
diff --git a/tests/data/app/expected-info.json b/tests/data/app/expected-info.json
index 3154644e472..d34f9e67ed3 100644
--- a/tests/data/app/expected-info.json
+++ b/tests/data/app/expected-info.json
@@ -1,5 +1,4 @@
{
- "info": [],
"remote": [],
"public": [],
"id": "files_encryption",
diff --git a/tests/data/app/navigation-one-item.json b/tests/data/app/navigation-one-item.json
index 2bd81461586..a9ce44f105d 100644
--- a/tests/data/app/navigation-one-item.json
+++ b/tests/data/app/navigation-one-item.json
@@ -72,7 +72,6 @@
}
]
},
- "info": [],
"remote": [],
"public": [],
"repair-steps": {
diff --git a/tests/data/app/navigation-two-items.json b/tests/data/app/navigation-two-items.json
index 4b081d3bbd9..118c9242794 100644
--- a/tests/data/app/navigation-two-items.json
+++ b/tests/data/app/navigation-two-items.json
@@ -78,7 +78,6 @@
}
]
},
- "info": [],
"remote": [],
"public": [],
"repair-steps": {
diff --git a/tests/data/app/various-single-item.json b/tests/data/app/various-single-item.json
index ff395f5199d..ad845adc4b0 100644
--- a/tests/data/app/various-single-item.json
+++ b/tests/data/app/various-single-item.json
@@ -22,7 +22,6 @@
"category": [
"monitoring"
],
- "info": [],
"background-jobs": [],
"activity": {
"filters": [],
diff --git a/tests/lib/App/InfoParserTest.php b/tests/lib/App/InfoParserTest.php
index b59c02198a0..09169801ff4 100644
--- a/tests/lib/App/InfoParserTest.php
+++ b/tests/lib/App/InfoParserTest.php
@@ -47,16 +47,19 @@ class InfoParserTest extends TestCase {
#[\PHPUnit\Framework\Attributes\DataProvider('appDataProvider')]
public function testApplyL10N(array $data, array $expected, string $language): void {
$parser = new InfoParser();
- $this->assertSame($expected, $parser->applyL10N($data, $language));
+ $this->assertEqualsCanonicalizing($expected, $parser->applyL10N($data, $language));
}
public static function providesInfoXml(): array {
return [
- ['expected-info.json', 'valid-info.xml'],
- [null, 'invalid-info.xml'],
- ['navigation-one-item.json', 'navigation-one-item.xml'],
- ['navigation-two-items.json', 'navigation-two-items.xml'],
- ['various-single-item.json', 'various-single-item.xml'],
+ 'Only one value in each list' => ['appinfo-multi-once.json', 'appinfo-multi-once.xml'],
+ 'Only one value in each list with attributes' => ['appinfo-attributes-once.json', 'appinfo-attributes-once.xml'],
+ 'Multiple values in each list' => ['appinfo-multi-twice.json', 'appinfo-multi-twice.xml'],
+ 'Valid info' => ['expected-info.json', 'valid-info.xml'],
+ 'Invalid info' => [null, 'invalid-info.xml'],
+ 'Navigation one item' => ['navigation-one-item.json', 'navigation-one-item.xml'],
+ 'Navigation two items' => ['navigation-two-items.json', 'navigation-two-items.xml'],
+ 'Various single item' => ['various-single-item.json', 'various-single-item.xml'],
];
}
@@ -83,17 +86,17 @@ class InfoParserTest extends TestCase {
// test trimming
[
['description' => " \t This is a multiline \n test with \n \t \n \n some new lines "],
- ['description' => "This is a multiline \n test with \n \t \n \n some new lines"],
+ ['description' => "This is a multiline \n test with \n \t \n \n some new lines", 'summary' => '', 'name' => ''],
'en'
],
[
['description' => " \t This is a multiline \n test with \n \t some new lines "],
- ['description' => "This is a multiline \n test with \n \t some new lines"],
+ ['description' => "This is a multiline \n test with \n \t some new lines", 'summary' => '', 'name' => ''],
'en'
],
[
['description' => hex2bin('5065726d657420646520732761757468656e7469666965722064616e732070697769676f20646972656374656d656e74206176656320736573206964656e74696669616e7473206f776e636c6f75642073616e73206c65732072657461706572206574206d657420c3a0206a6f757273206365757820636920656e20636173206465206368616e67656d656e74206465206d6f742064652070617373652e0d0a0d')],
- ['description' => "Permet de s'authentifier dans piwigo directement avec ses identifiants owncloud sans les retaper et met à jours ceux ci en cas de changement de mot de passe."],
+ ['description' => "Permet de s'authentifier dans piwigo directement avec ses identifiants owncloud sans les retaper et met à jours ceux ci en cas de changement de mot de passe.", 'summary' => '', 'name' => ''],
'fr'
],
// test proper translation handling