From 22fdcb1af3cb4ca94591e9a4ed976812fc577ead Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Tue, 14 Apr 2026 16:09:52 -0400 Subject: [PATCH 1/2] feat: Add icon validation to publish event To lockdown unwanted formats for icons, a check was added at publish time to ensure that files of those formats aren't included in the payload. --- .../eclipse/openvsx/ExtensionProcessor.java | 2 +- .../PublishExtensionVersionHandler.java | 42 +++++++++--- .../org/eclipse/openvsx/RegistryAPITest.java | 12 ++-- .../PublishExtensionVersionHandlerTest.java | 68 ++++++++++++++----- 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java index 35bfb52c8..9520c3e8f 100644 --- a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java +++ b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java @@ -472,7 +472,7 @@ private String tryGetAssetPath(String type) { return null; } - protected TempFile getIcon(ExtensionVersion extVersion) throws IOException { + public TempFile getIcon(ExtensionVersion extVersion) throws IOException { var iconPath = tryGetAssetPath(ExtensionQueryResult.ExtensionFile.FILE_ICON); if (StringUtils.isEmpty(iconPath)) { loadPackageJson(); diff --git a/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java b/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java index 484cd37fd..62888b228 100644 --- a/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java @@ -9,9 +9,12 @@ * ****************************************************************************** */ package org.eclipse.openvsx.publish; -import com.google.common.base.Joiner; -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.ExtensionProcessor; @@ -19,11 +22,19 @@ import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.UserService; import org.eclipse.openvsx.adapter.VSCodeIdNewExtensionJobRequest; -import org.eclipse.openvsx.entities.*; +import org.eclipse.openvsx.entities.Extension; +import org.eclipse.openvsx.entities.ExtensionScan; +import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.entities.FileResource; +import org.eclipse.openvsx.entities.PersonalAccessToken; +import org.eclipse.openvsx.entities.UserData; import org.eclipse.openvsx.extension_control.ExtensionControlService; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanService; -import org.eclipse.openvsx.util.*; +import org.eclipse.openvsx.util.ErrorResultException; +import org.eclipse.openvsx.util.ExtensionId; +import org.eclipse.openvsx.util.NamingUtil; +import org.eclipse.openvsx.util.TempFile; import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,10 +44,10 @@ import org.springframework.stereotype.Component; import org.springframework.web.server.ServerErrorException; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.List; -import java.util.function.Consumer; +import com.google.common.base.Joiner; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; @Component public class PublishExtensionVersionHandler { @@ -134,6 +145,8 @@ private ExtensionVersion createExtensionVersion(ExtensionProcessor processor, Us extVersion.setPublishedWith(token); extVersion.setActive(false); + validateIcon(processor, extVersion); + var extension = repositories.findExtension(extensionName, namespace); if (extension == null) { extension = new Extension(); @@ -202,6 +215,17 @@ private void checkLicense(ExtensionVersion extVersion, TempFile licenseFile) { } } + private void validateIcon(ExtensionProcessor processor, ExtensionVersion extVersion) { + try (var iconTmpFile = processor.getIcon(extVersion)) { + var ext = FilenameUtils.getExtension(iconTmpFile.getPath().toString()); + if ("svg".equalsIgnoreCase(ext)) { + throw new ErrorResultException("This extension cannot be accepted as it contains a denied icon image format."); + } + } catch (IOException e) { + logger.warn("Failed to check whether icon is a denied format or not", e); + } + } + private void validateMetadata(ExtensionVersion extVersion) { var metadataIssues = validator.validateMetadata(extVersion); if (!metadataIssues.isEmpty()) { diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index 61f6b5574..3763df785 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -1709,7 +1709,7 @@ void testPublishSameVersionDifferentTargetPlatformPreRelease() throws Exception return extensionVersion.getVersion().equals(extVersion.getVersion()); }); - var bytes = createExtensionPackage("bar", "1.0.0", null, true, TargetPlatform.NAME_LINUX_X64); + var bytes = createExtensionPackage("bar", "1.0.0", null, true, TargetPlatform.NAME_LINUX_X64, "/tmp/known-good-icon.jpg"); mockMvc.perform(post("/api/-/publish?token={token}", "my_token") .contentType(MediaType.APPLICATION_OCTET_STREAM) .content(bytes)) @@ -1731,7 +1731,7 @@ void testPublishSameVersionDifferentTargetPlatformStableRelease() throws Excepti return extensionVersion.getVersion().equals(extVersion.getVersion()); }); - var bytes = createExtensionPackage("bar", "1.5.0", null, false, TargetPlatform.NAME_ALPINE_ARM64); + var bytes = createExtensionPackage("bar", "1.5.0", null, false, TargetPlatform.NAME_ALPINE_ARM64,"/tmp/known-good-icon.png"); mockMvc.perform(post("/api/-/publish?token={token}", "my_token") .contentType(MediaType.APPLICATION_OCTET_STREAM) .content(bytes)) @@ -2483,10 +2483,10 @@ private String warningJson(String message) throws JsonProcessingException { } private byte[] createExtensionPackage(String name, String version, String license) throws IOException { - return createExtensionPackage(name, version, license, false, null); + return createExtensionPackage(name, version, license, false, null, "/tmp/known-good-icon.png"); } - private byte[] createExtensionPackage(String name, String version, String license, boolean preRelease, String targetPlatform) throws IOException { + private byte[] createExtensionPackage(String name, String version, String license, boolean preRelease, String targetPlatform, String iconPath) throws IOException { var bytes = new ByteArrayOutputStream(); var archive = new ZipOutputStream(bytes); archive.putNextEntry(new ZipEntry("extension.vsixmanifest")); @@ -2516,6 +2516,7 @@ private byte[] createExtensionPackage(String name, String version, String licens "" + "" + "" + + "" + "" + ""; archive.write(vsixmanifest.getBytes()); @@ -2529,6 +2530,9 @@ private byte[] createExtensionPackage(String name, String version, String licens "}"; archive.write(packageJson.getBytes()); archive.closeEntry(); + archive.putNextEntry(new ZipEntry(iconPath)); + archive.write("placeholder".getBytes()); + archive.closeEntry(); archive.finish(); return bytes.toByteArray(); } diff --git a/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java b/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java index b1fe8341c..c237148f9 100644 --- a/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java @@ -1,4 +1,4 @@ -/******************************************************************************** +/** ****************************************************************************** * Copyright (c) 2025 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -9,10 +9,20 @@ * http://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-2.0 - ********************************************************************************/ + ******************************************************************************* */ package org.eclipse.openvsx.publish; -import jakarta.persistence.EntityManager; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + import org.eclipse.openvsx.ExtensionProcessor; import org.eclipse.openvsx.ExtensionValidator; import org.eclipse.openvsx.UserService; @@ -24,23 +34,17 @@ import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanService; import org.eclipse.openvsx.util.ErrorResultException; +import org.eclipse.openvsx.util.TempFile; import org.jobrunr.scheduling.JobRequestScheduler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import jakarta.persistence.EntityManager; @ExtendWith(MockitoExtension.class) class PublishExtensionVersionHandlerTest { @@ -87,20 +91,21 @@ void setUp() throws Exception { extensionControl, scanService ); - + // Lenient: not all tests need this mock org.mockito.Mockito.lenient() - .when(extensionControl.getMaliciousExtensionIds()) - .thenReturn(Collections.emptyList()); + .when(extensionControl.getMaliciousExtensionIds()) + .thenReturn(Collections.emptyList()); } @Test - void shouldCreateExtensionWhenNamespaceExists() { + void shouldCreateExtensionWhenNamespaceExists() throws IOException { // Happy path: extension version gets persisted. var processor = org.mockito.Mockito.mock(ExtensionProcessor.class); when(processor.getNamespace()).thenReturn("publisher"); when(processor.getExtensionName()).thenReturn("demo"); when(processor.getVersion()).thenReturn("2.0.0"); + when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile("sample-icon-image", ".png")); when(processor.getExtensionDependencies()).thenReturn(List.of()); when(processor.getBundledExtensions()).thenReturn(List.of()); @@ -151,10 +156,39 @@ void shouldFailWhenNamespaceDoesNotExist() { .hasMessageContaining("Unknown publisher"); } + @Test + void shouldFailWhenImageFormatIsDisallowed() throws IOException { + + var processor = org.mockito.Mockito.mock(ExtensionProcessor.class); + when(processor.getNamespace()).thenReturn("publisher"); + when(processor.getExtensionName()).thenReturn("demo"); + when(processor.getVersion()).thenReturn("2.0.0"); + when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile("sample-icon-image", ".svg")); + + var metadata = new ExtensionVersion(); + metadata.setDisplayName("Demo OK"); + metadata.setVersion("2.0.0"); + metadata.setTargetPlatform("any"); + when(processor.getMetadata()).thenReturn(metadata); + + var namespace = buildNamespace("publisher"); + var user = new org.eclipse.openvsx.entities.UserData(); + var token = new PersonalAccessToken(); + token.setUser(user); + + when(repositories.findNamespace("publisher")).thenReturn(namespace); + when(users.hasPublishPermission(user, namespace)).thenReturn(true); + when(validator.validateExtensionVersion("2.0.0")).thenReturn(Optional.empty()); + when(validator.validateExtensionName("demo")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> handler.createExtensionVersion(processor, token, LocalDateTime.now(), false)) + .isInstanceOf(ErrorResultException.class) + .hasMessageContaining("contains a denied icon image format"); + } + private Namespace buildNamespace(String name) { var namespace = new Namespace(); namespace.setName(name); return namespace; } } - From 6d0b3412ecc044ed8d2899e119abfff6edb4fa04 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Mon, 20 Apr 2026 09:59:07 +0200 Subject: [PATCH 2/2] rework unsupport icon format check, introduce a PublishingConfig class and adapt unit tests --- .../eclipse/openvsx/ExtensionProcessor.java | 3 +- .../org/eclipse/openvsx/ExtensionService.java | 25 +- .../PublishExtensionVersionHandler.java | 45 +- .../openvsx/publish/PublishingConfig.java | 60 +++ .../org/eclipse/openvsx/util/TempFile.java | 2 +- .../org/eclipse/openvsx/RegistryAPITest.java | 37 +- .../java/org/eclipse/openvsx/UserAPITest.java | 2 + .../eclipse/openvsx/admin/AdminAPITest.java | 2 + .../openvsx/eclipse/EclipseServiceTest.java | 2 + .../PublishExtensionVersionHandlerTest.java | 421 ++++++++++-------- 10 files changed, 353 insertions(+), 246 deletions(-) create mode 100644 server/src/main/java/org/eclipse/openvsx/publish/PublishingConfig.java diff --git a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java index 9520c3e8f..2f74efdd0 100644 --- a/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java +++ b/server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.MissingNode; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import jakarta.annotation.Nullable; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.adapter.ExtensionQueryResult; @@ -472,7 +473,7 @@ private String tryGetAssetPath(String type) { return null; } - public TempFile getIcon(ExtensionVersion extVersion) throws IOException { + public @Nullable TempFile getIcon(ExtensionVersion extVersion) throws IOException { var iconPath = tryGetAssetPath(ExtensionQueryResult.ExtensionFile.FILE_ICON); if (StringUtils.isEmpty(iconPath)) { loadPackageJson(); diff --git a/server/src/main/java/org/eclipse/openvsx/ExtensionService.java b/server/src/main/java/org/eclipse/openvsx/ExtensionService.java index 641ef0b43..a39ba2596 100644 --- a/server/src/main/java/org/eclipse/openvsx/ExtensionService.java +++ b/server/src/main/java/org/eclipse/openvsx/ExtensionService.java @@ -21,15 +21,15 @@ import org.eclipse.openvsx.json.ResultJson; import org.eclipse.openvsx.json.TargetPlatformVersionJson; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; +import org.eclipse.openvsx.publish.PublishingConfig; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService; import org.eclipse.openvsx.scanning.ExtensionScanService; import org.eclipse.openvsx.search.SearchUtilService; import org.eclipse.openvsx.util.*; import org.jobrunr.scheduling.JobRequestScheduler; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import java.io.BufferedInputStream; import java.io.IOException; @@ -42,10 +42,9 @@ import java.util.Objects; import java.util.stream.Collectors; -@Component +@Service public class ExtensionService { - private static final int MAX_CONTENT_SIZE = 512 * 1024 * 1024; - + private final PublishingConfig publishingConfig; private final EntityManager entityManager; private final RepositoryService repositories; private final SearchUtilService search; @@ -56,10 +55,8 @@ public class ExtensionService { private final ExtensionScanService scanService; private final ExtensionScanPersistenceService scanPersistenceService; - @Value("${ovsx.publishing.max-content-size:" + MAX_CONTENT_SIZE + "}") - int maxContentSize; - public ExtensionService( + PublishingConfig publishingConfig, EntityManager entityManager, RepositoryService repositories, SearchUtilService search, @@ -70,6 +67,7 @@ public ExtensionService( ExtensionScanService scanService, ExtensionScanPersistenceService scanPersistenceService ) { + this.publishingConfig = publishingConfig; this.entityManager = entityManager; this.repositories = repositories; this.search = search; @@ -81,14 +79,8 @@ public ExtensionService( this.scanPersistenceService = scanPersistenceService; } - // For testing only - boolean isLicenseRequired() { - return publishHandler.isLicenseRequired(); - } - - // For testing only - void setLicenseRequired(boolean requireLicense) { - publishHandler.setLicenseRequired(requireLicense); + private long getMaxContentSize() { + return publishingConfig.getMaxContentSize(); } @Transactional @@ -164,6 +156,7 @@ private void doPublish(TempFile extensionFile, String binaryName, PersonalAccess } private TempFile createExtensionFile(InputStream content) { + long maxContentSize = getMaxContentSize(); try (var input = ByteStreams.limit(new BufferedInputStream(content), maxContentSize + 1)) { long size; var extensionFile = new TempFile("extension_", ".vsix"); diff --git a/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java b/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java index 62888b228..d40ad7f18 100644 --- a/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java +++ b/server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java @@ -10,9 +10,11 @@ package org.eclipse.openvsx.publish; import java.io.IOException; +import java.nio.file.Path; import java.time.LocalDateTime; import java.util.List; import java.util.function.Consumer; +import java.util.function.Predicate; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; @@ -38,7 +40,6 @@ import org.jobrunr.scheduling.JobRequestScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.retry.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @@ -51,12 +52,9 @@ @Component public class PublishExtensionVersionHandler { - protected final Logger logger = LoggerFactory.getLogger(PublishExtensionVersionHandler.class); - @Value("${ovsx.publishing.require-license:false}") - boolean requireLicense; - + private final PublishingConfig config; private final PublishExtensionVersionService service; private final ExtensionVersionIntegrityService integrityService; private final EntityManager entityManager; @@ -67,7 +65,10 @@ public class PublishExtensionVersionHandler { private final ExtensionControlService extensionControl; private final ExtensionScanService scanService; + private final Predicate unsupportedIconExtensions; + public PublishExtensionVersionHandler( + PublishingConfig config, PublishExtensionVersionService service, ExtensionVersionIntegrityService integrityService, EntityManager entityManager, @@ -78,6 +79,7 @@ public PublishExtensionVersionHandler( ExtensionControlService extensionControl, ExtensionScanService scanService ) { + this.config = config; this.service = service; this.integrityService = integrityService; this.entityManager = entityManager; @@ -87,14 +89,19 @@ public PublishExtensionVersionHandler( this.validator = validator; this.extensionControl = extensionControl; this.scanService = scanService; - } - public boolean isLicenseRequired() { - return requireLicense; + this.unsupportedIconExtensions = path -> { + if (path == null) { + return false; + } + + var fileExtension = FilenameUtils.getExtension(path.toString()); + return config.getUnsupportedIconFormats().stream().anyMatch(ext -> ext.equalsIgnoreCase(fileExtension)); + }; } - public void setLicenseRequired(boolean requireLicense) { - this.requireLicense = requireLicense; + public boolean isLicenseRequired() { + return config.isRequireLicense(); } @Transactional(rollbackOn = ErrorResultException.class) @@ -145,8 +152,6 @@ private ExtensionVersion createExtensionVersion(ExtensionProcessor processor, Us extVersion.setPublishedWith(token); extVersion.setActive(false); - validateIcon(processor, extVersion); - var extension = repositories.findExtension(extensionName, namespace); if (extension == null) { extension = new Extension(); @@ -173,6 +178,7 @@ private ExtensionVersion createExtensionVersion(ExtensionProcessor processor, Us extVersion.setExtension(extension); validateLicense(processor, extVersion); + validateIcon(processor, extVersion); validateMetadata(extVersion); entityManager.persist(extVersion); return extVersion; @@ -198,7 +204,7 @@ private void validateExtensionName(String namespaceName, String extensionName, S } private void validateLicense(ExtensionProcessor processor, ExtensionVersion extVersion) { - if (requireLicense) { + if (isLicenseRequired()) { // Check the extension's license try (var licenseFile = processor.getLicense(extVersion)) { checkLicense(extVersion, licenseFile); @@ -216,14 +222,13 @@ private void checkLicense(ExtensionVersion extVersion, TempFile licenseFile) { } private void validateIcon(ExtensionProcessor processor, ExtensionVersion extVersion) { - try (var iconTmpFile = processor.getIcon(extVersion)) { - var ext = FilenameUtils.getExtension(iconTmpFile.getPath().toString()); - if ("svg".equalsIgnoreCase(ext)) { - throw new ErrorResultException("This extension cannot be accepted as it contains a denied icon image format."); + try (var iconFile = processor.getIcon(extVersion)) { + if (iconFile != null && unsupportedIconExtensions.test(iconFile.getPath())) { + throw new ErrorResultException("This extension cannot be accepted as it uses an unsupported icon format."); + } + } catch (IOException e) { + throw new ServerErrorException("Failed to read icon file", e); } - } catch (IOException e) { - logger.warn("Failed to check whether icon is a denied format or not", e); - } } private void validateMetadata(ExtensionVersion extVersion) { diff --git a/server/src/main/java/org/eclipse/openvsx/publish/PublishingConfig.java b/server/src/main/java/org/eclipse/openvsx/publish/PublishingConfig.java new file mode 100644 index 000000000..6e08c6c73 --- /dev/null +++ b/server/src/main/java/org/eclipse/openvsx/publish/PublishingConfig.java @@ -0,0 +1,60 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ + +package org.eclipse.openvsx.publish; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConfigurationProperties("ovsx.publishing") +public class PublishingConfig { + private static final int MAX_CONTENT_SIZE = 512 * 1024 * 1024; + + private long maxContentSize = MAX_CONTENT_SIZE; + + private boolean requireLicense; + + /** + * Allows to specify a list of unsupported icon formats identified by their extension. + *

+ * See: Block SVG Images + */ + private List unsupportedIconFormats = List.of("svg"); + + public long getMaxContentSize() { + return maxContentSize; + } + + public void setMaxContentSize(int maxContentSize) { + this.maxContentSize = maxContentSize; + } + + public boolean isRequireLicense() { + return requireLicense; + } + + public void setRequireLicense(boolean requireLicense) { + this.requireLicense = requireLicense; + } + + public List getUnsupportedIconFormats() { + return unsupportedIconFormats; + } + + public void setUnsupportedIconFormats(List unsupportedIconFormats) { + this.unsupportedIconFormats = unsupportedIconFormats; + } +} diff --git a/server/src/main/java/org/eclipse/openvsx/util/TempFile.java b/server/src/main/java/org/eclipse/openvsx/util/TempFile.java index 62ece2b80..3a077d47f 100644 --- a/server/src/main/java/org/eclipse/openvsx/util/TempFile.java +++ b/server/src/main/java/org/eclipse/openvsx/util/TempFile.java @@ -61,6 +61,6 @@ public void setNamespace(Namespace namespace) { @Override public void close() throws IOException { - Files.delete(path); + Files.deleteIfExists(path); } } diff --git a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java index 3763df785..e8a8ac056 100644 --- a/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java @@ -29,6 +29,7 @@ import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; import org.eclipse.openvsx.publish.PublishExtensionVersionService; +import org.eclipse.openvsx.publish.PublishingConfig; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService; import org.eclipse.openvsx.scanning.ExtensionScanService; @@ -120,6 +121,9 @@ class RegistryAPITest { @Autowired MockMvc mockMvc; + @Autowired + PublishingConfig publishingConfig; + @Autowired ExtensionService extensionService; @@ -1525,9 +1529,9 @@ void testPublishOrphan() throws Exception { @Test void testPublishRequireLicenseNone() throws Exception { - var previousRequireLicense = extensionService.isLicenseRequired(); + var previousRequireLicense = publishingConfig.isRequireLicense(); try { - extensionService.setLicenseRequired(true); + publishingConfig.setRequireLicense(true); mockForPublish("contributor"); var bytes = createExtensionPackage("bar", "1.0.0", null); mockMvc.perform(post("/api/-/publish?token={token}", "my_token") @@ -1536,15 +1540,15 @@ void testPublishRequireLicenseNone() throws Exception { .andExpect(status().isBadRequest()) .andExpect(content().json(errorJson("This extension cannot be accepted because it has no license."))); } finally { - extensionService.setLicenseRequired(previousRequireLicense); + publishingConfig.setRequireLicense(previousRequireLicense); } } @Test void testPublishRequireLicenseOk() throws Exception { - var previousRequireLicense = extensionService.isLicenseRequired(); + var previousRequireLicense = publishingConfig.isRequireLicense(); try { - extensionService.setLicenseRequired(true); + publishingConfig.setRequireLicense(true); mockForPublish("contributor"); mockActiveVersion(); var bytes = createExtensionPackage("bar", "1.0.0", "MIT"); @@ -1563,7 +1567,7 @@ void testPublishRequireLicenseOk() throws Exception { e.setDownloadable(true); }))); } finally { - extensionService.setLicenseRequired(previousRequireLicense); + publishingConfig.setRequireLicense(previousRequireLicense); } } @@ -1709,7 +1713,7 @@ void testPublishSameVersionDifferentTargetPlatformPreRelease() throws Exception return extensionVersion.getVersion().equals(extVersion.getVersion()); }); - var bytes = createExtensionPackage("bar", "1.0.0", null, true, TargetPlatform.NAME_LINUX_X64, "/tmp/known-good-icon.jpg"); + var bytes = createExtensionPackage("bar", "1.0.0", null, true, TargetPlatform.NAME_LINUX_X64); mockMvc.perform(post("/api/-/publish?token={token}", "my_token") .contentType(MediaType.APPLICATION_OCTET_STREAM) .content(bytes)) @@ -1731,7 +1735,7 @@ void testPublishSameVersionDifferentTargetPlatformStableRelease() throws Excepti return extensionVersion.getVersion().equals(extVersion.getVersion()); }); - var bytes = createExtensionPackage("bar", "1.5.0", null, false, TargetPlatform.NAME_ALPINE_ARM64,"/tmp/known-good-icon.png"); + var bytes = createExtensionPackage("bar", "1.5.0", null, false, TargetPlatform.NAME_ALPINE_ARM64); mockMvc.perform(post("/api/-/publish?token={token}", "my_token") .contentType(MediaType.APPLICATION_OCTET_STREAM) .content(bytes)) @@ -2483,10 +2487,10 @@ private String warningJson(String message) throws JsonProcessingException { } private byte[] createExtensionPackage(String name, String version, String license) throws IOException { - return createExtensionPackage(name, version, license, false, null, "/tmp/known-good-icon.png"); + return createExtensionPackage(name, version, license, false, null); } - private byte[] createExtensionPackage(String name, String version, String license, boolean preRelease, String targetPlatform, String iconPath) throws IOException { + private byte[] createExtensionPackage(String name, String version, String license, boolean preRelease, String targetPlatform) throws IOException { var bytes = new ByteArrayOutputStream(); var archive = new ZipOutputStream(bytes); archive.putNextEntry(new ZipEntry("extension.vsixmanifest")); @@ -2516,7 +2520,6 @@ private byte[] createExtensionPackage(String name, String version, String licens "" + "" + "" + - "" + "" + ""; archive.write(vsixmanifest.getBytes()); @@ -2530,9 +2533,6 @@ private byte[] createExtensionPackage(String name, String version, String licens "}"; archive.write(packageJson.getBytes()); archive.closeEntry(); - archive.putNextEntry(new ZipEntry(iconPath)); - archive.write("placeholder".getBytes()); - archive.closeEntry(); archive.finish(); return bytes.toByteArray(); } @@ -2621,8 +2621,14 @@ LocalRegistryService localRegistryService( ); } + @Bean + PublishingConfig publishingConfig() { + return new PublishingConfig(); + } + @Bean PublishExtensionVersionHandler publishExtensionVersionHandler( + PublishingConfig publishingConfig, PublishExtensionVersionService service, ExtensionVersionIntegrityService integrityService, EntityManager entityManager, @@ -2634,6 +2640,7 @@ PublishExtensionVersionHandler publishExtensionVersionHandler( ExtensionScanService extensionScanService ) { return new PublishExtensionVersionHandler( + publishingConfig, service, integrityService, entityManager, @@ -2648,6 +2655,7 @@ PublishExtensionVersionHandler publishExtensionVersionHandler( @Bean ExtensionService extensionService( + PublishingConfig publishingConfig, EntityManager entityManager, RepositoryService repositories, SearchUtilService search, @@ -2659,6 +2667,7 @@ ExtensionService extensionService( ExtensionScanPersistenceService scanPersistenceService ) { return new ExtensionService( + publishingConfig, entityManager, repositories, search, diff --git a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java index 8c21a0c1f..8934cbb4f 100644 --- a/server/src/test/java/org/eclipse/openvsx/UserAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/UserAPITest.java @@ -24,6 +24,7 @@ import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; +import org.eclipse.openvsx.publish.PublishingConfig; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService; import org.eclipse.openvsx.scanning.ExtensionScanService; @@ -895,6 +896,7 @@ ExtensionService extensionService( ExtensionScanPersistenceService scanPersistenceService ) { return new ExtensionService( + new PublishingConfig(), entityManager, repositories, search, diff --git a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java index 22a580713..6c64b1a2a 100644 --- a/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/admin/AdminAPITest.java @@ -26,6 +26,7 @@ import org.eclipse.openvsx.mail.MailService; import org.eclipse.openvsx.publish.ExtensionVersionIntegrityService; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; +import org.eclipse.openvsx.publish.PublishingConfig; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService; import org.eclipse.openvsx.scanning.ExtensionScanService; @@ -1586,6 +1587,7 @@ ExtensionService extensionService( ExtensionScanPersistenceService scanPersistenceService ) { return new ExtensionService( + new PublishingConfig(), entityManager, repositories, search, diff --git a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java index 6552d775e..7d2dd0d4f 100644 --- a/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/eclipse/EclipseServiceTest.java @@ -21,6 +21,7 @@ import org.eclipse.openvsx.cache.LatestExtensionVersionCacheKeyGenerator; import org.eclipse.openvsx.entities.*; import org.eclipse.openvsx.publish.PublishExtensionVersionHandler; +import org.eclipse.openvsx.publish.PublishingConfig; import org.eclipse.openvsx.repositories.RepositoryService; import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService; import org.eclipse.openvsx.scanning.ExtensionScanService; @@ -420,6 +421,7 @@ ExtensionService extensionService( ExtensionScanPersistenceService scanPersistenceService ) { return new ExtensionService( + new PublishingConfig(), entityManager, repositories, search, diff --git a/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java b/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java index c237148f9..4eb3761ee 100644 --- a/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java +++ b/server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java @@ -1,194 +1,227 @@ -/** ****************************************************************************** - * Copyright (c) 2025 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * SPDX-License-Identifier: EPL-2.0 - ******************************************************************************* */ -package org.eclipse.openvsx.publish; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import org.eclipse.openvsx.ExtensionProcessor; -import org.eclipse.openvsx.ExtensionValidator; -import org.eclipse.openvsx.UserService; -import org.eclipse.openvsx.entities.Extension; -import org.eclipse.openvsx.entities.ExtensionVersion; -import org.eclipse.openvsx.entities.Namespace; -import org.eclipse.openvsx.entities.PersonalAccessToken; -import org.eclipse.openvsx.extension_control.ExtensionControlService; -import org.eclipse.openvsx.repositories.RepositoryService; -import org.eclipse.openvsx.scanning.ExtensionScanService; -import org.eclipse.openvsx.util.ErrorResultException; -import org.eclipse.openvsx.util.TempFile; -import org.jobrunr.scheduling.JobRequestScheduler; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import jakarta.persistence.EntityManager; - -@ExtendWith(MockitoExtension.class) -class PublishExtensionVersionHandlerTest { - - @Mock - PublishExtensionVersionService publishService; - - @Mock - ExtensionVersionIntegrityService integrityService; - - @Mock - EntityManager entityManager; - - @Mock - RepositoryService repositories; - - @Mock - JobRequestScheduler scheduler; - - @Mock - UserService users; - - @Mock - ExtensionValidator validator; - - @Mock - ExtensionControlService extensionControl; - - @Mock - ExtensionScanService scanService; - - private PublishExtensionVersionHandler handler; - - @BeforeEach - void setUp() throws Exception { - handler = new PublishExtensionVersionHandler( - publishService, - integrityService, - entityManager, - repositories, - scheduler, - users, - validator, - extensionControl, - scanService - ); - - // Lenient: not all tests need this mock - org.mockito.Mockito.lenient() - .when(extensionControl.getMaliciousExtensionIds()) - .thenReturn(Collections.emptyList()); - } - - @Test - void shouldCreateExtensionWhenNamespaceExists() throws IOException { - // Happy path: extension version gets persisted. - var processor = org.mockito.Mockito.mock(ExtensionProcessor.class); - when(processor.getNamespace()).thenReturn("publisher"); - when(processor.getExtensionName()).thenReturn("demo"); - when(processor.getVersion()).thenReturn("2.0.0"); - when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile("sample-icon-image", ".png")); - when(processor.getExtensionDependencies()).thenReturn(List.of()); - when(processor.getBundledExtensions()).thenReturn(List.of()); - - var metadata = new ExtensionVersion(); - metadata.setDisplayName("Demo OK"); - metadata.setVersion("2.0.0"); - metadata.setTargetPlatform("any"); - when(processor.getMetadata()).thenReturn(metadata); - - var namespace = buildNamespace("publisher"); - var user = new org.eclipse.openvsx.entities.UserData(); - var token = new PersonalAccessToken(); - token.setUser(user); - - when(repositories.findNamespace("publisher")).thenReturn(namespace); - when(users.hasPublishPermission(user, namespace)).thenReturn(true); - when(validator.validateExtensionVersion("2.0.0")).thenReturn(Optional.empty()); - when(validator.validateExtensionName("demo")).thenReturn(Optional.empty()); - when(validator.validateMetadata(metadata)).thenReturn(List.of()); - when(repositories.findExtension("demo", namespace)).thenReturn(null); - - var capturedExtension = ArgumentCaptor.forClass(Extension.class); - - var result = handler.createExtensionVersion(processor, token, LocalDateTime.now(), false); - - verify(entityManager).persist(capturedExtension.capture()); - verify(entityManager).persist(metadata); - assertThat(result).isSameAs(metadata); - assertThat(result.getPublishedWith()).isEqualTo(token); - assertThat(result.getExtension()).isSameAs(capturedExtension.getValue()); - assertThat(result.getExtension().getNamespace()).isSameAs(namespace); - } - - @Test - void shouldFailWhenNamespaceDoesNotExist() { - // When namespace doesn't exist, handler should throw an error. - var processor = org.mockito.Mockito.mock(ExtensionProcessor.class); - when(processor.getNamespace()).thenReturn("unknown"); - - var user = new org.eclipse.openvsx.entities.UserData(); - var token = new PersonalAccessToken(); - token.setUser(user); - - when(repositories.findNamespace("unknown")).thenReturn(null); - - assertThatThrownBy(() -> handler.createExtensionVersion(processor, token, LocalDateTime.now(), false)) - .isInstanceOf(ErrorResultException.class) - .hasMessageContaining("Unknown publisher"); - } - - @Test - void shouldFailWhenImageFormatIsDisallowed() throws IOException { - - var processor = org.mockito.Mockito.mock(ExtensionProcessor.class); - when(processor.getNamespace()).thenReturn("publisher"); - when(processor.getExtensionName()).thenReturn("demo"); - when(processor.getVersion()).thenReturn("2.0.0"); - when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile("sample-icon-image", ".svg")); - - var metadata = new ExtensionVersion(); - metadata.setDisplayName("Demo OK"); - metadata.setVersion("2.0.0"); - metadata.setTargetPlatform("any"); - when(processor.getMetadata()).thenReturn(metadata); - - var namespace = buildNamespace("publisher"); - var user = new org.eclipse.openvsx.entities.UserData(); - var token = new PersonalAccessToken(); - token.setUser(user); - - when(repositories.findNamespace("publisher")).thenReturn(namespace); - when(users.hasPublishPermission(user, namespace)).thenReturn(true); - when(validator.validateExtensionVersion("2.0.0")).thenReturn(Optional.empty()); - when(validator.validateExtensionName("demo")).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> handler.createExtensionVersion(processor, token, LocalDateTime.now(), false)) - .isInstanceOf(ErrorResultException.class) - .hasMessageContaining("contains a denied icon image format"); - } - - private Namespace buildNamespace(String name) { - var namespace = new Namespace(); - namespace.setName(name); - return namespace; - } -} +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ +package org.eclipse.openvsx.publish; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.eclipse.openvsx.ExtensionProcessor; +import org.eclipse.openvsx.ExtensionValidator; +import org.eclipse.openvsx.UserService; +import org.eclipse.openvsx.entities.Extension; +import org.eclipse.openvsx.entities.ExtensionVersion; +import org.eclipse.openvsx.entities.Namespace; +import org.eclipse.openvsx.entities.PersonalAccessToken; +import org.eclipse.openvsx.extension_control.ExtensionControlService; +import org.eclipse.openvsx.repositories.RepositoryService; +import org.eclipse.openvsx.scanning.ExtensionScanService; +import org.eclipse.openvsx.util.ErrorResultException; +import org.eclipse.openvsx.util.TempFile; +import org.jobrunr.scheduling.JobRequestScheduler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.persistence.EntityManager; + +@ExtendWith(MockitoExtension.class) +class PublishExtensionVersionHandlerTest { + + @Mock + PublishExtensionVersionService publishService; + + @Mock + ExtensionVersionIntegrityService integrityService; + + @Mock + EntityManager entityManager; + + @Mock + RepositoryService repositories; + + @Mock + JobRequestScheduler scheduler; + + @Mock + UserService users; + + @Mock + ExtensionValidator validator; + + @Mock + ExtensionControlService extensionControl; + + @Mock + ExtensionScanService scanService; + + private PublishingConfig config; + + private PublishExtensionVersionHandler handler; + + @BeforeEach + void setUp() throws Exception { + config = new PublishingConfig(); + + handler = new PublishExtensionVersionHandler( + config, + publishService, + integrityService, + entityManager, + repositories, + scheduler, + users, + validator, + extensionControl, + scanService + ); + + // Lenient: not all tests need this mock + org.mockito.Mockito.lenient() + .when(extensionControl.getMaliciousExtensionIds()) + .thenReturn(Collections.emptyList()); + } + + @Test + void shouldCreateExtensionWhenNamespaceExists() throws IOException { + // Happy path: extension version gets persisted. + try (var processor = org.mockito.Mockito.mock(ExtensionProcessor.class)) { + var metadata = mockExtensionVersion("publisher", "demo", "2.0.0", null, processor); + + when(processor.getExtensionDependencies()).thenReturn(List.of()); + when(processor.getBundledExtensions()).thenReturn(List.of()); + + var namespace = buildNamespace("publisher"); + var user = new org.eclipse.openvsx.entities.UserData(); + var token = new PersonalAccessToken(); + token.setUser(user); + + when(repositories.findNamespace("publisher")).thenReturn(namespace); + when(users.hasPublishPermission(user, namespace)).thenReturn(true); + when(validator.validateExtensionVersion("2.0.0")).thenReturn(Optional.empty()); + when(validator.validateExtensionName("demo")).thenReturn(Optional.empty()); + when(validator.validateMetadata(metadata)).thenReturn(List.of()); + when(repositories.findExtension("demo", namespace)).thenReturn(null); + + var capturedExtension = ArgumentCaptor.forClass(Extension.class); + + var result = handler.createExtensionVersion(processor, token, LocalDateTime.now(), false); + + verify(entityManager).persist(capturedExtension.capture()); + verify(entityManager).persist(metadata); + assertThat(result).isSameAs(metadata); + assertThat(result.getPublishedWith()).isEqualTo(token); + assertThat(result.getExtension()).isSameAs(capturedExtension.getValue()); + assertThat(result.getExtension().getNamespace()).isSameAs(namespace); + } + } + + @Test + void shouldFailWhenNamespaceDoesNotExist() { + // When namespace doesn't exist, handler should throw an error. + try (var processor = org.mockito.Mockito.mock(ExtensionProcessor.class)) { + when(processor.getNamespace()).thenReturn("unknown"); + + var user = new org.eclipse.openvsx.entities.UserData(); + var token = new PersonalAccessToken(); + token.setUser(user); + + when(repositories.findNamespace("unknown")).thenReturn(null); + + assertThatThrownBy(() -> handler.createExtensionVersion(processor, token, LocalDateTime.now(), false)) + .isInstanceOf(ErrorResultException.class) + .hasMessageContaining("Unknown publisher"); + } + } + + @Test + void shouldFailWhenImageFormatIsDisallowed() throws IOException { + try (var processor = org.mockito.Mockito.mock(ExtensionProcessor.class)) { + mockExtensionVersion("publisher", "demo", "2.0.0", "test.svg", processor); + + var namespace = buildNamespace("publisher"); + var user = new org.eclipse.openvsx.entities.UserData(); + var token = new PersonalAccessToken(); + token.setUser(user); + + when(repositories.findNamespace("publisher")).thenReturn(namespace); + when(users.hasPublishPermission(user, namespace)).thenReturn(true); + when(validator.validateExtensionVersion("2.0.0")).thenReturn(Optional.empty()); + when(validator.validateExtensionName("demo")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> handler.createExtensionVersion(processor, token, LocalDateTime.now(), false)) + .isInstanceOf(ErrorResultException.class) + .hasMessageContaining("uses an unsupported icon format"); + } + } + + @Test + void shouldSucceedWhenImageFormatIsAllowed() throws IOException { + var previousUnsupportedIconFormats = config.getUnsupportedIconFormats(); + try (var processor = org.mockito.Mockito.mock(ExtensionProcessor.class)) { + config.setUnsupportedIconFormats(List.of()); + + var metadata = mockExtensionVersion("publisher", "demo", "2.0.0", "test.svg", processor); + + var namespace = buildNamespace("publisher"); + var user = new org.eclipse.openvsx.entities.UserData(); + var token = new PersonalAccessToken(); + token.setUser(user); + + when(repositories.findNamespace("publisher")).thenReturn(namespace); + when(users.hasPublishPermission(user, namespace)).thenReturn(true); + when(validator.validateExtensionVersion(metadata.getVersion())).thenReturn(Optional.empty()); + when(validator.validateExtensionName("demo")).thenReturn(Optional.empty()); + + var ev = handler.createExtensionVersion(processor, token, LocalDateTime.now(), false); + assertThat(ev).isNotNull(); + } finally { + config.setUnsupportedIconFormats(previousUnsupportedIconFormats); + } + } + + private ExtensionVersion mockExtensionVersion(String namespace, String name, String version, String iconPath, ExtensionProcessor processor) throws IOException { + when(processor.getNamespace()).thenReturn(namespace); + when(processor.getExtensionName()).thenReturn(name); + when(processor.getVersion()).thenReturn(version); + if (iconPath != null) { + when(processor.getIcon(ArgumentMatchers.any())).thenReturn(new TempFile(Path.of(iconPath))); + } + + var ev = new ExtensionVersion(); + ev.setDisplayName("Demo OK"); + ev.setVersion("2.0.0"); + ev.setTargetPlatform("any"); + when(processor.getMetadata()).thenReturn(ev); + + return ev; + } + + private Namespace buildNamespace(String name) { + var namespace = new Namespace(); + namespace.setName(name); + return namespace; + } +}