|
| 1 | +package com.dke.data.agrirouter.test.usecase; |
| 2 | + |
| 3 | +import agrirouter.feed.response.FeedResponse; |
| 4 | +import agrirouter.request.Request; |
| 5 | +import agrirouter.response.Response; |
| 6 | +import com.dke.data.agrirouter.api.cancellation.DefaultCancellationToken; |
| 7 | +import com.dke.data.agrirouter.api.dto.encoding.DecodeMessageResponse; |
| 8 | +import com.dke.data.agrirouter.api.dto.messaging.FetchMessageResponse; |
| 9 | +import com.dke.data.agrirouter.api.dto.onboard.OnboardingResponse; |
| 10 | +import com.dke.data.agrirouter.api.enums.ContentMessageType; |
| 11 | +import com.dke.data.agrirouter.api.enums.SystemMessageType; |
| 12 | +import com.dke.data.agrirouter.api.env.QA; |
| 13 | +import com.dke.data.agrirouter.api.service.messaging.encoding.DecodeMessageService; |
| 14 | +import com.dke.data.agrirouter.api.service.messaging.encoding.EncodeMessageService; |
| 15 | +import com.dke.data.agrirouter.api.service.messaging.http.FetchMessageService; |
| 16 | +import com.dke.data.agrirouter.api.service.parameters.*; |
| 17 | +import com.dke.data.agrirouter.impl.common.MessageIdService; |
| 18 | +import com.dke.data.agrirouter.impl.common.UtcTimeService; |
| 19 | +import com.dke.data.agrirouter.impl.messaging.SequenceNumberService; |
| 20 | +import com.dke.data.agrirouter.impl.messaging.encoding.DecodeMessageServiceImpl; |
| 21 | +import com.dke.data.agrirouter.impl.messaging.encoding.EncodeMessageServiceImpl; |
| 22 | +import com.dke.data.agrirouter.impl.messaging.rest.*; |
| 23 | +import com.dke.data.agrirouter.test.AbstractIntegrationTest; |
| 24 | +import com.dke.data.agrirouter.test.Assertions; |
| 25 | +import com.dke.data.agrirouter.test.OnboardingResponseRepository; |
| 26 | +import com.dke.data.agrirouter.test.helper.ContentReader; |
| 27 | +import com.google.protobuf.ByteString; |
| 28 | +import java.io.IOException; |
| 29 | +import java.util.*; |
| 30 | +import java.util.concurrent.atomic.AtomicReference; |
| 31 | +import java.util.stream.Collectors; |
| 32 | +import java.util.stream.Stream; |
| 33 | +import org.apache.http.HttpStatus; |
| 34 | +import org.jetbrains.annotations.NotNull; |
| 35 | +import org.junit.jupiter.api.AfterEach; |
| 36 | +import org.junit.jupiter.api.BeforeEach; |
| 37 | +import org.junit.jupiter.params.ParameterizedTest; |
| 38 | +import org.junit.jupiter.params.provider.Arguments; |
| 39 | +import org.junit.jupiter.params.provider.MethodSource; |
| 40 | + |
| 41 | +/** Test case to show the behavior for chunked message sending. */ |
| 42 | +class SendAndReceiveChunkedMessagesTest extends AbstractIntegrationTest { |
| 43 | + |
| 44 | + private static final int MAX_CHUNK_SIZE = 1024000; |
| 45 | + |
| 46 | + @ParameterizedTest |
| 47 | + @MethodSource |
| 48 | + void givenRealMessageContentWhenSendingMessagesTheContentShouldMatchAfterReceivingAndMergingIt( |
| 49 | + ByteString messageContent, int expectedNrOfChunks) throws Throwable { |
| 50 | + actionsForSender(messageContent, expectedNrOfChunks); |
| 51 | + actionsForTheRecipient(messageContent, expectedNrOfChunks); |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * These are the actions for the recipient. The recipient is already set up and declared the |
| 56 | + * capabilities. The actions for the recipient are documented inline. |
| 57 | + * |
| 58 | + * @param messageContent - |
| 59 | + * @param expectedNrOfChunks - |
| 60 | + */ |
| 61 | + private void actionsForTheRecipient(ByteString messageContent, int expectedNrOfChunks) |
| 62 | + throws Throwable { |
| 63 | + // [1] Fetch all the messages within the feed. The number of headers should match the number of |
| 64 | + // chunks sent. |
| 65 | + final MessageQueryServiceImpl messageQueryService = new MessageQueryServiceImpl(new QA() {}); |
| 66 | + final MessageQueryParameters messageQueryParameters = new MessageQueryParameters(); |
| 67 | + final OnboardingResponse recipient = |
| 68 | + OnboardingResponseRepository.read( |
| 69 | + OnboardingResponseRepository.Identifier.COMMUNICATION_UNIT); |
| 70 | + messageQueryParameters.setOnboardingResponse(recipient); |
| 71 | + messageQueryParameters.setSentToInSeconds(UtcTimeService.inTheFuture(5).toEpochSecond()); |
| 72 | + messageQueryParameters.setSentFromInSeconds( |
| 73 | + UtcTimeService.inThePast(UtcTimeService.FOUR_WEEKS_AGO).toEpochSecond()); |
| 74 | + messageQueryService.send(messageQueryParameters); |
| 75 | + |
| 76 | + // [2] Wait for the agrirouter to process the message. |
| 77 | + waitForTheAgrirouterToProcessSingleMessage(); |
| 78 | + |
| 79 | + // [3] Fetch the chunks from the outbox. Since we have the same restrictions while receiving, |
| 80 | + // this has to be the same number of messages as it is chunks. |
| 81 | + FetchMessageService fetchMessageService = new FetchMessageServiceImpl(); |
| 82 | + Optional<List<FetchMessageResponse>> fetchMessageResponses = |
| 83 | + fetchMessageService.fetch( |
| 84 | + recipient, new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 85 | + Assertions.assertTrue(fetchMessageResponses.isPresent()); |
| 86 | + Assertions.assertEquals(expectedNrOfChunks, fetchMessageResponses.get().size()); |
| 87 | + |
| 88 | + // [4] Check if the response from the AR only contains valid results. |
| 89 | + final DecodeMessageService decodeMessageService = new DecodeMessageServiceImpl(); |
| 90 | + fetchMessageResponses.get().stream() |
| 91 | + .map( |
| 92 | + fetchMessageResponse -> |
| 93 | + decodeMessageService.decode(fetchMessageResponse.getCommand().getMessage())) |
| 94 | + .forEach( |
| 95 | + decodeMessageResponse -> |
| 96 | + Assertions.assertEquals( |
| 97 | + Response.ResponseEnvelope.ResponseBodyType.ACK_FOR_FEED_MESSAGE, |
| 98 | + decodeMessageResponse.getResponseEnvelope().getType())); |
| 99 | + |
| 100 | + // [5] Map the results from the query to 'real' messages within the feed and perform some |
| 101 | + // assertions. |
| 102 | + final List<FeedResponse.MessageQueryResponse.FeedMessage> feedMessages = |
| 103 | + fetchMessageResponses.get().stream() |
| 104 | + .map( |
| 105 | + fetchMessageResponse -> |
| 106 | + decodeMessageService.decode(fetchMessageResponse.getCommand().getMessage())) |
| 107 | + .map( |
| 108 | + decodeMessageResponse -> |
| 109 | + messageQueryService.decode( |
| 110 | + decodeMessageResponse.getResponsePayloadWrapper().getDetails().getValue())) |
| 111 | + .map(messageQueryResponse -> messageQueryResponse.getMessages(0)) |
| 112 | + .collect(Collectors.toList()); |
| 113 | + Assertions.assertEquals(expectedNrOfChunks, feedMessages.size()); |
| 114 | + feedMessages.forEach( |
| 115 | + feedMessage -> Assertions.assertNotNull(feedMessage.getHeader().getChunkContext())); |
| 116 | + Assertions.assertEquals( |
| 117 | + feedMessages.get(0).getHeader().getChunkContext().getTotal(), expectedNrOfChunks); |
| 118 | + final Set<String> chunkContextIds = |
| 119 | + feedMessages.stream() |
| 120 | + .map(feedMessage -> feedMessage.getHeader().getChunkContext().getContextId()) |
| 121 | + .collect(Collectors.toSet()); |
| 122 | + Assertions.assertEquals( |
| 123 | + 1, chunkContextIds.size(), "There should be only one chunk context ID."); |
| 124 | + |
| 125 | + // [6] Confirm the chunks to remove them from the feed. |
| 126 | + final List<String> messageIdsToConfirm = |
| 127 | + feedMessages.stream() |
| 128 | + .map(feedMessage -> feedMessage.getHeader().getMessageId()) |
| 129 | + .collect(Collectors.toList()); |
| 130 | + final MessageConfirmationServiceImpl messageConfirmationService = |
| 131 | + new MessageConfirmationServiceImpl(new QA() {}); |
| 132 | + final MessageConfirmationParameters messageConfirmationParameters = |
| 133 | + new MessageConfirmationParameters(); |
| 134 | + messageConfirmationParameters.setOnboardingResponse(recipient); |
| 135 | + messageConfirmationParameters.setMessageIds(messageIdsToConfirm); |
| 136 | + messageConfirmationService.send(messageConfirmationParameters); |
| 137 | + |
| 138 | + // [7] Fetch the response from the agrirouter after confirming the messages. |
| 139 | + waitForTheAgrirouterToProcessSingleMessage(); |
| 140 | + fetchMessageResponses = |
| 141 | + fetchMessageService.fetch( |
| 142 | + recipient, new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 143 | + Assertions.assertTrue(fetchMessageResponses.isPresent()); |
| 144 | + Assertions.assertEquals( |
| 145 | + 1, fetchMessageResponses.get().size(), "This should be a single response."); |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * These are the actions for the sender. The sender is already set up and declared the |
| 150 | + * capabilities. The actions for the sender are documented inline. |
| 151 | + * |
| 152 | + * @param messageContent - |
| 153 | + * @param expectedNrOfChunks - |
| 154 | + * @throws IOException - |
| 155 | + * @throws InterruptedException - |
| 156 | + */ |
| 157 | + private void actionsForSender(ByteString messageContent, int expectedNrOfChunks) |
| 158 | + throws IOException, InterruptedException { |
| 159 | + final EncodeMessageService encodeMessageService = new EncodeMessageServiceImpl(); |
| 160 | + final SendMessageServiceImpl sendMessageService = new SendMessageServiceImpl(); |
| 161 | + final OnboardingResponse onboardingResponse = |
| 162 | + OnboardingResponseRepository.read(OnboardingResponseRepository.Identifier.FARMING_SOFTWARE); |
| 163 | + |
| 164 | + // [1] Define the raw message, in this case this is the Base64 encoded message content, no |
| 165 | + // chunking needed. |
| 166 | + MessageHeaderParameters messageHeaderParameters = new MessageHeaderParameters(); |
| 167 | + messageHeaderParameters.setTechnicalMessageType(ContentMessageType.ISO_11783_TASKDATA_ZIP); |
| 168 | + messageHeaderParameters.setApplicationMessageId(MessageIdService.generateMessageId()); |
| 169 | + messageHeaderParameters.setApplicationMessageSeqNo( |
| 170 | + SequenceNumberService.generateSequenceNumberForEndpoint(onboardingResponse)); |
| 171 | + messageHeaderParameters.setMode(Request.RequestEnvelope.Mode.DIRECT); |
| 172 | + messageHeaderParameters.setRecipients( |
| 173 | + Collections.singletonList( |
| 174 | + OnboardingResponseRepository.read( |
| 175 | + OnboardingResponseRepository.Identifier.COMMUNICATION_UNIT) |
| 176 | + .getSensorAlternateId())); |
| 177 | + |
| 178 | + PayloadParameters payloadParameters = new PayloadParameters(); |
| 179 | + payloadParameters.setValue(messageContent); |
| 180 | + payloadParameters.setTypeUrl(SystemMessageType.EMPTY.getKey()); |
| 181 | + |
| 182 | + // [2] Chunk the message content using the SDK specific methods ('chunkAndEncode'). |
| 183 | + List<MessageParameterTuple> tuples = |
| 184 | + encodeMessageService.chunkAndBase64EncodeEachChunk( |
| 185 | + messageHeaderParameters, payloadParameters, onboardingResponse); |
| 186 | + |
| 187 | + tuples.forEach( |
| 188 | + messageParameterTuple -> |
| 189 | + Assertions.assertTrue( |
| 190 | + Objects.requireNonNull(messageParameterTuple.getPayloadParameters().getValue()) |
| 191 | + .toStringUtf8() |
| 192 | + .length() |
| 193 | + <= MAX_CHUNK_SIZE)); |
| 194 | + |
| 195 | + List<String> encodedMessages = encodeMessageService.encode(tuples); |
| 196 | + |
| 197 | + // [3] Send the chunks to the agrirouter. |
| 198 | + SendMessageParameters sendMessageParameters = new SendMessageParameters(); |
| 199 | + sendMessageParameters.setEncodedMessages(encodedMessages); |
| 200 | + sendMessageParameters.setOnboardingResponse(onboardingResponse); |
| 201 | + sendMessageService.send(sendMessageParameters); |
| 202 | + |
| 203 | + // [4] Wait for the AR to process the chunks. |
| 204 | + waitForTheAgrirouterToProcessMultipleMessages(); |
| 205 | + |
| 206 | + // [5] Check if the chunks were processed successfully. |
| 207 | + FetchMessageService fetchMessageService = new FetchMessageServiceImpl(); |
| 208 | + Optional<List<FetchMessageResponse>> fetchMessageResponses = |
| 209 | + fetchMessageService.fetch( |
| 210 | + onboardingResponse, |
| 211 | + new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 212 | + |
| 213 | + Assertions.assertTrue(fetchMessageResponses.isPresent()); |
| 214 | + Assertions.assertEquals(expectedNrOfChunks, fetchMessageResponses.get().size()); |
| 215 | + |
| 216 | + DecodeMessageService decodeMessageService = new DecodeMessageServiceImpl(); |
| 217 | + AtomicReference<DecodeMessageResponse> decodeMessageResponse = new AtomicReference<>(); |
| 218 | + fetchMessageResponses.get().stream() |
| 219 | + .map(FetchMessageResponse::getCommand) |
| 220 | + .forEach( |
| 221 | + message -> { |
| 222 | + Assertions.assertNotNull(message); |
| 223 | + decodeMessageResponse.set(decodeMessageService.decode(message.getMessage())); |
| 224 | + |
| 225 | + Assertions.assertMatchesAny( |
| 226 | + Arrays.asList(HttpStatus.SC_OK, HttpStatus.SC_CREATED, HttpStatus.SC_NO_CONTENT), |
| 227 | + decodeMessageResponse.get().getResponseEnvelope().getResponseCode()); |
| 228 | + }); |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Delivers fake message content for multiple test cases. |
| 233 | + * |
| 234 | + * @return - |
| 235 | + */ |
| 236 | + @SuppressWarnings("unused") |
| 237 | + private static @NotNull Stream<Arguments> givenRealMessageContentWhenSendingMessagesTheContentShouldMatchAfterReceivingAndMergingIt() throws Throwable { |
| 238 | + return Stream.of( |
| 239 | + Arguments.of( |
| 240 | + ByteString.copyFrom( |
| 241 | + ContentReader.readRawData(ContentReader.Identifier.BIG_TASK_DATA)), |
| 242 | + 3)); |
| 243 | + } |
| 244 | + |
| 245 | + /** |
| 246 | + * Cleanup before and after each test case. These actions are necessary because it could be the |
| 247 | + * case, that there are dangling messages from former tests. |
| 248 | + */ |
| 249 | + @BeforeEach |
| 250 | + @AfterEach |
| 251 | + public void prepareTestEnvironment() throws Throwable { |
| 252 | + FetchMessageService fetchMessageService = new FetchMessageServiceImpl(); |
| 253 | + final OnboardingResponse recipient = |
| 254 | + OnboardingResponseRepository.read( |
| 255 | + OnboardingResponseRepository.Identifier.COMMUNICATION_UNIT); |
| 256 | + final MessageHeaderQueryServiceImpl messageHeaderQueryService = |
| 257 | + new MessageHeaderQueryServiceImpl(new QA() {}); |
| 258 | + |
| 259 | + // [1] Clean the outbox of the endpoint. |
| 260 | + fetchMessageService.fetch( |
| 261 | + recipient, new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 262 | + |
| 263 | + // [2] Fetch all message headers for the last 4 weeks (maximum retention time within the |
| 264 | + // agrirouter). |
| 265 | + final MessageQueryParameters messageQueryParameters = new MessageQueryParameters(); |
| 266 | + messageQueryParameters.setOnboardingResponse(recipient); |
| 267 | + messageQueryParameters.setSentToInSeconds(UtcTimeService.inTheFuture(5).toEpochSecond()); |
| 268 | + messageQueryParameters.setSentFromInSeconds( |
| 269 | + UtcTimeService.inThePast(UtcTimeService.FOUR_WEEKS_AGO).toEpochSecond()); |
| 270 | + messageHeaderQueryService.send(messageQueryParameters); |
| 271 | + waitForTheAgrirouterToProcessSingleMessage(); |
| 272 | + Optional<List<FetchMessageResponse>> fetchMessageResponses = |
| 273 | + fetchMessageService.fetch( |
| 274 | + recipient, new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 275 | + Assertions.assertTrue(fetchMessageResponses.isPresent()); |
| 276 | + Assertions.assertEquals( |
| 277 | + 1, fetchMessageResponses.get().size(), "This should be a single response."); |
| 278 | + final DecodeMessageService decodeMessageService = new DecodeMessageServiceImpl(); |
| 279 | + final DecodeMessageResponse decodeMessageResponse = |
| 280 | + decodeMessageService.decode(fetchMessageResponses.get().get(0).getCommand().getMessage()); |
| 281 | + Assertions.assertEquals( |
| 282 | + Response.ResponseEnvelope.ResponseBodyType.ACK_FOR_FEED_HEADER_LIST, |
| 283 | + decodeMessageResponse.getResponseEnvelope().getType()); |
| 284 | + final FeedResponse.HeaderQueryResponse headerQueryResponse = |
| 285 | + messageHeaderQueryService.decode( |
| 286 | + decodeMessageResponse.getResponsePayloadWrapper().getDetails().getValue()); |
| 287 | + |
| 288 | + // [3] Delete the dangling messages from the feed of the endpoint if necessary. |
| 289 | + if (headerQueryResponse.getQueryMetrics().getTotalMessagesInQuery() > 0) { |
| 290 | + final DeleteMessageServiceImpl deleteMessageService = new DeleteMessageServiceImpl(); |
| 291 | + final DeleteMessageParameters deleteMessageParameters = new DeleteMessageParameters(); |
| 292 | + deleteMessageParameters.setOnboardingResponse(recipient); |
| 293 | + final List<String> messageIds = |
| 294 | + headerQueryResponse.getFeedList().stream() |
| 295 | + .map(FeedResponse.HeaderQueryResponse.Feed::getHeadersList) |
| 296 | + .flatMap(Collection::stream) |
| 297 | + .map(FeedResponse.HeaderQueryResponse.Header::getMessageId) |
| 298 | + .collect(Collectors.toList()); |
| 299 | + deleteMessageParameters.setMessageIds(messageIds); |
| 300 | + deleteMessageService.send(deleteMessageParameters); |
| 301 | + waitForTheAgrirouterToProcessSingleMessage(); |
| 302 | + fetchMessageService.fetch( |
| 303 | + recipient, new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 304 | + } |
| 305 | + |
| 306 | + // [4] Clean the outbox of the endpoint. |
| 307 | + fetchMessageService.fetch( |
| 308 | + recipient, new DefaultCancellationToken(MAX_TRIES_BEFORE_FAILURE, DEFAULT_INTERVAL)); |
| 309 | + } |
| 310 | +} |
0 commit comments