diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..4b801a8 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,61 @@ +# This workflow will build a Java Spring Boot Application with Maven, and run test cases. +name: Build and Test Java Spring Boot Application + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + run-unit-tests: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:latest + + env: + POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: smart_inventory + + # Set health checks to wait until postgres has started. + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + ports: + # Maps tcp port 5432 on service container to the host. + - 5432:5432 + + steps: + - name: Pull repository + uses: actions/checkout@v4.1.2 + + - name: Set up JDK + uses: actions/setup-java@v4.2.1 + with: + java-version: '21' + distribution: 'temurin' + # cache: maven # Uncomment and configure if needed. + + - name: Run unit tests + run: mvn test + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive. + # - name: Update dependency graph + # uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + + - name: Build Jar file + run: mvn package -DskipTests # Skip tests since they were already run. + + - name: Save Jar file + uses: actions/upload-artifact@v4.3.1 + with: + name: smart-inventory-0.0.1-SNAPSHOT + path: target/smart-inventory-0.0.1-SNAPSHOT.jar + retention-days: 1 diff --git a/src/main/java/sg/com/smartinventory/DataLoader.java b/src/main/java/sg/com/smartinventory/DataLoader.java index 686b65d..9346109 100644 --- a/src/main/java/sg/com/smartinventory/DataLoader.java +++ b/src/main/java/sg/com/smartinventory/DataLoader.java @@ -1,6 +1,6 @@ package sg.com.smartinventory; -import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; @@ -13,44 +13,53 @@ @Component public class DataLoader { - //Inject all repositories + // Inject all repositories. private CustomerRepository customerRepository; private ProductRepository productRepository; private ReviewRepository reviewRepository; - @Autowired - public DataLoader(CustomerRepository customerRepository, ProductRepository productRepository, ReviewRepository reviewRepository){ + // @Autowired + public DataLoader(CustomerRepository customerRepository, ProductRepository productRepository, + ReviewRepository reviewRepository) { this.customerRepository = customerRepository; this.productRepository = productRepository; this.reviewRepository = reviewRepository; } @PostConstruct - public void loadData(){ - //Clear all data + public void loadData() { + // Clear all data. customerRepository.deleteAll(); productRepository.deleteAll(); reviewRepository.deleteAll(); - //Create fake data - customerRepository.save(new Customer("John", "Doe", "USA", "123 Main St", 12345, 1234567890, "john.doe@example.com", 101)); - customerRepository.save(new Customer("Alice", "Smith", "Canada", "456 Maple Ave", 54321, 987654321, "alice.smith@example.com", 102)); - customerRepository.save(new Customer("Michael", "Johnson", "UK", "789 Oak Rd", 67890, 987612345, "michael.johnson@example.com", 103)); - customerRepository.save(new Customer("Emily", "Brown", "Australia", "321 Elm St", 13579, 456789123, "emily.brown@example.com", 104)); - customerRepository.save(new Customer("David", "Wilson", "Germany", "654 Pine Rd", 98765, 369852147, "david.wilson@example.com", 105)); + // Create fake data. + customerRepository + .save(new Customer("John", "Doe", "USA", "123 Main St", 123456, 12345678, "john.doe@example.com", 101)); + customerRepository.save( + new Customer("Alice", "Smith", "Canada", "456 Maple Ave", 543210, 98765432, "alice.smith@example.com", 102)); + customerRepository.save( + new Customer("Michael", "Johnson", "UK", "789 Oak Rd", 567890, 98761234, "michael.johnson@example.com", 103)); + customerRepository.save( + new Customer("Emily", "Brown", "Australia", "321 Elm St", 135790, 45678912, "emily.brown@example.com", 104)); + customerRepository.save( + new Customer("David", "Wilson", "Germany", "654 Pine Rd", 987655, 36985214, "david.wilson@example.com", 105)); - productRepository.save(new Product("Electronics", "Smartphone", "High-end smartphone with advanced features", 999.99, 100, "101")); - productRepository.save(new Product("Clothing", "Men's T-Shirt", "Comfortable cotton t-shirt for everyday wear", 29.99, 500, "102")); - productRepository.save(new Product("Home & Kitchen", "Coffee Maker", "Automatic coffee maker with programmable settings", 49.99, 50, "103")); - productRepository.save(new Product("Beauty", "Perfume", "Elegant fragrance with floral and citrus notes", 79.99, 200, "104")); - productRepository.save(new Product("Books", "Science Fiction Novel", "Bestselling sci-fi novel set in a dystopian future", 14.99, 300, "105")); + productRepository.save( + new Product("Electronics", "Smartphone", "High-end smartphone with advanced features", 999.99, 100, "101")); + productRepository.save( + new Product("Clothing", "Men's T-Shirt", "Comfortable cotton t-shirt for everyday wear", 29.99, 500, "102")); + productRepository.save(new Product("Home & Kitchen", "Coffee Maker", + "Automatic coffee maker with programmable settings", 49.99, 50, "103")); + productRepository + .save(new Product("Beauty", "Perfume", "Elegant fragrance with floral and citrus notes", 79.99, 200, "104")); + productRepository.save(new Product("Books", "Science Fiction Novel", + "Bestselling sci-fi novel set in a dystopian future", 14.99, 300, "105")); reviewRepository.save(new Review("Electronics", "Great smartphone with excellent features.", 5, 101, 201)); reviewRepository.save(new Review("Clothing", "Very comfortable t-shirt, fits perfectly.", 4, 102, 202)); reviewRepository.save(new Review("Home & Kitchen", "Makes delicious coffee, easy to use.", 4, 103, 203)); reviewRepository.save(new Review("Beauty", "Lovely fragrance, long-lasting.", 5, 104, 204)); reviewRepository.save(new Review("Books", "Intriguing plot, couldn't put it down.", 5, 105, 205)); - } - -} +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/SmartInventoryApplication.java b/src/main/java/sg/com/smartinventory/SmartInventoryApplication.java index 6f5794d..34138f0 100644 --- a/src/main/java/sg/com/smartinventory/SmartInventoryApplication.java +++ b/src/main/java/sg/com/smartinventory/SmartInventoryApplication.java @@ -5,9 +5,7 @@ @SpringBootApplication public class SmartInventoryApplication { - public static void main(String[] args) { SpringApplication.run(SmartInventoryApplication.class, args); } - -} +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/controllers/CustomerController.java b/src/main/java/sg/com/smartinventory/controllers/CustomerController.java new file mode 100644 index 0000000..58567c1 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/controllers/CustomerController.java @@ -0,0 +1,35 @@ +package sg.com.smartinventory.controllers; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import sg.com.smartinventory.entity.Customer; +import sg.com.smartinventory.services.CustomerService; + +@RestController +@RequestMapping("/customers") +public class CustomerController { + private CustomerService customerService; + + // @Autowired + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + // CREATE. + @PostMapping("") + public ResponseEntity createCustomer(@Valid @RequestBody Customer customer) { + + // if(bindingResult.hasErrors()) { + // return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + // } + + Customer newCustomer = customerService.createCustomer(customer); + return new ResponseEntity<>(newCustomer, HttpStatus.CREATED); + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/entity/Customer.java b/src/main/java/sg/com/smartinventory/entity/Customer.java index 03706a0..6c0e758 100644 --- a/src/main/java/sg/com/smartinventory/entity/Customer.java +++ b/src/main/java/sg/com/smartinventory/entity/Customer.java @@ -6,41 +6,63 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.Setter; @Getter @Setter +@Builder +@AllArgsConstructor @Entity -@Table(name="customer") +@Table(name = "customer") public class Customer { - + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") - private Long id; //PK - - @Column(name="first_name") + private Long id; // PK. + + @NotBlank(message = "First name is mandatory. ") + @Column(name = "first_name") private String firstName; - @Column(name="last_name") + + @Column(name = "last_name") private String lastName; - @Column(name="country") + + @Column(name = "country") private String country; - @Column(name="address") + + @Column(name = "address") private String address; - @Column(name="postal_code") + + @Digits(fraction = 0, integer = 6, message = "Postal code should be 6 digits. ") + @Column(name = "postal_code") private int postalCode; - @Column(name="mobile_number") + + @Digits(fraction = 0, integer = 8, message = "Mobile no should be 8 digits. ") + @Column(name = "mobile_number") private int mobileNumber; - @Column(name="email") + + @Email(message = "Email should be valid. ") + @Column(name = "email") private String email; - @Column(name="review_id") - private int reviewId; //FK - //Define Constructor for dataLoader + @Column(name = "review_id") + private int reviewId; // FK. + + public Customer() { + } + + // Define Constructor for DataLoader. public Customer(String firstName, String lastName, String country, String address, int postalCode, int mobileNumber, String email, int reviewId) { + this(); + this.firstName = firstName; this.lastName = lastName; this.country = country; @@ -50,6 +72,4 @@ public Customer(String firstName, String lastName, String country, String addres this.email = email; this.reviewId = reviewId; } - - -} +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/entity/Product.java b/src/main/java/sg/com/smartinventory/entity/Product.java index 0b06557..188a996 100644 --- a/src/main/java/sg/com/smartinventory/entity/Product.java +++ b/src/main/java/sg/com/smartinventory/entity/Product.java @@ -7,34 +7,47 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.Setter; @Getter @Setter -@Entity -@Table(name="product") +@Builder @AllArgsConstructor +@Entity +@Table(name = "product") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="id") + @Column(name = "id") private Long id; - @Column(name="category") + + @Column(name = "category") private String category; - @Column(name="name") + + @Column(name = "name") private String name; - @Column(name="description") + + @Column(name = "description") private String description; - @Column(name="price") + + @Column(name = "price") private double price; - @Column(name="stock_quantity") + + @Column(name = "stock_quantity") private int stockQuantity; - @Column(name="review_id") - private String reviewId; //FK - //Define Constructor for dataLoader + @Column(name = "review_id") + private String reviewId; // FK. + + public Product() { + } + + // Define Constructor for DataLoader. public Product(String category, String name, String description, double price, int stockQuantity, String reviewId) { + this(); + this.category = category; this.name = name; this.description = description; @@ -42,6 +55,4 @@ public Product(String category, String name, String description, double price, i this.stockQuantity = stockQuantity; this.reviewId = reviewId; } - - -} +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/entity/Review.java b/src/main/java/sg/com/smartinventory/entity/Review.java index 2549696..f42ad88 100644 --- a/src/main/java/sg/com/smartinventory/entity/Review.java +++ b/src/main/java/sg/com/smartinventory/entity/Review.java @@ -7,37 +7,48 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.Setter; @Getter @Setter -@Entity -@Table(name="review") +@Builder @AllArgsConstructor +@Entity +@Table(name = "review") public class Review { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name="id") - private Long id; //PK - @Column(name="category") + @Column(name = "id") + private Long id; // PK. + + @Column(name = "category") private String category; - @Column(name="review_content") + + @Column(name = "review_content") private String reviewContent; - @Column(name="rating") + + @Column(name = "rating") private int rating; - @Column(name="customer_id") - private int customerId; //FK - @Column(name="product_id") - private int productId; //FK - - // Define Constructor for dataLoader + + @Column(name = "customer_id") + private int customerId; // FK. + + @Column(name = "product_id") + private int productId; // FK. + + public Review() { + } + + // Define Constructor for DataLoader. public Review(String category, String reviewContent, int rating, int customerId, int productId) { + this(); + this.category = category; this.reviewContent = reviewContent; this.rating = rating; this.customerId = customerId; this.productId = productId; } - -} +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/repository/CustomerRepository.java b/src/main/java/sg/com/smartinventory/repository/CustomerRepository.java index ee1d8af..0bbe59b 100644 --- a/src/main/java/sg/com/smartinventory/repository/CustomerRepository.java +++ b/src/main/java/sg/com/smartinventory/repository/CustomerRepository.java @@ -6,5 +6,5 @@ //CustomerRepository simply extends JpaRepository, giving us a lot of default methods to access db without the hardwork public interface CustomerRepository extends JpaRepository { - -} + +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/repository/ProductRepository.java b/src/main/java/sg/com/smartinventory/repository/ProductRepository.java index 435cc2f..3c702ab 100644 --- a/src/main/java/sg/com/smartinventory/repository/ProductRepository.java +++ b/src/main/java/sg/com/smartinventory/repository/ProductRepository.java @@ -4,6 +4,6 @@ import sg.com.smartinventory.entity.Product; -public interface ProductRepository extends JpaRepository { - -} +public interface ProductRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/repository/ReviewRepository.java b/src/main/java/sg/com/smartinventory/repository/ReviewRepository.java index 3b289da..09fb7fa 100644 --- a/src/main/java/sg/com/smartinventory/repository/ReviewRepository.java +++ b/src/main/java/sg/com/smartinventory/repository/ReviewRepository.java @@ -4,6 +4,6 @@ import sg.com.smartinventory.entity.Review; -public interface ReviewRepository extends JpaRepository { - -} +public interface ReviewRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/serviceImpls/CustomerServiceImpl.java b/src/main/java/sg/com/smartinventory/serviceImpls/CustomerServiceImpl.java new file mode 100644 index 0000000..39082f8 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/serviceImpls/CustomerServiceImpl.java @@ -0,0 +1,24 @@ +package sg.com.smartinventory.serviceImpls; + +import org.springframework.stereotype.Service; + +import sg.com.smartinventory.entity.Customer; +import sg.com.smartinventory.repository.CustomerRepository; +import sg.com.smartinventory.services.CustomerService; + +@Service +public class CustomerServiceImpl implements CustomerService { + private CustomerRepository customerRepository; + + // @Autowired + public CustomerServiceImpl(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + @Override + public Customer createCustomer(Customer customer) { + Customer newCustomer = customerRepository.save(customer); + + return newCustomer; + } +} \ No newline at end of file diff --git a/src/main/java/sg/com/smartinventory/services/CustomerService.java b/src/main/java/sg/com/smartinventory/services/CustomerService.java new file mode 100644 index 0000000..bcd40f8 --- /dev/null +++ b/src/main/java/sg/com/smartinventory/services/CustomerService.java @@ -0,0 +1,7 @@ +package sg.com.smartinventory.services; + +import sg.com.smartinventory.entity.Customer; + +public interface CustomerService { + Customer createCustomer(Customer customer); +} \ No newline at end of file diff --git a/src/test/java/sg/com/smartinventory/CustomerControllerTest.java b/src/test/java/sg/com/smartinventory/CustomerControllerTest.java new file mode 100644 index 0000000..8381ac3 --- /dev/null +++ b/src/test/java/sg/com/smartinventory/CustomerControllerTest.java @@ -0,0 +1,48 @@ +package sg.com.smartinventory; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import sg.com.smartinventory.entity.Customer; + +@SpringBootTest +@AutoConfigureMockMvc +public class CustomerControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @DisplayName("Create customer") + @Test + public void createCustomerTest() throws Exception { + // Step 1: Create a Customer object + Customer newCustomer = Customer.builder().firstName("Jackie").lastName("Chan").country("Hong Kong") + .address("123 HK St") + .postalCode(654321).mobileNumber(87654321).email("jackie.chan@example.com").reviewId(110).build(); + + // Step 2: Convert the Java object to JSON using ObjectMapper. + String newCustomerAsJSON = objectMapper.writeValueAsString(newCustomer); + + // Step 3: Build the request. + RequestBuilder request = MockMvcRequestBuilders.post("/customers").contentType(MediaType.APPLICATION_JSON) + .content(newCustomerAsJSON); + + // Step 4: Perform the request and get the response and assert. + mockMvc.perform(request).andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.firstName").value("Jackie")).andExpect(jsonPath("$.lastName").value("Chan")); + } +} \ No newline at end of file diff --git a/src/test/java/sg/com/smartinventory/CustomerServiceImplTest.java b/src/test/java/sg/com/smartinventory/CustomerServiceImplTest.java new file mode 100644 index 0000000..12aa3ee --- /dev/null +++ b/src/test/java/sg/com/smartinventory/CustomerServiceImplTest.java @@ -0,0 +1,45 @@ +package sg.com.smartinventory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.boot.test.context.SpringBootTest; + +import sg.com.smartinventory.entity.Customer; +import sg.com.smartinventory.repository.CustomerRepository; +import sg.com.smartinventory.serviceImpls.CustomerServiceImpl; + +@SpringBootTest +public class CustomerServiceImplTest { + @Mock + private CustomerRepository customerRepository; + + @InjectMocks + CustomerServiceImpl customerService; + + @Test + public void createCustomerTest() { + + // 1. SETUP + // Create a new customer. + Customer customer = Customer.builder().firstName("John").lastName("Wick").country("USA").address("123 Main St") + .postalCode(123456).mobileNumber(12345678).email("john.wick@example.com").reviewId(111).build(); + + // Mock the save method of the customer repository. + when((customerRepository.save(customer))).thenReturn(customer); + + // 2. EXECUTE. + Customer savedCustomer = customerService.createCustomer(customer); + + // 3. ASSERT. + assertEquals(customer, savedCustomer, "The saved customer should be the same as the new customer created. "); + + // Verify that the save method of the customer repository is called once only. + verify(customerRepository, times(1)).save(customer); + } +} \ No newline at end of file diff --git a/src/test/java/sg/com/smartinventory/smartinventory/SmartInventoryApplicationTests.java b/src/test/java/sg/com/smartinventory/SmartInventoryApplicationTests.java similarity index 78% rename from src/test/java/sg/com/smartinventory/smartinventory/SmartInventoryApplicationTests.java rename to src/test/java/sg/com/smartinventory/SmartInventoryApplicationTests.java index 5fa5bd4..74e01b9 100644 --- a/src/test/java/sg/com/smartinventory/smartinventory/SmartInventoryApplicationTests.java +++ b/src/test/java/sg/com/smartinventory/SmartInventoryApplicationTests.java @@ -1,13 +1,11 @@ -package sg.com.smartinventory.smartinventory; +package sg.com.smartinventory; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SmartInventoryApplicationTests { - @Test void contextLoads() { } - -} +} \ No newline at end of file