差異處

這裏顯示兩個版本的差異處。

連向這個比對檢視

Both sides previous revision 前次修改
下次修改
前次修改
java:wiremock:record-events-from-webhook [2021/08/14 23:01]
tony
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 機制,操作流程如下:​
行 7: 行 7:
   - 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 ==== ==== Pre-notice ====
行 25: 行 28:
   - WireMock產生SUT與External Service互動過程的執行腳本(mappings)。   - WireMock產生SUT與External Service互動過程的執行腳本(mappings)。
   - Record Program針對腳本命名。   - Record Program針對腳本命名。
-==== Thinking & Design ​====+==== Thinking & Analysis ​====
 針對Pre-notice的流程,我有幾個問題需要解決:​ 針對Pre-notice的流程,我有幾個問題需要解決:​
   - WireMock如何捕捉External Service發送給SUT的callback event?   - WireMock如何捕捉External Service發送給SUT的callback event?
   - 補捉之後如何將這些events放置到腳本的Post中?​   - 補捉之後如何將這些events放置到腳本的Post中?​
   - 放置到腳本中後,如何在適當的時間由WireMock返回給SUT?​   - 放置到腳本中後,如何在適當的時間由WireMock返回給SUT?​
-針對以上三個問題,暫不考慮其中技術問題,如果產生如下方的腳本內容,應該可以解決問題。讓我先針對內容做說明:​+首先討論第三個問題,如果有辦法產生如下方的腳本內容,應該可以解決問題。讓我先針對內容做說明:​
   - webhook callback的內容會被放在postServerActions中,會在WireMock回應SUT後觸發這些動作。   - webhook callback的內容會被放在postServerActions中,會在WireMock回應SUT後觸發這些動作。
-  - 啟用webhook delay的機制,讓它根據時間延後發送;而這個時間其實是SUT註冊callback URL到WireMock收到callback event的duration。+  - 啟用webhook delay的機制,讓它根據時間延後發送;而這個時間其實是SUT註冊callback URL到WireMock收到callback event的duration。
 {{:​java:​wiremock:​ws_recording_webhook_json.png|}}\\ {{:​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的執行,所以捨棄這個方法。 Note. 第一個問題,我曾想過2個解決方法。第一個方法是同時註冊SUT與WireMock的callback URL給External Service,讓External Service同時發event給兩者;但由於無法確定event收到的先後順序而影響到步驟6的執行,所以捨棄這個方法。
- +==== Design & Implement ==== 
- +要達成前半段分析的流程,這次我們有兩個WireMock extension必須要實作:​ 
- +  - RequestFilter:​ 處理SUT與External Serviec的請求。 
-==== Implement ​==== +  - 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~~