From 940064386efb2e91058be0131ce13eb6cb96d9f8 Mon Sep 17 00:00:00 2001 From: Nikolas Falco Date: Sat, 18 Jan 2025 14:59:38 +0100 Subject: [PATCH] [JENKINS-75157] Bitbucket client fail to retrieve resource with HTTP 404 for bitbucket server (#970) Remove the HttpHost parameter when calling in Apache client as the request could be different than the host configured in jenkins, for example in Bitbucket Server request using mirror link. Rethrow the FileNotFoundException in executeMethod instead of encapsulate into other exception to respect the SCMFile interface. Rewrote the BitbucketSCMFile logic to not assume the SCMFile.type but resolve at runtime when needed. --- .../plugins/bitbucket/api/BitbucketApi.java | 15 +++++- .../client/BitbucketCloudApiClient.java | 37 ++++++++------ .../repository/BitbucketRepositorySource.java | 13 ++--- .../filesystem/BitbucketSCMFile.java | 46 ++++++++--------- .../filesystem/BitbucketSCMFileSystem.java | 27 +++++----- .../impl/client/AbstractBitbucketApi.java | 36 +++++--------- .../client/BitbucketServerAPIClient.java | 19 ++++++- .../BitbucketIntegrationClientFactory.java | 20 +++----- .../filesystem/BitbucketSCMFileTest.java | 49 +++++++++++++++++++ 9 files changed, 166 insertions(+), 96 deletions(-) create mode 100644 src/test/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileTest.java diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java index ae8f2d9ed..a1efc1d8a 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java @@ -302,17 +302,30 @@ List getRepositories(@CheckForNull UserRoleInRepo * @throws IOException if there was a network communications error. * @throws InterruptedException if interrupted while waiting on remote communications. */ + @NonNull @Restricted(NoExternalUse.class) Iterable getDirectoryContent(BitbucketSCMFile parent) throws IOException, InterruptedException; /** * Return an input stream for the given file. * - * @param file and instance of SCM file + * @param file an instance of SCM file * @return the stream of the given {@link SCMFile} * @throws IOException if there was a network communications error. * @throws InterruptedException if interrupted while waiting on remote communications. */ @Restricted(NoExternalUse.class) InputStream getFileContent(BitbucketSCMFile file) throws IOException, InterruptedException; + + /** + * Return the metadata for the given file. + * + * @param file an instance of SCM file + * @return a {@link SCMFile} file with updated the metadata + * @throws IOException if there was a network communications error. + * @throws InterruptedException if interrupted while waiting on remote communications. + */ + @NonNull + @Restricted(NoExternalUse.class) + SCMFile getFile(@NonNull BitbucketSCMFile file) throws IOException, InterruptedException; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index 7ed4edd77..c0bc2b7ea 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -836,27 +836,23 @@ public Iterable getDirectoryContent(final BitbucketSCMFile parent) thro .set("path", parent.getPath()) .expand(); List result = new ArrayList<>(); - String response = getRequest(url); - BitbucketCloudPage page = JsonParser.mapper.readValue(response, - new TypeReference>(){}); - for(BitbucketRepositorySource source:page.getValues()){ - result.add(source.toBitbucketScmFile(parent)); - } + String pageURL = url; + BitbucketCloudPage page; + do { + String response = getRequest(pageURL); + page = JsonParser.mapper.readValue(response, new TypeReference>(){}); - while (!page.isLastPage()){ - response = getRequest(page.getNext()); - page = JsonParser.mapper.readValue(response, - new TypeReference>(){}); - for(BitbucketRepositorySource source:page.getValues()){ - result.add(source.toBitbucketScmFile(parent)); + for(BitbucketRepositorySource source : page.getValues()){ + result.add(source.toBitbucketSCMFile(parent)); } - } + pageURL = page.getNext(); + } while (!page.isLastPage()); return result; } @Override - public InputStream getFileContent(BitbucketSCMFile file) throws IOException, InterruptedException { + public InputStream getFileContent(@NonNull BitbucketSCMFile file) throws IOException, InterruptedException { String url = UriTemplate.fromTemplate(REPO_URL_TEMPLATE + "/src{/branchOrHash,path}") .set("owner", owner) .set("repo", repositoryName) @@ -865,4 +861,17 @@ public InputStream getFileContent(BitbucketSCMFile file) throws IOException, Int .expand(); return getRequestAsInputStream(url); } + + @Override + public SCMFile getFile(@NonNull BitbucketSCMFile file) throws IOException, InterruptedException { + String url = UriTemplate.fromTemplate(REPO_URL_TEMPLATE + "/src{/branchOrHash,path}?format=meta") + .set("owner", owner) + .set("repo", repositoryName) + .set("branchOrHash", file.getRef()) + .set("path", file.getPath()) + .expand(); + String response = getRequest(url); + BitbucketRepositorySource src = JsonParser.mapper.readValue(response, BitbucketRepositorySource.class); + return src.toBitbucketSCMFile((BitbucketSCMFile) file.parent()); + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketRepositorySource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketRepositorySource.java index 5a84fd757..561e24b78 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketRepositorySource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketRepositorySource.java @@ -68,23 +68,24 @@ public String getHash() { @JsonIgnore public boolean isDirectory() { - return type.equals("commit_directory"); + return "commit_directory".equals(type); } - public BitbucketSCMFile toBitbucketScmFile(BitbucketSCMFile parent){ + public BitbucketSCMFile toBitbucketSCMFile(BitbucketSCMFile parent) { SCMFile.Type fileType; - if(isDirectory()){ + if (isDirectory()) { fileType = SCMFile.Type.DIRECTORY; } else { fileType = SCMFile.Type.REGULAR_FILE; - for(String attribute: getAttributes()){ - if(attribute.equals("link")){ + for (String attribute : getAttributes()) { + if ("link".equals(attribute)) { fileType = SCMFile.Type.LINK; - } else if(attribute.equals("subrepository")){ + } else if ("subrepository".equals(attribute)) { fileType = SCMFile.Type.OTHER; // sub-module or sub-repo } } } return new BitbucketSCMFile(parent, path, fileType, hash); } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFile.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFile.java index 06634e77f..c1978264f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFile.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFile.java @@ -28,13 +28,15 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; import jenkins.scm.api.SCMFile; -public class BitbucketSCMFile extends SCMFile { +public class BitbucketSCMFile extends SCMFile { private final BitbucketApi api; private String ref; private final String hash; + private boolean resolved; public String getRef() { return ref; @@ -44,26 +46,12 @@ public void setRef(String ref) { this.ref = ref; } - @Deprecated - public BitbucketSCMFile(BitbucketSCMFileSystem bitBucketSCMFileSystem, - BitbucketApi api, - String ref) { - this(bitBucketSCMFileSystem, api, ref, null); - } - - public BitbucketSCMFile(BitbucketSCMFileSystem bitBucketSCMFileSystem, - BitbucketApi api, - String ref, String hash) { - super(); + public BitbucketSCMFile(BitbucketApi api, String ref, String hash) { type(Type.DIRECTORY); this.api = api; this.ref = ref; this.hash = hash; - } - - @Deprecated - public BitbucketSCMFile(@NonNull BitbucketSCMFile parent, String name, Type type) { - this(parent, name, type, null); + this.resolved = false; } public BitbucketSCMFile(@NonNull BitbucketSCMFile parent, String name, Type type, String hash) { @@ -72,6 +60,8 @@ public BitbucketSCMFile(@NonNull BitbucketSCMFile parent, String name, Type type this.ref = parent.ref; this.hash = hash; type(type); + // method called by the client when list content folder + this.resolved = true; } public String getHash() { @@ -80,12 +70,12 @@ public String getHash() { @Override @NonNull - public Iterable children() throws IOException, - InterruptedException { + public Iterable children() throws IOException, InterruptedException { if (this.isDirectory()) { return api.getDirectoryContent(this); } else { - throw new IOException("Cannot get children from a regular file"); + // respect the interface javadoc + return Collections.emptyList(); } } @@ -101,19 +91,29 @@ public InputStream content() throws IOException, InterruptedException { @Override public long lastModified() throws IOException, InterruptedException { - // TODO: Return valid value when Tag support is implemented - return 0; + return 0L; } @Override @NonNull protected SCMFile newChild(String name, boolean assumeIsDirectory) { - return new BitbucketSCMFile(this, name, assumeIsDirectory?Type.DIRECTORY:Type.REGULAR_FILE, hash); + BitbucketSCMFile child = new BitbucketSCMFile(this, name, assumeIsDirectory ? Type.DIRECTORY : Type.REGULAR_FILE, hash); + child.resolved = false; + return child; } @Override @NonNull protected Type type() throws IOException, InterruptedException { + if (!resolved) { + try { + SCMFile metadata = api.getFile(this); + type(metadata.getType()); + } catch(IOException e) { + type(Type.NONEXISTENT); + } + resolved = true; + } return this.getType(); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java index 94621f31c..b8d4ba76b 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileSystem.java @@ -43,11 +43,11 @@ import hudson.Util; import hudson.model.Item; import hudson.model.Queue; -import hudson.model.queue.Tasks; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.security.ACL; import java.io.IOException; +import java.lang.annotation.Inherited; import jenkins.authentication.tokens.api.AuthenticationTokens; import jenkins.scm.api.SCMFile; import jenkins.scm.api.SCMFileSystem; @@ -69,13 +69,10 @@ protected BitbucketSCMFileSystem(BitbucketApi api, String ref, SCMRevision rev) } /** - * Return timestamp of last commit or of tag if its annotated tag. - * - * @return timestamp of last commit or of tag if its annotated tag + * {@link Inherited} */ @Override public long lastModified() throws IOException { - // TODO figure out how to implement this return 0L; } @@ -83,7 +80,7 @@ public long lastModified() throws IOException { @Override public SCMFile getRoot() { SCMRevision revision = getRevision(); - return new BitbucketSCMFile(this, api, ref, revision == null ? null : revision.toString()); + return new BitbucketSCMFile(api, ref, revision == null ? null : revision.toString()); } @Extension @@ -116,22 +113,24 @@ public SCMFileSystem build(@NonNull Item owner, @NonNull SCM scm, @CheckForNull } private static StandardCredentials lookupScanCredentials(@CheckForNull Item context, - @CheckForNull String scanCredentialsId, String serverUrl) { - if (Util.fixEmpty(scanCredentialsId) == null) { + @CheckForNull String scanCredentialsId, + String serverURL) { + scanCredentialsId = Util.fixEmpty(scanCredentialsId); + if (scanCredentialsId == null) { return null; } else { return CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( + CredentialsProvider.lookupCredentialsInItem( StandardCredentials.class, context, - context instanceof Queue.Task - ? Tasks.getDefaultAuthenticationOf((Queue.Task) context) - : ACL.SYSTEM, - URIRequirementBuilder.fromUri(serverUrl).build() + context instanceof Queue.Task task + ? task.getDefaultAuthentication2() + : ACL.SYSTEM2, + URIRequirementBuilder.fromUri(serverURL).build() ), CredentialsMatchers.allOf( CredentialsMatchers.withId(scanCredentialsId), - AuthenticationTokens.matcher(BitbucketAuthenticator.authenticationContext(serverUrl)) + AuthenticationTokens.matcher(BitbucketAuthenticator.authenticationContext(serverURL)) ) ); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java index 4787827af..90572b7d3 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java @@ -36,8 +36,6 @@ import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.TimeUnit; @@ -220,21 +218,21 @@ private void setClientProxyParams(String host, HttpClientBuilder builder) { @NonNull protected abstract CloseableHttpClient getClient(); - protected CloseableHttpResponse executeMethod(HttpHost host, - HttpRequestBase httpMethod, - boolean requireAuthentication) throws IOException { + protected CloseableHttpResponse executeMethod(HttpRequestBase request, boolean requireAuthentication) throws IOException { if (requireAuthentication && authenticator != null) { - authenticator.configureRequest(httpMethod); + authenticator.configureRequest(request); } - return getClient().execute(host, httpMethod, context); + // the Apache client determinate the host from request.getURI() + // in some cases like requests to mirror or avatar, the host could not be the same of configured in Jenkins + return getClient().execute(request, context); } - protected CloseableHttpResponse executeMethod(HttpHost host, HttpRequestBase httpMethod) throws IOException { - return executeMethod(host, httpMethod, true); + protected CloseableHttpResponse executeMethod(HttpRequestBase httpMethod) throws IOException { + return executeMethod(httpMethod, true); } protected String doRequest(HttpRequestBase request, boolean requireAuthentication) throws IOException { - try (CloseableHttpResponse response = executeMethod(getHost(), request, requireAuthentication)) { + try (CloseableHttpResponse response = executeMethod(request, requireAuthentication)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_NOT_FOUND) { throw new FileNotFoundException("URL: " + request.getURI()); @@ -250,7 +248,7 @@ protected String doRequest(HttpRequestBase request, boolean requireAuthenticatio throw buildResponseException(response, content); } return content; - } catch (BitbucketRequestException e) { + } catch (FileNotFoundException | BitbucketRequestException e) { throw e; } catch (IOException e) { throw new IOException("Communication error for url: " + request, e); @@ -276,19 +274,7 @@ private void release(HttpRequestBase method) { */ protected InputStream getRequestAsInputStream(String path) throws IOException { HttpGet httpget = new HttpGet(path); - HttpHost host = getHost(); - - // Extract host from URL, if present - try { - URI uri = new URI(host.toURI()); - if (uri.isAbsolute() && ! uri.isOpaque()) { - host = HttpHost.create(uri.getScheme() + "://" + uri.getAuthority()); - } - } catch (URISyntaxException ex) { - // use default - } - - CloseableHttpResponse response = executeMethod(host, httpget); + CloseableHttpResponse response = executeMethod(httpget); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_NOT_FOUND) { EntityUtils.consume(response.getEntity()); @@ -303,7 +289,7 @@ protected InputStream getRequestAsInputStream(String path) throws IOException { protected int headRequestStatus(String path) throws IOException { HttpHead httpHead = new HttpHead(path); - try (CloseableHttpResponse response = executeMethod(getHost(), httpHead)) { + try (CloseableHttpResponse response = executeMethod(httpHead)) { EntityUtils.consume(response.getEntity()); return response.getStatusLine().getStatusCode(); } catch (IOException e) { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index 4a4e02b7d..81fe7ddad 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -952,7 +952,7 @@ protected CloseableHttpClient getClient() { protected HttpHost getHost() { String url = baseURL; try { - // it's really needed? + // it's needed because the serverURL can contains a context root different than '/' and the HttpHost must contains only schema, host and port URL tmp = new URL(baseURL); String schema = tmp.getProtocol() == null ? "http" : tmp.getProtocol(); return new HttpHost(tmp.getHost(), tmp.getPort(), schema); @@ -1050,4 +1050,21 @@ private Map collectLines(String response, final List line return content; } + @NonNull + @Override + public SCMFile getFile(@NonNull BitbucketSCMFile file) throws IOException, InterruptedException { + String branchOrHash = file.getHash().contains("+") ? file.getRef() : file.getHash(); + String url = UriTemplate.fromTemplate(this.baseURL + API_BROWSE_PATH + "{&type,blame,size") + .set("owner", getUserCentricOwner()) + .set("repo", repositoryName) + .set("path", file.getPath().split(Operator.PATH.getSeparator())) + .set("at", branchOrHash) + .set("type", true) + .set("blame", false) + .expand(); + String response = getRequest(url); + // TODO deserialize response... + return new BitbucketSCMFile(this, file.getRef(), file.getHash()); + } + } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java index a4edbc7a6..939c99704 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketIntegrationClientFactory.java @@ -27,12 +27,12 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerAPIClient; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpRequestBase; @@ -53,7 +53,7 @@ default void request(HttpRequestBase request) { default CloseableHttpResponse loadResponseFromResources(Class resourceBase, String path, String payloadPath) throws IOException { try (InputStream json = resourceBase.getResourceAsStream(payloadPath)) { if (json == null) { - throw new IllegalStateException("Payload for the REST path " + path + " could not be found: " + payloadPath); + throw new FileNotFoundException("Payload for the REST path " + path + " could not be found: " + payloadPath); } HttpEntity entity = mock(HttpEntity.class); String jsonString = IOUtils.toString(json, StandardCharsets.UTF_8); @@ -104,19 +104,17 @@ private BitbucketServerIntegrationClient(String payloadRootPath, String baseURL, } @Override - protected CloseableHttpResponse executeMethod(HttpHost host, - HttpRequestBase httpMethod, - boolean requireAuthentication) throws IOException { - String path = httpMethod.getURI().toString(); - audit.request(httpMethod); + protected CloseableHttpResponse executeMethod(HttpRequestBase request, boolean requireAuthentication) throws IOException { + String requestURI = request.getURI().toString(); + audit.request(request); - String payloadPath = path.substring(path.indexOf("/rest/")) + String payloadPath = requestURI.substring(requestURI.indexOf("/rest/")) .replace("/rest/api/", "") .replace("/rest/", "") .replace('/', '-').replaceAll("[=%&?]", "_"); payloadPath = payloadRootPath + payloadPath + ".json"; - return loadResponseFromResources(getClass(), path, payloadPath); + return loadResponseFromResources(getClass(), requestURI, payloadPath); } @Override @@ -155,9 +153,7 @@ private BitbucketClouldIntegrationClient(String payloadRootPath, String owner, S } @Override - protected CloseableHttpResponse executeMethod(HttpHost host, - HttpRequestBase httpMethod, - boolean requireAuthentication) throws IOException { + protected CloseableHttpResponse executeMethod(HttpRequestBase httpMethod, boolean requireAuthentication) throws IOException { String path = httpMethod.getURI().toString(); audit.request(httpMethod); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileTest.java new file mode 100644 index 000000000..71faf756f --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/filesystem/BitbucketSCMFileTest.java @@ -0,0 +1,49 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.cloudbees.jenkins.plugins.bitbucket.filesystem; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketIntegrationClientFactory; +import java.io.FileNotFoundException; +import jenkins.scm.api.SCMFile.Type; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BitbucketSCMFileTest { + + @WithJenkins + @Issue("JENKINS-75157") + @Test + void test(JenkinsRule r) { + BitbucketApi client = BitbucketIntegrationClientFactory.getApiMockClient("https://acme.bitbucket.com"); + + BitbucketSCMFile parent = new BitbucketSCMFile(client, "master", "hash"); + BitbucketSCMFile file = new BitbucketSCMFile(parent, "pipeline_config.groovy", Type.REGULAR_FILE, "046d9a3c1532acf4cf08fe93235c00e4d673c1d2"); + assertThatThrownBy(file::content).isInstanceOf(FileNotFoundException.class); + } +}