Skip to content

feat: Update Spring Boot, Java version, and add tests #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# QR-code Generator and Reader

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

###### 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).
###### 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).
Copy link
Preview

Copilot AI Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word 'genrate' is misspelled; consider correcting it to 'generate'.

Suggested change
###### 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).
###### Spring boot application exposing REST API endpoint to generate 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).

Copilot uses AI. Check for mistakes.


<center>
<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>
</center>

## Local Setup

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

Recommended way is to use [sdkman](https://sdkman.io/) for installing both maven and java
Expand All @@ -33,3 +33,27 @@ Go to the below url to view swagger-ui (API docs)
```
http://localhost:9090/swagger-ui.html
```

## Testing

The core QR code generation and reading functionalities are tested in `src/test/java/com/onurdesk/iris/service/QrCodeServiceTests.java`. These tests cover:

* **Positive Scenarios:**
* Successful QR code generation with valid text input.
* Successful reading and decoding of a valid QR code image.
* **Negative Scenarios:**
* Attempting QR code generation with null or invalid DTO.
* Handling of empty title during QR code generation.
* Attempting to read invalid image files (not images or not QR codes).
* Attempting to read QR codes with unexpected content (not deserializable to the expected DTO).
* Handling I/O exceptions during file reading.

### Running Tests

You can run the tests using Maven:

```bash
mvn test
```

Alternatively, running `mvn clean install` will also execute the tests as part of the build lifecycle.
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<version>3.5.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.onurdesk</groupId>
Expand All @@ -15,7 +15,7 @@
<name>spring-boot-qr-code-generator-reader</name>
<description>Spring Boot Application exposing REST APi endpoints to generate QR code representing custom messages and another endpoint to read it.</description>
<properties>
<java.version>15</java.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -43,8 +43,8 @@
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.8</version>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.onurdesk.iris.dto;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

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

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/onurdesk/iris/service/QrCodeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;

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

final var outputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
QRCodeWriter writer = new QRCodeWriter();
Expand Down
197 changes: 197 additions & 0 deletions src/test/java/com/onurdesk/iris/service/QrCodeServiceTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package com.onurdesk.iris.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.onurdesk.iris.dto.QrCodeGenerationRequestDto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import jakarta.servlet.ServletOutputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class QrCodeServiceTests {

@InjectMocks
private QrCodeService qrCodeService;

private QrCodeGenerationRequestDto sampleDto;
private ObjectMapper objectMapper = new ObjectMapper();

@BeforeEach
void setUp() {
sampleDto = QrCodeGenerationRequestDto.builder()
.title("Test QR")
.message("This is a test payload") // Assuming 'payload' maps to 'message' in DTO based on schema
.generatedByName("JUnit") // Assuming 'generatedBy' maps to 'generatedByName'
.generatedForName("Test Target") // Adding a value for this field
.build();
}

@Test
void testGenerateQrCode_success() throws IOException, WriterException {
MockHttpServletResponse mockResponse = new MockHttpServletResponse();

qrCodeService.generate(sampleDto, mockResponse);

assertEquals("attachment;filename=Test_QR.png", mockResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION));
assertEquals("image/png", mockResponse.getContentType()); // MatrixToImageWriter sets this implicitly

// Verify the output stream contains PNG data (basic check for non-empty)
assertTrue(mockResponse.getContentAsByteArray().length > 0);

// Further verification could involve trying to read the byte array as an image
// and potentially decoding it, but that might be too much for a unit test.
// For now, we trust that if MatrixToImageWriter.writeToStream ran without error
// and produced bytes, it's likely correct.

// We can also try to verify the content of the QR code if we mock the writer part.
// Let's try to capture the string passed to the QRCodeWriter.encode
// This requires QRCodeWriter to be a mock or using a static mock for MatrixToImageWriter
// For simplicity, the current check on headers and non-empty output is a good start.
}

@Test
void testGenerateQrCode_nullDto() {
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
assertThrows(NullPointerException.class, () -> {
// The ObjectMapper().writeValueAsString(null) will throw NullPointerException
qrCodeService.generate(null, mockResponse);
});
}

@Test
void testGenerateQrCode_emptyTitleInDto() throws IOException, WriterException {
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
QrCodeGenerationRequestDto dtoWithEmptyTitle = QrCodeGenerationRequestDto.builder()
.title("") // Empty title
.message("Some payload")
.generatedByName("JUnit")
.generatedForName("Test Target")
.build();

qrCodeService.generate(dtoWithEmptyTitle, mockResponse);

// Expecting "attachment;filename=.png" or similar, depending on implementation logic for empty title
assertEquals("attachment;filename=.png", mockResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION));
Copy link
Preview

Copilot AI Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider handling empty title values explicitly—either by validating the DTO or providing a default filename—to avoid generating a filename like ".png".

Suggested change
assertEquals("attachment;filename=.png", mockResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION));
assertEquals("attachment;filename=default.png", mockResponse.getHeader(HttpHeaders.CONTENT_DISPOSITION));

Copilot uses AI. Check for mistakes.

assertTrue(mockResponse.getContentAsByteArray().length > 0);
}


@Test
void testReadQrCode_success() throws Exception {
// 1. Prepare a valid QR code image as byte array
String originalContent = objectMapper.writeValueAsString(sampleDto);
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(originalContent, BarcodeFormat.QR_CODE, 200, 200);
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
byte[] qrCodeBytes = pngOutputStream.toByteArray();

// 2. Mock MultipartFile
MultipartFile multipartFile = mock(MultipartFile.class);
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(qrCodeBytes));

// 3. Call read method
ResponseEntity<?> responseEntity = qrCodeService.read(multipartFile);

// 4. Assertions
assertNotNull(responseEntity);
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertTrue(responseEntity.getBody() instanceof QrCodeGenerationRequestDto);
QrCodeGenerationRequestDto resultDto = (QrCodeGenerationRequestDto) responseEntity.getBody();
assertEquals(sampleDto.getTitle(), resultDto.getTitle());
assertEquals(sampleDto.getMessage(), resultDto.getMessage()); // Changed from getPayload to getMessage
assertEquals(sampleDto.getGeneratedByName(), resultDto.getGeneratedByName()); // Changed from getGeneratedBy
}

@Test
void testReadQrCode_invalidImageFormat() throws IOException {
MultipartFile multipartFile = mock(MultipartFile.class);
// Simulate a file that is not a valid image (e.g., random bytes)
byte[] invalidImageBytes = "This is not an image".getBytes();
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(invalidImageBytes));

// ImageIO.read is expected to return null for non-image formats it doesn't understand
// which will then cause NullPointerException in BufferedImageLuminanceSource constructor
assertThrows(NullPointerException.class, () -> {
qrCodeService.read(multipartFile);
}, "Should throw NullPointerException when ImageIO.read returns null for invalid image format that is not decodable by ImageIO");
}

@Test
void testReadQrCode_notAQrCode() throws IOException {
// Create a valid PNG image, but not a QR code (e.g., a blank image)
BufferedImage blankImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(blankImage, "png", baos);
byte[] blankImageBytes = baos.toByteArray();

MultipartFile multipartFile = mock(MultipartFile.class);
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(blankImageBytes));

// Expect NotFoundException because MultiFormatReader won't find a QR code
assertThrows(com.google.zxing.NotFoundException.class, () -> {
qrCodeService.read(multipartFile);
});
}

@Test
void testReadQrCode_ioExceptionOnInputStream() throws IOException {
MultipartFile multipartFile = mock(MultipartFile.class);
when(multipartFile.getInputStream()).thenThrow(new IOException("Failed to read input stream"));

assertThrows(IOException.class, () -> {
qrCodeService.read(multipartFile);
});
}

@Test
void testReadQrCode_unexpectedContent() throws Exception {
// 1. Prepare a QR code with content that is not a valid JSON for QrCodeGenerationRequestDto
String nonJsonContent = "Just some plain text, not JSON";
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(nonJsonContent, BarcodeFormat.QR_CODE, 200, 200);
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
byte[] qrCodeBytes = pngOutputStream.toByteArray();

// 2. Mock MultipartFile
MultipartFile multipartFile = mock(MultipartFile.class);
when(multipartFile.getInputStream()).thenReturn(new ByteArrayInputStream(qrCodeBytes));

// 3. Call read method and expect a Jackson mapping/parsing exception
// The service tries to map result.getText() to QrCodeGenerationRequestDto.class
// This will fail if the text is not a JSON representation of that DTO.
assertThrows(com.fasterxml.jackson.core.JsonProcessingException.class, () -> { // Changed to broader JsonProcessingException
qrCodeService.read(multipartFile);
});
}
}
57 changes: 57 additions & 0 deletions target/classes/META-INF/spring-configuration-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"groups": [
{
"name": "com.onurdesk.iris.swagger",
"type": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties"
},
{
"name": "com.onurdesk.iris.swagger.properties",
"type": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties",
"sourceMethod": "getProperties()"
},
{
"name": "com.onurdesk.iris.swagger.properties.contact",
"type": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties",
"sourceMethod": "getContact()"
}
],
"properties": [
{
"name": "com.onurdesk.iris.swagger.properties.api-version",
"type": "java.lang.String",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties"
},
{
"name": "com.onurdesk.iris.swagger.properties.contact.email",
"type": "java.lang.String",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact"
},
{
"name": "com.onurdesk.iris.swagger.properties.contact.name",
"type": "java.lang.String",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact"
},
{
"name": "com.onurdesk.iris.swagger.properties.contact.url",
"type": "java.lang.String",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties$Contact"
},
{
"name": "com.onurdesk.iris.swagger.properties.description",
"type": "java.lang.String",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties"
},
{
"name": "com.onurdesk.iris.swagger.properties.title",
"type": "java.lang.String",
"sourceType": "com.onurdesk.iris.configuration.properties.OpenApiConfigurationProperties$Properties"
}
],
"hints": [],
"ignored": {
"properties": []
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified target/classes/com/onurdesk/iris/service/QrCodeService.class
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
META-INF/spring-configuration-metadata.json
com/onurdesk/iris/dto/QrCodeGenerationRequestDto$QrCodeGenerationRequestDtoBuilder.class
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/app/src/main/java/com/onurdesk/iris/SpringBootQrCodeGeneratorReaderApplication.java
/app/src/main/java/com/onurdesk/iris/configuration/OpenApiConfiguration.java
/app/src/main/java/com/onurdesk/iris/configuration/properties/OpenApiConfigurationProperties.java
/app/src/main/java/com/onurdesk/iris/controller/QrCodeController.java
/app/src/main/java/com/onurdesk/iris/dto/QrCodeGenerationRequestDto.java
/app/src/main/java/com/onurdesk/iris/exception/handler/GenericExceptionHandler.java
/app/src/main/java/com/onurdesk/iris/exception/handler/ValidationFailureExceptionHandler.java
/app/src/main/java/com/onurdesk/iris/service/QrCodeService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com/onurdesk/iris/service/QrCodeServiceTests.class
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/app/src/test/java/com/onurdesk/iris/SpringBootQrCodeGeneratorReaderApplicationTests.java
/app/src/test/java/com/onurdesk/iris/service/QrCodeServiceTests.java
Loading