feat(back-end): integration of clamAVS
This commit is contained in:
@@ -25,6 +25,7 @@ repositories {
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'xyz.capybara:clamav-client:2.1.2'
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
@@ -28,20 +28,23 @@ public class OrderController {
|
||||
private final QuoteSessionRepository quoteSessionRepo;
|
||||
private final QuoteLineItemRepository quoteLineItemRepo;
|
||||
private final CustomerRepository customerRepo;
|
||||
private final com.printcalculator.service.StorageService storageService;
|
||||
|
||||
// TODO: Inject Storage Service or use a base path property
|
||||
private static final String STORAGE_ROOT = "storage_orders";
|
||||
// private static final String STORAGE_ROOT = "storage_orders";
|
||||
|
||||
public OrderController(OrderRepository orderRepo,
|
||||
OrderItemRepository orderItemRepo,
|
||||
QuoteSessionRepository quoteSessionRepo,
|
||||
QuoteLineItemRepository quoteLineItemRepo,
|
||||
CustomerRepository customerRepo) {
|
||||
CustomerRepository customerRepo,
|
||||
com.printcalculator.service.StorageService storageService) {
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderItemRepo = orderItemRepo;
|
||||
this.quoteSessionRepo = quoteSessionRepo;
|
||||
this.quoteLineItemRepo = quoteLineItemRepo;
|
||||
this.customerRepo = customerRepo;
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +72,9 @@ public class OrderController {
|
||||
.orElseGet(() -> {
|
||||
Customer newC = new Customer();
|
||||
newC.setEmail(request.getCustomer().getEmail());
|
||||
newC.setCustomerType(request.getCustomer().getCustomerType());
|
||||
newC.setCreatedAt(OffsetDateTime.now());
|
||||
newC.setUpdatedAt(OffsetDateTime.now());
|
||||
return customerRepo.save(newC);
|
||||
});
|
||||
// Update customer details?
|
||||
@@ -135,6 +140,13 @@ public class OrderController {
|
||||
|
||||
BigDecimal subtotal = BigDecimal.ZERO;
|
||||
|
||||
// Initialize financial fields to defaults to satisfy DB constraints
|
||||
order.setSubtotalChf(BigDecimal.ZERO);
|
||||
order.setTotalChf(BigDecimal.ZERO);
|
||||
order.setDiscountChf(BigDecimal.ZERO);
|
||||
order.setSetupCostChf(session.getSetupCostChf()); // Or 0 if null, but session has it
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default
|
||||
|
||||
// Save Order first to get ID
|
||||
order = orderRepo.save(order);
|
||||
|
||||
@@ -162,6 +174,7 @@ public class OrderController {
|
||||
oItem.setStoredFilename(storedFilename);
|
||||
oItem.setStoredRelativePath("PENDING"); // Placeholder
|
||||
oItem.setMimeType("application/octet-stream"); // specific type if known
|
||||
oItem.setCreatedAt(OffsetDateTime.now());
|
||||
|
||||
oItem = orderItemRepo.save(oItem);
|
||||
|
||||
@@ -174,11 +187,9 @@ public class OrderController {
|
||||
try {
|
||||
Path sourcePath = Paths.get(qItem.getStoredPath());
|
||||
if (Files.exists(sourcePath)) {
|
||||
Path targetPath = Paths.get(STORAGE_ROOT, relativePath);
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
Files.copy(sourcePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
storageService.store(sourcePath, Paths.get(relativePath));
|
||||
|
||||
oItem.setFileSizeBytes(Files.size(targetPath));
|
||||
oItem.setFileSizeBytes(Files.size(sourcePath));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(); // Log error but allow order creation? Or fail?
|
||||
@@ -195,6 +206,7 @@ public class OrderController {
|
||||
order.setSubtotalChf(subtotal);
|
||||
order.setSetupCostChf(session.getSetupCostChf());
|
||||
order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default shipping? or 0?
|
||||
order.setDiscountChf(BigDecimal.ZERO);
|
||||
// TODO: Calc implementation for shipping
|
||||
|
||||
BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO);
|
||||
@@ -239,14 +251,7 @@ public class OrderController {
|
||||
}
|
||||
|
||||
// Save file to disk
|
||||
Path absolutePath = Paths.get(STORAGE_ROOT, relativePath);
|
||||
Files.createDirectories(absolutePath.getParent());
|
||||
|
||||
if (Files.exists(absolutePath)) {
|
||||
Files.delete(absolutePath); // Overwrite?
|
||||
}
|
||||
|
||||
Files.copy(file.getInputStream(), absolutePath);
|
||||
storageService.store(file, Paths.get(relativePath));
|
||||
|
||||
item.setFileSizeBytes(file.getSize());
|
||||
item.setMimeType(file.getContentType());
|
||||
|
||||
@@ -40,6 +40,7 @@ public class QuoteSessionController {
|
||||
private final QuoteCalculator quoteCalculator;
|
||||
private final PrinterMachineRepository machineRepo;
|
||||
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
|
||||
private final com.printcalculator.service.StorageService storageService;
|
||||
|
||||
// Defaults
|
||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
||||
@@ -50,13 +51,15 @@ public class QuoteSessionController {
|
||||
SlicerService slicerService,
|
||||
QuoteCalculator quoteCalculator,
|
||||
PrinterMachineRepository machineRepo,
|
||||
com.printcalculator.repository.PricingPolicyRepository pricingRepo) {
|
||||
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
|
||||
com.printcalculator.service.StorageService storageService) {
|
||||
this.sessionRepo = sessionRepo;
|
||||
this.lineItemRepo = lineItemRepo;
|
||||
this.slicerService = slicerService;
|
||||
this.quoteCalculator = quoteCalculator;
|
||||
this.machineRepo = machineRepo;
|
||||
this.pricingRepo = pricingRepo;
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
// 1. Start a new empty session
|
||||
@@ -100,9 +103,7 @@ public class QuoteSessionController {
|
||||
if (file.isEmpty()) throw new IOException("File is empty");
|
||||
|
||||
// 1. Define Persistent Storage Path
|
||||
// Structure: storage_quotes/{sessionId}/{uuid}.{ext}
|
||||
String storageDir = "storage_quotes/" + session.getId();
|
||||
Files.createDirectories(Paths.get(storageDir));
|
||||
// Structure: quotes/{sessionId}/{uuid}.{ext} (inside storage root)
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String ext = originalFilename != null && originalFilename.contains(".")
|
||||
@@ -110,10 +111,13 @@ public class QuoteSessionController {
|
||||
: ".stl";
|
||||
|
||||
String storedFilename = UUID.randomUUID() + ext;
|
||||
Path persistentPath = Paths.get(storageDir, storedFilename);
|
||||
Path relativePath = Paths.get("quotes", session.getId().toString(), storedFilename);
|
||||
|
||||
// Save file
|
||||
Files.copy(file.getInputStream(), persistentPath);
|
||||
storageService.store(file, relativePath);
|
||||
|
||||
// Resolve absolute path for slicing and storage usage
|
||||
Path persistentPath = storageService.loadAsResource(relativePath).getFile().toPath();
|
||||
|
||||
try {
|
||||
// Apply Basic/Advanced Logic
|
||||
@@ -206,7 +210,9 @@ public class QuoteSessionController {
|
||||
|
||||
} catch (Exception e) {
|
||||
// Cleanup if failed
|
||||
Files.deleteIfExists(persistentPath);
|
||||
try {
|
||||
storageService.delete(Paths.get("quotes", session.getId().toString(), storedFilename));
|
||||
} catch (Exception ignored) {}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -330,6 +336,24 @@ public class QuoteSessionController {
|
||||
}
|
||||
|
||||
Path path = Paths.get(item.getStoredPath());
|
||||
// Since storedPath is absolute, we can't directly use loadAsResource with it unless we resolve relative.
|
||||
// But loadAsResource expects relative path?
|
||||
// Actually FileSystemStorageService.loadAsResource uses rootLocation.resolve(path).
|
||||
// If path is absolute, resolve might fail or behave weirdly.
|
||||
// But wait, we stored absolute path in DB: item.setStoredPath(persistentPath.toString());
|
||||
// If we want to use storageService.loadAsResource, we need the relative path.
|
||||
// Or we just access the file directly if we trust the absolute path.
|
||||
// But we want to use StorageService abstraction.
|
||||
|
||||
// Option 1: Reconstruct relative path.
|
||||
// We know structure: quotes/{sessionId}/{filename}...
|
||||
// But filename is UUID+ext. We don't have storedFilename in QuoteLineItem easily?
|
||||
// QuoteLineItem doesn't seem to have storedFilename field, only storedPath.
|
||||
|
||||
// If we trust the file is on disk, we can use UrlResource directly here as before,
|
||||
// relying on the fact that storedPath is the absolute path to the file.
|
||||
// But we should verify it exists.
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.printcalculator.exception;
|
||||
|
||||
public class StorageException extends RuntimeException {
|
||||
|
||||
public StorageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public StorageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.capybara.clamav.ClamavClient;
|
||||
import xyz.capybara.clamav.commands.scan.result.ScanResult;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class ClamAVService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClamAVService.class);
|
||||
|
||||
private final ClamavClient clamavClient;
|
||||
|
||||
public ClamAVService(
|
||||
@Value("${clamav.host:localhost}") String host,
|
||||
@Value("${clamav.port:3310}") int port
|
||||
) {
|
||||
logger.info("Initializing ClamAV client at {}:{}", host, port);
|
||||
this.clamavClient = new ClamavClient(host, port);
|
||||
}
|
||||
|
||||
public boolean scan(InputStream inputStream) {
|
||||
try {
|
||||
ScanResult result = clamavClient.scan(inputStream);
|
||||
if (result instanceof ScanResult.OK) {
|
||||
return true;
|
||||
} else if (result instanceof ScanResult.VirusFound) {
|
||||
Map<String, Collection<String>> viruses = ((ScanResult.VirusFound) result).getFoundViruses();
|
||||
logger.warn("Virus found: {}", viruses);
|
||||
return false;
|
||||
} else {
|
||||
logger.warn("Unknown scan result: {}", result);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error scanning file with ClamAV", e);
|
||||
// Fail safe? Or fail secure?
|
||||
// Usually if scanner fails, we should probably reject to be safe, or allow with warning depending on policy.
|
||||
// For now, let's reject to be safe.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.printcalculator.exception.StorageException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
@Service
|
||||
public class FileSystemStorageService implements StorageService {
|
||||
|
||||
private final Path rootLocation;
|
||||
private final ClamAVService clamAVService;
|
||||
|
||||
public FileSystemStorageService(@Value("${storage.location:storage_orders}") String storageLocation, ClamAVService clamAVService) {
|
||||
this.rootLocation = Paths.get(storageLocation);
|
||||
this.clamAVService = clamAVService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
try {
|
||||
Files.createDirectories(rootLocation);
|
||||
} catch (IOException e) {
|
||||
throw new StorageException("Could not initialize storage", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(MultipartFile file, Path destinationRelativePath) throws IOException {
|
||||
Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath();
|
||||
if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) {
|
||||
// Security check
|
||||
throw new StorageException("Cannot store file outside current directory.");
|
||||
}
|
||||
|
||||
// Scan stream
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
if (!clamAVService.scan(inputStream)) {
|
||||
throw new StorageException("File rejected by antivirus scanner.");
|
||||
}
|
||||
}
|
||||
|
||||
// Reset stream? MultipartFile.getInputStream() returns a new stream usually,
|
||||
// but let's verify if we need to open it again.
|
||||
// Yes, we consumed the stream for scanning. We need to open it again for copying.
|
||||
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
Files.createDirectories(destinationFile.getParent());
|
||||
Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(Path source, Path destinationRelativePath) throws IOException {
|
||||
Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath();
|
||||
if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) {
|
||||
throw new StorageException("Cannot store file outside current directory.");
|
||||
}
|
||||
Files.createDirectories(destinationFile.getParent());
|
||||
// We assume source is already safe/scanned if it is internal?
|
||||
// Or should we scan it too?
|
||||
// If it comes from QuoteSession (which was scanned on upload), it is safe.
|
||||
// If we want to be paranoid, we can scan again, but maybe overkill.
|
||||
// Let's assume it is safe for internal copies.
|
||||
Files.copy(source, destinationFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Path path) throws IOException {
|
||||
Path file = rootLocation.resolve(path);
|
||||
Files.deleteIfExists(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource loadAsResource(Path path) throws IOException {
|
||||
try {
|
||||
Path file = rootLocation.resolve(path);
|
||||
Resource resource = new UrlResource(file.toUri());
|
||||
if (resource.exists() || resource.isReadable()) {
|
||||
return resource;
|
||||
} else {
|
||||
throw new RuntimeException("Could not read file: " + path);
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException("Could not read file: " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import java.nio.file.Path;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface StorageService {
|
||||
void init();
|
||||
void store(MultipartFile file, Path destination) throws IOException;
|
||||
void store(Path source, Path destination) throws IOException;
|
||||
void delete(Path path) throws IOException;
|
||||
Resource loadAsResource(Path path) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.printcalculator.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.printcalculator.dto.CreateOrderRequest;
|
||||
import com.printcalculator.dto.CustomerDto;
|
||||
import com.printcalculator.dto.AddressDto;
|
||||
import com.printcalculator.entity.QuoteLineItem;
|
||||
import com.printcalculator.entity.QuoteSession;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.util.FileSystemUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class OrderIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private QuoteSessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
private QuoteLineItemRepository lineItemRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private UUID sessionId;
|
||||
private UUID lineItemId;
|
||||
private final String TEST_FILENAME = "test_model.stl";
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
// 1. Create Quote Session
|
||||
QuoteSession session = new QuoteSession();
|
||||
session.setStatus("ACTIVE");
|
||||
session.setMaterialCode("PLA");
|
||||
session.setPricingVersion("v1");
|
||||
session.setCreatedAt(OffsetDateTime.now());
|
||||
session.setExpiresAt(OffsetDateTime.now().plusDays(7));
|
||||
session.setSetupCostChf(BigDecimal.valueOf(5.00));
|
||||
session.setSupportsEnabled(false);
|
||||
session = sessionRepository.save(session);
|
||||
this.sessionId = session.getId();
|
||||
|
||||
// 2. Create Dummy File on Disk (storage_quotes)
|
||||
Path sessionDir = Paths.get("storage_quotes", sessionId.toString());
|
||||
Files.createDirectories(sessionDir);
|
||||
Path filePath = sessionDir.resolve(UUID.randomUUID() + ".stl");
|
||||
Files.writeString(filePath, "dummy content");
|
||||
|
||||
// 3. Create Quote Line Item
|
||||
QuoteLineItem item = new QuoteLineItem();
|
||||
item.setQuoteSession(session);
|
||||
item.setStatus("READY");
|
||||
item.setOriginalFilename(TEST_FILENAME);
|
||||
item.setStoredPath(filePath.toString());
|
||||
item.setQuantity(2);
|
||||
item.setPrintTimeSeconds(120);
|
||||
item.setMaterialGrams(BigDecimal.valueOf(10.5));
|
||||
item.setUnitPriceChf(BigDecimal.valueOf(10.00));
|
||||
item.setCreatedAt(OffsetDateTime.now());
|
||||
item.setUpdatedAt(OffsetDateTime.now());
|
||||
item = lineItemRepository.save(item);
|
||||
this.lineItemId = item.getId();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() throws Exception {
|
||||
// Cleanup generated files
|
||||
FileSystemUtils.deleteRecursively(Paths.get("storage_quotes"));
|
||||
FileSystemUtils.deleteRecursively(Paths.get("storage_orders"));
|
||||
|
||||
// Clean DB
|
||||
orderRepository.deleteAll();
|
||||
lineItemRepository.deleteAll();
|
||||
sessionRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateOrderFromQuote_ShouldCopyFilesAndUpdateStatus() throws Exception {
|
||||
// Prepare Request
|
||||
CreateOrderRequest request = new CreateOrderRequest();
|
||||
|
||||
CustomerDto customer = new CustomerDto();
|
||||
customer.setEmail("integration@test.com");
|
||||
customer.setCustomerType("PRIVATE");
|
||||
request.setCustomer(customer);
|
||||
|
||||
AddressDto billing = new AddressDto();
|
||||
billing.setFirstName("John");
|
||||
billing.setLastName("Doe");
|
||||
billing.setAddressLine1("Street 1");
|
||||
billing.setCity("City");
|
||||
billing.setZip("1000");
|
||||
billing.setCountryCode("CH");
|
||||
request.setBillingAddress(billing);
|
||||
|
||||
request.setShippingSameAsBilling(true);
|
||||
|
||||
// Execute Request
|
||||
mockMvc.perform(post("/api/orders/from-quote/" + sessionId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Verify Session Status
|
||||
QuoteSession updatedSession = sessionRepository.findById(sessionId).orElseThrow();
|
||||
assertEquals("CONVERTED", updatedSession.getStatus(), "Session status should be CONVERTED");
|
||||
assertNotNull(updatedSession.getConvertedOrderId(), "Converted Order ID should be set");
|
||||
|
||||
UUID orderId = updatedSession.getConvertedOrderId();
|
||||
|
||||
// Verify File Copy
|
||||
Path orderStorageDir = Paths.get("storage_orders");
|
||||
// We need to find the specific file. Structure: storage_orders/orderId/3d-files/orderItemId/filename
|
||||
// Since we don't know OrderItemId easily without querying DB, let's walk the dir.
|
||||
|
||||
try (var stream = Files.walk(orderStorageDir)) {
|
||||
boolean fileFound = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.anyMatch(path -> {
|
||||
try {
|
||||
return Files.readString(path).equals("dummy content");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
assertTrue(fileFound, "The file should have been copied to storage_orders with correct content");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,19 @@ services:
|
||||
- MARKUP_PERCENT=20
|
||||
- TEMP_DIR=/app/temp
|
||||
- PROFILES_DIR=/app/profiles
|
||||
- CLAMAV_HOST=clamav
|
||||
- CLAMAV_PORT=3310
|
||||
depends_on:
|
||||
- db
|
||||
- clamav
|
||||
restart: unless-stopped
|
||||
|
||||
clamav:
|
||||
platform: linux/amd64
|
||||
image: clamav/clamav:latest
|
||||
container_name: print-calculator-clamav
|
||||
ports:
|
||||
- "3310:3310"
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
|
||||
Reference in New Issue
Block a user