View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2012-2026 hazendaz
6    *
7    * Portions of initial baseline code (getter/setter test) by Rob Dawson (CodeBox)
8    */
9   package com.codebox.bean;
10  
11  import java.lang.reflect.Constructor;
12  import java.lang.reflect.InvocationTargetException;
13  import java.lang.reflect.Method;
14  
15  import lombok.Data;
16  
17  import org.junit.jupiter.api.Assertions;
18  import org.junit.jupiter.api.Test;
19  
20  /**
21   * The Class ByteBuddyBeanCopierTest.
22   */
23  class ByteBuddyBeanCopierTest {
24  
25      /**
26       * The Class SourceBean.
27       */
28      @Data
29      static class SourceBean {
30          /** The name. */
31          private String name;
32  
33          /** The age. */
34          private int age;
35  
36          /** The active. */
37          private boolean active;
38  
39          /** The enabled. */
40          private Boolean enabled;
41      }
42  
43      /**
44       * The Class TargetBean.
45       */
46      @Data
47      static class TargetBean {
48          /** The name. */
49          private String name;
50  
51          /** The age. */
52          private int age;
53  
54          /** The active. */
55          private boolean active;
56  
57          /** The enabled. */
58          private Boolean enabled;
59      }
60  
61      /**
62       * Test copy null source or target.
63       */
64      @Test
65      void testCopyNullSourceOrTarget() {
66          var target = new TargetBean();
67          Assertions.assertDoesNotThrow(() -> ByteBuddyBeanCopier.copy(null, target, null));
68          Assertions.assertDoesNotThrow(() -> ByteBuddyBeanCopier.copy(new SourceBean(), null, null));
69      }
70  
71      @Test
72      void testCopyPropertiesAndConverter() {
73          var source = new SourceBean();
74          source.setName("name");
75          source.setAge(42);
76          source.setActive(true);
77          source.setEnabled(Boolean.TRUE);
78  
79          var target = new SourceBean();
80  
81          ByteBuddyBeanCopier.copy(source, target, (value, targetType) -> Boolean.FALSE);
82  
83          Assertions.assertEquals("name", target.getName());
84          Assertions.assertEquals(42, target.getAge());
85          Assertions.assertFalse(target.isActive());
86          Assertions.assertFalse(target.getEnabled());
87      }
88  
89      @Test
90      void testCopySkipsPrimitiveBooleanWhenNoGetter() {
91          var source = new PrimitiveSetterOnlySource();
92          source.setActive(false);
93  
94          var target = new PrimitiveSetterOnlySource();
95          target.setActive(true);
96  
97          ByteBuddyBeanCopier.copy(source, target, null);
98  
99          Assertions.assertTrue(target.active());
100     }
101 
102     @Test
103     void testCopySkipsPrimitiveBooleanWhenFallbackIsMethodIsNotBoolean() {
104         var source = new NonBooleanIsMethodSource();
105         source.setActive(false);
106 
107         var target = new NonBooleanIsMethodSource();
108         target.setActive(true);
109 
110         ByteBuddyBeanCopier.copy(source, target, null);
111 
112         Assertions.assertTrue(target.active());
113     }
114 
115     @Test
116     void testCopyWrapsGetterInvocationFailures() {
117         var source = new FailingGetterSource();
118         var target = new FailingGetterSource();
119 
120         RuntimeException exception = Assertions.assertThrows(RuntimeException.class,
121                 () -> ByteBuddyBeanCopier.copy(source, target, null));
122         Assertions.assertTrue(exception.getMessage().startsWith("Failed to copy property"));
123     }
124 
125     @Test
126     void testCopyUsesBooleanIsFallbackGetter() {
127         var source = new BooleanIsFallbackSource();
128         source.setActive(Boolean.TRUE);
129 
130         var target = new BooleanIsFallbackSource();
131         ByteBuddyBeanCopier.copy(source, target, null);
132 
133         Assertions.assertTrue(target.isActive());
134     }
135 
136     @Test
137     void testUtilityConstructorThrowsAssertionError() throws Exception {
138         Constructor<ByteBuddyBeanCopier> constructor = ByteBuddyBeanCopier.class.getDeclaredConstructor();
139         constructor.setAccessible(true);
140 
141         InvocationTargetException exception = Assertions.assertThrows(InvocationTargetException.class,
142                 constructor::newInstance);
143         Assertions.assertInstanceOf(AssertionError.class, exception.getCause());
144     }
145 
146     @Test
147     void testExtractPropertyNameRejectsNonGetterMethod() throws Exception {
148         Method extract = ByteBuddyBeanCopier.class.getDeclaredMethod("extractPropertyName", Method.class);
149         extract.setAccessible(true);
150 
151         Method nonGetter = NonGetterMethodBean.class.getMethod("name");
152         InvocationTargetException exception = Assertions.assertThrows(InvocationTargetException.class,
153                 () -> extract.invoke(null, nonGetter));
154         Assertions.assertInstanceOf(IllegalArgumentException.class, exception.getCause());
155     }
156 
157     @Test
158     void testCopyUsesBooleanFallbackWithMismatchedSetterPropertyName() {
159         var source = new BooleanFallbackMismatchBean();
160         source.setuRL(true);
161 
162         var target = new BooleanFallbackMismatchBean();
163         ByteBuddyBeanCopier.copy(source, target, null);
164 
165         Assertions.assertTrue(target.isURL());
166     }
167 
168     static class PrimitiveSetterOnlySource {
169         private boolean active;
170 
171         public void setActive(boolean active) {
172             this.active = active;
173         }
174 
175         public boolean active() {
176             return this.active;
177         }
178     }
179 
180     static class NonBooleanIsMethodSource {
181         private boolean active;
182 
183         public void setActive(boolean active) {
184             this.active = active;
185         }
186 
187         public boolean active() {
188             return this.active;
189         }
190 
191         public String isActive() {
192             return "not-boolean";
193         }
194     }
195 
196     static class BooleanIsFallbackSource {
197         private Boolean active;
198 
199         public void setActive(Boolean active) {
200             this.active = active;
201         }
202 
203         public boolean isActive() {
204             return Boolean.TRUE.equals(this.active);
205         }
206     }
207 
208     static class NonGetterMethodBean {
209         public String name() {
210             return "value";
211         }
212     }
213 
214     static class BooleanFallbackMismatchBean {
215         private boolean url;
216 
217         @SuppressWarnings("java:S100")
218         public void setuRL(final boolean url) { // Intentional mixed-case setter to exercise fallback property mapping
219             this.url = url;
220         }
221 
222         public boolean isURL() {
223             return this.url;
224         }
225     }
226 
227     /**
228      * Test copy with acronym (double-uppercase) getter covers the decapitalize "all-caps" branch (L157-158).
229      */
230     @Test
231     void testCopyWithAcronymGetter() {
232         var source = new AcronymBean();
233         source.setURL("https://example.com");
234 
235         var target = new AcronymBean();
236         ByteBuddyBeanCopier.copy(source, target, null);
237 
238         Assertions.assertEquals("https://example.com", target.getURL());
239     }
240 
241     /**
242      * Test that a setter named "set" (empty property name) does not throw and exercises the decapitalize/capitalize
243      * empty-string branches (L154-155, L171-172).
244      */
245     @Test
246     void testCopyWithEmptyPropertyName() {
247         var source = new EmptyPropertyNameBean();
248         var target = new EmptyPropertyNameBean();
249         Assertions.assertDoesNotThrow(() -> ByteBuddyBeanCopier.copy(source, target, null));
250     }
251 
252     /**
253      * Test that a void-returning "getter" (not recognised by isGetter) causes the copier to treat the property as
254      * getter-less and exercises the non-boolean setter without getter branch (L65 false, L101 void-getter branch).
255      */
256     @Test
257     void testCopyWithVoidReturningGetterMethod() {
258         var source = new VoidGetterBean();
259         var target = new VoidGetterBean();
260         // Should copy silently: void "getter" not recognised, String setter copies null
261         Assertions.assertDoesNotThrow(() -> ByteBuddyBeanCopier.copy(source, target, null));
262     }
263 
264     /**
265      * Test copy with single-char property name exercises the decapitalize length-1 branch (L157 false branch when
266      * name.length() == 1) and the getXxx/setXxx method named "get" exactly exercises the length > 3 false branch of
267      * isGetter (L101).
268      */
269     @Test
270     void testCopyWithSingleCharAndExactGetProperty() {
271         var source = new SingleCharPropertyBean();
272         source.setX("hello");
273 
274         var target = new SingleCharPropertyBean();
275         ByteBuddyBeanCopier.copy(source, target, null);
276 
277         Assertions.assertEquals("hello", target.getX());
278     }
279 
280     /**
281      * Test copy with a fluent (non-void returning) setter exercises the isSetter false branch for non-void return type
282      * (L113 and L114 non-void branch).
283      */
284     @Test
285     void testCopyWithFluentSetter() {
286         var source = new FluentSetterBean();
287         source.setName("test");
288 
289         var target = new FluentSetterBean();
290         ByteBuddyBeanCopier.copy(source, target, null);
291 
292         // Fluent setter is NOT recognised by isSetter() → name is not copied
293         Assertions.assertNull(target.getName());
294     }
295 
296     static class AcronymBean {
297         /** The url. */
298         private String url;
299 
300         /**
301          * Gets the URL.
302          *
303          * @return the URL
304          */
305         public String getURL() {
306             return this.url;
307         }
308 
309         /**
310          * Sets the URL.
311          *
312          * @param url
313          *            the URL
314          */
315         public void setURL(final String url) {
316             this.url = url;
317         }
318     }
319 
320     /** Bean whose only setter is named "set" producing an empty property name. */
321     static class EmptyPropertyNameBean {
322         /** The val. */
323         @SuppressWarnings({ "unused", "java:S100" })
324         private boolean val;
325 
326         /**
327          * Sets the (empty-property-name setter).
328          *
329          * @param val
330          *            the val
331          */
332         @SuppressWarnings("java:S100")
333         public void set(final boolean val) { // NOSONAR intentional empty-name property for edge-case test
334             this.val = val;
335         }
336     }
337 
338     /** Bean with a void "getter" – not recognised by isGetter, exercises L101 branch and L65 false branch. */
339     static class VoidGetterBean {
340         /** The name. */
341         private String name;
342 
343         /**
344          * Gets the name (void – not a valid getter).
345          */
346         @SuppressWarnings("java:S4144")
347         public void getName() { // NOSONAR intentional void "getter" for branch-coverage test
348             // void return — not recognised as a getter by ByteBuddyBeanCopier.isGetter()
349         }
350 
351         /**
352          * Sets the name.
353          *
354          * @param name
355          *            the name
356          */
357         public void setName(final String name) {
358             this.name = name;
359         }
360     }
361 
362     /**
363      * Bean with a single-char property 'x' and an extra 'get()' method (length 3, not > 3) to exercise the isGetter
364      * length-check false branch (L101 A+B+C=F path).
365      */
366     static class SingleCharPropertyBean {
367         /** The x. */
368         private String x;
369 
370         /**
371          * Gets the x.
372          *
373          * @return the x
374          */
375         public String getX() {
376             return this.x;
377         }
378 
379         /**
380          * Sets the x.
381          *
382          * @param x
383          *            the x
384          */
385         public void setX(final String x) {
386             this.x = x;
387         }
388 
389         /**
390          * A method named "get" exactly (length 3, not > 3) – exercises the isGetter length > 3 false branch (L101).
391          *
392          * @return null
393          */
394         @SuppressWarnings("java:S100")
395         public String get() { // NOSONAR intentional method named "get" for branch-coverage of isGetter
396             return null;
397         }
398     }
399 
400     /** Bean with a fluent (non-void) setter – exercises the isSetter non-void-return false branch (L113-114). */
401     static class FluentSetterBean {
402         /** The name. */
403         private String name;
404 
405         /**
406          * Gets the name.
407          *
408          * @return the name
409          */
410         public String getName() {
411             return this.name;
412         }
413 
414         /**
415          * Sets the name (fluent – returns {@code this}; NOT recognised as a setter by isSetter).
416          *
417          * @param name
418          *            the name
419          * @return this
420          */
421         public FluentSetterBean setName(final String name) {
422             this.name = name;
423             return this;
424         }
425     }
426 
427     static class FailingGetterSource {
428         public void setName(String name) {
429         }
430 
431         public String getName() {
432             throw new IllegalStateException("boom");
433         }
434     }
435 
436 }