Skip to content

Commit 917e786

Browse files
Copilotvalfirst
andauthored
[xray-exported] Add Xray Cloud support (#6651)
Co-authored-by: Valery Yatsynovich <valery_yatsynovich@epam.com>
1 parent e646c54 commit 917e786

16 files changed

Lines changed: 1246 additions & 96 deletions

File tree

docs/modules/integrations/pages/xray-exporter.adoc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,49 @@ xray-exporter.project-key=ABBA
7878
xray-exporter.json-results-directory=/Users/happytester/Repositories/app-tests/output/results/jbehave
7979
----
8080

81+
=== Xray Cloud Configuration
82+
83+
By default, VIVIDUS exports results to an on-premise Xray Server (Jira plugin). To export to https://www.getxray.app/[Xray Cloud], enable the cloud mode and provide API credentials.
84+
85+
[cols="2,1,3", options="header"]
86+
|===
87+
88+
|Property
89+
|Required
90+
|Description
91+
92+
|`xray-exporter.cloud.enabled`
93+
|false
94+
|Set to `true` to enable Xray Cloud mode (default: `false`)
95+
96+
|*`xray-exporter.cloud.client-id`*
97+
|true (when cloud is enabled)
98+
|Xray Cloud API client ID (obtained from the Xray app settings in Jira)
99+
100+
|*`xray-exporter.cloud.client-secret`*
101+
|true (when cloud is enabled)
102+
|Xray Cloud API client secret (obtained from the Xray app settings in Jira)
103+
104+
|`xray-exporter.cloud.api-base-url`
105+
|false
106+
a|Base URL for the Xray Cloud REST API (the `/api/v2` path is appended automatically). Default: `https://xray.cloud.getxray.app/`.
107+
108+
Override this value when using a regional Xray Cloud deployment, for example:
109+
* `https://us.xray.cloud.getxray.app/`
110+
* `https://eu.xray.cloud.getxray.app/`
111+
112+
|===
113+
114+
NOTE: When `xray-exporter.cloud.enabled=true`, the `xray-exporter.jira-instance-key` property is not used.
115+
116+
.application.properties (Xray Cloud example)
117+
[source,properties]
118+
----
119+
xray-exporter.cloud.enabled=true
120+
xray-exporter.cloud.client-id=<YOUR_CLIENT_ID>
121+
xray-exporter.cloud.client-secret=<YOUR_CLIENT_SECRET>
122+
----
123+
81124
=== Test Execution
82125

83126
[cols="2,1,3", options="header"]

vividus-to-xray-exporter/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies {
1919

2020
implementation project(':vividus-util')
2121
implementation project(':vividus-engine')
22+
implementation project(':vividus-http-client')
2223
implementation project(':vividus-facade-jira')
2324
implementation project(':vividus-exporter-commons')
2425

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.vividus.xray.configuration;
18+
19+
import java.security.GeneralSecurityException;
20+
21+
import org.springframework.beans.factory.annotation.Value;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.vividus.http.client.HttpClientConfig;
26+
import org.vividus.http.client.IHttpClientFactory;
27+
import org.vividus.jira.JiraClientProvider;
28+
import org.vividus.xray.facade.XrayClient;
29+
import org.vividus.xray.facade.XrayCloudClient;
30+
import org.vividus.xray.facade.XrayServerClient;
31+
32+
@Configuration
33+
public class XrayClientConfiguration
34+
{
35+
@Bean
36+
@ConditionalOnProperty(name = "xray-exporter.cloud.enabled", havingValue = "false", matchIfMissing = true)
37+
public XrayClient xrayServerClient(JiraClientProvider jiraClientProvider,
38+
@Value("${xray-exporter.jira-instance-key:}") String jiraInstanceKey)
39+
{
40+
return new XrayServerClient(jiraClientProvider, jiraInstanceKey.isEmpty() ? null : jiraInstanceKey);
41+
}
42+
43+
@Bean
44+
@ConditionalOnProperty(name = "xray-exporter.cloud.enabled", havingValue = "true")
45+
public XrayClient xrayCloudClient(XrayExporterOptions options, IHttpClientFactory httpClientFactory)
46+
throws GeneralSecurityException
47+
{
48+
XrayExporterOptions.CloudOptions cloudOptions = options.getCloudOptions();
49+
return new XrayCloudClient(
50+
cloudOptions.getApiBaseUrl(),
51+
cloudOptions.getClientId(),
52+
cloudOptions.getClientSecret(),
53+
httpClientFactory.buildHttpClient(new HttpClientConfig()));
54+
}
55+
}

vividus-to-xray-exporter/src/main/java/org/vividus/xray/configuration/XrayExporterOptions.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2025 the original author or authors.
2+
* Copyright 2019-2026 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,8 @@ public class XrayExporterOptions
3232
private TestCaseOptions testCaseOptions;
3333
@Name("test-execution")
3434
private TestExecutionOptions testExecutionOptions;
35+
@Name("cloud")
36+
private CloudOptions cloudOptions = new CloudOptions();
3537

3638
public Path getJsonResultsDirectory()
3739
{
@@ -83,6 +85,64 @@ public void setTestExecutionOptions(TestExecutionOptions testExecutionOptions)
8385
this.testExecutionOptions = testExecutionOptions;
8486
}
8587

88+
public CloudOptions getCloudOptions()
89+
{
90+
return cloudOptions;
91+
}
92+
93+
public void setCloudOptions(CloudOptions cloudOptions)
94+
{
95+
this.cloudOptions = cloudOptions;
96+
}
97+
98+
public static final class CloudOptions
99+
{
100+
private boolean enabled;
101+
private String apiBaseUrl;
102+
private String clientId;
103+
private String clientSecret;
104+
105+
public boolean isEnabled()
106+
{
107+
return enabled;
108+
}
109+
110+
public void setEnabled(boolean enabled)
111+
{
112+
this.enabled = enabled;
113+
}
114+
115+
public String getApiBaseUrl()
116+
{
117+
return apiBaseUrl;
118+
}
119+
120+
public void setApiBaseUrl(String apiBaseUrl)
121+
{
122+
this.apiBaseUrl = apiBaseUrl;
123+
}
124+
125+
public String getClientId()
126+
{
127+
return clientId;
128+
}
129+
130+
public void setClientId(String clientId)
131+
{
132+
this.clientId = clientId;
133+
}
134+
135+
public String getClientSecret()
136+
{
137+
return clientSecret;
138+
}
139+
140+
public void setClientSecret(String clientSecret)
141+
{
142+
this.clientSecret = clientSecret;
143+
}
144+
}
145+
86146
public static final class TestCaseOptions
87147
{
88148
private boolean useScenarioTitleAsDescription;

vividus-to-xray-exporter/src/main/java/org/vividus/xray/configuration/validator/XrayExporterOptionsValidator.java

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2025 the original author or authors.
2+
* Copyright 2019-2026 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,19 +20,21 @@
2020
import java.io.UncheckedIOException;
2121
import java.nio.file.Files;
2222
import java.nio.file.Path;
23-
import java.util.List;
2423

2524
import org.apache.commons.lang3.StringUtils;
2625
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2726
import org.springframework.stereotype.Component;
2827
import org.springframework.validation.Errors;
2928
import org.springframework.validation.Validator;
3029
import org.vividus.xray.configuration.XrayExporterOptions;
30+
import org.vividus.xray.configuration.XrayExporterOptions.CloudOptions;
3131

3232
@Component(EnableConfigurationProperties.VALIDATOR_BEAN_NAME)
3333
public class XrayExporterOptionsValidator implements Validator
3434
{
3535
private static final String TEST_EXECUTION_ATTACHMENTS_FIELD = "test-execution.attachments";
36+
private static final String CLOUD_CLIENT_ID_FIELD = "cloud.client-id";
37+
private static final String CLOUD_CLIENT_SECRET_FIELD = "cloud.client-secret";
3638

3739
@Override
3840
public boolean supports(Class<?> clazz)
@@ -44,20 +46,25 @@ public boolean supports(Class<?> clazz)
4446
public void validate(Object target, Errors errors)
4547
{
4648
XrayExporterOptions options = (XrayExporterOptions) target;
47-
List<Path> attachments = options.getTestExecutionOptions().getAttachments();
49+
options.getTestExecutionOptions().getAttachments().forEach(
50+
attachment -> validateAttachment(attachment, errors));
51+
validateCloudOptions(options.getCloudOptions(), errors);
52+
}
4853

49-
attachments.forEach(attachment ->
54+
private void validateAttachment(Path attachment, Errors errors)
55+
{
56+
if (!Files.exists(attachment))
5057
{
51-
if (!Files.exists(attachment))
52-
{
53-
errors.rejectValue(TEST_EXECUTION_ATTACHMENTS_FIELD, StringUtils.EMPTY,
54-
"The attachment file at path " + attachment + " does not exist");
55-
return;
56-
}
58+
errors.rejectValue(TEST_EXECUTION_ATTACHMENTS_FIELD, StringUtils.EMPTY,
59+
"The attachment file at path " + attachment + " does not exist");
60+
return;
61+
}
5762

58-
try
63+
if (Files.isDirectory(attachment))
64+
{
65+
try (var stream = Files.list(attachment))
5966
{
60-
if (Files.isDirectory(attachment) && Files.list(attachment).findAny().isEmpty())
67+
if (stream.findAny().isEmpty())
6168
{
6269
errors.rejectValue(TEST_EXECUTION_ATTACHMENTS_FIELD, StringUtils.EMPTY,
6370
"The attachment folder at path " + attachment + " is empty");
@@ -68,12 +75,43 @@ public void validate(Object target, Errors errors)
6875
{
6976
throw new UncheckedIOException(e);
7077
}
78+
}
7179

72-
if (attachment.getRoot().equals(attachment))
80+
Path root = attachment.getRoot();
81+
if (root != null && root.equals(attachment))
82+
{
83+
errors.rejectValue(TEST_EXECUTION_ATTACHMENTS_FIELD, StringUtils.EMPTY,
84+
"Please do not try to publish the file system root as the attachment");
85+
}
86+
}
87+
88+
private void validateCloudOptions(CloudOptions cloud, Errors errors)
89+
{
90+
if (cloud.isEnabled())
91+
{
92+
if (StringUtils.isBlank(cloud.getClientId()))
93+
{
94+
errors.rejectValue(CLOUD_CLIENT_ID_FIELD, StringUtils.EMPTY,
95+
"Xray Cloud client ID must be set when cloud mode is enabled");
96+
}
97+
if (StringUtils.isBlank(cloud.getClientSecret()))
98+
{
99+
errors.rejectValue(CLOUD_CLIENT_SECRET_FIELD, StringUtils.EMPTY,
100+
"Xray Cloud client secret must be set when cloud mode is enabled");
101+
}
102+
}
103+
else
104+
{
105+
if (StringUtils.isNotBlank(cloud.getClientId()))
106+
{
107+
errors.rejectValue(CLOUD_CLIENT_ID_FIELD, StringUtils.EMPTY,
108+
"Xray Cloud client ID must not be set when cloud mode is disabled");
109+
}
110+
if (StringUtils.isNotBlank(cloud.getClientSecret()))
73111
{
74-
errors.rejectValue(TEST_EXECUTION_ATTACHMENTS_FIELD, StringUtils.EMPTY,
75-
"Please do not try to publish the file system root as the attachment");
112+
errors.rejectValue(CLOUD_CLIENT_SECRET_FIELD, StringUtils.EMPTY,
113+
"Xray Cloud client secret must not be set when cloud mode is disabled");
76114
}
77-
});
115+
}
78116
}
79117
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.vividus.xray.facade;
18+
19+
import java.io.IOException;
20+
import java.util.List;
21+
22+
public interface XrayClient
23+
{
24+
/**
25+
* Imports a test execution. Returns the Jira issue key of the created/updated test execution.
26+
*
27+
* @param executionJson serialized test execution JSON
28+
* @return the Jira issue key of the test execution
29+
* @throws IOException in case of any I/O errors
30+
*/
31+
String importExecution(String executionJson) throws IOException;
32+
33+
/**
34+
* Adds test cases to a test set identified by the given Jira issue key.
35+
*
36+
* @param testSetKey the Jira issue key of the target test set
37+
* @param testCaseKeys the list of Jira issue keys of test cases to add
38+
* @throws IOException in case of any I/O errors
39+
*/
40+
void addTestsToTestSet(String testSetKey, List<String> testCaseKeys) throws IOException;
41+
}

0 commit comments

Comments
 (0)