我有一隻待測程式(SUT)會相依於外部服務(External Service)的webhook 機制,操作流程如下:
WireMock有提供webhook的extension,讓你可以自行編寫程式或mappingfile腳本去做到在“特定操作後,發送webhook操作”,但它並不支援Recording的功能。本篇文章,主要分享如何讓WireMock擁有錄製webhook的功能。
Note. 範例程式碼link可自行取用。這是依照我需求撰寫的,只要弄懂方法,就可以依照你需求自行調整。
Limitation. 如果webhook的反註冊是基於回應內容的URL,目前還沒想到辦法可以根據SUT的IP與情境去做對應的內容回應。
由於Solution與整個Record流程息息相關,我必須先說明我所採用的流程。在我的故事中,有四隻程式:
針對Pre-notice的流程,我有幾個問題需要解決:
首先討論第三個問題,如果有辦法產生如下方的腳本內容,應該可以解決問題。讓我先針對內容做說明:
接著回到第一個問題,我採用的方式是將註冊的callback url改到WireMock身上,流程如下圖。另外會把原本的callback url與註冊時間記錄下來,以用來處理收到callback event後的轉送與計算問題三個delay時間:
延續上圖流程,當收到callback event後,就可以透過上圖記錄的原始callback url,送回給SUT並記錄收到event的時間:
在還不考慮技術與實作細節的情況下,這solution似乎是可行的。讓我們繼續看下去。
Note. 第一個問題,我曾想過2個解決方法。第一個方法是同時註冊SUT與WireMock的callback URL給External Service,讓External Service同時發event給兩者;但由於無法確定event收到的先後順序而影響到步驟6的執行,所以捨棄這個方法。
要達成前半段分析的流程,這次我們有兩個WireMock extension必須要實作:
我實作的ExternalServiceWebHookRequestFilter主要會處理這幾件事情:
而SUT所註冊request body重點內容如下,Destionation為callback URL,Context為註冊識別號:
{ "Destination": "http://10.146.16.150:18556/callback", "Context": "tony_test", .. }當收到SUT的webhook註冊請求後,會使用EventRecorder將context與destination記錄下來,EventRecorder會自行標記時間;接著將destination的port改為WireMock的HTTP port,並調整請求的Body內容讓WireMock繼續對External Service做註冊動作:
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); }在處理完註冊動作後,WireMock就會開始收到callback event。收到這些event後,首先會透過EventRecorder依照context記錄下來,EventRecorder會標記註冊後要delay多少時間才發送給SUT;接著會透過EventRecorder取出原本的callback destination並轉發回去給SUT;最後就是讓這請求到這裡結束並回應給External Service OK,少了這步驟會讓WireMock繼續處理這訊息而導致External Service產生非預期結果:
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()); }
有了RequestFilter所記錄下來的資料後,StubMappingTransformer在WireMock收到stop後,就會開始處理腳本的產生。最重要的處理在下圖的步驟二,包含註冊request內容的調整與webhook postServerAction的產生:
首先你要先確定,要處理的StubMapping是屬於SUT註冊webhook的請求,我採用的方式是判斷Request的URL:
@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; }接著由於註冊的request body中,destination包含著SUT的IP,而SUT會隨著執行環境而改變,這將導致錄製出來的腳本無法萬用。因此我必須將腳本內RequestBody的Destination移除,如果要考慮情境比較複雜可以直接套Library去移除: (註冊的URL就不需要處理了,因為當下的執行前後文是由你的腳本所促成,URL絕對會相同)
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); }最後就是重頭戲:
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; }完成以上實作並錄製腳本所產生出來的東西,其實我已經在Thinking & Analysis的部分show給大家看過了。