From dbe053a8e7d03c071d44f565e6b639477a5a3d6f Mon Sep 17 00:00:00 2001 From: dtuchs Date: Wed, 20 Nov 2024 17:23:04 +0500 Subject: [PATCH] stream #22 - oauth --- niffler-e-2-e-tests/build.gradle | 1 + .../java/guru/qa/niffler/api/AuthApi.java | 44 +++---- .../qa/niffler/api/core/CodeInterceptor.java | 30 +++++ .../guru/qa/niffler/api/core/RestClient.java | 4 + .../niffler/jupiter/annotation/ApiLogin.java | 21 +++ .../qa/niffler/jupiter/annotation/Token.java | 11 ++ .../jupiter/extension/ApiLoginExtension.java | 120 ++++++++++++++++++ .../jupiter/extension/UserExtension.java | 20 ++- .../niffler/service/impl/AuthApiClient.java | 57 +++++++++ .../niffler/test/{web => fake}/JdbcTest.java | 2 +- .../guru/qa/niffler/test/fake/OAuthTest.java | 22 ++++ .../guru/qa/niffler/test/web/ProfileTest.java | 11 +- .../guru/qa/niffler/utils/OAuthUtils.java | 28 ++++ 13 files changed, 336 insertions(+), 35 deletions(-) create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/CodeInterceptor.java create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/ApiLogin.java create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Token.java create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/ApiLoginExtension.java create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/impl/AuthApiClient.java rename niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/{web => fake}/JdbcTest.java (97%) create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/OAuthTest.java create mode 100644 niffler-e-2-e-tests/src/test/java/guru/qa/niffler/utils/OAuthUtils.java 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/AuthApi.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApi.java index d5df5a95e..d1aea59f8 100644 --- a/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApi.java +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/AuthApi.java @@ -10,33 +10,33 @@ public interface AuthApi { - @GET("oauth2/authorize") - Call authorize( - @Query("response_type") String responseType, - @Query("client_id") String clientId, - @Query("scope") String scope, - @Query(value = "redirect_uri", encoded = true) String redirectUri, - @Query("code_challenge") String codeChallenge, - @Query("code_challenge_method") String codeChallengeMethod); + @GET("register") + Call requestRegisterForm(); - @POST("oauth2/token") - @FormUrlEncoded - Call token( - @Field("client_id") String clientId, - @Field(value = "redirect_uri", encoded = true) String redirectUri, - @Field("grant_type") String grantType, - @Field("code") String code, - @Field("code_verifier") String codeChallenge); + @GET("oauth2/authorize") + Call authorize(@Query("response_type") String responseType, + @Query("client_id") String clientId, + @Query("scope") String scope, + @Query(value = "redirect_uri", encoded = true) String redirectUri, + @Query("code_challenge") String codeChallenge, + @Query("code_challenge_method") String codeChallengeMethod + ); @POST("login") @FormUrlEncoded - Call login( - @Field("username") String username, - @Field("password") String password, - @Field("_csrf") String csrf); + Call login(@Field("username") String username, + @Field("password") String password, + @Field("_csrf") String csrf + ); - @GET("register") - Call requestRegisterForm(); + @POST("oauth2/token") + @FormUrlEncoded + Call token(@Field("code") String code, + @Field(value = "redirect_uri", encoded = true) String redirectUri, + @Field("client_id") String clientId, + @Field("code_verifier") String codeVerifier, + @Field("grant_type") String grantType + ); @POST("register") @FormUrlEncoded 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 9cffeb2b6..910cb58ac 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]); } 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..f1529340f --- /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.rest.TestData; +import guru.qa.niffler.model.rest.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 532f4ccfe..e4a14fc31 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 @@ -30,10 +30,7 @@ public void beforeEach(ExtensionContext context) throws Exception { usersClient.addIncomeInvitation(testUser, userAnno.incomeInvitations()); usersClient.addOutcomeInvitation(testUser, userAnno.outcomeInvitations()); usersClient.addFriend(testUser, userAnno.friends()); - context.getStore(NAMESPACE).put( - context.getUniqueId(), - testUser - ); + setUser(testUser); } }); } @@ -45,6 +42,19 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon @Override public UserJson resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return extensionContext.getStore(NAMESPACE).get(extensionContext.getUniqueId(), UserJson.class); + 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/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..de9ea3fa0 --- /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 guru.qa.niffler.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.generateCodeChallange(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/test/web/JdbcTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/JdbcTest.java similarity index 97% 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 54123e2e5..b356ff76e 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.rest.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..c2904c1c3 --- /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.rest.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/ProfileTest.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/ProfileTest.java index bfd1cba51..5f79e5e96 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 @@ -1,6 +1,7 @@ package guru.qa.niffler.test.web; import com.codeborne.selenide.Selenide; +import guru.qa.niffler.jupiter.annotation.ApiLogin; import guru.qa.niffler.jupiter.annotation.Category; import guru.qa.niffler.jupiter.annotation.User; import guru.qa.niffler.jupiter.annotation.meta.WebTest; @@ -53,16 +54,12 @@ void activeCategoryShouldPresentInCategoriesList(UserJson user) { } @User + @ApiLogin @Test - void shouldUpdateProfileWithAllFieldsSet(UserJson user) { + void shouldUpdateProfileWithAllFieldsSet() { 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/guru/qa/niffler/utils/OAuthUtils.java b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/utils/OAuthUtils.java new file mode 100644 index 000000000..5b4fd538d --- /dev/null +++ b/niffler-e-2-e-tests/src/test/java/guru/qa/niffler/utils/OAuthUtils.java @@ -0,0 +1,28 @@ +package guru.qa.niffler.utils; + +import lombok.SneakyThrows; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +public class OAuthUtils { + + private static SecureRandom secureRandom = new SecureRandom(); + + public static String generateCodeVerifier() { + byte[] codeVerifier = new byte[32]; + secureRandom.nextBytes(codeVerifier); + return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier); + } + + @SneakyThrows + public static String generateCodeChallange(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); + } +}