Skip to content

Commit 1d60a20

Browse files
committed
Increase test coverage of Donate items resolver & controller, Update architecture MD
1 parent 65de703 commit 1d60a20

4 files changed

Lines changed: 254 additions & 2 deletions

File tree

ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,5 +197,5 @@ The application implements three primary data flow patterns based on the query t
197197

198198
## 6. Test Standards and Metrics
199199
To guarantee build safety and code correctness, the project enforces strict test coverage limits (**Minimum 80% coverage on all modifications**):
200-
- **Java Backend (JUnit 5, Mockito, Jacoco)**: Maintains an overall instruction coverage of **~88%** and line coverage of **~89%**.
200+
- **Java Backend (JUnit 5, Mockito, Jacoco)**: Maintains an overall instruction coverage of **~92%** and line coverage of **~92%**.
201201
- **React Frontend (Jest, React Testing Library)**: Maintains overall statement and line coverage above **~91%**.

frontend/package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/test/java/net/ironoc/portfolio/controller/DonateGraphqlControllerTest.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,110 @@ void testAddCharityOption_DoesNotEmitToSubscriptionWhenNotAdded() {
279279
subscription.dispose();
280280
}
281281

282+
@Test
283+
void testCharityOptionByFounded_NotFound() {
284+
// Arrange
285+
when(donateItemsResolver.getDonateItems()).thenReturn(new ArrayList<>());
286+
287+
// Act
288+
Donate result = donateGraphqlController.charityOptionByFounded(2005);
289+
290+
// Assert
291+
assertThat(result, is(notNullValue()));
292+
assertThat(result.getName(), is(org.hamcrest.Matchers.nullValue()));
293+
}
294+
295+
@Test
296+
void testCharityOptionByDonateLink_NotFound() {
297+
// Arrange
298+
when(donateItemsResolver.getDonateItems()).thenReturn(new ArrayList<>());
299+
300+
// Act
301+
Donate result = donateGraphqlController.charityOptionByDonateLink("https://donate.notfound.com");
302+
303+
// Assert
304+
assertThat(result, is(notNullValue()));
305+
assertThat(result.getName(), is(org.hamcrest.Matchers.nullValue()));
306+
}
307+
308+
@Test
309+
void testCharityOptionByName_NotFound() {
310+
// Arrange
311+
when(donateItemsResolver.getDonateItems()).thenReturn(new ArrayList<>());
312+
313+
// Act
314+
Donate result = donateGraphqlController.charityOptionByName("NotFound");
315+
316+
// Assert
317+
assertThat(result, is(notNullValue()));
318+
assertThat(result.getName(), is(org.hamcrest.Matchers.nullValue()));
319+
}
320+
321+
@Test
322+
void test_mapDonateItemsToCharityOptions_withNullAndEmptyValues() {
323+
// Arrange
324+
List<Map<String, Object>> mockDonateItems = new ArrayList<>();
325+
Map<String, Object> item = new HashMap<>();
326+
item.put("alt", null);
327+
item.put("name", "Charity Nulls");
328+
item.put("link", "https://example.com");
329+
item.put("donate", "https://donate.example.com");
330+
item.put("img", "image.png");
331+
item.put("overview", "An overview");
332+
item.put("founded", 2000);
333+
item.put("phone", null);
334+
mockDonateItems.add(item);
335+
when(donateItemsResolver.getDonateItems()).thenReturn(mockDonateItems);
336+
337+
// Act
338+
Collection<Donate> result = donateGraphqlController.donateItemsSchemaMapping();
339+
340+
// Assert
341+
assertThat(result, is(notNullValue()));
342+
assertThat(result, hasSize(1));
343+
Donate donate = result.iterator().next();
344+
assertThat(donate.getAlt(), is(org.hamcrest.Matchers.nullValue()));
345+
assertThat(donate.getPhone(), is(org.hamcrest.Matchers.nullValue()));
346+
}
347+
348+
@Test
349+
void test_mapDonateItemsToCharityOptions_withNullItemInList() {
350+
// Arrange
351+
List<Map<String, Object>> mockDonateItems = new ArrayList<>();
352+
mockDonateItems.add(null);
353+
when(donateItemsResolver.getDonateItems()).thenReturn(mockDonateItems);
354+
355+
// Act
356+
Collection<Donate> result = donateGraphqlController.donateItemsSchemaMapping();
357+
358+
// Assert
359+
assertThat(result, is(notNullValue()));
360+
assertThat(result, hasSize(0));
361+
}
362+
363+
@Test
364+
void testAddCharityOption_NoSubscribers() {
365+
// Arrange
366+
when(donateItemsResolver.addDonateItem(any(Donate.class))).thenReturn(true);
367+
368+
// Act
369+
Donate result = donateGraphqlController.addCharityOption(
370+
"Alt",
371+
"Charity No Sub",
372+
"https://charity-nosub.org",
373+
"https://charity-nosub.org/donate",
374+
"nosub.png",
375+
"No subscribers",
376+
2023,
377+
"+353123456789"
378+
);
379+
380+
// Assert
381+
assertThat(result, is(notNullValue()));
382+
assertThat(result.getName(), is("Charity No Sub"));
383+
verify(donateItemsResolver, times(1)).addDonateItem(any(Donate.class));
384+
}
385+
282386
@NotNull
283387
private static List<Map<String, Object>> getMockDonateItems() {
284388
List<Map<String, Object>> mockDonateItems = new ArrayList<>();

src/test/java/net/ironoc/portfolio/graph/DonateItemsResolverTest.java

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,153 @@ void testAddDonateItem_DoesNotAddWhenNameAlreadyPresent() {
296296
assertThat(donateItemsResolver.getDonateItems().size(), is(originalSize));
297297
assertThat(donateItemsResolver.getDonateItems(), not(hasItem(equalTo(duplicateDonateMap))));
298298
}
299+
300+
@Test
301+
void testAddDonateItem_DoesNotAddWhenInvalidFields() {
302+
// 1. Invalid alt containing non-alphanumeric chars
303+
Donate invalidAlt = Donate.builder()
304+
.name("The Jack and Jill Children’s Foundation")
305+
.alt("<script>")
306+
.link("https://valid.org")
307+
.donate("https://valid.org/donate")
308+
.img("valid")
309+
.overview("Valid overview")
310+
.founded(2000)
311+
.phone("+3531234567")
312+
.build();
313+
assertThat(donateItemsResolver.addDonateItem(invalidAlt), is(false));
314+
315+
// 2. Invalid link
316+
Donate invalidLink = Donate.builder()
317+
.name("The Jack and Jill Children’s Foundation")
318+
.alt("Valid")
319+
.link("ftp://invalid.org")
320+
.donate("https://valid.org/donate")
321+
.img("valid")
322+
.overview("Valid overview")
323+
.founded(2000)
324+
.phone("+3531234567")
325+
.build();
326+
assertThat(donateItemsResolver.addDonateItem(invalidLink), is(false));
327+
328+
// 3. Invalid founded (too low)
329+
Donate invalidFoundedLow = Donate.builder()
330+
.name("The Jack and Jill Children’s Foundation")
331+
.alt("Valid")
332+
.link("https://valid.org")
333+
.donate("https://valid.org/donate")
334+
.img("valid")
335+
.overview("Valid overview")
336+
.founded(999)
337+
.phone("+3531234567")
338+
.build();
339+
assertThat(donateItemsResolver.addDonateItem(invalidFoundedLow), is(false));
340+
341+
// 4. Invalid founded (too high)
342+
Donate invalidFoundedHigh = Donate.builder()
343+
.name("The Jack and Jill Children’s Foundation")
344+
.alt("Valid")
345+
.link("https://valid.org")
346+
.donate("https://valid.org/donate")
347+
.img("valid")
348+
.overview("Valid overview")
349+
.founded(2200)
350+
.phone("+3531234567")
351+
.build();
352+
assertThat(donateItemsResolver.addDonateItem(invalidFoundedHigh), is(false));
353+
354+
// 5. Invalid phone (too short)
355+
Donate invalidPhone = Donate.builder()
356+
.name("The Jack and Jill Children’s Foundation")
357+
.alt("Valid")
358+
.link("https://valid.org")
359+
.donate("https://valid.org/donate")
360+
.img("valid")
361+
.overview("Valid overview")
362+
.founded(2000)
363+
.phone("123")
364+
.build();
365+
assertThat(donateItemsResolver.addDonateItem(invalidPhone), is(false));
366+
}
367+
368+
@Test
369+
void testHelpers_NullAndCornerCases() throws Exception {
370+
// Verify null check in isAlreadyPresent
371+
java.lang.reflect.Method isAlreadyPresentMethod = DonateItemsResolver.class.getDeclaredMethod("isAlreadyPresent", String.class);
372+
isAlreadyPresentMethod.setAccessible(true);
373+
boolean resultNull = (boolean) isAlreadyPresentMethod.invoke(donateItemsResolver, (String) null);
374+
assertThat(resultNull, is(false));
375+
376+
// Verify null name branch in memory list
377+
java.lang.reflect.Field field = DonateItemsResolver.class.getDeclaredField("donateItems");
378+
field.setAccessible(true);
379+
@SuppressWarnings("unchecked")
380+
List<Donate> internalList = (List<Donate>) field.get(donateItemsResolver);
381+
internalList.add(Donate.builder().name(null).build());
382+
383+
boolean resultWithNullInMemory = (boolean) isAlreadyPresentMethod.invoke(donateItemsResolver, "Some Name");
384+
assertThat(resultWithNullInMemory, is(false));
385+
}
386+
387+
@Test
388+
void testValidation_Directly() throws Exception {
389+
java.lang.reflect.Method isValidDonateMethod = DonateItemsResolver.class.getDeclaredMethod("isValidDonate", Donate.class);
390+
isValidDonateMethod.setAccessible(true);
391+
392+
// Test null phone/email
393+
Donate nullPhone = Donate.builder()
394+
.alt("Valid")
395+
.name("Valid")
396+
.link("https://valid.org")
397+
.donate("https://valid.org/donate")
398+
.img("valid")
399+
.overview("Valid overview")
400+
.founded(2000)
401+
.phone(null)
402+
.build();
403+
boolean resNullPhone = (boolean) isValidDonateMethod.invoke(donateItemsResolver, nullPhone);
404+
assertThat(resNullPhone, is(false));
405+
406+
// Test empty phone/email
407+
Donate emptyPhone = Donate.builder()
408+
.alt("Valid")
409+
.name("Valid")
410+
.link("https://valid.org")
411+
.donate("https://valid.org/donate")
412+
.img("valid")
413+
.overview("Valid overview")
414+
.founded(2000)
415+
.phone(" ")
416+
.build();
417+
boolean resEmptyPhone = (boolean) isValidDonateMethod.invoke(donateItemsResolver, emptyPhone);
418+
assertThat(resEmptyPhone, is(false));
419+
420+
// Test valid email
421+
Donate validEmail = Donate.builder()
422+
.alt("Valid")
423+
.name("Valid")
424+
.link("https://valid.org")
425+
.donate("https://valid.org/donate")
426+
.img("valid")
427+
.overview("Valid overview")
428+
.founded(2000)
429+
.phone("test@charity.org")
430+
.build();
431+
boolean resValidEmail = (boolean) isValidDonateMethod.invoke(donateItemsResolver, validEmail);
432+
assertThat(resValidEmail, is(true));
433+
434+
// Test valid phone with space
435+
Donate validPhoneWithSpace = Donate.builder()
436+
.alt("Valid")
437+
.name("Valid")
438+
.link("https://valid.org")
439+
.donate("https://valid.org/donate")
440+
.img("valid")
441+
.overview("Valid overview")
442+
.founded(2000)
443+
.phone("+353 1 234 5678")
444+
.build();
445+
boolean resValidPhone = (boolean) isValidDonateMethod.invoke(donateItemsResolver, validPhoneWithSpace);
446+
assertThat(resValidPhone, is(true));
447+
}
299448
}

0 commit comments

Comments
 (0)