Skip to content

Commit d4f7661

Browse files
Thorsten Schlathoelterbbortt
authored andcommitted
fix(#1415): allow "null" retrieval from
- improve error message
1 parent b6e532a commit d4f7661

File tree

11 files changed

+846
-196
lines changed

11 files changed

+846
-196
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.citrusframework.exceptions;
2+
3+
public final class SegmentEvaluationException extends Exception {
4+
5+
private final String renderedObject;
6+
7+
public SegmentEvaluationException(String reason, String renderedObject) {
8+
super(reason);
9+
this.renderedObject = renderedObject;
10+
}
11+
12+
public String getRenderedObject() {
13+
return renderedObject;
14+
}
15+
}

core/citrus-api/src/main/java/org/citrusframework/variable/SegmentVariableExtractorRegistry.java

Lines changed: 149 additions & 84 deletions
Large diffs are not rendered by default.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package org.citrusframework.variable;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
import org.citrusframework.context.TestContext;
7+
import org.citrusframework.exceptions.CitrusRuntimeException;
8+
import org.citrusframework.variable.SegmentVariableExtractorRegistry.MapVariableExtractor;
9+
import org.citrusframework.variable.SegmentVariableExtractorRegistry.ObjectFieldValueExtractor;
10+
import org.testng.annotations.Test;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
14+
import static org.assertj.core.api.InstanceOfAssertFactories.STRING;
15+
16+
public class IndexedSegmentVariableExtractorsTest {
17+
18+
private final TestContext context = new TestContext();
19+
20+
private static VariableExpressionSegmentMatcher matcher(String segmentExpr) {
21+
VariableExpressionSegmentMatcher m = new VariableExpressionSegmentMatcher(segmentExpr);
22+
assertThat(m.nextMatch()).as("first segment should match").isTrue();
23+
return m;
24+
}
25+
26+
@Test
27+
public void mapExtractor_listIndex_success() {
28+
Map<String, Object> ctx = Map.of("names", List.of("A", "B", "C"));
29+
var extractor = MapVariableExtractor.INSTANCE;
30+
31+
var m = matcher("names[1]");
32+
33+
assertThat(extractor.canExtract(context, ctx, m)).isTrue();
34+
assertThat(extractor.extractValue(context, ctx, m)).isEqualTo("B");
35+
}
36+
37+
@Test
38+
public void mapExtractor_arrayIndex_success() {
39+
Map<String, Object> ctx = Map.of("nums", new int[] {10, 20, 30});
40+
var extractor = MapVariableExtractor.INSTANCE;
41+
42+
var m = matcher("nums[2]");
43+
44+
assertThat(extractor.canExtract(context, ctx, m)).isTrue();
45+
assertThat(extractor.extractValue(context, ctx, m)).isEqualTo(30);
46+
}
47+
48+
@Test
49+
public void mapExtractor_arrayIndex_nullElement_success() {
50+
Map<String, Object> ctx = Map.of("nums", new Integer[] {10, 20, null});
51+
var extractor = MapVariableExtractor.INSTANCE;
52+
53+
var m = matcher("nums[2]");
54+
55+
assertThat(extractor.canExtract(context, ctx, m)).isTrue();
56+
assertThat(extractor.extractValue(context, ctx, m)).isNull();
57+
}
58+
59+
@Test
60+
public void mapExtractor_unknownKey_failsWithHelpfulMessage() {
61+
Map<String, Object> ctx = Map.of("names", List.of("A", "B"));
62+
var extractor = MapVariableExtractor.INSTANCE;
63+
64+
var m = matcher("missing");
65+
66+
assertThatThrownBy(() -> extractor.extractValue(context, ctx, m))
67+
.isInstanceOf(CitrusRuntimeException.class)
68+
.extracting("message", STRING)
69+
.isEqualToIgnoringWhitespace("""
70+
Unable to extract value using expression 'missing'
71+
Reason: Unknown key 'missing' in Map
72+
From object (java.util.ImmutableCollections$Map1):
73+
{names=[A, B]}""");
74+
}
75+
76+
@Test
77+
public void mapExtractor_indexOutOfBounds_failsWithSizeInfo() {
78+
Map<String, Object> ctx = Map.of("names", List.of("A", "B", "C"));
79+
var extractor = MapVariableExtractor.INSTANCE;
80+
81+
var m = matcher("names[3]"); // OOB
82+
83+
assertThatThrownBy(() -> extractor.extractValue(context, ctx, m))
84+
.isInstanceOf(CitrusRuntimeException.class)
85+
.hasMessageContaining("Unable to extract value using expression 'names[3]'")
86+
.hasMessageContaining("Index 3 out of bounds (list size 3) for segment 'names'");
87+
}
88+
89+
@Test
90+
public void mapExtractor_wrongTypeForIndexing_failsWithTypeInfo() {
91+
Map<String, Object> ctx = Map.of("names", 42); // not list/array
92+
var extractor = MapVariableExtractor.INSTANCE;
93+
94+
var m = matcher("names[0]");
95+
96+
assertThatThrownBy(() -> extractor.extractValue(context, ctx, m))
97+
.isInstanceOf(CitrusRuntimeException.class)
98+
.hasMessageContaining("Unable to extract value using expression 'names[0]'")
99+
.hasMessageContaining("Expected array or List for indexed access, but was java.lang.Integer (segment 'names')");
100+
}
101+
102+
public static class Person {
103+
int[] numbers = {10, 20, 30};
104+
List<String> tags = List.of("alpha", "beta");
105+
String name = "Peter";
106+
String nullField = null;
107+
}
108+
109+
@Test
110+
public void objectFieldExtractor_arrayIndex_success() {
111+
var person = new Person();
112+
var extractor = ObjectFieldValueExtractor.INSTANCE;
113+
114+
var m = matcher("numbers[2]");
115+
116+
assertThat(extractor.canExtract(context, person, m)).isTrue();
117+
assertThat(extractor.extractValue(context, person, m)).isEqualTo(30);
118+
}
119+
120+
@Test
121+
public void objectFieldExtractor_listIndex_success() {
122+
var person = new Person();
123+
var extractor = ObjectFieldValueExtractor.INSTANCE;
124+
125+
var m = matcher("tags[0]");
126+
127+
assertThat(extractor.canExtract(context, person, m)).isTrue();
128+
assertThat(extractor.extractValue(context, person, m)).isEqualTo("alpha");
129+
}
130+
131+
@Test
132+
public void objectFieldExtractor_nullValueFromField_success() {
133+
var person = new Person();
134+
var extractor = ObjectFieldValueExtractor.INSTANCE;
135+
136+
var m = matcher("nullField");
137+
138+
assertThat(extractor.canExtract(context, person, m)).isTrue();
139+
assertThat(extractor.extractValue(context, person, m)).isNull();
140+
}
141+
142+
@Test
143+
public void objectFieldExtractor_unknownField_failsWithHelpfulMessage() {
144+
var person = new Person();
145+
var extractor = ObjectFieldValueExtractor.INSTANCE;
146+
147+
var m = matcher("missing");
148+
149+
assertThatThrownBy(() -> extractor.extractValue(context, person, m))
150+
.isInstanceOf(CitrusRuntimeException.class)
151+
.hasMessageContaining("Unable to extract value using expression 'missing'")
152+
.hasMessageContaining("Reason: Unknown field 'missing' on type org.citrusframework.variable.IndexedSegmentVariableExtractorsTest$Person");
153+
}
154+
155+
@Test
156+
public void objectFieldExtractor_wrongTypeForIndexing_failsWithTypeInfo() {
157+
var person = new Person(); // field 'name' is String
158+
var extractor = ObjectFieldValueExtractor.INSTANCE;
159+
160+
var m = matcher("name[0]");
161+
162+
assertThatThrownBy(() -> extractor.extractValue(context, person, m))
163+
.isInstanceOf(CitrusRuntimeException.class)
164+
.hasMessageContaining("Unable to extract value using expression 'name[0]'")
165+
.hasMessageContaining("Expected array or List for indexed access, but was java.lang.String (segment 'name')");
166+
}
167+
168+
@Test
169+
public void objectFieldExtractor_indexOutOfBounds_failsWithLengthInfo() {
170+
var person = new Person();
171+
var extractor = ObjectFieldValueExtractor.INSTANCE;
172+
173+
var m = matcher("numbers[9]");
174+
175+
assertThatThrownBy(() -> extractor.extractValue(context, person, m))
176+
.isInstanceOf(CitrusRuntimeException.class)
177+
.hasMessageContaining("Unable to extract value using expression 'numbers[9]'")
178+
.hasMessageContaining("Index 9 out of bounds (array length 3) for segment 'numbers'");
179+
}
180+
}

core/citrus-base/src/test/java/org/citrusframework/actions/LoadPropertiesActionTest.java

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,50 @@
1616

1717
package org.citrusframework.actions;
1818

19-
import java.text.SimpleDateFormat;
20-
import java.util.Date;
21-
2219
import org.citrusframework.UnitTestSupport;
2320
import org.citrusframework.exceptions.CitrusRuntimeException;
2421
import org.testng.Assert;
2522
import org.testng.annotations.Test;
2623

24+
import java.text.SimpleDateFormat;
25+
import java.util.Date;
26+
27+
import static java.lang.String.format;
28+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
29+
2730
public class LoadPropertiesActionTest extends UnitTestSupport {
2831

29-
@Test
30-
public void testLoadProperties() {
31-
LoadPropertiesAction loadProperties = new LoadPropertiesAction.Builder()
32-
.filePath("classpath:org/citrusframework/actions/load.properties")
33-
.build();
32+
@Test
33+
public void testLoadProperties() {
34+
LoadPropertiesAction loadProperties = new LoadPropertiesAction.Builder()
35+
.filePath("classpath:org/citrusframework/actions/load.properties")
36+
.build();
3437

35-
loadProperties.execute(context);
38+
loadProperties.execute(context);
3639

37-
Assert.assertNotNull(context.getVariable("${myVariable}"));
38-
Assert.assertEquals(context.getVariable("${myVariable}"), "test");
39-
Assert.assertNotNull(context.getVariable("${user}"));
40+
Assert.assertNotNull(context.getVariable("${myVariable}"));
41+
Assert.assertEquals(context.getVariable("${myVariable}"), "test");
42+
Assert.assertNotNull(context.getVariable("${user}"));
4043
Assert.assertEquals(context.getVariable("${user}"), "Citrus");
41-
Assert.assertNotNull(context.getVariable("${welcomeText}"));
42-
Assert.assertEquals(context.getVariable("${welcomeText}"), "Hello Citrus!");
43-
Assert.assertNotNull(context.getVariable("${todayDate}"));
44+
Assert.assertNotNull(context.getVariable("${welcomeText}"));
45+
Assert.assertEquals(context.getVariable("${welcomeText}"), "Hello Citrus!");
46+
Assert.assertNotNull(context.getVariable("${todayDate}"));
4447
Assert.assertEquals(context.getVariable("${todayDate}"),
4548
"Today is " + new SimpleDateFormat("yyyy-MM-dd").format(new Date(System.currentTimeMillis())) + "!");
46-
}
49+
}
4750

48-
@Test
51+
@Test
4952
public void testUnknownVariableInLoadProperties() {
50-
LoadPropertiesAction loadProperties = new LoadPropertiesAction.Builder()
51-
.filePath("classpath:org/citrusframework/actions/load-error.properties")
52-
.build();
53-
54-
try {
55-
loadProperties.execute(context);
56-
} catch(CitrusRuntimeException e) {
57-
Assert.assertEquals(e.getMessage(), "Unknown variable 'unknownVar'");
58-
return;
59-
}
60-
61-
Assert.fail("Missing exception for unkown variable in property file");
62-
}
53+
LoadPropertiesAction loadProperties = new LoadPropertiesAction.Builder()
54+
.filePath("classpath:org/citrusframework/actions/load-error.properties")
55+
.build();
56+
57+
assertThatThrownBy(() -> loadProperties.execute(context))
58+
.isInstanceOf(CitrusRuntimeException.class)
59+
.hasMessage(
60+
format(
61+
"Unable to extract value using expression 'unknownVar'!%nReason: Unknown key 'unknownVar' in Map.%nFrom object (java.util.concurrent.ConcurrentHashMap):%n{}"
62+
)
63+
);
64+
}
6365
}

core/citrus-base/src/test/java/org/citrusframework/actions/ReceiveMessageActionTest.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@
6464
import java.util.List;
6565
import java.util.Map;
6666

67+
import static java.lang.String.format;
6768
import static org.assertj.core.api.Assertions.assertThat;
69+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6870
import static org.citrusframework.message.MessageType.JSON;
6971
import static org.citrusframework.message.MessageType.PLAINTEXT;
7072
import static org.citrusframework.message.MessageType.XHTML;
@@ -526,11 +528,14 @@ public void testReceiveMessageWithUnknownVariablesInMessageHeaders() {
526528
.endpoint(endpoint)
527529
.message(controlMessageBuilder)
528530
.build();
529-
try {
530-
receiveAction.execute(context);
531-
} catch (CitrusRuntimeException e) {
532-
Assert.assertEquals(e.getMessage(), "Unknown variable 'myOperation'");
533-
}
531+
532+
assertThatThrownBy(() -> receiveAction.execute(context))
533+
.isInstanceOf(CitrusRuntimeException.class)
534+
.hasMessage(
535+
format(
536+
"Unable to extract value using expression 'myOperation'!%nReason: Unknown key 'myOperation' in Map.%nFrom object (java.util.concurrent.ConcurrentHashMap):%n{}"
537+
)
538+
);
534539
}
535540

536541
@Test
@@ -563,11 +568,14 @@ public void testReceiveMessageWithUnknownVariableInMessagePayload() {
563568
.endpoint(endpoint)
564569
.message(controlMessageBuilder)
565570
.build();
566-
try {
567-
receiveAction.execute(context);
568-
} catch (CitrusRuntimeException e) {
569-
Assert.assertEquals(e.getMessage(), "Unknown variable 'myText'");
570-
}
571+
572+
assertThatThrownBy(() -> receiveAction.execute(context))
573+
.isInstanceOf(CitrusRuntimeException.class)
574+
.hasMessage(
575+
format(
576+
"Unable to extract value using expression 'myText'!%nReason: Unknown key 'myText' in Map.%nFrom object (java.util.concurrent.ConcurrentHashMap):%n{}"
577+
)
578+
);
571579
}
572580

573581
@Test

core/citrus-base/src/test/java/org/citrusframework/actions/SendMessageActionTest.java

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@
1616

1717
package org.citrusframework.actions;
1818

19-
import java.io.UnsupportedEncodingException;
20-
import java.util.Arrays;
21-
import java.util.Collections;
22-
import java.util.HashMap;
23-
import java.util.Map;
24-
2519
import org.citrusframework.DefaultTestCase;
2620
import org.citrusframework.TestActor;
2721
import org.citrusframework.TestCase;
@@ -48,6 +42,14 @@
4842
import org.testng.Assert;
4943
import org.testng.annotations.Test;
5044

45+
import java.io.UnsupportedEncodingException;
46+
import java.util.Arrays;
47+
import java.util.Collections;
48+
import java.util.HashMap;
49+
import java.util.Map;
50+
51+
import static java.lang.String.format;
52+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5153
import static org.mockito.ArgumentMatchers.eq;
5254
import static org.mockito.Mockito.any;
5355
import static org.mockito.Mockito.doAnswer;
@@ -314,14 +316,14 @@ public void testSendMessageWithUnknownVariableInMessagePayload() {
314316
.endpoint(endpoint)
315317
.message(messageBuilder)
316318
.build();
317-
try {
318-
sendAction.execute(context);
319-
} catch(CitrusRuntimeException e) {
320-
Assert.assertEquals(e.getMessage(), "Unknown variable 'myText'");
321-
return;
322-
}
323319

324-
Assert.fail("Missing " + CitrusRuntimeException.class + " with unknown variable error message");
320+
assertThatThrownBy(() -> sendAction.execute(context))
321+
.isInstanceOf(CitrusRuntimeException.class)
322+
.hasMessage(
323+
format(
324+
"Unable to extract value using expression 'myText'!%nReason: Unknown key 'myText' in Map.%nFrom object (java.util.concurrent.ConcurrentHashMap):%n{}"
325+
)
326+
);
325327
}
326328

327329
@Test
@@ -342,14 +344,14 @@ public void testSendMessageWithUnknownVariableInHeaders() {
342344
.endpoint(endpoint)
343345
.message(messageBuilder)
344346
.build();
345-
try {
346-
sendAction.execute(context);
347-
} catch(CitrusRuntimeException e) {
348-
Assert.assertEquals(e.getMessage(), "Unknown variable 'myOperation'");
349-
return;
350-
}
351347

352-
Assert.fail("Missing " + CitrusRuntimeException.class + " with unknown variable error message");
348+
assertThatThrownBy(() -> sendAction.execute(context))
349+
.isInstanceOf(CitrusRuntimeException.class)
350+
.hasMessage(
351+
format(
352+
"Unable to extract value using expression 'myOperation'!%nReason: Unknown key 'myOperation' in Map.%nFrom object (java.util.concurrent.ConcurrentHashMap):%n{}"
353+
)
354+
);
353355
}
354356

355357
@Test

0 commit comments

Comments
 (0)