diff --git a/niffler-e-2-e-tests/build.gradle b/niffler-e-2-e-tests/build.gradle index d277af319..2db41d6f0 100644 --- a/niffler-e-2-e-tests/build.gradle +++ b/niffler-e-2-e-tests/build.gradle @@ -72,6 +72,7 @@ dependencies { // REST testImplementation "com.squareup.retrofit2:retrofit:${retrofitVersion}" testImplementation "com.squareup.retrofit2:converter-jackson:${retrofitVersion}" + testImplementation "com.squareup.retrofit2:converter-scalars:${retrofitVersion}" testImplementation "com.squareup.okhttp3:logging-interceptor:${okhttp3Version}" testImplementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttp3Version}" testImplementation "org.springframework.data:spring-data-commons:${springDataCommonsVersion}" diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApiClient.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApiClient.java index 2d7e8fdcc..20c54440b 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApiClient.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApiClient.java @@ -10,6 +10,7 @@ import java.io.IOException; import okhttp3.ResponseBody; import retrofit2.Response; +import retrofit2.http.Field; public class AuthApiClient extends RestClient { @@ -38,6 +39,18 @@ public void requestRegisterForm() { assertEquals(200, response.code()); } + @Step("Register") + public void register(String username, String password, String passwordSubmit, String csrf) { + final Response response; + try { + response = authApi.register(username, password, passwordSubmit, csrf) + .execute(); + } catch (IOException e) { + throw new AssertionError(e); + } + assertEquals(200, response.code()); + } + @Step("Authorize") public void authorize(String codeChallenge) { final Response response; diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/CodeInterceptor.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/CodeInterceptor.java new file mode 100644 index 000000000..c5747ad14 --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/CodeInterceptor.java @@ -0,0 +1,30 @@ +package guru.qa.niffler.api.core; + +import guru.qa.niffler.jupiter.extension.ApiLoginExtension; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.Objects; + +public class CodeInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + final Response response = chain.proceed(chain.request()); + if (response.isRedirect()) { + String location = Objects.requireNonNull( + response.header("Location") + ); + if (location.contains("code=")) { + ApiLoginExtension.setCode( + StringUtils.substringAfter( + location, "code=" + ) + ); + } + } + return response; + } +} diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/RestClient.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/RestClient.java index e26d26b6a..989a9a1f9 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/RestClient.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/RestClient.java @@ -31,6 +31,10 @@ public RestClient(String baseUrl, boolean followRedirect) { this(baseUrl, followRedirect, JacksonConverterFactory.create(), HEADERS, new Interceptor[0]); } + public RestClient(String baseUrl, boolean followRedirect, @Nullable Interceptor... interceptors) { + this(baseUrl, followRedirect, JacksonConverterFactory.create(), HEADERS, interceptors); + } + public RestClient(String baseUrl, HttpLoggingInterceptor.Level loggingLevel) { this(baseUrl, false, JacksonConverterFactory.create(), loggingLevel, new Interceptor[0]); } @@ -75,4 +79,8 @@ public RestClient(String baseUrl, boolean followRedirect, Converter.Factory conv .addConverterFactory(converterFactory) .build(); } + + public T create(final Class service) { + return this.retrofit.create(service); + } } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/ApiLogin.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/ApiLogin.java new file mode 100644 index 000000000..dd6ca3845 --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/ApiLogin.java @@ -0,0 +1,21 @@ +package guru.qa.niffler.jupiter.annotation; + +import guru.qa.niffler.jupiter.extension.ApiLoginExtension; +import guru.qa.niffler.jupiter.extension.UserExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@ExtendWith({ + UserExtension.class, + ApiLoginExtension.class +}) +public @interface ApiLogin { + String username() default ""; + String password() default ""; +} diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Token.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Token.java new file mode 100644 index 000000000..449e466a7 --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Token.java @@ -0,0 +1,11 @@ +package guru.qa.niffler.jupiter.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Token { +} diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java new file mode 100644 index 000000000..d7ece1478 --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java @@ -0,0 +1,120 @@ +package guru.qa.niffler.jupiter.extension; + +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.WebDriverRunner; +import guru.qa.niffler.api.core.ThreadSafeCookieStore; +import guru.qa.niffler.config.Config; +import guru.qa.niffler.jupiter.annotation.ApiLogin; +import guru.qa.niffler.jupiter.annotation.Token; +import guru.qa.niffler.model.TestData; +import guru.qa.niffler.model.UserJson; +import guru.qa.niffler.page.MainPage; +import guru.qa.niffler.service.impl.AuthApiClient; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.platform.commons.support.AnnotationSupport; +import org.openqa.selenium.Cookie; + + +public class ApiLoginExtension implements BeforeEachCallback, ParameterResolver { + + private static final Config CFG = Config.getInstance(); + public static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(ApiLoginExtension.class); + + private final AuthApiClient authApiClient = new AuthApiClient(); + private final boolean setupBrowser; + + private ApiLoginExtension(boolean setupBrowser) { + this.setupBrowser = setupBrowser; + } + + public ApiLoginExtension() { + this.setupBrowser = true; + } + + public static ApiLoginExtension restApiLoginExtension() { + return new ApiLoginExtension(false); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + AnnotationSupport.findAnnotation(context.getRequiredTestMethod(), ApiLogin.class) + .ifPresent(apiLogin -> { + + final UserJson userToLogin; + final UserJson userFromUserExtension = UserExtension.getUserJson(); + if ("".equals(apiLogin.username()) || "".equals(apiLogin.password())) { + if (userFromUserExtension == null) { + throw new IllegalStateException("@User must be present in case that @ApiLogin is empty!"); + } + userToLogin = userFromUserExtension; + } else { + UserJson fakeUser = new UserJson( + apiLogin.username(), + new TestData( + apiLogin.password() + ) + ); + if (userFromUserExtension != null) { + throw new IllegalStateException("@User must not be present in case that @ApiLogin contains username or password!"); + } + UserExtension.setUser(fakeUser); + userToLogin = fakeUser; + } + + final String token = authApiClient.login( + userToLogin.username(), + userToLogin.testData().password() + ); + setToken(token); + if (setupBrowser) { + Selenide.open(CFG.frontUrl()); + Selenide.localStorage().setItem("id_token", getToken()); + WebDriverRunner.getWebDriver().manage().addCookie( + new Cookie( + "JSESSIONID", + ThreadSafeCookieStore.INSTANCE.cookieValue("JSESSIONID") + ) + ); + Selenide.open(MainPage.URL, MainPage.class).checkThatPageLoaded(); + } + }); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.getParameter().getType().isAssignableFrom(String.class) + && AnnotationSupport.isAnnotated(parameterContext.getParameter(), Token.class); + } + + @Override + public String resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getToken(); + } + + public static void setToken(String token) { + TestMethodContextExtension.context().getStore(NAMESPACE).put("token", token); + } + + public static String getToken() { + return TestMethodContextExtension.context().getStore(NAMESPACE).get("token", String.class); + } + + public static void setCode(String code) { + TestMethodContextExtension.context().getStore(NAMESPACE).put("code", code); + } + + public static String getCode() { + return TestMethodContextExtension.context().getStore(NAMESPACE).get("code", String.class); + } + + public static Cookie getJsessionIdCookie() { + return new Cookie( + "JSESSIONID", + ThreadSafeCookieStore.INSTANCE.cookieValue("JSESSIONID") + ); + } +} diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/UserExtension.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/UserExtension.java index 80ecd882f..7d2d25b89 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/UserExtension.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/UserExtension.java @@ -31,10 +31,7 @@ public void beforeEach(ExtensionContext context) throws Exception { usersClient.addIncomeInvitations(testUser, userAnno.incomeInvitations()); usersClient.addOutcomeInvitations(testUser, userAnno.outcomeInvitations()); usersClient.addFriends(testUser, userAnno.friends()); - context.getStore(NAMESPACE).put( - context.getUniqueId(), - testUser - ); + setUser(testUser); } }); } @@ -46,8 +43,20 @@ public boolean supportsParameter(ParameterContext parameterContext, } @Override - public UserJson resolveParameter(ParameterContext parameterContext, - ExtensionContext extensionContext) throws ParameterResolutionException { - return extensionContext.getStore(NAMESPACE).get(extensionContext.getUniqueId(), UserJson.class); + public UserJson resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return getUserJson(); + } + + public static void setUser(UserJson testUser) { + final ExtensionContext context = TestMethodContextExtension.context(); + context.getStore(NAMESPACE).put( + context.getUniqueId(), + testUser + ); + } + + public static UserJson getUserJson() { + final ExtensionContext context = TestMethodContextExtension.context(); + return context.getStore(NAMESPACE).get(context.getUniqueId(), UserJson.class); } } diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/UserJson.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/UserJson.java index 08130ce01..4dc84bcaf 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/UserJson.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/UserJson.java @@ -6,6 +6,8 @@ import guru.qa.niffler.data.entity.userdata.UserEntity; import java.nio.charset.StandardCharsets; import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import jaxb.userdata.FriendState; public record UserJson( @@ -47,6 +49,14 @@ public static UserJson fromEntity(UserEntity entity, FriendState friendState) { ); } + public UserJson(@Nonnull String username) { + this(username, null); + } + + public UserJson(@Nonnull String username, @Nullable TestData testData) { + this(null, username, null, null, null, null, null, null, null, testData); + } + public UserJson addTestData(TestData testData) { return new UserJson( id, username, firstname, surname, fullname, currency, photo, photoSmall, friendState, testData diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/MainPage.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/MainPage.java index 8cdbd27d8..5b9483a83 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/MainPage.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/MainPage.java @@ -24,6 +24,7 @@ @ParametersAreNonnullByDefault public class MainPage extends BasePage { + public static final String URL = CFG.frontUrl() + "main"; private final SelenideElement tableRows = $("#spendings tbody"); private SelenideElement statisticsBlock = $x("//div[h2[contains(text(), 'Statistics')]]"); private final SelenideElement historyOfSpending = $x( diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/ProfilePage.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/ProfilePage.java index b0b782b50..de33cd5d5 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/ProfilePage.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/ProfilePage.java @@ -27,7 +27,7 @@ @ParametersAreNonnullByDefault public class ProfilePage extends BasePage { - public static String url = Config.getInstance().frontUrl() + "profile"; + public static final String URL = CFG.frontUrl() + "profile"; private final SelenideElement avatar = $("#image__input").parent().$("img"); private final SelenideElement userName = $("#username"); private final SelenideElement nameInput = $("#name"); diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/AuthApiClient.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/AuthApiClient.java new file mode 100644 index 000000000..d141bd301 --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/AuthApiClient.java @@ -0,0 +1,57 @@ +package guru.qa.niffler.service.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import guru.qa.niffler.api.AuthApi; +import guru.qa.niffler.api.core.CodeInterceptor; +import guru.qa.niffler.api.core.RestClient; +import guru.qa.niffler.api.core.ThreadSafeCookieStore; +import guru.qa.niffler.config.Config; +import guru.qa.niffler.jupiter.extension.ApiLoginExtension; +import utils.OAuthUtils; +import lombok.SneakyThrows; +import retrofit2.Response; + + +public class AuthApiClient extends RestClient { + + private static final Config CFG = Config.getInstance(); + private final AuthApi authApi; + + public AuthApiClient() { + super(CFG.authUrl(), true, new CodeInterceptor()); + this.authApi = create(AuthApi.class); + } + + @SneakyThrows + public String login(String username, String password) { + final String codeVerifier = OAuthUtils.generateCodeVerifier(); + final String codeChallenge = OAuthUtils.generateCodeChallenge(codeVerifier); + final String redirectUri = CFG.frontUrl() + "authorized"; + final String clientId = "client"; + + authApi.authorize( + "code", + clientId, + "openid", + redirectUri, + codeChallenge, + "S256" + ).execute(); + + authApi.login( + username, + password, + ThreadSafeCookieStore.INSTANCE.cookieValue("XSRF-TOKEN") + ).execute(); + + Response tokenResponse = authApi.token( + ApiLoginExtension.getCode(), + redirectUri, + clientId, + codeVerifier, + "authorization_code" + ).execute(); + + return tokenResponse.body().get("id_token").asText(); + } +} diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/UsersRestClient.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/UsersRestClient.java index 38d7c8bf9..e13418f8e 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/UsersRestClient.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/UsersRestClient.java @@ -8,7 +8,6 @@ import guru.qa.niffler.model.UserJson; import guru.qa.niffler.service.UsersClient; import io.qameta.allure.Step; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/JdbcTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/JdbcTest.java similarity index 99% rename from niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/JdbcTest.java rename to niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/JdbcTest.java index c6f0cac58..aab40e60b 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/JdbcTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/JdbcTest.java @@ -1,4 +1,4 @@ -package guru.qa.niffler.test.web; +package guru.qa.niffler.test.fake; import guru.qa.niffler.jupiter.extension.UsersClientExtension; import guru.qa.niffler.model.CategoryJson; diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/OAuthTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/OAuthTest.java new file mode 100644 index 000000000..6bafebe4a --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/OAuthTest.java @@ -0,0 +1,22 @@ +package guru.qa.niffler.test.fake; + +import guru.qa.niffler.config.Config; +import guru.qa.niffler.jupiter.annotation.ApiLogin; +import guru.qa.niffler.jupiter.annotation.Token; +import guru.qa.niffler.model.UserJson; +import guru.qa.niffler.service.impl.AuthApiClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class OAuthTest { + + private static final Config CFG = Config.getInstance(); + private final AuthApiClient authApiClient = new AuthApiClient(); + + @Test + @ApiLogin(username = "duck", password = "12345") + void oauthTest(@Token String token, UserJson user) { + System.out.println(user); + Assertions.assertNotNull(token); + } +} diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/OAuthTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/OAuthTest.java index c0e76ff19..c1a66903d 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/OAuthTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/OAuthTest.java @@ -6,7 +6,7 @@ import java.io.IOException; import java.security.NoSuchAlgorithmException; import org.junit.jupiter.api.Test; -import utils.OauthUtils; +import utils.OAuthUtils; public class OAuthTest { @@ -14,8 +14,8 @@ public class OAuthTest { @Test void oauthTest() throws IOException, NoSuchAlgorithmException { - String codeVerifier = OauthUtils.generateCodeVerifier(); - String codeChallenge = OauthUtils.generateCodeChallenge(codeVerifier); + String codeVerifier = OAuthUtils.generateCodeVerifier(); + String codeChallenge = OAuthUtils.generateCodeChallenge(codeVerifier); apiClient.authorize(codeChallenge); apiClient.login("moon", "moon123"); diff --git a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/ProfileTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/ProfileTest.java index f22dace40..475226961 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/ProfileTest.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/ProfileTest.java @@ -4,6 +4,7 @@ import com.codeborne.selenide.Selenide; import guru.qa.niffler.config.Config; +import guru.qa.niffler.jupiter.annotation.ApiLogin; import guru.qa.niffler.jupiter.annotation.Category; import guru.qa.niffler.jupiter.annotation.ScreenShotTest; import guru.qa.niffler.jupiter.annotation.User; @@ -59,18 +60,14 @@ void activeCategoryShouldPresentInCategoriesList(UserJson user) { } @User + @ApiLogin @Test @ScreenShotTest("img/expected-cat.png") void shouldUpdateProfileWithAllFieldsSet(UserJson user, BufferedImage expectedAvatar) throws IOException { final String newName = randomName(); - ProfilePage profilePage = Selenide.open(LoginPage.URL, LoginPage.class) - .fillLoginPage(user.username(), user.testData().password()) - .submit(new MainPage()) - .checkThatPageLoaded() - .getHeader() - .toProfilePage() + ProfilePage profilePage = Selenide.open(ProfilePage.URL, ProfilePage.class) .uploadPhotoFromClasspath("img/cat.jpeg") .setName(newName) .submitProfile() diff --git a/niffler-e-2-e-tests/src/test/java/utils/OauthUtils.java b/niffler-e-2-e-tests/src/test/java/utils/OauthUtils.java index 787fb3f6e..7405258ac 100644 --- a/niffler-e-2-e-tests/src/test/java/utils/OauthUtils.java +++ b/niffler-e-2-e-tests/src/test/java/utils/OauthUtils.java @@ -1,28 +1,28 @@ package utils; -import java.io.UnsupportedEncodingException; +import lombok.SneakyThrows; + +import java.nio.charset.Charset; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; -public class OauthUtils { +public class OAuthUtils { + + private static SecureRandom secureRandom = new SecureRandom(); - public static String generateCodeVerifier() throws UnsupportedEncodingException { - SecureRandom secureRandom = new SecureRandom(); + public static String generateCodeVerifier() { byte[] codeVerifier = new byte[32]; secureRandom.nextBytes(codeVerifier); - return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier); } - public static String generateCodeChallenge(String codeVerifier) - throws UnsupportedEncodingException, NoSuchAlgorithmException { - byte[] bytes = codeVerifier.getBytes("US-ASCII"); + @SneakyThrows + public static String generateCodeChallenge(String codeVerifier) { + byte[] bytes = codeVerifier.getBytes(Charset.forName("US-ASCII")); MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(bytes, 0, bytes.length); byte[] digest = messageDigest.digest(); - return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); } }