Skip to content
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
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 13 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,27 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up JDK 21
uses: actions/setup-java@v4
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'
- name: Grant execute permissions for gradlew
run: chmod +x gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@v3

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
env:
SPRING_PROFILES_ACTIVE: integration-test-ci
with:
arguments: clean build
build-root-directory: .

- name: Run build in a root
working-directory: .
run: ./gradlew clean build

- name: Add coverage to PR
id: jacoco
uses: madrapps/jacoco-report@v1.7.2
Expand All @@ -40,7 +43,7 @@ jobs:
min-coverage-overall: 60
min-coverage-changed-files: 50
- name: Generate JaCoCo Badge
uses: cicirello/jacoco-badge-generator@v2.11.0
uses: cicirello/jacoco-badge-generator@v2
with:
generate-branches-badge: true
jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#### Stage 1: Build the application
FROM amazoncorretto:21 AS BUILD_IMAGE
FROM amazoncorretto:25 AS BUILD_IMAGE

# Set the current working directory inside the image
WORKDIR /app
Expand Down Expand Up @@ -39,7 +39,7 @@ RUN mkdir -p dependency \
&& (cd dependency || return; jar -xf ../libs/*.jar)

#### Stage 2: A minimal docker image with command to run the app
FROM --platform=linux/amd64 amazoncorretto:21 AS RUNNER
FROM --platform=linux/amd64 amazoncorretto:25 AS RUNNER

# Set the current working directory inside the image
WORKDIR /app
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ A highly opinionated and complete starter for Spring Boot production ready proje

## Built With

* [Spring Boot 3.5.5](https://projects.spring.io/spring-boot/) - Inversion of Control Framework
* [Spring Boot 3.5.7](https://projects.spring.io/spring-boot/) - Inversion of Control Framework
* [Bootstrap 5](https://getbootstrap.com/docs/5.0/getting-started/introduction/) - HTML, CSS, and
JavaScript framework
* [Thymeleaf](https://www.thymeleaf.org/) - Modern server-side Java template engine
Expand Down
28 changes: 17 additions & 11 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id 'org.springframework.boot' version '3.5.5'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.owasp.dependencycheck' version '12.1.6'
id 'org.owasp.dependencycheck' version '12.1.8'
id 'com.diffplug.spotless' version '8.0.0'
id 'eclipse'
id 'jacoco'
Expand All @@ -14,7 +14,7 @@ version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
// You can uncomment this to include javadoc jar generation
// withJavadocJar()
Expand All @@ -39,7 +39,13 @@ ext {
set('springDocVersion', '2.8.13')
set('javadocScribeVersion', '0.15.0')
set('mapstructVersion', '1.6.3')
set('awsSdkS3Version', '2.31.45')
set('awsSdkS3Version', '2.38.2')
set('jsoupVersion', '1.21.2')
set('commonsIoVersion', '2.21.0')
set('datafakerVersion', '2.5.3')
set('gsonVersion', '2.13.2')
set('commonsValidatorVersion', '1.10.0')
set('springDataJpaDataTablesVersion', '7.1.0')
}

dependencies {
Expand All @@ -63,15 +69,15 @@ dependencies {
implementation 'org.ehcache:ehcache-transactions:3.10.8'

// Utilities
implementation 'org.jsoup:jsoup:1.21.2'
implementation "org.jsoup:jsoup:${jsoupVersion}"
implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
implementation 'commons-io:commons-io:2.20.0'
implementation 'net.datafaker:datafaker:2.5.1'
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'commons-validator:commons-validator:1.10.0'
implementation 'com.github.darrachequesne:spring-data-jpa-datatables:7.1.0'
implementation "commons-io:commons-io:${commonsIoVersion}"
implementation "net.datafaker:datafaker:${datafakerVersion}"
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation "commons-validator:commons-validator:${commonsValidatorVersion}"
implementation "com.github.darrachequesne:spring-data-jpa-datatables:${springDataJpaDataTablesVersion}"

// Documentation and API Tools
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springDocVersion}"
Expand Down Expand Up @@ -110,7 +116,7 @@ testing {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'io.findify:s3mock_2.13:0.2.6'
implementation 'com.icegreen:greenmail-junit5:2.1.3'
implementation 'com.icegreen:greenmail-junit5:2.1.7'
implementation 'com.jparams:to-string-verifier:1.4.8'
implementation 'nl.jqno.equalsverifier:equalsverifier:4.0'
implementation 'org.springframework.security:spring-security-test'
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.9'

services:

# Postgres database
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
21 changes: 21 additions & 0 deletions src/integrationTest/java/com/developersboard/TestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import java.net.URI;

/**
* This class provides every bean, and other configurations needed to be used in the testing phase.
Expand Down Expand Up @@ -79,6 +82,24 @@ public S3Client s3Client(AwsProperties props) {
AwsBasicCredentials.create(props.getAccessKeyId(), props.getSecretAccessKey());

return S3Client.builder()
.endpointOverride(URI.create("http://localhost:8001")) // match your S3Mock port
.region(Region.of(props.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.serviceConfiguration(
S3Configuration.builder()
.pathStyleAccessEnabled(true) // must be true for S3Mock
.build()
)
.build();
}

@Bean
public S3Presigner s3Presigner(AwsProperties props) {
AwsBasicCredentials credentials =
AwsBasicCredentials.create(props.getAccessKeyId(), props.getSecretAccessKey());

return S3Presigner.builder()
.endpointOverride(URI.create("http://localhost:8001")) // S3Mock
.region(Region.of(props.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.developersboard.backend.service.storage;

import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.developersboard.IntegrationTestUtils;
import com.developersboard.constant.StorageConstants;
import com.developersboard.exception.InvalidFileFormatException;
Expand All @@ -10,40 +9,53 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.net.URI;
import javax.imageio.ImageIO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.mockito.Mockito;

@Disabled("S3 Mocking needs to be refactored to use v2 AWS. S3Mock won't support v2")
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;

/**
* This annotation tells JUnit to reuse the same test instance for the entire test class.
* Then @BeforeAll does NOT need to be static anymore.
* <p>
* To use awsProperties inside @BeforeAll,
* you need to avoid the static requirement that JUnit puts on @BeforeAll
* with @TestInstance(TestInstance.Lifecycle.PER_CLASS)
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AmazonS3ServiceIntegrationTest extends IntegrationTestUtils {

/*
S3Mock.create(8001, "/tmp/s3");
*/
private transient S3Mock api;

@BeforeEach
void setUp(TestInfo testInfo) {
multipartFile = getMultipartFile(testInfo.getDisplayName(), false);
private S3Mock api;

@BeforeAll
void beforeAll() {
api =
new S3Mock.Builder()
.withPort(Integer.parseInt(awsProperties.getServicePort()))
.withInMemoryBackend()
.build();

api.start();
}

@AfterEach
void tearDown() {
api.shutdown(); // kills the underlying actor system. Use api.stop() to just unbind the port.
@AfterAll
void afterAll() {
api.stop();
}

@BeforeEach
void setUp(TestInfo testInfo) {
multipartFile = getMultipartFile(testInfo.getDisplayName(), false);
}

@Test
Expand Down Expand Up @@ -114,14 +126,16 @@ void generatedPreSignedUrl(TestInfo testInfo) throws Exception {
var imageUrl = amazonS3Service.storeProfileImage(multipartFile, testInfo.getDisplayName());

var preSignedUrl = amazonS3Service.generatePreSignedUrl(imageUrl);
var expectedUrl =
String.format(
"%s/%s/profileImages/%s/profileImage.png",
awsProperties.getServiceEndpoint(),
awsProperties.getS3BucketName(),
URLEncoder.encode(testInfo.getDisplayName(), StandardCharsets.UTF_8));
URI uri = URI.create(preSignedUrl);

var expectedPath = String.format(
"/profileImages/%s/profileImage.png",
testInfo.getDisplayName()
);

Assertions.assertEquals(expectedUrl, preSignedUrl);
Assertions.assertEquals(expectedPath, uri.getPath());
Assertions.assertTrue(preSignedUrl.contains("X-Amz-Expires"));
Assertions.assertTrue(preSignedUrl.contains("X-Amz-Signature"));
}

@Test
Expand All @@ -135,7 +149,7 @@ void deleteProfileImage(TestInfo testInfo) throws IOException, InterruptedExcept
amazonS3Service.delete(imageUrl);

// We will get a AmazonS3Exception 404 error if the key doesn't exist.
Assertions.assertThrows(AmazonS3Exception.class, () -> amazonS3Service.getFile(imageUrl));
Assertions.assertThrows(NoSuchKeyException.class, () -> amazonS3Service.getFile(imageUrl));
}

@Test
Expand All @@ -148,7 +162,7 @@ void renameFile(TestInfo testInfo) throws IOException, InterruptedException {
Assertions.assertEquals(newKey, renameFile);

// We will get a AmazonS3Exception 404 error if the key doesn't exist.
Assertions.assertThrows(AmazonS3Exception.class, () -> amazonS3Service.getFile(imageUrl));
Assertions.assertThrows(NoSuchKeyException.class, () -> amazonS3Service.getFile(imageUrl));

try (InputStream storedImageUrl = amazonS3Service.getFile(newKey)) {
Assertions.assertNotNull(storedImageUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ spring:
port: 3025
username: ${EMAIL_USERNAME:admin}
password: ${EMAIL_PASSWORD:admin}
protocol: smtp
protocol: smtp

aws:
access-key-id: accessKey
secret-access-key: secretKey
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.mail.MailSendException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
Expand Down Expand Up @@ -78,7 +79,7 @@ public void sendHtmlEmailWithAttachment(final HtmlEmailRequest emailRequest) {
mailSender.send(prepareMimeMessage(emailRequest));

LOG.info(EmailConstants.MAIL_SUCCESS_MESSAGE);
} catch (MessagingException | FileNotFoundException | UnsupportedEncodingException e) {
} catch (MailSendException | MessagingException | FileNotFoundException | UnsupportedEncodingException e) {
throw new InvalidServiceRequestException(e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.auditing.DateTimeProvider;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

Expand Down Expand Up @@ -43,6 +45,13 @@ public DateTimeProvider dateTimeProvider() {

@Bean
public S3Presigner s3Presigner(AwsProperties props) {
return S3Presigner.builder().region(Region.of(props.getRegion())).build();
// Create the credential provider
var credentials =
AwsBasicCredentials.create(props.getAccessKeyId(), props.getSecretAccessKey());

return S3Presigner.builder()
.region(Region.of(props.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
}