From 08e76f72b015d66dcc041490a909ac93e6253d87 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Mon, 11 Mar 2024 12:56:47 +0200 Subject: [PATCH 1/3] Make Owner addressable via RFC 2822 standard --- .../plugins/testresult-owner/OwnerView.hbs | 10 +++- .../plugins/testresult-owner/OwnerView.js | 3 +- .../src/main/javascript/utils/parseAddress.js | 60 +++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 allure-generator/src/main/javascript/utils/parseAddress.js diff --git a/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.hbs b/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.hbs index 9702ac9e5..e4144bbbd 100644 --- a/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.hbs +++ b/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.hbs @@ -1,4 +1,8 @@ {{#if owner}} -

{{t 'testResult.owner.name'}}

-
{{owner}}
-{{/if}} \ No newline at end of file + {{t 'testResult.owner.name'}}: + {{#if owner.url}} + {{owner.displayName}} + {{else}} + {{owner.displayName}} + {{/if}} +{{/if}} diff --git a/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js b/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js index 2cb24ed0b..c84a599e8 100644 --- a/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js +++ b/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js @@ -1,5 +1,6 @@ import { View } from "backbone.marionette"; import { className } from "../../decorators/index"; +import parseAddress from "../../utils/parseAddress"; import template from "./OwnerView.hbs"; @className("pane__section") @@ -9,7 +10,7 @@ class OwnerView extends View { serializeData() { const extra = this.model.get("extra"); return { - owner: extra ? extra.owner : null, + owner: extra ? parseAddress(extra.owner) : null, }; } } diff --git a/allure-generator/src/main/javascript/utils/parseAddress.js b/allure-generator/src/main/javascript/utils/parseAddress.js new file mode 100644 index 000000000..43fe1f1e7 --- /dev/null +++ b/allure-generator/src/main/javascript/utils/parseAddress.js @@ -0,0 +1,60 @@ +const RFC2822_ADDRESS = /^(.*) <(.*)>$/; +const LOOKS_LIKE_EMAIL = /^[^@]+@[^@]+$/; + +/** + * Parse a potentially RFC 2822 address into a display name and an address. + * + * @param {string | null | undefined} maybeAddress + * @returns {{ displayName: string, url?: string } | null + */ +export default function parseAddress(maybeAddress) { + if (!maybeAddress) { + return null; + } + + const match = maybeAddress.match(RFC2822_ADDRESS); + if (match) { + return { + displayName: match[1], + url: toHref(match[2]), + }; + } + + return { + displayName: maybeAddress, + url: toHref(maybeAddress), + }; +} + +/** + * If the address is a valid URL, returns the URL. + * If the address resembles an email address, returns a mailto: URL. + * Otherwise, returns undefined. + * + * @param {string} address + * @returns {string | undefined} + */ +function toHref(address) { + if (isValidURL(address)) { + return address; + } + + if (LOOKS_LIKE_EMAIL.test(address)) { + return `mailto:${address}`; + } +} + +/** + * If the address is a valid URL, returns the URL. + * + * @param {string} maybeURL + * @returns {boolean} + */ +function isValidURL(maybeURL) { + try { + new URL(maybeURL); + return true; + } catch (_error) { + return false; + } +} From a67a7147333befa8cb7605852711e3c7e9aedb8f Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 27 Mar 2024 09:58:10 +0200 Subject: [PATCH 2/3] Rewrite Owner address parsing to Java --- .../io/qameta/allure/owner/OwnerAddress.java | 29 ++++++++ .../allure/owner/OwnerAddressParser.java | 64 +++++++++++++++++ .../io/qameta/allure/owner/OwnerPlugin.java | 5 +- .../plugins/testresult-owner/OwnerView.js | 3 +- .../src/main/javascript/utils/parseAddress.js | 60 ---------------- .../allure/owner/OwnerAddressParserTest.java | 72 +++++++++++++++++++ 6 files changed, 170 insertions(+), 63 deletions(-) create mode 100644 allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddress.java create mode 100644 allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java delete mode 100644 allure-generator/src/main/javascript/utils/parseAddress.js create mode 100644 allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java diff --git a/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddress.java b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddress.java new file mode 100644 index 000000000..e42ea5b77 --- /dev/null +++ b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddress.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2024 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.owner; + +import lombok.Getter; + +@Getter +public class OwnerAddress { + private final String displayName; + private final String url; + + public OwnerAddress(final String displayName, final String url) { + this.displayName = displayName; + this.url = url; + } +} diff --git a/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java new file mode 100644 index 000000000..6e9c43683 --- /dev/null +++ b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2024 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.owner; + +import java.net.MalformedURLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class OwnerAddressParser { + private static final Pattern RFC2822_ADDRESS = Pattern.compile("^(.*) <(.*)>$"); + private static final Pattern LOOKS_LIKE_EMAIL = Pattern.compile("^[^@]+@[^@]+$"); + + private OwnerAddressParser() { + } + + public static OwnerAddress parseAddress(final String maybeAddress) { + if (maybeAddress == null || maybeAddress.isEmpty()) { + return null; + } + + final Matcher matcher = RFC2822_ADDRESS.matcher(maybeAddress); + if (matcher.matches()) { + final String displayName = matcher.group(1); + final String url = toHref(matcher.group(2)); + return new OwnerAddress(displayName, url); + } + + return new OwnerAddress(maybeAddress, toHref(maybeAddress)); + } + + private static String toHref(final String address) { + if (isValidURL(address)) { + return address; + } + + if (LOOKS_LIKE_EMAIL.matcher(address).matches()) { + return "mailto:" + address; + } + + return null; + } + + private static boolean isValidURL(final String maybeURL) { + try { + new java.net.URL(maybeURL); + return true; + } catch (MalformedURLException e) { + return false; + } + } +} diff --git a/allure-generator/src/main/java/io/qameta/allure/owner/OwnerPlugin.java b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerPlugin.java index 8b3d413d7..77886321f 100644 --- a/allure-generator/src/main/java/io/qameta/allure/owner/OwnerPlugin.java +++ b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerPlugin.java @@ -44,6 +44,9 @@ public void aggregate(final Configuration configuration, private void setOwner(final TestResult result) { result.findOneLabel(LabelName.OWNER) - .ifPresent(owner -> result.addExtraBlock(OWNER_BLOCK_NAME, owner)); + .map(OwnerAddressParser::parseAddress) + .ifPresent(ownerAddress -> + result.addExtraBlock(OWNER_BLOCK_NAME, ownerAddress) + ); } } diff --git a/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js b/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js index c84a599e8..2cb24ed0b 100644 --- a/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js +++ b/allure-generator/src/main/javascript/plugins/testresult-owner/OwnerView.js @@ -1,6 +1,5 @@ import { View } from "backbone.marionette"; import { className } from "../../decorators/index"; -import parseAddress from "../../utils/parseAddress"; import template from "./OwnerView.hbs"; @className("pane__section") @@ -10,7 +9,7 @@ class OwnerView extends View { serializeData() { const extra = this.model.get("extra"); return { - owner: extra ? parseAddress(extra.owner) : null, + owner: extra ? extra.owner : null, }; } } diff --git a/allure-generator/src/main/javascript/utils/parseAddress.js b/allure-generator/src/main/javascript/utils/parseAddress.js deleted file mode 100644 index 43fe1f1e7..000000000 --- a/allure-generator/src/main/javascript/utils/parseAddress.js +++ /dev/null @@ -1,60 +0,0 @@ -const RFC2822_ADDRESS = /^(.*) <(.*)>$/; -const LOOKS_LIKE_EMAIL = /^[^@]+@[^@]+$/; - -/** - * Parse a potentially RFC 2822 address into a display name and an address. - * - * @param {string | null | undefined} maybeAddress - * @returns {{ displayName: string, url?: string } | null - */ -export default function parseAddress(maybeAddress) { - if (!maybeAddress) { - return null; - } - - const match = maybeAddress.match(RFC2822_ADDRESS); - if (match) { - return { - displayName: match[1], - url: toHref(match[2]), - }; - } - - return { - displayName: maybeAddress, - url: toHref(maybeAddress), - }; -} - -/** - * If the address is a valid URL, returns the URL. - * If the address resembles an email address, returns a mailto: URL. - * Otherwise, returns undefined. - * - * @param {string} address - * @returns {string | undefined} - */ -function toHref(address) { - if (isValidURL(address)) { - return address; - } - - if (LOOKS_LIKE_EMAIL.test(address)) { - return `mailto:${address}`; - } -} - -/** - * If the address is a valid URL, returns the URL. - * - * @param {string} maybeURL - * @returns {boolean} - */ -function isValidURL(maybeURL) { - try { - new URL(maybeURL); - return true; - } catch (_error) { - return false; - } -} diff --git a/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java b/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java new file mode 100644 index 000000000..1ebecedf0 --- /dev/null +++ b/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016-2024 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.owner; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OwnerAddressParserTest { + @Test + void shouldReturnNullForNullInput() { + assertNull(OwnerAddressParser.parseAddress(null)); + } + + @Test + void shouldReturnNullForEmptyInput() { + assertNull(OwnerAddressParser.parseAddress("")); + } + + @Test + void shouldParseRFC2822FormattedStringWithEmail() { + String input = "John Doe "; + OwnerAddress expected = new OwnerAddress("John Doe", "mailto:john.doe@example.com"); + assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(input).getDisplayName()); + assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(input).getUrl()); + } + + @Test + void shouldParseRFC2822FormattedStringWithURL() { + String input = "John Doe "; + OwnerAddress expected = new OwnerAddress("John Doe", "https://github.com/john.doe"); + assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(input).getDisplayName()); + assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(input).getUrl()); + } + + @Test + void shouldReturnDisplayNameForPlainTextInput() { + String displayName = "John Doe"; + OwnerAddress expected = new OwnerAddress(displayName, null); + assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(displayName).getDisplayName()); + assertNull(OwnerAddressParser.parseAddress(displayName).getUrl()); + } + + @Test + void shouldReturnDisplayNameAndUrlForEmailAddress() { + String email = "john.doe@example.com"; + OwnerAddress expected = new OwnerAddress(email, "mailto:" + email); + assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(email).getDisplayName()); + assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(email).getUrl()); + } + + @Test + void shouldReturnDisplayNameAndUrlForValidURL() { + String validUrl = "https://github.com/john.doe"; + OwnerAddress expected = new OwnerAddress(validUrl, validUrl); + assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(validUrl).getDisplayName()); + assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(validUrl).getUrl()); + } +} From 8b73a1722e8d5cb99a958999a3216cb8064b0bb1 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 9 May 2024 10:23:14 +0300 Subject: [PATCH 3/3] Improve OwnerAddressParser on strictness and test coverage --- allure-generator/build.gradle.kts | 1 + .../allure/owner/OwnerAddressParser.java | 54 +++++++++++-------- .../allure/owner/OwnerAddressParserTest.java | 38 +++++++++++-- 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/allure-generator/build.gradle.kts b/allure-generator/build.gradle.kts index 7dbd7c326..869303636 100644 --- a/allure-generator/build.gradle.kts +++ b/allure-generator/build.gradle.kts @@ -106,6 +106,7 @@ dependencies { implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") implementation("com.fasterxml.jackson.module:jackson-module-jaxb-annotations") implementation("commons-io:commons-io") + implementation("commons-validator:commons-validator:1.7") implementation("io.qameta.allure:allure-model") implementation("javax.xml.bind:jaxb-api") implementation("org.allurefw:allure1-model") diff --git a/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java index 6e9c43683..499b0d5ad 100644 --- a/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java +++ b/allure-generator/src/main/java/io/qameta/allure/owner/OwnerAddressParser.java @@ -15,50 +15,62 @@ */ package io.qameta.allure.owner; -import java.net.MalformedURLException; +import org.apache.commons.validator.routines.EmailValidator; +import org.apache.commons.validator.routines.UrlValidator; + import java.util.regex.Matcher; import java.util.regex.Pattern; public final class OwnerAddressParser { - private static final Pattern RFC2822_ADDRESS = Pattern.compile("^(.*) <(.*)>$"); - private static final Pattern LOOKS_LIKE_EMAIL = Pattern.compile("^[^@]+@[^@]+$"); + private static final Pattern RFC2822_ADDRESS = Pattern.compile("^([^<>]+)\\s+<\\s*(\\S*)\\s*>$"); private OwnerAddressParser() { } + @SuppressWarnings("ReturnCount") public static OwnerAddress parseAddress(final String maybeAddress) { if (maybeAddress == null || maybeAddress.isEmpty()) { return null; } + // Prevent performance degradation for plain text + if (!isLikelyAddress(maybeAddress)) { + return new OwnerAddress(maybeAddress, null); + } + + String displayName = maybeAddress; + String urlOrEmail = maybeAddress; + final Matcher matcher = RFC2822_ADDRESS.matcher(maybeAddress); if (matcher.matches()) { - final String displayName = matcher.group(1); - final String url = toHref(matcher.group(2)); - return new OwnerAddress(displayName, url); + displayName = matcher.group(1); + urlOrEmail = matcher.group(2); } - return new OwnerAddress(maybeAddress, toHref(maybeAddress)); - } + // e.g.: John Doe <> + if (urlOrEmail.isEmpty()) { + return new OwnerAddress(displayName, null); + } - private static String toHref(final String address) { - if (isValidURL(address)) { - return address; + // e.g.: John Doe + if (UrlValidator.getInstance().isValid(urlOrEmail)) { + return new OwnerAddress(displayName, urlOrEmail); } - if (LOOKS_LIKE_EMAIL.matcher(address).matches()) { - return "mailto:" + address; + // e.g.: John Doe + if (EmailValidator.getInstance().isValid(urlOrEmail)) { + return new OwnerAddress(displayName, "mailto:" + urlOrEmail); } - return null; + // Non-compliant addresses are treated as plain text + return new OwnerAddress(maybeAddress, null); } - private static boolean isValidURL(final String maybeURL) { - try { - new java.net.URL(maybeURL); - return true; - } catch (MalformedURLException e) { - return false; - } + /** + * Checks if the given string is likely to be a plain text (not an email or URL). + * Regular expressions are slow, therefore we just check for common characters. + */ + private static boolean isLikelyAddress(final String input) { + return input.contains("@") || input.contains(":") || input.contains("<"); } } diff --git a/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java b/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java index 1ebecedf0..9bf57440a 100644 --- a/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java +++ b/allure-generator/src/test/java/io/qameta/allure/owner/OwnerAddressParserTest.java @@ -32,7 +32,7 @@ void shouldReturnNullForEmptyInput() { @Test void shouldParseRFC2822FormattedStringWithEmail() { - String input = "John Doe "; + String input = "John Doe < john.doe@example.com >"; OwnerAddress expected = new OwnerAddress("John Doe", "mailto:john.doe@example.com"); assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(input).getDisplayName()); assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(input).getUrl()); @@ -40,12 +40,20 @@ void shouldParseRFC2822FormattedStringWithEmail() { @Test void shouldParseRFC2822FormattedStringWithURL() { - String input = "John Doe "; - OwnerAddress expected = new OwnerAddress("John Doe", "https://github.com/john.doe"); + String input = "John Doe "; + OwnerAddress expected = new OwnerAddress("John Doe", "https://github.com/@john.doe"); assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(input).getDisplayName()); assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(input).getUrl()); } + @Test + void shouldReturnOnlyDisplayNameForEmptyRFC822Address() { + String emptyAddress = "John Doe <>"; + OwnerAddress actual = OwnerAddressParser.parseAddress(emptyAddress); + assertEquals("John Doe", actual.getDisplayName()); + assertNull(actual.getUrl()); + } + @Test void shouldReturnDisplayNameForPlainTextInput() { String displayName = "John Doe"; @@ -69,4 +77,28 @@ void shouldReturnDisplayNameAndUrlForValidURL() { assertEquals(expected.getDisplayName(), OwnerAddressParser.parseAddress(validUrl).getDisplayName()); assertEquals(expected.getUrl(), OwnerAddressParser.parseAddress(validUrl).getUrl()); } + + @Test + void shouldReturnOnlyDisplayNameForInvalidURL() { + String invalidUrl = "htp:/www.example.com/page"; + OwnerAddress actual = OwnerAddressParser.parseAddress(invalidUrl); + assertEquals(invalidUrl, actual.getDisplayName()); + assertNull(actual.getUrl()); + } + + @Test + void shouldReturnOnlyDisplayNameForInvalidEmail() { + String invalidEmail = "user@.example.com"; + OwnerAddress actual = OwnerAddressParser.parseAddress(invalidEmail); + assertEquals(invalidEmail, actual.getDisplayName()); + assertNull(actual.getUrl()); + } + + @Test + void shouldReturnInvalidRFC822AddressUnchanged() { + String invalidAddress = "John Doe "; + OwnerAddress actual = OwnerAddressParser.parseAddress(invalidAddress); + assertEquals(invalidAddress, actual.getDisplayName()); + assertNull(actual.getUrl()); + } }