diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/OrkesClients.java b/orkes-client/src/main/java/io/orkes/conductor/client/OrkesClients.java index 5016bf87..1c26d042 100644 --- a/orkes-client/src/main/java/io/orkes/conductor/client/OrkesClients.java +++ b/orkes-client/src/main/java/io/orkes/conductor/client/OrkesClients.java @@ -61,6 +61,14 @@ public OrkesTokenClient getTokenClient() { return new OrkesTokenClient(client); } + public OrkesSharedResourceClient getSharedResourceClient() { + return new OrkesSharedResourceClient(client); + } + + public OrkesWebhookClient getWebhookClient() { + return new OrkesWebhookClient(client); + } + public IntegrationClient getIntegrationClient() { return new OrkesIntegrationClient(client); } diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesSharedResourceClient.java b/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesSharedResourceClient.java new file mode 100644 index 00000000..f1e2cdf8 --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesSharedResourceClient.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Conductor Authors. + *

+ * 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.orkes.conductor.client.http; + +import java.util.List; + +import com.netflix.conductor.client.http.ConductorClient; + +import io.orkes.conductor.client.model.SharedResourceModel; + +/** + * Client for managing shared resources in Orkes Conductor. + * Provides functionality to share various types of resources (workflows, tasks, secrets, etc.) + * with other users, groups, or applications within the Conductor ecosystem. + */ +public class OrkesSharedResourceClient { + + private final SharedResource sharedResource; + + /** + * Constructs a new OrkesSharedResourceClient with the specified ConductorClient. + * + * @param client the ConductorClient to use for making HTTP requests to the server + */ + public OrkesSharedResourceClient(ConductorClient client) { + this.sharedResource = new SharedResource(client); + } + + /** + * Shares a resource with another user, group, or application. + * + * @param resourceType the type of resource to share (e.g., "WORKFLOW", "TASK", "SECRET") + * @param resourceName the name/identifier of the specific resource to share + * @param sharedWith the identifier of the entity to share with (user email, group name, or application ID) + */ + public void shareResource(String resourceType, String resourceName, String sharedWith) { + sharedResource.shareResource(resourceType, resourceName, sharedWith); + } + + /** + * Removes sharing access for a resource from a specific entity. + * + * @param resourceType the type of resource to stop sharing (e.g., "WORKFLOW", "TASK", "SECRET") + * @param resourceName the name/identifier of the specific resource to stop sharing + * @param sharedWith the identifier of the entity to remove sharing from (user email, group name, or application ID) + */ + public void removeSharingResource(String resourceType, String resourceName, String sharedWith) { + sharedResource.removeSharingResource(resourceType, resourceName, sharedWith); + } + + /** + * Retrieves a list of all resources that are currently shared by or with the current user/application. + * + * @return a list of SharedResourceModel objects representing all shared resources + */ + public List getSharedResources() { + return sharedResource.getSharedResources(); + } +} \ No newline at end of file diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesWebhookClient.java b/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesWebhookClient.java new file mode 100644 index 00000000..be555f3b --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/http/OrkesWebhookClient.java @@ -0,0 +1,166 @@ +/* + * Copyright 2022 Conductor Authors. + *

+ * 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.orkes.conductor.client.http; + +import java.util.List; + +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.ConductorClientResponse; + +import io.orkes.conductor.client.model.Tag; +import io.orkes.conductor.client.model.WebhookConfig; + +/** + * Client for managing webhook configurations in Orkes Conductor. + * Provides functionality to create, update, delete, and manage webhook configurations + * that can trigger workflows based on external events. + */ +public class OrkesWebhookClient { + + private final WebhookResource webhookResource; + + /** + * Constructs a new OrkesWebhookClient with the specified ConductorClient. + * + * @param client the ConductorClient to use for making HTTP requests to the server + */ + public OrkesWebhookClient(ConductorClient client) { + this.webhookResource = new WebhookResource(client); + } + + /** + * Create a new webhook configuration. + * + * @param webhookConfig the webhook configuration to create + * @return ConductorClientResponse containing the created webhook configuration + */ + public ConductorClientResponse createWebhook(WebhookConfig webhookConfig) { + return webhookResource.createWebhook(webhookConfig); + } + + /** + * Update an existing webhook configuration. + * + * @param id the webhook ID to update + * @param webhookConfig the updated webhook configuration + * @return ConductorClientResponse containing the updated webhook configuration + */ + public ConductorClientResponse updateWebhook(String id, WebhookConfig webhookConfig) { + return webhookResource.updateWebhook(id, webhookConfig); + } + + /** + * Delete a webhook configuration by ID. + * + * @param id the webhook ID to delete + * @return ConductorClientResponse for the delete operation + */ + public ConductorClientResponse deleteWebhook(String id) { + return webhookResource.deleteWebhook(id); + } + + /** + * Get a webhook configuration by ID. + * + * @param id the webhook ID to retrieve + * @return ConductorClientResponse containing the webhook configuration + */ + public ConductorClientResponse getWebhook(String id) { + return webhookResource.getWebhook(id); + } + + /** + * Get all webhook configurations. + * + * @return ConductorClientResponse containing a list of all webhook configurations + */ + public ConductorClientResponse> getAllWebhooks() { + return webhookResource.getAllWebhooks(); + } + + /** + * Get tags for a webhook by ID. + * + * @param id the webhook ID + * @return ConductorClientResponse containing a list of tags + */ + public ConductorClientResponse> getTagsForWebhook(String id) { + return webhookResource.getTagsForWebhook(id); + } + + /** + * Put tags for a webhook by ID. + * + * @param id the webhook ID + * @param tags the tags to set + * @return ConductorClientResponse for the put operation + */ + public ConductorClientResponse putTagsForWebhook(String id, List tags) { + return webhookResource.putTagsForWebhook(id, tags); + } + + /** + * Delete tags for a webhook by ID. + * + * @param id the webhook ID + * @param tags the tags to remove + * @return ConductorClientResponse for the delete operation + */ + public ConductorClientResponse deleteTagsForWebhook(String id, List tags) { + return webhookResource.deleteTagsForWebhook(id, tags); + } + + /** + * Convenience method to get a webhook configuration directly without wrapper. + * + * @param id the webhook ID to retrieve + * @return WebhookConfig object or null if failed + */ + public WebhookConfig getWebhookConfig(String id) { + try { + ConductorClientResponse response = getWebhook(id); + return response != null ? response.getData() : null; + } catch (Exception e) { + return null; + } + } + + /** + * Convenience method to get all webhook configurations directly without wrapper. + * + * @return List of WebhookConfig objects or empty list if failed + */ + public List getWebhookConfigs() { + try { + ConductorClientResponse> response = getAllWebhooks(); + return response != null && response.getData() != null ? response.getData() : List.of(); + } catch (Exception e) { + return List.of(); + } + } + + /** + * Check if a webhook exists by ID. + * + * @param id the webhook ID to check + * @return true if webhook exists, false otherwise + */ + public boolean webhookExists(String id) { + try { + ConductorClientResponse response = getWebhook(id); + return response != null && response.getData() != null; + } catch (Exception e) { + return false; + } + } +} diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/http/SharedResource.java b/orkes-client/src/main/java/io/orkes/conductor/client/http/SharedResource.java new file mode 100644 index 00000000..a592afaf --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/http/SharedResource.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Conductor Authors. + *

+ * 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.orkes.conductor.client.http; + +import java.util.List; + +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.ConductorClientRequest; +import com.netflix.conductor.client.http.ConductorClientResponse; + +import io.orkes.conductor.client.model.SharedResourceModel; + +import com.fasterxml.jackson.core.type.TypeReference; + +public class SharedResource { + private final ConductorClient client; + + public SharedResource(ConductorClient client) { + this.client = client; + } + + public void shareResource(String resourceType, String resourceName, String sharedWith) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(ConductorClientRequest.Method.POST) + .path("/share/shareResource") + .addQueryParam("resourceType", resourceType) + .addQueryParam("resourceName", resourceName) + .addQueryParam("sharedWith", sharedWith) + .build(); + + client.execute(request); + } + + public void removeSharingResource(String resourceType, String resourceName, String sharedWith) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(ConductorClientRequest.Method.DELETE) + .path("/share/removeSharingResource") + .addQueryParam("resourceType", resourceType) + .addQueryParam("resourceName", resourceName) + .addQueryParam("sharedWith", sharedWith) + .build(); + + client.execute(request); + } + + public List getSharedResources() { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(ConductorClientRequest.Method.GET) + .path("/share/getSharedResources") + .build(); + + ConductorClientResponse> resp = client.execute(request, new TypeReference<>() { + }); + + return resp.getData(); + } +} diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/http/WebhookResource.java b/orkes-client/src/main/java/io/orkes/conductor/client/http/WebhookResource.java new file mode 100644 index 00000000..27cb5ab7 --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/http/WebhookResource.java @@ -0,0 +1,172 @@ +/* + * Copyright 2022 Conductor Authors. + *

+ * 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.orkes.conductor.client.http; + +import java.util.List; + +import com.netflix.conductor.client.http.ConductorClient; +import com.netflix.conductor.client.http.ConductorClientRequest; +import com.netflix.conductor.client.http.ConductorClientRequest.Method; +import com.netflix.conductor.client.http.ConductorClientResponse; + +import io.orkes.conductor.client.model.Tag; +import io.orkes.conductor.client.model.WebhookConfig; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * Resource class for webhook configuration operations. + * Provides HTTP client methods to interact with webhook endpoints. + */ +public class WebhookResource { + + private final ConductorClient client; + + public WebhookResource(ConductorClient client) { + this.client = client; + } + + /** + * Create a new webhook configuration. + * + * @param webhookConfig the webhook configuration to create + * @return ConductorClientResponse containing the created webhook configuration + */ + public ConductorClientResponse createWebhook(WebhookConfig webhookConfig) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.POST) + .path("/metadata/webhook") + .body(webhookConfig) + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Update an existing webhook configuration. + * + * @param id the webhook ID to update + * @param webhookConfig the updated webhook configuration + * @return ConductorClientResponse containing the updated webhook configuration + */ + public ConductorClientResponse updateWebhook(String id, WebhookConfig webhookConfig) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.PUT) + .path("/metadata/webhook/" + id) + .body(webhookConfig) + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Delete a webhook configuration by ID. + * + * @param id the webhook ID to delete + * @return ConductorClientResponse for the delete operation + */ + public ConductorClientResponse deleteWebhook(String id) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.DELETE) + .path("/metadata/webhook/" + id) + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Get a webhook configuration by ID. + * + * @param id the webhook ID to retrieve + * @return ConductorClientResponse containing the webhook configuration + */ + public ConductorClientResponse getWebhook(String id) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.GET) + .path("/metadata/webhook/" + id) + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Get all webhook configurations. + * + * @return ConductorClientResponse containing a list of all webhook configurations + */ + public ConductorClientResponse> getAllWebhooks() { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.GET) + .path("/metadata/webhook") + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Get tags for a webhook by ID. + * + * @param id the webhook ID + * @return ConductorClientResponse containing a list of tags + */ + public ConductorClientResponse> getTagsForWebhook(String id) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.GET) + .path("/metadata/webhook/" + id + "/tags") + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Put tags for a webhook by ID. + * + * @param id the webhook ID + * @param tags the tags to set + * @return ConductorClientResponse for the put operation + */ + public ConductorClientResponse putTagsForWebhook(String id, List tags) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.PUT) + .path("/metadata/webhook/" + id + "/tags") + .body(tags) + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } + + /** + * Delete tags for a webhook by ID. + * + * @param id the webhook ID + * @param tags the tags to remove + * @return ConductorClientResponse for the delete operation + */ + public ConductorClientResponse deleteTagsForWebhook(String id, List tags) { + ConductorClientRequest request = ConductorClientRequest.builder() + .method(Method.DELETE) + .path("/metadata/webhook/" + id + "/tags") + .body(tags) + .build(); + + return client.execute(request, new TypeReference<>() { + }); + } +} \ No newline at end of file diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/model/SharedResourceModel.java b/orkes-client/src/main/java/io/orkes/conductor/client/model/SharedResourceModel.java new file mode 100644 index 00000000..12b5a191 --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/model/SharedResourceModel.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Conductor Authors. + *

+ * 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.orkes.conductor.client.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class SharedResourceModel { + + String resourceType; + String resourceName; + String sharedBy; + String sharedWith; +} diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/model/Tag.java b/orkes-client/src/main/java/io/orkes/conductor/client/model/Tag.java index e681f9d3..80876a3d 100644 --- a/orkes-client/src/main/java/io/orkes/conductor/client/model/Tag.java +++ b/orkes-client/src/main/java/io/orkes/conductor/client/model/Tag.java @@ -17,9 +17,9 @@ @Data @NoArgsConstructor @Builder -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PUBLIC) public class Tag { - private String key; - private String value; + public String key; + public String value; } diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/model/WebhookConfig.java b/orkes-client/src/main/java/io/orkes/conductor/client/model/WebhookConfig.java new file mode 100644 index 00000000..806b7c6a --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/model/WebhookConfig.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 Conductor Authors. + *

+ * 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.orkes.conductor.client.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@Builder +@RequiredArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class WebhookConfig { + + private String name; + + private String id; + + private Map receiverWorkflowNamesToVersions; + + private Map workflowsToStart; + + private boolean urlVerified; + + private String sourcePlatform; + + private Verifier verifier; + + private Map headers; + + private String headerKey; // Required for signature_based verifier. + + private String secretKey; + + private String secretValue; + + private String createdBy; + private List tags; + + private List webhookExecutionHistory;//TODO Remove this + + private String expression; + private String evaluatorType; + + public enum Verifier { + SLACK_BASED, + SIGNATURE_BASED, + HEADER_BASED, + STRIPE, + TWITTER, + HMAC_BASED, + SENDGRID + } + + @JsonIgnore + public List getWorkflowNames() { + return receiverWorkflowNamesToVersions == null ? List.of() : new ArrayList<>(receiverWorkflowNamesToVersions.keySet()); + } + + public void accept(WebhookConfigVisitor visitor) { + visitor.visit(this); + } + + public interface WebhookConfigVisitor { + default void visit(WebhookConfig webhookConfig) { + } + } + +} diff --git a/orkes-client/src/main/java/io/orkes/conductor/client/model/WebhookExecutionHistory.java b/orkes-client/src/main/java/io/orkes/conductor/client/model/WebhookExecutionHistory.java new file mode 100644 index 00000000..3722c330 --- /dev/null +++ b/orkes-client/src/main/java/io/orkes/conductor/client/model/WebhookExecutionHistory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 Conductor Authors. + *

+ * 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.orkes.conductor.client.model; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Builder +@Data +@AllArgsConstructor +@RequiredArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class WebhookExecutionHistory { + + private String eventId; + private boolean matched; + private Set workflowIds; + private String payload; + private long timeStamp; +} \ No newline at end of file diff --git a/tests/src/test/java/io/orkes/conductor/client/http/WebhookClientE2ETest.java b/tests/src/test/java/io/orkes/conductor/client/http/WebhookClientE2ETest.java new file mode 100644 index 00000000..63271844 --- /dev/null +++ b/tests/src/test/java/io/orkes/conductor/client/http/WebhookClientE2ETest.java @@ -0,0 +1,587 @@ +/* + * Copyright 2025 Conductor Authors. + *

+ * 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.orkes.conductor.client.http; + +import com.google.common.util.concurrent.Uninterruptibles; +import com.netflix.conductor.client.http.ConductorClientResponse; +import com.netflix.conductor.common.metadata.tasks.TaskDef; +import com.netflix.conductor.common.metadata.workflow.WorkflowDef; +import com.netflix.conductor.common.metadata.workflow.WorkflowTask; +import io.orkes.conductor.client.OrkesClients; +import io.orkes.conductor.client.model.Tag; +import io.orkes.conductor.client.model.WebhookConfig; +import io.orkes.conductor.client.util.ClientTestUtil; +import io.orkes.conductor.client.util.TestUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * E2E tests for OrkesWebhookClient demonstrating webhook configuration management + */ +public class WebhookClientE2ETest { + + private static OrkesWebhookClient webhookClient; + private static OrkesClients orkesClients; + private String testWebhookId; + + @BeforeAll + public static void setup() { + orkesClients = ClientTestUtil.getOrkesClients(); + webhookClient = orkesClients.getWebhookClient(); + } + + @AfterEach + public void cleanup() { + // Clean up any created webhooks + if (testWebhookId != null) { + try { + webhookClient.deleteWebhook(testWebhookId); + } catch (Exception e) { + // Ignore cleanup errors + } + testWebhookId = null; + } + } + + @Test + public void testCreateWebhook() { + // Arrange + String webhookName = "e2e_test_webhook_" + System.currentTimeMillis(); + + // Create a simple workflow to trigger from webhook + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .workflowsToStart(Map.of(workflowName, 1)) + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .secretValue("slack_secret_value") + .build(); + + // Act + ConductorClientResponse response = webhookClient.createWebhook(webhookConfig); + + // Assert + assertNotNull(response, "Response should not be null"); + assertNotNull(response.getData(), "Response data should not be null"); + + WebhookConfig createdWebhook = response.getData(); + assertNotNull(createdWebhook.getId(), "Webhook ID should be generated"); + assertEquals(webhookName, createdWebhook.getName(), "Webhook name should match"); + assertEquals("SLACK", createdWebhook.getSourcePlatform(), "Source platform should match"); + assertEquals(WebhookConfig.Verifier.SLACK_BASED, createdWebhook.getVerifier(), "Verifier should match"); + assertNotNull(createdWebhook.getReceiverWorkflowNamesToVersions(), "Receiver workflows should not be null"); + assertTrue(createdWebhook.getReceiverWorkflowNamesToVersions().containsKey(workflowName), + "Should contain target workflow"); + + // Store for cleanup + testWebhookId = createdWebhook.getId(); + + System.out.println("✅ Successfully created webhook with ID: " + testWebhookId); + } + + @Test + public void testCreateWebhookWithSignatureVerifier() { + // Arrange + String webhookName = "e2e_signature_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("GITHUB") + .verifier(WebhookConfig.Verifier.SIGNATURE_BASED) + .headerKey("X-Hub-Signature-256") + .secretKey("webhook_secret") + .secretValue("my_secret_value") + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + // Act + ConductorClientResponse response = webhookClient.createWebhook(webhookConfig); + + // Assert + assertNotNull(response); + assertNotNull(response.getData()); + + WebhookConfig createdWebhook = response.getData(); + assertNotNull(createdWebhook.getId()); + assertEquals(webhookName, createdWebhook.getName()); + assertEquals("GITHUB", createdWebhook.getSourcePlatform()); + assertEquals(WebhookConfig.Verifier.SIGNATURE_BASED, createdWebhook.getVerifier()); + assertEquals("X-Hub-Signature-256", createdWebhook.getHeaderKey()); + assertEquals("webhook_secret", createdWebhook.getSecretKey()); + + // Store for cleanup + testWebhookId = createdWebhook.getId(); + + System.out.println("✅ Successfully created signature-based webhook with ID: " + testWebhookId); + } + + @Test + public void testCreateWebhookWithHeaderBasedVerifier() { + // Arrange + String webhookName = "e2e_header_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("CUSTOM") + .verifier(WebhookConfig.Verifier.HEADER_BASED) + .headerKey("X-Custom-Token") + .secretValue("custom_token_123") + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .headers(Map.of( + "Content-Type", "application/json", + "X-Custom-Header", "webhook-integration" + )) + .build(); + + // Act + ConductorClientResponse response = webhookClient.createWebhook(webhookConfig); + + // Assert + assertNotNull(response); + assertNotNull(response.getData()); + + WebhookConfig createdWebhook = response.getData(); + assertNotNull(createdWebhook.getId()); + assertEquals(webhookName, createdWebhook.getName()); + assertEquals("CUSTOM", createdWebhook.getSourcePlatform()); + assertEquals(WebhookConfig.Verifier.HEADER_BASED, createdWebhook.getVerifier()); + assertEquals("X-Custom-Token", createdWebhook.getHeaderKey()); + assertEquals("custom_token_123", createdWebhook.getSecretValue()); + + // Store for cleanup + testWebhookId = createdWebhook.getId(); + + System.out.println("✅ Successfully created header-based webhook with ID: " + testWebhookId); + } + + @Test + public void testCreateWebhookWithMultipleWorkflows() { + // Arrange + String webhookName = "e2e_multi_webhook_" + System.currentTimeMillis(); + String workflow1Name = "e2e_webhook_target1_" + System.currentTimeMillis(); + String workflow2Name = "e2e_webhook_target2_" + System.currentTimeMillis(); + + createTestWorkflow(workflow1Name); + createTestWorkflow(workflow2Name); + + Map workflowsToVersions = new HashMap<>(); + workflowsToVersions.put(workflow1Name, 1); + workflowsToVersions.put(workflow2Name, 1); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("STRIPE") + .verifier(WebhookConfig.Verifier.STRIPE) + .secretValue("whsec_test_secret") + .receiverWorkflowNamesToVersions(workflowsToVersions) + .build(); + + // Act + ConductorClientResponse response = webhookClient.createWebhook(webhookConfig); + + // Assert + assertNotNull(response); + assertNotNull(response.getData()); + + WebhookConfig createdWebhook = response.getData(); + assertNotNull(createdWebhook.getId()); + assertEquals(webhookName, createdWebhook.getName()); + assertEquals("STRIPE", createdWebhook.getSourcePlatform()); + assertEquals(WebhookConfig.Verifier.STRIPE, createdWebhook.getVerifier()); + + Map receiverWorkflows = createdWebhook.getReceiverWorkflowNamesToVersions(); + assertNotNull(receiverWorkflows); + assertEquals(2, receiverWorkflows.size()); + assertTrue(receiverWorkflows.containsKey(workflow1Name)); + assertTrue(receiverWorkflows.containsKey(workflow2Name)); + + // Store for cleanup + testWebhookId = createdWebhook.getId(); + + System.out.println("✅ Successfully created multi-workflow webhook with ID: " + testWebhookId); + } + + @Test + public void testCreateWebhookValidation() { + // Test creating webhook with missing required fields + WebhookConfig invalidConfig = WebhookConfig.builder() + .name("") // Empty name should fail + .build(); + + try { + ConductorClientResponse response = webhookClient.createWebhook(invalidConfig); + // If this doesn't throw an exception, check the response for errors + if (response != null && response.getData() != null) { + fail("Expected webhook creation to fail with invalid config"); + } + } catch (Exception e) { + // Expected - webhook creation should fail with invalid config + System.out.println("✅ Webhook creation properly rejected invalid config: " + e.getMessage()); + } + } + + @Test + public void testGetWebhook() { + // Arrange - First create a webhook + String webhookName = "e2e_get_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse createResponse = webhookClient.createWebhook(webhookConfig); + assertNotNull(createResponse.getData()); + testWebhookId = createResponse.getData().getId(); + + // Act + ConductorClientResponse response = webhookClient.getWebhook(testWebhookId); + + // Assert + assertNotNull(response); + assertNotNull(response.getData()); + + WebhookConfig retrievedWebhook = response.getData(); + assertEquals(testWebhookId, retrievedWebhook.getId()); + assertEquals(webhookName, retrievedWebhook.getName()); + assertEquals("SLACK", retrievedWebhook.getSourcePlatform()); + assertEquals(WebhookConfig.Verifier.SLACK_BASED, retrievedWebhook.getVerifier()); + + System.out.println("✅ Successfully retrieved webhook: " + testWebhookId); + } + + @Test + public void testUpdateWebhook() { + // Arrange - First create a webhook + String webhookName = "e2e_update_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse createResponse = webhookClient.createWebhook(webhookConfig); + assertNotNull(createResponse.getData()); + testWebhookId = createResponse.getData().getId(); + + // Act - Update the webhook + WebhookConfig updatedConfig = WebhookConfig.builder() + .name(webhookName + "_updated") + .sourcePlatform("GITHUB") + .verifier(WebhookConfig.Verifier.SIGNATURE_BASED) + .headerKey("X-Hub-Signature-256") + .secretKey("updated_secret") + .secretValue("updated_secret_value") + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse updateResponse = webhookClient.updateWebhook(testWebhookId, updatedConfig); + + // Assert + assertNotNull(updateResponse); + assertNotNull(updateResponse.getData()); + + WebhookConfig updated = updateResponse.getData(); + assertEquals(testWebhookId, updated.getId()); + assertEquals(webhookName + "_updated", updated.getName()); + assertEquals("GITHUB", updated.getSourcePlatform()); + assertEquals(WebhookConfig.Verifier.SIGNATURE_BASED, updated.getVerifier()); + assertEquals("X-Hub-Signature-256", updated.getHeaderKey()); + assertEquals("updated_secret", updated.getSecretKey()); + + System.out.println("✅ Successfully updated webhook: " + testWebhookId); + } + + @Test + public void testGetAllWebhooks() { + // Arrange - Create a test webhook + String webhookName = "e2e_list_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse createResponse = webhookClient.createWebhook(webhookConfig); + assertNotNull(createResponse.getData()); + testWebhookId = createResponse.getData().getId(); + + // Act + ConductorClientResponse> response = webhookClient.getAllWebhooks(); + + // Assert + assertNotNull(response); + assertNotNull(response.getData()); + + List webhooks = response.getData(); + assertTrue(webhooks.size() > 0, "Should have at least one webhook"); + + // Verify our test webhook is in the list + boolean found = webhooks.stream() + .anyMatch(wh -> testWebhookId.equals(wh.getId())); + assertTrue(found, "Should find our test webhook in the list"); + + System.out.println("✅ Successfully retrieved " + webhooks.size() + " webhooks"); + } + + @Test + public void testWebhookTagOperations() { + // Arrange - Create a webhook + String webhookName = "e2e_tag_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse createResponse = webhookClient.createWebhook(webhookConfig); + assertNotNull(createResponse.getData()); + testWebhookId = createResponse.getData().getId(); + + // Create tags + Tag tag1 = new Tag(); + tag1.setKey("environment"); + tag1.setValue("testing"); + + Tag tag2 = new Tag(); + tag2.setKey("team"); + tag2.setValue("e2e"); + + List tags = List.of(tag1, tag2); + + // Act - Put tags + ConductorClientResponse putResponse = webhookClient.putTagsForWebhook(testWebhookId, tags); + assertNotNull(putResponse); + + // Act - Get tags + ConductorClientResponse> getResponse = webhookClient.getTagsForWebhook(testWebhookId); + assertNotNull(getResponse); + assertNotNull(getResponse.getData()); + + List retrievedTags = getResponse.getData(); + assertEquals(2, retrievedTags.size()); + + // Verify tags + boolean hasEnvTag = retrievedTags.stream() + .anyMatch(tag -> "environment".equals(tag.getKey()) && "testing".equals(tag.getValue())); + boolean hasTeamTag = retrievedTags.stream() + .anyMatch(tag -> "team".equals(tag.getKey()) && "e2e".equals(tag.getValue())); + + assertTrue(hasEnvTag, "Should have environment tag"); + assertTrue(hasTeamTag, "Should have team tag"); + + System.out.println("✅ Successfully tested webhook tag operations"); + } + + @Test + public void testDeleteWebhook() { + // Arrange - Create a webhook + String webhookName = "e2e_delete_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse createResponse = webhookClient.createWebhook(webhookConfig); + assertNotNull(createResponse.getData()); + String webhookId = createResponse.getData().getId(); + + // Verify webhook exists + ConductorClientResponse getResponse = webhookClient.getWebhook(webhookId); + assertNotNull(getResponse.getData()); + + // Act - Delete webhook + ConductorClientResponse deleteResponse = webhookClient.deleteWebhook(webhookId); + assertNotNull(deleteResponse); + + // Assert - Verify webhook is deleted + try { + ConductorClientResponse getAfterDeleteResponse = webhookClient.getWebhook(webhookId); + // If we get here, the webhook might still exist (depending on API behavior) + // or the API might return an empty response + if (getAfterDeleteResponse != null && getAfterDeleteResponse.getData() != null) { + fail("Webhook should have been deleted"); + } + } catch (Exception e) { + // Expected - webhook should not be found after deletion + System.out.println("✅ Webhook properly deleted, get request failed as expected: " + e.getMessage()); + } + + // Clear testWebhookId since it's already deleted + testWebhookId = null; + + System.out.println("✅ Successfully deleted webhook: " + webhookId); + } + + @Test + public void testWebhookConvenienceMethods() { + // Arrange - Create a webhook + String webhookName = "e2e_convenience_webhook_" + System.currentTimeMillis(); + String workflowName = "e2e_webhook_target_" + System.currentTimeMillis(); + createTestWorkflow(workflowName); + + WebhookConfig webhookConfig = WebhookConfig.builder() + .name(webhookName) + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .receiverWorkflowNamesToVersions(Map.of(workflowName, 1)) + .build(); + + ConductorClientResponse createResponse = webhookClient.createWebhook(webhookConfig); + assertNotNull(createResponse.getData()); + testWebhookId = createResponse.getData().getId(); + + // Test convenience methods + + // Test getWebhookConfig + WebhookConfig retrievedConfig = webhookClient.getWebhookConfig(testWebhookId); + assertNotNull(retrievedConfig); + assertEquals(testWebhookId, retrievedConfig.getId()); + assertEquals(webhookName, retrievedConfig.getName()); + + // Test getWebhookConfigs + List allConfigs = webhookClient.getWebhookConfigs(); + assertNotNull(allConfigs); + assertTrue(allConfigs.size() > 0); + + boolean found = allConfigs.stream() + .anyMatch(wh -> testWebhookId.equals(wh.getId())); + assertTrue(found, "Should find our test webhook in the list"); + + // Test webhookExists + boolean exists = webhookClient.webhookExists(testWebhookId); + assertTrue(exists, "Webhook should exist"); + + boolean nonExistentExists = webhookClient.webhookExists("non-existent-id"); + assertFalse(nonExistentExists, "Non-existent webhook should not exist"); + + System.out.println("✅ Successfully tested convenience methods"); + } + + @Test + public void testWebhookErrorHandling() { + // Test error scenarios + + // Test getting non-existent webhook + try { + ConductorClientResponse response = webhookClient.getWebhook("non-existent-webhook-id"); + if (response != null && response.getData() != null) { + fail("Should not be able to get non-existent webhook"); + } + } catch (Exception e) { + System.out.println("✅ Properly handled getting non-existent webhook: " + e.getMessage()); + } + + // Test updating non-existent webhook + try { + WebhookConfig updateConfig = WebhookConfig.builder() + .name("update_test") + .sourcePlatform("SLACK") + .verifier(WebhookConfig.Verifier.SLACK_BASED) + .build(); + + ConductorClientResponse response = webhookClient.updateWebhook("non-existent-webhook-id", updateConfig); + if (response != null && response.getData() != null) { + fail("Should not be able to update non-existent webhook"); + } + } catch (Exception e) { + System.out.println("✅ Properly handled updating non-existent webhook: " + e.getMessage()); + } + + // Test deleting non-existent webhook + try { + ConductorClientResponse response = webhookClient.deleteWebhook("non-existent-webhook-id"); + // Some APIs might return success even for non-existent resources + System.out.println("✅ Delete non-existent webhook handled gracefully"); + } catch (Exception e) { + System.out.println("✅ Properly handled deleting non-existent webhook: " + e.getMessage()); + } + } + + /** + * Helper method to create a simple test workflow for webhook targets + */ + private void createTestWorkflow(String workflowName) { + try { + // Create a simple task definition + String taskName = workflowName + "_task"; + TaskDef taskDef = new TaskDef(taskName); + taskDef.setRetryCount(1); + taskDef.setOwnerEmail("test@orkes.io"); + + TestUtil.retryMethodCall(() -> + orkesClients.getMetadataClient().registerTaskDefs(List.of(taskDef))); + + // Create workflow definition + WorkflowDef workflowDef = new WorkflowDef(); + workflowDef.setName(workflowName); + workflowDef.setVersion(1); + workflowDef.setOwnerEmail("test@orkes.io"); + + // Add a simple task to the workflow + WorkflowTask workflowTask = new WorkflowTask(); + workflowTask.setName(taskName); + workflowTask.setTaskReferenceName(taskName + "_ref"); + workflowTask.setType("SIMPLE"); + + workflowDef.setTasks(List.of(workflowTask)); + + TestUtil.retryMethodCall(() -> + orkesClients.getMetadataClient().registerWorkflowDef(workflowDef)); + + // Wait a bit for registration to complete + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + + } catch (Exception e) { + System.err.println("Failed to create test workflow: " + workflowName + " - " + e.getMessage()); + // Don't fail the test if workflow creation fails, as this is just setup + } + } +}