Skip to content

Commit 2f40685

Browse files
authored
Merge pull request #1 from thecoder8890/update-spring-and-tests
feat: Update Spring Boot, Java version, and add tests
2 parents 960900c + 74ebe5b commit 2f40685

28 files changed

+472
-12
lines changed

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
# QR-code Generator and Reader
22

3-
## Application used [Java 15](https://onurdesk.com/what-are-preview-features-in-java-15/) | [Onurdesk](https://onurdesk.com/)
3+
## Application used [Java 17](https://onurdesk.com/what-are-preview-features-in-java-17/) and Spring Boot 3.5.0 | [Onurdesk](https://onurdesk.com/)
44

5-
###### Spring boot application exposing REST API endpoint to genrate QR-code representing custom message and another endpoint to read the decoded message, built using Java [Spring boot](https://onurdesk.com/category/spring/spring-boot/) and [google's zxing library](https://opensource.google/projects/zxing).
5+
###### Spring boot application exposing REST API endpoint to genrate QR-code representing custom message and another endpoint to read the decoded message, built using Java, [Spring Boot 3.5.0](https://spring.io/projects/spring-boot/) and [google's zxing library](https://opensource.google/projects/zxing).
66

77
<center>
88
<a target='_blank' href='https://spring-boot-qr-code-generator.herokuapp.com/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config'>Running Application</a>
99
</center>
1010

1111
## Local Setup
1212

13-
* Install [Java 15](https://onurdesk.com/what-are-preview-features-in-java-15/)
13+
* Install [Java 17](https://onurdesk.com/what-are-preview-features-in-java-17/)
1414
* Install [Maven](https://onurdesk.com/what-is-maven-plugin/)
1515

1616
Recommended way is to use [sdkman](https://sdkman.io/) for installing both maven and java
@@ -33,3 +33,27 @@ Go to the below url to view swagger-ui (API docs)
3333
```
3434
http://localhost:9090/swagger-ui.html
3535
```
36+
37+
## Testing
38+
39+
The core QR code generation and reading functionalities are tested in `src/test/java/com/onurdesk/iris/service/QrCodeServiceTests.java`. These tests cover:
40+
41+
* **Positive Scenarios:**
42+
* Successful QR code generation with valid text input.
43+
* Successful reading and decoding of a valid QR code image.
44+
* **Negative Scenarios:**
45+
* Attempting QR code generation with null or invalid DTO.
46+
* Handling of empty title during QR code generation.
47+
* Attempting to read invalid image files (not images or not QR codes).
48+
* Attempting to read QR codes with unexpected content (not deserializable to the expected DTO).
49+
* Handling I/O exceptions during file reading.
50+
51+
### Running Tests
52+
53+
You can run the tests using Maven:
54+
55+
```bash
56+
mvn test
57+
```
58+
59+
Alternatively, running `mvn clean install` will also execute the tests as part of the build lifecycle.

pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<parent>
77
<groupId>org.springframework.boot</groupId>
88
<artifactId>spring-boot-starter-parent</artifactId>
9-
<version>2.5.0</version>
9+
<version>3.5.0</version>
1010
<relativePath /> <!-- lookup parent from repository -->
1111
</parent>
1212
<groupId>com.onurdesk</groupId>
@@ -15,7 +15,7 @@
1515
<name>spring-boot-qr-code-generator-reader</name>
1616
<description>Spring Boot Application exposing REST APi endpoints to generate QR code representing custom messages and another endpoint to read it.</description>
1717
<properties>
18-
<java.version>15</java.version>
18+
<java.version>17</java.version>
1919
</properties>
2020
<dependencies>
2121
<dependency>
@@ -43,8 +43,8 @@
4343
</dependency>
4444
<dependency>
4545
<groupId>org.springdoc</groupId>
46-
<artifactId>springdoc-openapi-ui</artifactId>
47-
<version>1.5.8</version>
46+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
47+
<version>2.5.0</version>
4848
</dependency>
4949
<dependency>
5050
<groupId>org.springframework.boot</groupId>

src/main/java/com/onurdesk/iris/controller/QrCodeController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import java.io.IOException;
44

5-
import javax.servlet.http.HttpServletResponse;
6-
import javax.validation.Valid;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import jakarta.validation.Valid;
77

88
import org.springframework.http.HttpStatus;
99
import org.springframework.http.ResponseEntity;

src/main/java/com/onurdesk/iris/dto/QrCodeGenerationRequestDto.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.onurdesk.iris.dto;
22

3-
import javax.validation.constraints.NotBlank;
4-
import javax.validation.constraints.Size;
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
55

66
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
77

src/main/java/com/onurdesk/iris/service/QrCodeService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import java.io.IOException;
66

77
import javax.imageio.ImageIO;
8-
import javax.servlet.http.HttpServletResponse;
8+
import jakarta.servlet.http.HttpServletResponse;
99

1010
import org.springframework.http.HttpHeaders;
1111
import org.springframework.http.ResponseEntity;
@@ -42,6 +42,7 @@ public void generate(final QrCodeGenerationRequestDto qrCodeGenerationRequestDto
4242
final HttpServletResponse httpServletResponse) throws IOException, WriterException {
4343
httpServletResponse.setHeader(HttpHeaders.CONTENT_DISPOSITION,
4444
"attachment;filename=" + qrCodeGenerationRequestDto.getTitle().trim().replace(" ", "_") + ".png");
45+
httpServletResponse.setContentType("image/png"); // Explicitly set content type
4546

4647
final var outputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
4748
QRCodeWriter writer = new QRCodeWriter();
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.onurdesk.iris.service;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.google.zxing.BarcodeFormat;
5+
import com.google.zxing.WriterException;
6+
import com.google.zxing.client.j2se.MatrixToImageWriter;
7+
import com.google.zxing.common.BitMatrix;
8+
import com.google.zxing.qrcode.QRCodeWriter;
9+
import com.onurdesk.iris.dto.QrCodeGenerationRequestDto;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.mockito.ArgumentCaptor;
14+
import org.mockito.InjectMocks;
15+
import org.mockito.Mock;
16+
import org.mockito.MockedStatic;
17+
import org.mockito.Mockito;
18+
import org.mockito.junit.jupiter.MockitoExtension;
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.http.HttpStatus;
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.mock.web.MockHttpServletResponse;
23+
import org.springframework.web.multipart.MultipartFile;
24+
25+
import javax.imageio.ImageIO;
26+
import jakarta.servlet.ServletOutputStream;
27+
import java.awt.image.BufferedImage;
28+
import java.io.ByteArrayInputStream;
29+
import java.io.ByteArrayOutputStream;
30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
33+
import static org.junit.jupiter.api.Assertions.*;
34+
import static org.mockito.ArgumentMatchers.any;
35+
import static org.mockito.ArgumentMatchers.anyInt;
36+
import static org.mockito.ArgumentMatchers.anyString;
37+
import static org.mockito.Mockito.*;
38+
39+
@ExtendWith(MockitoExtension.class)
40+
class QrCodeServiceTests {
41+
42+
@InjectMocks
43+
private QrCodeService qrCodeService;
44+
45+
private QrCodeGenerationRequestDto sampleDto;
46+
private ObjectMapper objectMapper = new ObjectMapper();
47+
48+
@BeforeEach
49+
void setUp() {
50+
sampleDto = QrCodeGenerationRequestDto.builder()
51+
.title("Test QR")
52+
.message("This is a test payload") // Assuming 'payload' maps to 'message' in DTO based on schema
53+
.generatedByName("JUnit") // Assuming 'generatedBy' maps to 'generatedByName'
54+
.generatedForName("Test Target") // Adding a value for this field
55+
.build();
56+
}
57+
58+
@Test
59+
void testGenerateQrCode_success() throws IOException, WriterException {
60+
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
61+
62+
qrCodeService.generate(sampleDto, mockResponse);
63+
64+
assertEquals("attachment;filename=Test_QR.png", mockResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION));
65+
assertEquals("image/png", mockResponse.getContentType()); // MatrixToImageWriter sets this implicitly
66+
67+
// Verify the output stream contains PNG data (basic check for non-empty)
68+
assertTrue(mockResponse.getContentAsByteArray().length > 0);
69+
70+
// Further verification could involve trying to read the byte array as an image
71+
// and potentially decoding it, but that might be too much for a unit test.
72+
// For now, we trust that if MatrixToImageWriter.writeToStream ran without error
73+
// and produced bytes, it's likely correct.
74+
75+
// We can also try to verify the content of the QR code if we mock the writer part.
76+
// Let's try to capture the string passed to the QRCodeWriter.encode
77+
// This requires QRCodeWriter to be a mock or using a static mock for MatrixToImageWriter
78+
// For simplicity, the current check on headers and non-empty output is a good start.
79+
}
80+
81+
@Test
82+
void testGenerateQrCode_nullDto() {
83+
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
84+
assertThrows(NullPointerException.class, () -> {
85+
// The ObjectMapper().writeValueAsString(null) will throw NullPointerException
86+
qrCodeService.generate(null, mockResponse);
87+
});
88+
}
89+
90+
@Test
91+
void testGenerateQrCode_emptyTitleInDto() throws IOException, WriterException {
92+
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
93+
QrCodeGenerationRequestDto dtoWithEmptyTitle = QrCodeGenerationRequestDto.builder()
94+
.title("") // Empty title
95+
.message("Some payload")
96+
.generatedByName("JUnit")
97+
.generatedForName("Test Target")
98+
.build();
99+
100+
qrCodeService.generate(dtoWithEmptyTitle, mockResponse);
101+
102+
// Expecting "attachment;filename=.png" or similar, depending on implementation logic for empty title
103+
assertEquals("attachment;filename=.png", mockResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION));
104+
assertTrue(mockResponse.getContentAsByteArray().length > 0);
105+
}
106+
107+
108+
@Test
109+
void testReadQrCode_success() throws Exception {
110+
// 1. Prepare a valid QR code image as byte array
111+
String originalContent = objectMapper.writeValueAsString(sampleDto);
112+
QRCodeWriter qrCodeWriter = new QRCodeWriter();
113+
BitMatrix bitMatrix = qrCodeWriter.encode(originalContent, BarcodeFormat.QR_CODE, 200, 200);
114+
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
115+
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
116+
byte[] qrCodeBytes = pngOutputStream.toByteArray();
117+
118+
// 2. Mock MultipartFile
119+
MultipartFile multipartFile = mock(MultipartFile.class);
120+
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(qrCodeBytes));
121+
122+
// 3. Call read method
123+
ResponseEntity<?> responseEntity = qrCodeService.read(multipartFile);
124+
125+
// 4. Assertions
126+
assertNotNull(responseEntity);
127+
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
128+
assertTrue(responseEntity.getBody() instanceof QrCodeGenerationRequestDto);
129+
QrCodeGenerationRequestDto resultDto = (QrCodeGenerationRequestDto) responseEntity.getBody();
130+
assertEquals(sampleDto.getTitle(), resultDto.getTitle());
131+
assertEquals(sampleDto.getMessage(), resultDto.getMessage()); // Changed from getPayload to getMessage
132+
assertEquals(sampleDto.getGeneratedByName(), resultDto.getGeneratedByName()); // Changed from getGeneratedBy
133+
}
134+
135+
@Test
136+
void testReadQrCode_invalidImageFormat() throws IOException {
137+
MultipartFile multipartFile = mock(MultipartFile.class);
138+
// Simulate a file that is not a valid image (e.g., random bytes)
139+
byte[] invalidImageBytes = "This is not an image".getBytes();
140+
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(invalidImageBytes));
141+
142+
// ImageIO.read is expected to return null for non-image formats it doesn't understand
143+
// which will then cause NullPointerException in BufferedImageLuminanceSource constructor
144+
assertThrows(NullPointerException.class, () -> {
145+
qrCodeService.read(multipartFile);
146+
}, "Should throw NullPointerException when ImageIO.read returns null for invalid image format that is not decodable by ImageIO");
147+
}
148+
149+
@Test
150+
void testReadQrCode_notAQrCode() throws IOException {
151+
// Create a valid PNG image, but not a QR code (e.g., a blank image)
152+
BufferedImage blankImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
153+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
154+
ImageIO.write(blankImage, "png", baos);
155+
byte[] blankImageBytes = baos.toByteArray();
156+
157+
MultipartFile multipartFile = mock(MultipartFile.class);
158+
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(blankImageBytes));
159+
160+
// Expect NotFoundException because MultiFormatReader won't find a QR code
161+
assertThrows(com.google.zxing.NotFoundException.class, () -> {
162+
qrCodeService.read(multipartFile);
163+
});
164+
}
165+
166+
@Test
167+
void testReadQrCode_ioExceptionOnInputStream() throws IOException {
168+
MultipartFile multipartFile = mock(MultipartFile.class);
169+
when(multipartFile.getInputStream()).thenThrow(new IOException("Failed to read input stream"));
170+
171+
assertThrows(IOException.class, () -> {
172+
qrCodeService.read(multipartFile);
173+
});
174+
}
175+
176+
@Test
177+
void testReadQrCode_unexpectedContent() throws Exception {
178+
// 1. Prepare a QR code with content that is not a valid JSON for QrCodeGenerationRequestDto
179+
String nonJsonContent = "Just some plain text, not JSON";
180+
QRCodeWriter qrCodeWriter = new QRCodeWriter();
181+
BitMatrix bitMatrix = qrCodeWriter.encode(nonJsonContent, BarcodeFormat.QR_CODE, 200, 200);
182+
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
183+
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
184+
byte[] qrCodeBytes = pngOutputStream.toByteArray();
185+
186+
// 2. Mock MultipartFile
187+
MultipartFile multipartFile = mock(MultipartFile.class);
188+
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(qrCodeBytes));
189+
190+
// 3. Call read method and expect a Jackson mapping/parsing exception
191+
// The service tries to map result.getText() to QrCodeGenerationRequestDto.class
192+
// This will fail if the text is not a JSON representation of that DTO.
193+
assertThrows(com.fasterxml.jackson.core.JsonProcessingException.class, () -> { // Changed to broader JsonProcessingException
194+
qrCodeService.read(multipartFile);
195+
});
196+
}
197+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"groups": [
3+
{
4+
"name": "com.onurdesk.iris.swagger",
5+
"type": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties",
6+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties"
7+
},
8+
{
9+
"name": "com.onurdesk.iris.swagger.properties",
10+
"type": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties",
11+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties",
12+
"sourceMethod": "getProperties()"
13+
},
14+
{
15+
"name": "com.onurdesk.iris.swagger.properties.contact",
16+
"type": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact",
17+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties",
18+
"sourceMethod": "getContact()"
19+
}
20+
],
21+
"properties": [
22+
{
23+
"name": "com.onurdesk.iris.swagger.properties.api-version",
24+
"type": "java.lang.String",
25+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties"
26+
},
27+
{
28+
"name": "com.onurdesk.iris.swagger.properties.contact.email",
29+
"type": "java.lang.String",
30+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact"
31+
},
32+
{
33+
"name": "com.onurdesk.iris.swagger.properties.contact.name",
34+
"type": "java.lang.String",
35+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact"
36+
},
37+
{
38+
"name": "com.onurdesk.iris.swagger.properties.contact.url",
39+
"type": "java.lang.String",
40+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact"
41+
},
42+
{
43+
"name": "com.onurdesk.iris.swagger.properties.description",
44+
"type": "java.lang.String",
45+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties"
46+
},
47+
{
48+
"name": "com.onurdesk.iris.swagger.properties.title",
49+
"type": "java.lang.String",
50+
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties"
51+
}
52+
],
53+
"hints": [],
54+
"ignored": {
55+
"properties": []
56+
}
57+
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)