差異處
這裏顯示兩個版本的差異處。
Both sides previous revision 前次修改 下次修改 | 前次修改 | ||
java:wiremock:record-events-from-webhook [2021/08/14 20:33] tony [Problem] |
java:wiremock:record-events-from-webhook [2023/06/25 09:48] (目前版本) |
||
---|---|---|---|
行 1: | 行 1: | ||
{{tag>wiremock}} | {{tag>wiremock}} | ||
- | ====== WireMock - Record events from webhook ====== | + | ====== WireMock - Record Webhook Events ====== |
===== Problem ===== | ===== Problem ===== | ||
- | 我有一隻待測程式(SUT)會相依於外部服務(External Service)的WebHook機制,操作流程如下: | + | 我有一隻待測程式(SUT)會相依於外部服務(External Service)的webhook 機制,操作流程如下: |
- | - SUT會對外部服務特定事件註冊WebHook位置。 | + | - SUT會對外部服務特定事件註冊webhook 位置。 |
- 當外部服務發生特定事件時,會發送event到SUT所註冊的位置。 | - 當外部服務發生特定事件時,會發送event到SUT所註冊的位置。 | ||
- | - SUT對外部服務反註冊WebHook位置。 | + | - SUT對外部服務反註冊webhook 位置。 |
{{:java:wiremock:wc_test_with_webhook.png|}}\\ | {{:java:wiremock:wc_test_with_webhook.png|}}\\ | ||
- | WireMock有提供[[http://wiremock.org/docs/webhooks-and-callbacks/|webhook]]的extension,讓你可以自行編寫程式或mappingfile腳本去做到在"特定操作後,發送webhook操作",但它並不支援Recording的功能。本篇文章,主要分享如何讓WireMock擁有錄製webhook的功能。 | + | WireMock有提供[[http://wiremock.org/docs/webhooks-and-callbacks/|webhook]]的extension,讓你可以自行編寫程式或mappingfile腳本去做到在"特定操作後,發送webhook操作",但它並不支援Recording的功能。本篇文章,主要分享如何讓WireMock擁有錄製webhook的功能。\\ |
+ | \\ | ||
+ | Note. 範例程式碼[[https://github.com/frank007love/wiremock-redfish|link]]可自行取用。這是依照我需求撰寫的,只要弄懂方法,就可以依照你需求自行調整。\\ | ||
+ | Limitation. 如果webhook的反註冊是基於回應內容的URL,目前還沒想到辦法可以根據SUT的IP與情境去做對應的內容回應。 | ||
===== How to? ===== | ===== How to? ===== | ||
- | + | ==== Pre-notice ==== | |
+ | 由於Solution與整個Record流程息息相關,我必須先說明我所採用的流程。在我的故事中,有四隻程式: | ||
+ | - Record Program: 負責控制整個WireMock腳本記錄流程的程式。 | ||
+ | - SUT: 待測程式,透過REST API提供與External Service相關功能的服務。 | ||
+ | - WireMock: 負責記錄SUT與External Service互動的過程並產生腳本,提供REST API讓Record Program控制Record流程。 | ||
+ | - External Service: SUT所互動的外部服務。 | ||
+ | {{:java:wiremock:ws_recording_procedure.png|}}\\ | ||
+ | 詳細流程如下: | ||
+ | - Record Program請WireMock開始記錄SUT與External Service互動過程。 | ||
+ | - Record Program對SUT觸發command,並開始poll command完成結果。 | ||
+ | - SUT對WireMock做操作。 | ||
+ | - WireMock將操作轉送給External Service。 | ||
+ | - Record Program發現command執行完成。 | ||
+ | - Record Program請WireMock停止記錄。 | ||
+ | - WireMock產生SUT與External Service互動過程的執行腳本(mappings)。 | ||
+ | - Record Program針對腳本命名。 | ||
+ | ==== Thinking & Analysis ==== | ||
+ | 針對Pre-notice的流程,我有幾個問題需要解決: | ||
+ | - WireMock如何捕捉External Service發送給SUT的callback event? | ||
+ | - 補捉之後如何將這些events放置到腳本的Post中? | ||
+ | - 放置到腳本中後,如何在適當的時間由WireMock返回給SUT? | ||
+ | 首先討論第三個問題,如果有辦法產生如下方的腳本內容,應該可以解決問題。讓我先針對內容做說明: | ||
+ | - webhook callback的內容會被放在postServerActions中,會在WireMock回應SUT後觸發這些動作。 | ||
+ | - 啟用webhook delay的機制,讓它根據時間延後發送;而這個時間其實是SUT註冊callback URL到WireMock收到callback event的duration。 | ||
+ | {{:java:wiremock:ws_recording_webhook_json.png|}}\\ | ||
+ | 接著回到第一個問題,我採用的方式是將註冊的callback url改到WireMock身上,流程如下圖。另外會把原本的callback url與註冊時間記錄下來,以用來處理收到callback event後的轉送與計算問題三個delay時間:\\ | ||
+ | {{:java:wiremock:ws_refine_webhook_callback_url.png|}}\\ | ||
+ | 延續上圖流程,當收到callback event後,就可以透過上圖記錄的原始callback url,送回給SUT並記錄收到event的時間:\\ | ||
+ | {{:java:wiremock:ws_forward_callback_events.png|}}\\ | ||
+ | 在還不考慮技術與實作細節的情況下,這solution似乎是可行的。讓我們繼續看下去。\\ | ||
+ | \\ | ||
+ | Note. 第一個問題,我曾想過2個解決方法。第一個方法是同時註冊SUT與WireMock的callback URL給External Service,讓External Service同時發event給兩者;但由於無法確定event收到的先後順序而影響到步驟6的執行,所以捨棄這個方法。 | ||
+ | ==== Design & Implement ==== | ||
+ | 要達成前半段分析的流程,這次我們有兩個WireMock extension必須要實作: | ||
+ | - RequestFilter: 處理SUT與External Serviec的請求。 | ||
+ | - StubMappingTransformer: 根據收集到的資訊產生我們要的腳本。 | ||
+ | === RequestFilter === | ||
+ | {{:java:wiremock:ws_externalservice_webhook_request_filter.png|}}\\ | ||
+ | 我實作的ExternalServiceWebHookRequestFilter主要會處理這幾件事情: | ||
+ | - 監聽WireMock recording start與stop,用以控制記錄的資料。 | ||
+ | - 監聽SUT的webhook註冊請求,記錄並將callback URL改為WireMock位置。 | ||
+ | - 監聽External Service的callback event,記錄並轉送回SUT。 | ||
+ | 而SUT所註冊request body重點內容如下,Destionation為callback URL,Context為註冊識別號: | ||
+ | <code json> | ||
+ | { | ||
+ | "Destination": "http://10.146.16.150:18556/callback", | ||
+ | "Context": "tony_test", | ||
+ | .. | ||
+ | } | ||
+ | </code> | ||
+ | 當收到SUT的webhook註冊請求後,會使用EventRecorder將context與destination記錄下來,EventRecorder會自行標記時間;接著將destination的port改為WireMock的HTTP port,並調整請求的Body內容讓WireMock繼續對External Service做註冊動作: | ||
+ | <code java> | ||
+ | private RequestFilterAction handleSubscribeRequest(Request request, | ||
+ | SubscribeRequestBody eventRequestBody) { | ||
+ | EventRecorder.getInstance().markSubscription(eventRequestBody.getContext(), | ||
+ | eventRequestBody.getDestination()); | ||
+ | |||
+ | Request wrapRequest = RequestWrapper.create() | ||
+ | .transformBody(body->new Body(body.asString().replaceAll(":\\d+", ":"+WIREMOCK_HTTP_PORT))) | ||
+ | .wrap(request); | ||
+ | |||
+ | return RequestFilterAction.continueWith(wrapRequest); | ||
+ | } | ||
+ | </code> | ||
+ | 在處理完註冊動作後,WireMock就會開始收到callback event。收到這些event後,首先會透過EventRecorder依照context記錄下來,EventRecorder會標記註冊後要delay多少時間才發送給SUT;接著會透過EventRecorder取出原本的callback destination並轉發回去給SUT;最後就是讓這請求到這裡結束並回應給External Service OK,少了這步驟會讓WireMock繼續處理這訊息而導致External Service產生非預期結果: | ||
+ | <code java> | ||
+ | private RequestFilterAction handlePostRedfishEventsRequest(Request request, CallBackEvent eventRequestBody) { | ||
+ | String context = eventRequestBody.getEvents().get(0).getContext(); | ||
+ | EventRecorder.getInstance().addEvent(context, request.getBodyAsString()); | ||
+ | |||
+ | Optional<String> destinationOpt = EventRecorder.getInstance().getDestination(context); | ||
+ | destinationOpt.ifPresent(destination->publishEventToSource(destination, request.getBodyAsString())); | ||
+ | return RequestFilterAction.stopWith(ResponseDefinitionBuilder.okForEmptyJson().build()); | ||
+ | } | ||
+ | </code> | ||
+ | === StubMappingTransformer === | ||
+ | 有了RequestFilter所記錄下來的資料後,StubMappingTransformer在WireMock收到stop後,就會開始處理腳本的產生。最重要的處理在下圖的步驟二,包含註冊request內容的調整與webhook postServerAction的產生:\\ | ||
+ | {{:java:wiremock:ws_generate_webhook_operation_mappings.png|}}\\ | ||
+ | 首先你要先確定,要處理的StubMapping是屬於SUT註冊webhook的請求,我採用的方式是判斷Request的URL: | ||
+ | <code java> | ||
+ | @Override | ||
+ | public StubMapping transform(StubMapping stubMapping, FileSource files, Parameters parameters) { | ||
+ | RequestPattern requestPattern = stubMapping.getRequest(); | ||
+ | if(notSupportedRequest(requestPattern)) | ||
+ | return stubMapping; | ||
+ | |||
+ | removeDestination(requestPattern); | ||
+ | applyWebHookEvents(stubMapping); | ||
+ | |||
+ | return stubMapping; | ||
+ | } | ||
+ | </code> | ||
+ | 接著由於註冊的request body中,destination包含著SUT的IP,而SUT會隨著執行環境而改變,這將導致錄製出來的腳本無法萬用。因此我必須將腳本內RequestBody的Destination移除,如果要考慮情境比較複雜可以直接套Library去移除: (註冊的URL就不需要處理了,因為當下的執行前後文是由你的腳本所促成,URL絕對會相同) | ||
+ | <code java> | ||
+ | private void removeDestination(RequestPattern requestPattern) { | ||
+ | EqualToJsonPattern bodyPattern = (EqualToJsonPattern)requestPattern.getBodyPatterns().get(0); | ||
+ | String removedDestBodyString = bodyPattern.getEqualToJson().replaceAll("\"Destination\":.*,", ""); | ||
+ | EqualToJsonPattern newBodyPattern = new EqualToJsonPattern(removedDestBodyString, true, true); | ||
+ | requestPattern.getBodyPatterns().clear(); | ||
+ | requestPattern.getBodyPatterns().add(newBodyPattern); | ||
+ | } | ||
+ | </code> | ||
+ | 最後就是重頭戲:\\ | ||
+ | {{:java:wiremock:ws_externalservice_stub_mapping_transformer.png|}}\\ | ||
+ | 首先根據context去取得所有對應的callback events,再將這些events轉為webhook的PostServeActionDefinition。Parameters需注意的有兩點,第一個是url的設定,這還是因為destination不會是固定的,因此必須根據當下請求的destination去做回應,在這使用了jsonPath寫法給大家參考;第二個則是delay的type要設定為fixed,代表是固定多久時間後要觸發這個callback,milliseconds則直接從EventRecorder記錄的PostEvent拿了: | ||
+ | <code java> | ||
+ | private void applyWebHookEvents(StubMapping stubMapping) { | ||
+ | String context = parseContext(stubMapping); | ||
+ | if(context == null) return; | ||
+ | List<Event> postEvents = EventRecorder.getInstance().getEvents(context); | ||
+ | List<PostServeActionDefinition> postServerActionDefinitions = postEvents.stream() | ||
+ | .map(this::toParameters) | ||
+ | .map(parameters->new PostServeActionDefinition("webhook", parameters)) | ||
+ | .collect(Collectors.toList()); | ||
+ | |||
+ | stubMapping.setPostServeActions(postServerActionDefinitions); | ||
+ | } | ||
+ | |||
+ | private Parameters toParameters(Event postEvent) { | ||
+ | Parameters parameters = new Parameters(); | ||
+ | parameters.put("method", "POST"); | ||
+ | parameters.put("url", "{{jsonPath originalRequest.body '$.Destination'}}"); | ||
+ | parameters.put("body", postEvent.getRawData()); | ||
+ | |||
+ | Map<String, Object> delay = new HashMap<>(); | ||
+ | delay.put("type", "fixed"); | ||
+ | delay.put("milliseconds", postEvent.getDelay()); | ||
+ | parameters.put("delay", delay); | ||
+ | |||
+ | return parameters; | ||
+ | } | ||
+ | </code> | ||
+ | 完成以上實作並錄製腳本所產生出來的東西,其實我已經在Thinking & Analysis的部分show給大家看過了。 | ||
===== ===== | ===== ===== | ||
---- | ---- | ||
\\ | \\ | ||
~~DISQUS~~ | ~~DISQUS~~ |