-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pipedrive oAuth support #7968
Pipedrive oAuth support #7968
Changes from 4 commits
7e96bbc
919b80f
41cda86
922acaf
6f12aec
6ee4418
1c04fe4
45ed525
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import com.google.common.annotations.VisibleForTesting; | ||
import com.google.common.collect.ImmutableMap; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.oauth.BaseOAuth2Flow; | ||
import java.io.IOException; | ||
import java.net.URISyntaxException; | ||
import java.net.http.HttpClient; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import java.util.function.Supplier; | ||
import org.apache.http.client.utils.URIBuilder; | ||
|
||
/** | ||
* Following docs from https://pipedrive.readme.io/docs/marketplace-oauth-authorization | ||
*/ | ||
public class PipeDriveOAuthFlow extends BaseOAuth2Flow { | ||
|
||
final String AUTHORIZE_URL = "https://oauth.pipedrive.com/oauth/authorize"; | ||
final String ACCESS_TOKEN_URL = "https://oauth.pipedrive.com/oauth/token"; | ||
|
||
public PipeDriveOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient) { | ||
super(configRepository, httpClient); | ||
} | ||
|
||
@VisibleForTesting | ||
public PipeDriveOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier<String> stateSupplier) { | ||
super(configRepository, httpClient, stateSupplier); | ||
} | ||
|
||
@Override | ||
protected String formatConsentUrl(final UUID definitionId, final String clientId, final String redirectUrl) throws IOException { | ||
try { | ||
return new URIBuilder(AUTHORIZE_URL) | ||
.addParameter("client_id", clientId) | ||
.addParameter("redirect_uri", redirectUrl) | ||
.addParameter("state", getState()) | ||
.build().toString(); | ||
} catch (final URISyntaxException e) { | ||
throw new IOException("Failed to format Consent URL for OAuth flow", e); | ||
} | ||
} | ||
|
||
@Override | ||
protected Map<String, String> getAccessTokenQueryParameters(final String clientId, | ||
final String clientSecret, | ||
final String authCode, | ||
final String redirectUrl) { | ||
return ImmutableMap.<String, String>builder() | ||
.putAll(super.getAccessTokenQueryParameters(clientId, clientSecret, authCode, redirectUrl)) | ||
.put("grant_type", "authorization_code") | ||
.build(); | ||
} | ||
|
||
@Override | ||
protected String getAccessTokenUrl() { | ||
return ACCESS_TOKEN_URL; | ||
} | ||
|
||
@Override | ||
protected List<String> getDefaultOAuthOutputPath() { | ||
return List.of("authorization"); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.mockito.Mockito.when; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.common.collect.ImmutableMap; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.SourceOAuthParameter; | ||
import io.airbyte.config.persistence.ConfigNotFoundException; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.oauth.OAuthFlowImplementation; | ||
import io.airbyte.validation.json.JsonValidationException; | ||
import java.io.IOException; | ||
import java.net.http.HttpClient; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import org.junit.jupiter.api.Test; | ||
|
||
public class PipeDriveOAuthFlowIntegrationTest extends OAuthFlowIntegrationTest { | ||
|
||
@Override | ||
protected Path getCredentialsPath() { | ||
return Path.of("secrets/pipedrive.json"); | ||
} | ||
|
||
@Override | ||
protected String getRedirectUrl() { | ||
return "http://localhost:3000/auth_flow"; | ||
} | ||
|
||
protected int getServerListeningPort() { | ||
return 3000; | ||
} | ||
|
||
@Override | ||
protected OAuthFlowImplementation getFlowImplementation(ConfigRepository configRepository, HttpClient httpClient) { | ||
return new PipeDriveOAuthFlow(configRepository, httpClient); | ||
} | ||
|
||
@Test | ||
public void testFullPipeDriveOAuthFlow() throws InterruptedException, ConfigNotFoundException, IOException, JsonValidationException { | ||
final UUID workspaceId = UUID.randomUUID(); | ||
final UUID definitionId = UUID.randomUUID(); | ||
final String fullConfigAsString = new String(Files.readAllBytes(getCredentialsPath())); | ||
final JsonNode credentialsJson = Jsons.deserialize(fullConfigAsString); | ||
when(configRepository.listSourceOAuthParam()).thenReturn(List.of(new SourceOAuthParameter() | ||
.withOauthParameterId(UUID.randomUUID()) | ||
.withSourceDefinitionId(definitionId) | ||
.withWorkspaceId(workspaceId) | ||
.withConfiguration(Jsons.jsonNode(Map.of("authorization", ImmutableMap.builder() | ||
.put("client_id", credentialsJson.get("client_id").asText()) | ||
.put("client_secret", credentialsJson.get("client_secret").asText()) | ||
.build()))))); | ||
final String url = getFlowImplementation(configRepository, httpClient).getSourceConsentUrl(workspaceId, definitionId, getRedirectUrl()); | ||
LOGGER.info("Waiting for user consent at: {}", url); | ||
waitForResponse(20); | ||
assertTrue(serverHandler.isSucceeded(), "Failed to get User consent on time"); | ||
final Map<String, Object> params = flow.completeSourceOAuth(workspaceId, definitionId, | ||
Map.of("code", serverHandler.getParamValue()), getRedirectUrl()); | ||
LOGGER.info("Response from completing OAuth Flow is: {}", params.toString()); | ||
assertTrue(params.containsKey("authorization")); | ||
final var creds = (Map<String, String>) params.get("authorization"); | ||
assertTrue(creds.get("refresh_token").toString().length() > 0); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
import com.google.common.collect.ImmutableMap; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.SourceOAuthParameter; | ||
import io.airbyte.config.persistence.ConfigNotFoundException; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.validation.json.JsonValidationException; | ||
import java.io.IOException; | ||
import java.net.http.HttpClient; | ||
import java.net.http.HttpResponse; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
|
||
public class PipeDriveOAuthFlowTest { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, could you update your branch and make the unit test extends from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, nice refactoring, I love it |
||
|
||
private UUID workspaceId; | ||
private UUID definitionId; | ||
private ConfigRepository configRepository; | ||
private PipeDriveOAuthFlow pipedriveOAuthFlow; | ||
private HttpClient httpClient; | ||
|
||
private static final String REDIRECT_URL = "https://airbyte.io"; | ||
|
||
private static String getConstantState() { | ||
return "state"; | ||
} | ||
|
||
@BeforeEach | ||
public void setup() throws IOException, JsonValidationException { | ||
workspaceId = UUID.randomUUID(); | ||
definitionId = UUID.randomUUID(); | ||
configRepository = mock(ConfigRepository.class); | ||
httpClient = mock(HttpClient.class); | ||
when(configRepository.listSourceOAuthParam()).thenReturn(List.of(new SourceOAuthParameter() | ||
.withOauthParameterId(UUID.randomUUID()) | ||
.withSourceDefinitionId(definitionId) | ||
.withWorkspaceId(workspaceId) | ||
.withConfiguration(Jsons.jsonNode(Map.of("authorization", ImmutableMap.builder() | ||
.put("client_id", "test_client_id") | ||
.put("client_secret", "test_client_secret") | ||
.build()))))); | ||
pipedriveOAuthFlow = new PipeDriveOAuthFlow(configRepository, httpClient, PipeDriveOAuthFlowTest::getConstantState); | ||
|
||
} | ||
|
||
@Test | ||
public void testGetSourceConsentUrl() throws IOException, InterruptedException, ConfigNotFoundException { | ||
final String consentUrl = pipedriveOAuthFlow.getSourceConsentUrl(workspaceId, definitionId, REDIRECT_URL); | ||
assertEquals( | ||
"https://oauth.pipedrive.com/oauth/authorize?client_id=test_client_id&redirect_uri=https%3A%2F%2Fairbyte.io&state=state", | ||
consentUrl); | ||
} | ||
|
||
@Test | ||
public void testCompleteSourceOAuth() throws IOException, JsonValidationException, InterruptedException, ConfigNotFoundException { | ||
|
||
final Map<String, String> returnedCredentials = Map.of("refresh_token", "refresh_token_response"); | ||
final HttpResponse response = mock(HttpResponse.class); | ||
when(response.body()).thenReturn(Jsons.serialize(returnedCredentials)); | ||
when(httpClient.send(any(), any())).thenReturn(response); | ||
final Map<String, Object> queryParams = Map.of("code", "test_code"); | ||
final Map<String, Object> actualQueryParams = | ||
pipedriveOAuthFlow.completeSourceOAuth(workspaceId, definitionId, queryParams, REDIRECT_URL); | ||
assertEquals(Map.of("authorization", returnedCredentials), actualQueryParams); | ||
} | ||
|
||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since this is a source, is that normal to set destination modes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, missed that, will remove