View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.junit.bct;
18  
19  import static org.apache.juneau.junit.bct.BctAssertions.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.text.*;
23  import java.time.*;
24  import java.util.*;
25  import java.util.function.*;
26  
27  import org.apache.juneau.*;
28  import org.junit.jupiter.api.*;
29  
30  /**
31   * Unit tests for {@link AssertionArgs}.
32   *
33   * <p>Tests the configuration and behavior of the assertion arguments class including
34   * bean converter customization, error message composition, and fluent API functionality.</p>
35   */
36  class AssertionArgs_Test extends TestBase {
37  
38  	// Test objects for assertions
39  	static class TestBean {
40  		private String name;
41  		private int age;
42  		private boolean active;
43  
44  		public TestBean(String name, int age, boolean active) {
45  			this.name = name;
46  			this.age = age;
47  			this.active = active;
48  		}
49  
50  		public String getName() { return name; }
51  		public int getAge() { return age; }
52  		public boolean isActive() { return active; }
53  	}
54  
55  	static class CustomObject {
56  		private String value;
57  
58  		public CustomObject(String value) {
59  			this.value = value;
60  		}
61  
62  		public String getValue() { return value; }
63  
64  		@Override
65  		public String toString() {
66  			return "CustomObject[" + value + "]";
67  		}
68  	}
69  
70  	@Test
71  	void a01_defaultConstruction() {
72  		var args = new AssertionArgs();
73  
74  		// Should have no custom converter
75  		assertTrue(args.getBeanConverter().isEmpty());
76  
77  		// Should have no custom message
78  		assertNull(args.getMessage());
79  	}
80  
81  	@Test
82  	void a02_fluentAPIReturnsThis() {
83  		var args = new AssertionArgs();
84  		var mockConverter = createMockConverter();
85  
86  		// Fluent methods should return the same instance
87  		assertSame(args, args.setBeanConverter(mockConverter));
88  		assertSame(args, args.setMessage("test message"));
89  		assertSame(args, args.setMessage(() -> "dynamic message"));
90  	}
91  
92  	@Test
93  	void b01_beanConverterConfiguration() {
94  		var args = new AssertionArgs();
95  		var mockConverter = createMockConverter();
96  
97  		// Initially empty
98  		assertTrue(args.getBeanConverter().isEmpty());
99  
100 		// Set converter
101 		args.setBeanConverter(mockConverter);
102 		assertTrue(args.getBeanConverter().isPresent());
103 		assertSame(mockConverter, args.getBeanConverter().get());
104 
105 		// Set to null should clear
106 		args.setBeanConverter(null);
107 		assertTrue(args.getBeanConverter().isEmpty());
108 	}
109 
110 	@Test
111 	void b02_customConverterInAssertion() {
112 		// Create a mock custom converter for testing
113 		var customConverter = createCustomConverter();
114 
115 		var args = args().setBeanConverter(customConverter);
116 		var obj = new TestBeanWithCustomObject("test", new CustomObject("value"));
117 
118 		// Should use custom converter for stringification
119 		assertBean(args, obj, "custom", "CUSTOM:value");
120 	}
121 
122 	static class TestBeanWithCustomObject {
123 		private String name;
124 		private CustomObject custom;
125 
126 		public TestBeanWithCustomObject(String name, CustomObject custom) {
127 			this.name = name;
128 			this.custom = custom;
129 		}
130 
131 		public String getName() { return name; }
132 		public CustomObject getCustom() { return custom; }
133 	}
134 
135 	@Test
136 	void c01_messageSupplierConfiguration() {
137 		var args = new AssertionArgs();
138 
139 		// Initially null
140 		assertNull(args.getMessage());
141 
142 		// Set supplier
143 		Supplier<String> supplier = () -> "test message";
144 		args.setMessage(supplier);
145 		assertNotNull(args.getMessage());
146 		assertEquals("test message", args.getMessage().get());
147 
148 		// Set different supplier
149 		args.setMessage(() -> "different message");
150 		assertEquals("different message", args.getMessage().get());
151 	}
152 
153 	@Test
154 	void c02_parameterizedMessageConfiguration() {
155 		var args = new AssertionArgs();
156 
157 		// Simple parameter substitution
158 		args.setMessage("Hello {0}", "World");
159 		assertEquals("Hello World", args.getMessage().get());
160 
161 		// Multiple parameters
162 		args.setMessage("User {0} has {1} points", "John", 100);
163 		assertEquals("User John has 100 points", args.getMessage().get());
164 
165 		// Number formatting
166 		args.setMessage("Value: {0,number,#.##}", 123.456);
167 		assertEquals("Value: 123.46", args.getMessage().get());
168 	}
169 
170 	@Test
171 	void c03_dynamicMessageSupplier() {
172 		var counter = new int[1]; // Mutable counter for testing
173 		var args = new AssertionArgs();
174 
175 		args.setMessage(() -> "Call #" + (++counter[0]));
176 
177 		// Each call should increment the counter
178 		assertEquals("Call #1", args.getMessage().get());
179 		assertEquals("Call #2", args.getMessage().get());
180 		assertEquals("Call #3", args.getMessage().get());
181 	}
182 
183 	@Test
184 	void d01_messageCompositionWithoutCustomMessage() {
185 		var args = new AssertionArgs();
186 
187 		// No custom message, should return assertion message as-is
188 		var composedMessage = args.getMessage("Bean assertion failed");
189 		assertEquals("Bean assertion failed", composedMessage.get());
190 
191 		// With parameters
192 		var composedWithParams = args.getMessage("Element at index {0} did not match", 5);
193 		assertEquals("Element at index 5 did not match", composedWithParams.get());
194 	}
195 
196 	@Test
197 	void d02_messageCompositionWithCustomMessage() {
198 		var args = new AssertionArgs();
199 		args.setMessage("User validation failed");
200 
201 		// Should compose: custom + assertion
202 		var composedMessage = args.getMessage("Bean assertion failed");
203 		assertEquals("User validation failed, Caused by: Bean assertion failed", composedMessage.get());
204 
205 		// With parameters in assertion message
206 		var composedWithParams = args.getMessage("Element at index {0} did not match", 3);
207 		assertEquals("User validation failed, Caused by: Element at index 3 did not match", composedWithParams.get());
208 	}
209 
210 	@Test
211 	void d03_messageCompositionWithParameterizedCustomMessage() {
212 		var args = new AssertionArgs();
213 		args.setMessage("Test {0} failed on iteration {1}", "UserValidation", 42);
214 
215 		var composedMessage = args.getMessage("Bean assertion failed");
216 		assertEquals("Test UserValidation failed on iteration 42, Caused by: Bean assertion failed", composedMessage.get());
217 	}
218 
219 	@Test
220 	void d04_messageCompositionWithDynamicCustomMessage() {
221 		var timestamp = Instant.now().toString();
222 		var args = new AssertionArgs();
223 		args.setMessage(() -> "Test failed at " + timestamp);
224 
225 		var composedMessage = args.getMessage("Bean assertion failed");
226 		assertEquals("Test failed at " + timestamp + ", Caused by: Bean assertion failed", composedMessage.get());
227 	}
228 
229 	@Test
230 	void e01_fluentConfigurationChaining() {
231 		var converter = createMockConverter();
232 
233 		// Chain multiple configurations
234 		var args = new AssertionArgs()
235 			.setBeanConverter(converter)
236 			.setMessage("Integration test failed for module {0}", "AuthModule");
237 
238 		// Verify both configurations applied
239 		assertTrue(args.getBeanConverter().isPresent());
240 		assertSame(converter, args.getBeanConverter().get());
241 		assertEquals("Integration test failed for module AuthModule", args.getMessage().get());
242 	}
243 
244 	@Test
245 	void e02_configurationOverwriting() {
246 		var args = new AssertionArgs();
247 		var converter1 = createMockConverter();
248 		var converter2 = createMockConverter();
249 
250 		// Set initial values
251 		args.setBeanConverter(converter1).setMessage("First message");
252 
253 		// Overwrite with new values
254 		args.setBeanConverter(converter2).setMessage("Second message");
255 
256 		// Should have latest values
257 		assertSame(converter2, args.getBeanConverter().get());
258 		assertEquals("Second message", args.getMessage().get());
259 	}
260 
261 	@Test
262 	void f01_integrationWithAssertBean() {
263 		var bean = new TestBean("John", 30, true);
264 		var args = args().setMessage("User test failed");
265 
266 		// Should work with custom message
267 		assertBean(args, bean, "name,age,active", "John,30,true");
268 
269 		// Test assertion failure message composition
270 		var exception = assertThrows(AssertionError.class, () -> {
271 			assertBean(args, bean, "name", "Jane");
272 		});
273 
274 		assertTrue(exception.getMessage().contains("User test failed"));
275 		assertTrue(exception.getMessage().contains("Caused by:"));
276 	}
277 
278 	@Test
279 	void f02_integrationWithAssertBeans() {
280 		var beans = List.of(
281 			new TestBean("Alice", 25, true),
282 			new TestBean("Bob", 35, false)
283 		);
284 		var args = args().setMessage("Batch validation failed");
285 
286 		// Should work with custom message
287 		assertBeans(args, beans, "name,age", "Alice,25", "Bob,35");
288 
289 		// Test assertion failure message composition
290 		var exception = assertThrows(AssertionError.class, () -> {
291 			assertBeans(args, beans, "name", "Charlie", "David");
292 		});
293 
294 		assertTrue(exception.getMessage().contains("Batch validation failed"));
295 		assertTrue(exception.getMessage().contains("Caused by:"));
296 	}
297 
298 	@Test
299 	void f03_integrationWithAssertList() {
300 		var list = List.of("apple", "banana", "cherry");
301 		var args = args().setMessage("List validation failed");
302 
303 		// Should work with custom message
304 		assertList(args, list, "apple", "banana", "cherry");
305 
306 		// Test assertion failure message composition
307 		var exception = assertThrows(AssertionError.class, () -> {
308 			assertList(args, list, "orange", "banana", "cherry");
309 		});
310 
311 		assertTrue(exception.getMessage().contains("List validation failed"));
312 		assertTrue(exception.getMessage().contains("Caused by:"));
313 	}
314 
315 	@Test
316 	void g01_edgeCaseNullValues() {
317 		var args = new AssertionArgs();
318 
319 		// Null converter should work
320 		args.setBeanConverter(null);
321 		assertTrue(args.getBeanConverter().isEmpty());
322 
323 		// Null message supplier should work
324 		args.setMessage((Supplier<String>) null);
325 		assertNull(args.getMessage());
326 	}
327 
328 	@Test
329 	void g02_edgeCaseEmptyMessages() {
330 		var args = new AssertionArgs();
331 
332 		// Empty string message
333 		args.setMessage("");
334 		assertEquals("", args.getMessage().get());
335 
336 		// Empty supplier result
337 		args.setMessage(() -> "");
338 		assertEquals("", args.getMessage().get());
339 
340 		// Composition with empty custom message
341 		var composedMessage = args.getMessage("Bean assertion failed");
342 		assertEquals(", Caused by: Bean assertion failed", composedMessage.get());
343 	}
344 
345 	@Test
346 	void g03_edgeCaseComplexParameterFormatting() {
347 		var args = new AssertionArgs();
348 		var date = new Date();
349 
350 		// Date formatting
351 		args.setMessage("Test executed on {0,date,short}", date);
352 		var expectedDatePart = DateFormat.getDateInstance(DateFormat.SHORT).format(date);
353 		assertTrue(args.getMessage().get().contains(expectedDatePart));
354 
355 		// Complex number formatting
356 		args.setMessage("Processing {0,number,percent} complete", 0.85);
357 		assertTrue(args.getMessage().get().contains("85%"));
358 	}
359 
360 	@Test
361 	void h01_threadSafetyDocumentationCompliance() {
362 		// This test documents that AssertionArgs is NOT thread-safe
363 		// Each thread should create its own instance
364 
365 		var sharedArgs = new AssertionArgs();
366 		var results = Collections.synchronizedList(new ArrayList<String>());
367 
368 		// Simulate multiple threads modifying the same instance
369 		var threads = new Thread[5];
370 		for (int i = 0; i < threads.length; i++) {
371 			final int threadId = i;
372 			threads[i] = new Thread(() -> {
373 				sharedArgs.setMessage("Thread " + threadId + " message");
374 				// Small delay to increase chance of race condition
375 				try { Thread.sleep(1); } catch (InterruptedException e) {}
376 				results.add(sharedArgs.getMessage().get());
377 			});
378 		}
379 
380 		// Start all threads
381 		for (var thread : threads) {
382 			thread.start();
383 		}
384 
385 		// Wait for completion
386 		for (var thread : threads) {
387 			try { thread.join(); } catch (InterruptedException e) {}
388 		}
389 
390 		// Due to race conditions, we may not get the expected messages
391 		// This demonstrates why each test should create its own instance
392 		assertEquals(5, results.size());
393 		// Note: We don't assert specific values due to race conditions
394 	}
395 
396 	@Test
397 	void h02_recommendedUsagePattern() {
398 		// Demonstrate the recommended pattern: create new instance per test
399 
400 		// Test 1: User validation
401 		var userArgs = args().setMessage("User validation test");
402 		var user = new TestBean("Alice", 25, true);
403 		assertBean(userArgs, user, "name,active", "Alice,true");
404 
405 		// Test 2: Product validation (separate instance)
406 		var productArgs = args().setMessage("Product validation test");
407 		var products = List.of("Laptop", "Phone", "Tablet");
408 		assertList(productArgs, products, "Laptop", "Phone", "Tablet");
409 
410 		// Each test has its own configuration without interference
411 		assertEquals("User validation test", userArgs.getMessage().get());
412 		assertEquals("Product validation test", productArgs.getMessage().get());
413 	}
414 
415 	// Helper method to create a mock converter for testing
416 	private BeanConverter createMockConverter() {
417 		return new BeanConverter() {
418 			@Override
419 			public String stringify(Object o) {
420 				return String.valueOf(o);
421 			}
422 
423 			@Override
424 			public List<Object> listify(Object o) {
425 				if (o instanceof List) return (List<Object>) o;
426 				return List.of(o);
427 			}
428 
429 			@Override
430 			public boolean canListify(Object o) {
431 				return true;
432 			}
433 
434 			@Override
435 			public Object swap(Object o) {
436 				return o;
437 			}
438 
439 			@Override
440 			public Object getProperty(Object object, String name) {
441 				// Simple mock implementation
442 				if ("name".equals(name) && object instanceof TestBean) {
443 					return ((TestBean) object).getName();
444 				}
445 				if ("custom".equals(name) && object instanceof TestBeanWithCustomObject) {
446 					return ((TestBeanWithCustomObject) object).getCustom();
447 				}
448 				return null;
449 			}
450 
451 			@Override
452 			public <T> T getSetting(String key, T defaultValue) {
453 				return defaultValue;
454 			}
455 
456 			@Override
457 			public String getNested(Object o, NestedTokenizer.Token token) {
458 				var propValue = getProperty(o, token.getValue());
459 				return stringify(propValue);
460 			}
461 		};
462 	}
463 
464 	// Helper method to create a custom converter for testing
465 	private BeanConverter createCustomConverter() {
466 		return new BeanConverter() {
467 			@Override
468 			public String stringify(Object o) {
469 				if (o instanceof CustomObject) {
470 					return "CUSTOM:" + ((CustomObject) o).getValue();
471 				}
472 				return String.valueOf(o);
473 			}
474 
475 			@Override
476 			public List<Object> listify(Object o) {
477 				if (o instanceof List) return (List<Object>) o;
478 				return List.of(o);
479 			}
480 
481 			@Override
482 			public boolean canListify(Object o) {
483 				return true;
484 			}
485 
486 			@Override
487 			public Object swap(Object o) {
488 				return o;
489 			}
490 
491 			@Override
492 			public Object getProperty(Object object, String name) {
493 				if ("custom".equals(name) && object instanceof TestBeanWithCustomObject) {
494 					return ((TestBeanWithCustomObject) object).getCustom();
495 				}
496 				return null;
497 			}
498 
499 			@Override
500 			public <T> T getSetting(String key, T defaultValue) {
501 				return defaultValue;
502 			}
503 
504 			@Override
505 			public String getNested(Object o, NestedTokenizer.Token token) {
506 				var propValue = getProperty(o, token.getValue());
507 				return stringify(propValue);
508 			}
509 		};
510 	}
511 }