fix-tests-cleanup
This commit is contained in:
@@ -4,6 +4,8 @@ import auctiora.db.*;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jdbi.v3.core.Jdbi;
|
import org.jdbi.v3.core.Jdbi;
|
||||||
|
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,204 +17,254 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DatabaseService {
|
public class DatabaseService {
|
||||||
|
|
||||||
private final Jdbi jdbi;
|
private final Jdbi jdbi;
|
||||||
private final LotRepository lotRepository;
|
private final LotRepository lotRepository;
|
||||||
private final AuctionRepository auctionRepository;
|
private final AuctionRepository auctionRepository;
|
||||||
private final ImageRepository imageRepository;
|
private final ImageRepository imageRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for programmatic instantiation (tests, CLI tools).
|
* Constructor for programmatic instantiation (tests, CLI tools).
|
||||||
*/
|
*/
|
||||||
public DatabaseService(String dbPath) {
|
private final String url;
|
||||||
String url = "jdbc:sqlite:" + dbPath + "?journal_mode=WAL&busy_timeout=10000";
|
|
||||||
this.jdbi = Jdbi.create(url);
|
public DatabaseService(String dbPath) {
|
||||||
|
this.url = "jdbc:sqlite:" + dbPath + "?journal_mode=WAL&busy_timeout=10000";
|
||||||
// Initialize schema
|
this.jdbi = Jdbi.create(url);
|
||||||
DatabaseSchema.ensureSchema(jdbi);
|
|
||||||
|
// Initialize schema
|
||||||
// Create repositories
|
DatabaseSchema.ensureSchema(jdbi);
|
||||||
this.lotRepository = new LotRepository(jdbi);
|
|
||||||
this.auctionRepository = new AuctionRepository(jdbi);
|
// Create repositories
|
||||||
this.imageRepository = new ImageRepository(jdbi);
|
this.lotRepository = new LotRepository(jdbi);
|
||||||
}
|
this.auctionRepository = new AuctionRepository(jdbi);
|
||||||
|
this.imageRepository = new ImageRepository(jdbi);
|
||||||
/**
|
}
|
||||||
* Constructor with JDBI instance (for dependency injection).
|
|
||||||
*/
|
/**
|
||||||
public DatabaseService(Jdbi jdbi) {
|
* Constructor with JDBI instance (for dependency injection).
|
||||||
this.jdbi = jdbi;
|
*/
|
||||||
DatabaseSchema.ensureSchema(jdbi);
|
public DatabaseService(Jdbi jdbi) {
|
||||||
|
this.jdbi = jdbi;
|
||||||
this.lotRepository = new LotRepository(jdbi);
|
this.url = null; // Use null as this constructor doesn't use the URL
|
||||||
this.auctionRepository = new AuctionRepository(jdbi);
|
|
||||||
this.imageRepository = new ImageRepository(jdbi);
|
DatabaseSchema.ensureSchema(jdbi);
|
||||||
}
|
|
||||||
|
this.lotRepository = new LotRepository(jdbi);
|
||||||
// ==================== LEGACY COMPATIBILITY METHODS ====================
|
this.auctionRepository = new AuctionRepository(jdbi);
|
||||||
// These methods delegate to repositories for backward compatibility
|
this.imageRepository = new ImageRepository(jdbi);
|
||||||
|
}
|
||||||
void ensureSchema() {
|
|
||||||
DatabaseSchema.ensureSchema(jdbi);
|
// ==================== LEGACY COMPATIBILITY METHODS ====================
|
||||||
}
|
// These methods delegate to repositories for backward compatibility
|
||||||
|
|
||||||
synchronized void upsertAuction(AuctionInfo auction) {
|
void ensureSchema() {
|
||||||
auctionRepository.upsert(auction);
|
DatabaseSchema.ensureSchema(jdbi);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized List<AuctionInfo> getAllAuctions() {
|
synchronized void upsertAuction(AuctionInfo auction) {
|
||||||
return auctionRepository.getAll();
|
auctionRepository.upsert(auction);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized List<AuctionInfo> getAuctionsByCountry(String countryCode) {
|
synchronized List<AuctionInfo> getAllAuctions() {
|
||||||
return auctionRepository.getByCountry(countryCode);
|
return auctionRepository.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void upsertLot(Lot lot) {
|
synchronized List<AuctionInfo> getAuctionsByCountry(String countryCode) {
|
||||||
lotRepository.upsert(lot);
|
return auctionRepository.getByCountry(countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void upsertLotWithIntelligence(Lot lot) {
|
synchronized void upsertLot(Lot lot) {
|
||||||
lotRepository.upsertWithIntelligence(lot);
|
retry(() -> {
|
||||||
}
|
try (var connection = DriverManager.getConnection(url)) {
|
||||||
|
connection.setAutoCommit(false); // Start transaction
|
||||||
synchronized void updateLotCurrentBid(Lot lot) {
|
lotRepository.upsert(lot); // Perform update
|
||||||
lotRepository.updateCurrentBid(lot);
|
connection.commit(); // Commit transaction
|
||||||
}
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed to upsert lot", e);
|
||||||
synchronized void updateLotNotificationFlags(Lot lot) {
|
}
|
||||||
lotRepository.updateNotificationFlags(lot);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized List<Lot> getActiveLots() {
|
void upsertLots(List<Lot> lots) { // Batch import with transactions
|
||||||
return lotRepository.getActiveLots();
|
retry(() -> {
|
||||||
}
|
try (var connection = DriverManager.getConnection(url)) {
|
||||||
|
connection.setAutoCommit(false); // Start transaction
|
||||||
synchronized List<Lot> getAllLots() {
|
for (Lot lot : lots) {
|
||||||
return lotRepository.getAllLots();
|
lotRepository.upsert(lot); // Upsert individual lot
|
||||||
}
|
}
|
||||||
|
connection.commit(); // Commit transaction
|
||||||
synchronized List<BidHistory> getBidHistory(String lotId) {
|
} catch (SQLException e) {
|
||||||
return lotRepository.getBidHistory(lotId);
|
throw new RuntimeException("Failed to upsert lots", e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
synchronized void insertBidHistory(List<BidHistory> bidHistory) {
|
}
|
||||||
lotRepository.insertBidHistory(bidHistory);
|
|
||||||
}
|
// Retry logic for transient database failures
|
||||||
|
private void retry(Runnable action) {
|
||||||
synchronized void insertImage(long lotId, String url, String filePath, List<String> labels) {
|
final int maxRetries = 3;
|
||||||
imageRepository.insert(lotId, url, filePath, labels);
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
}
|
try {
|
||||||
|
action.run(); // Attempt action
|
||||||
synchronized void updateImageLabels(int imageId, List<String> labels) {
|
return; // Exit on success
|
||||||
imageRepository.updateLabels(imageId, labels);
|
} catch (RuntimeException e) {
|
||||||
}
|
boolean isBusy = e.getCause() instanceof SQLException
|
||||||
|
&& e.getCause().getMessage().contains("[SQLITE_BUSY]");
|
||||||
synchronized List<String> getImageLabels(int imageId) {
|
if (isBusy && attempt < maxRetries) {
|
||||||
return imageRepository.getLabels(imageId);
|
log.warn("Database locked, retrying {} of {}", attempt, maxRetries);
|
||||||
}
|
try {
|
||||||
|
Thread.sleep(500L * attempt); // Backoff
|
||||||
synchronized List<ImageRecord> getImagesForLot(long lotId) {
|
} catch (InterruptedException interrupted) {
|
||||||
return imageRepository.getImagesForLot(lotId)
|
Thread.currentThread().interrupt();
|
||||||
.stream()
|
}
|
||||||
.map(img -> new ImageRecord(img.id(), img.lotId(), img.url(), img.filePath(), img.labels()))
|
} else {
|
||||||
.toList();
|
throw e; // Non-retryable error or max retries exceeded
|
||||||
}
|
}
|
||||||
|
}
|
||||||
synchronized List<ImageDetectionRecord> getImagesNeedingDetection() {
|
}
|
||||||
return imageRepository.getImagesNeedingDetection()
|
}
|
||||||
.stream()
|
|
||||||
.map(img -> new ImageDetectionRecord(img.id(), img.lotId(), img.filePath()))
|
synchronized void upsertLotWithIntelligence(Lot lot) {
|
||||||
.toList();
|
lotRepository.upsertWithIntelligence(lot);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int getImageCount() {
|
synchronized void updateLotCurrentBid(Lot lot) {
|
||||||
return imageRepository.getImageCount();
|
lotRepository.updateCurrentBid(lot);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized List<AuctionInfo> importAuctionsFromScraper() {
|
synchronized void updateLotNotificationFlags(Lot lot) {
|
||||||
return jdbi.withHandle(handle -> {
|
lotRepository.updateNotificationFlags(lot);
|
||||||
var sql = """
|
}
|
||||||
SELECT
|
|
||||||
l.auction_id,
|
synchronized List<Lot> getActiveLots() {
|
||||||
MIN(l.title) as title,
|
return lotRepository.getActiveLots();
|
||||||
MIN(l.location) as location,
|
}
|
||||||
MIN(l.url) as url,
|
|
||||||
COUNT(*) as lots_count,
|
synchronized List<Lot> getAllLots() {
|
||||||
MIN(l.closing_time) as first_lot_closing_time,
|
return lotRepository.getAllLots();
|
||||||
MIN(l.scraped_at) as scraped_at
|
}
|
||||||
FROM lots l
|
|
||||||
WHERE l.auction_id IS NOT NULL
|
synchronized List<BidHistory> getBidHistory(String lotId) {
|
||||||
GROUP BY l.auction_id
|
return lotRepository.getBidHistory(lotId);
|
||||||
""";
|
}
|
||||||
|
|
||||||
return handle.createQuery(sql)
|
synchronized void insertBidHistory(List<BidHistory> bidHistory) {
|
||||||
.map((rs, ctx) -> {
|
lotRepository.insertBidHistory(bidHistory);
|
||||||
try {
|
}
|
||||||
var auction = ScraperDataAdapter.fromScraperAuction(rs);
|
|
||||||
if (auction.auctionId() != 0L) {
|
synchronized void insertImage(long lotId, String url, String filePath, List<String> labels) {
|
||||||
auctionRepository.upsert(auction);
|
imageRepository.insert(lotId, url, filePath, labels);
|
||||||
return auction;
|
}
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
synchronized void updateImageLabels(int imageId, List<String> labels) {
|
||||||
log.warn("Failed to import auction: {}", e.getMessage());
|
imageRepository.updateLabels(imageId, labels);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
})
|
synchronized List<String> getImageLabels(int imageId) {
|
||||||
.list()
|
return imageRepository.getLabels(imageId);
|
||||||
.stream()
|
}
|
||||||
.filter(a -> a != null)
|
|
||||||
.toList();
|
synchronized List<ImageRecord> getImagesForLot(long lotId) {
|
||||||
});
|
return imageRepository.getImagesForLot(lotId)
|
||||||
}
|
.stream()
|
||||||
|
.map(img -> new ImageRecord(img.id(), img.lotId(), img.url(), img.filePath(), img.labels()))
|
||||||
synchronized List<Lot> importLotsFromScraper() {
|
.toList();
|
||||||
return jdbi.withHandle(handle -> {
|
}
|
||||||
var sql = "SELECT * FROM lots";
|
|
||||||
|
synchronized List<ImageDetectionRecord> getImagesNeedingDetection() {
|
||||||
return handle.createQuery(sql)
|
return imageRepository.getImagesNeedingDetection()
|
||||||
.map((rs, ctx) -> {
|
.stream()
|
||||||
try {
|
.map(img -> new ImageDetectionRecord(img.id(), img.lotId(), img.filePath()))
|
||||||
var lot = ScraperDataAdapter.fromScraperLot(rs);
|
.toList();
|
||||||
if (lot.lotId() != 0L && lot.saleId() != 0L) {
|
}
|
||||||
lotRepository.upsert(lot);
|
|
||||||
return lot;
|
synchronized int getImageCount() {
|
||||||
}
|
return imageRepository.getImageCount();
|
||||||
} catch (Exception e) {
|
}
|
||||||
log.warn("Failed to import lot: {}", e.getMessage());
|
|
||||||
}
|
synchronized List<AuctionInfo> importAuctionsFromScraper() {
|
||||||
return null;
|
return jdbi.withHandle(handle -> {
|
||||||
})
|
var sql = """
|
||||||
.list()
|
SELECT
|
||||||
.stream()
|
l.auction_id,
|
||||||
.filter(l -> l != null)
|
MIN(l.title) as title,
|
||||||
.toList();
|
MIN(l.location) as location,
|
||||||
});
|
MIN(l.url) as url,
|
||||||
}
|
COUNT(*) as lots_count,
|
||||||
|
MIN(l.closing_time) as first_lot_closing_time,
|
||||||
// ==================== DIRECT REPOSITORY ACCESS ====================
|
MIN(l.scraped_at) as scraped_at
|
||||||
// Expose repositories for modern usage patterns
|
FROM lots l
|
||||||
|
WHERE l.auction_id IS NOT NULL
|
||||||
public LotRepository lots() {
|
GROUP BY l.auction_id
|
||||||
return lotRepository;
|
""";
|
||||||
}
|
|
||||||
|
return handle.createQuery(sql)
|
||||||
public AuctionRepository auctions() {
|
.map((rs, ctx) -> {
|
||||||
return auctionRepository;
|
try {
|
||||||
}
|
var auction = ScraperDataAdapter.fromScraperAuction(rs);
|
||||||
|
if (auction.auctionId() != 0L) {
|
||||||
public ImageRepository images() {
|
auctionRepository.upsert(auction);
|
||||||
return imageRepository;
|
return auction;
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
public Jdbi getJdbi() {
|
log.warn("Failed to import auction: {}", e.getMessage());
|
||||||
return jdbi;
|
}
|
||||||
}
|
return null;
|
||||||
|
})
|
||||||
// ==================== LEGACY RECORDS ====================
|
.list()
|
||||||
// Keep records for backward compatibility with existing code
|
.stream()
|
||||||
|
.filter(a -> a != null)
|
||||||
public record ImageRecord(int id, long lotId, String url, String filePath, String labels) {}
|
.toList();
|
||||||
|
});
|
||||||
public record ImageDetectionRecord(int id, long lotId, String filePath) {}
|
}
|
||||||
|
|
||||||
|
synchronized List<Lot> importLotsFromScraper() {
|
||||||
|
return jdbi.withHandle(handle -> {
|
||||||
|
var sql = "SELECT * FROM lots";
|
||||||
|
|
||||||
|
return handle.createQuery(sql)
|
||||||
|
.map((rs, ctx) -> {
|
||||||
|
try {
|
||||||
|
var lot = ScraperDataAdapter.fromScraperLot(rs);
|
||||||
|
if (lot.lotId() != 0L && lot.saleId() != 0L) {
|
||||||
|
lotRepository.upsert(lot);
|
||||||
|
return lot;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to import lot: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.list()
|
||||||
|
.stream()
|
||||||
|
.filter(l -> l != null)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DIRECT REPOSITORY ACCESS ====================
|
||||||
|
// Expose repositories for modern usage patterns
|
||||||
|
|
||||||
|
public LotRepository lots() {
|
||||||
|
return lotRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuctionRepository auctions() {
|
||||||
|
return auctionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageRepository images() {
|
||||||
|
return imageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jdbi getJdbi() {
|
||||||
|
return jdbi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== LEGACY RECORDS ====================
|
||||||
|
// Keep records for backward compatibility with existing code
|
||||||
|
|
||||||
|
public record ImageRecord(int id, long lotId, String url, String filePath, String labels) { }
|
||||||
|
|
||||||
|
public record ImageDetectionRecord(int id, long lotId, String filePath) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,32 +45,32 @@ public record NotificationService(Config cfg) {
|
|||||||
|
|
||||||
var type = prio > 0 ? TrayIcon.MessageType.WARNING : TrayIcon.MessageType.INFO;
|
var type = prio > 0 ? TrayIcon.MessageType.WARNING : TrayIcon.MessageType.INFO;
|
||||||
icon.displayMessage(title, msg, type);
|
icon.displayMessage(title, msg, type);
|
||||||
|
|
||||||
// Remove tray icon asynchronously to avoid blocking the caller
|
// Remove tray icon asynchronously to avoid blocking the caller
|
||||||
int delayMs = Integer.getInteger("auctiora.desktop.delay.ms", 0);
|
int delayMs = Integer.getInteger("auctiora.desktop.delay.ms", 0);
|
||||||
if (delayMs <= 0) {
|
if (delayMs <= 0) {
|
||||||
var t = new Thread(() -> {
|
var t = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(50);
|
Thread.sleep(50);
|
||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException ignored) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
tray.remove(icon);
|
tray.remove(icon);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
}, "tray-remove");
|
}, "tray-remove");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
t.start();
|
t.start();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(delayMs);
|
Thread.sleep(delayMs);
|
||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException ignored) {
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
tray.remove(icon);
|
tray.remove(icon);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("Desktop notification: {}", title);
|
log.info("Desktop notification: {}", title);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -88,34 +88,34 @@ public record NotificationService(Config cfg) {
|
|||||||
props.put("mail.smtp.port", "587");
|
props.put("mail.smtp.port", "587");
|
||||||
props.put("mail.smtp.ssl.trust", "smtp.gmail.com");
|
props.put("mail.smtp.ssl.trust", "smtp.gmail.com");
|
||||||
props.put("mail.smtp.ssl.protocols", "TLSv1.2");
|
props.put("mail.smtp.ssl.protocols", "TLSv1.2");
|
||||||
|
|
||||||
// Connection timeouts (configurable; short during tests, longer otherwise)
|
// Connection timeouts (configurable; short during tests, longer otherwise)
|
||||||
int smtpTimeoutMs = Integer.getInteger("auctiora.smtp.timeout.ms", isUnderTest() ? 200 : 10000);
|
int smtpTimeoutMs = Integer.getInteger("auctiora.smtp.timeout.ms", isUnderTest() ? 200 : 10000);
|
||||||
String t = String.valueOf(smtpTimeoutMs);
|
String t = String.valueOf(smtpTimeoutMs);
|
||||||
props.put("mail.smtp.connectiontimeout", t);
|
props.put("mail.smtp.connectiontimeout", t);
|
||||||
props.put("mail.smtp.timeout", t);
|
props.put("mail.smtp.timeout", t);
|
||||||
props.put("mail.smtp.writetimeout", t);
|
props.put("mail.smtp.writetimeout", t);
|
||||||
|
|
||||||
var session = Session.getInstance(props, new Authenticator() {
|
var session = Session.getInstance(props, new Authenticator() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected PasswordAuthentication getPasswordAuthentication() {
|
protected PasswordAuthentication getPasswordAuthentication() {
|
||||||
return new PasswordAuthentication(cfg.smtpUsername(), cfg.smtpPassword());
|
return new PasswordAuthentication(cfg.smtpUsername(), cfg.smtpPassword());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var m = new MimeMessage(session);
|
var m = new MimeMessage(session);
|
||||||
m.setFrom(new InternetAddress(cfg.smtpUsername()));
|
m.setFrom(new InternetAddress(cfg.smtpUsername()));
|
||||||
m.setRecipients(Message.RecipientType.TO, InternetAddress.parse(cfg.toEmail()));
|
m.setRecipients(Message.RecipientType.TO, InternetAddress.parse(cfg.toEmail()));
|
||||||
m.setSubject("[Troostwijk] " + title);
|
m.setSubject("[Troostwijk] " + title);
|
||||||
m.setText(msg);
|
m.setText(msg);
|
||||||
m.setSentDate(new Date());
|
m.setSentDate(new Date());
|
||||||
|
|
||||||
if (prio > 0) {
|
if (prio > 0) {
|
||||||
m.setHeader("X-Priority", "1");
|
m.setHeader("X-Priority", "1");
|
||||||
m.setHeader("Importance", "High");
|
m.setHeader("Importance", "High");
|
||||||
}
|
}
|
||||||
|
|
||||||
Transport.send(m);
|
Transport.send(m);
|
||||||
log.info("Email notification sent: {}", title);
|
log.info("Email notification sent: {}", title);
|
||||||
} catch (javax.mail.AuthenticationFailedException e) {
|
} catch (javax.mail.AuthenticationFailedException e) {
|
||||||
@@ -151,15 +151,15 @@ public record NotificationService(Config cfg) {
|
|||||||
throw new IllegalArgumentException("Use 'desktop' or 'smtp:username:password:toEmail'");
|
throw new IllegalArgumentException("Use 'desktop' or 'smtp:username:password:toEmail'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isUnderTest() {
|
private static boolean isUnderTest() {
|
||||||
try {
|
try {
|
||||||
// Explicit override
|
// Explicit override
|
||||||
if (Boolean.getBoolean("auctiora.test")) return true;
|
if (Boolean.getBoolean("auctiora.test")) return true;
|
||||||
|
|
||||||
// Maven Surefire commonly sets this property
|
// Maven Surefire commonly sets this property
|
||||||
if (System.getProperty("surefire.test.class.path") != null) return true;
|
if (System.getProperty("surefire.test.class.path") != null) return true;
|
||||||
|
|
||||||
// Fallback: check classpath hint
|
// Fallback: check classpath hint
|
||||||
String cp = System.getProperty("java.class.path", "");
|
String cp = System.getProperty("java.class.path", "");
|
||||||
return cp.contains("surefire") || cp.contains("junit");
|
return cp.contains("surefire") || cp.contains("junit");
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class NotificationServiceTest {
|
|||||||
@DisplayName("Should include both desktop and email when SMTP configured")
|
@DisplayName("Should include both desktop and email when SMTP configured")
|
||||||
void testBothNotificationChannels() {
|
void testBothNotificationChannels() {
|
||||||
var service = new NotificationService(
|
var service = new NotificationService(
|
||||||
"smtp:user@gmail.com:password:recipient@example.com"
|
"smtp:michael.bakker1986@gmail.com:agrepolhlnvhipkv:michael.bakker1986@gmail.com"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Both desktop and email should be attempted
|
// Both desktop and email should be attempted
|
||||||
|
|||||||
65
src/test/resources/application.properties
Normal file
65
src/test/resources/application.properties
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Application Configuration
|
||||||
|
# Values will be injected from pom.xml during build
|
||||||
|
quarkus.application.name=${project.artifactId}
|
||||||
|
quarkus.application.version=${project.version}
|
||||||
|
# Custom properties for groupId if needed
|
||||||
|
application.groupId=${project.groupId}
|
||||||
|
application.artifactId=${project.artifactId}
|
||||||
|
application.version=${project.version}
|
||||||
|
|
||||||
|
|
||||||
|
# HTTP Configuration
|
||||||
|
quarkus.http.port=8081
|
||||||
|
# ========== DEVELOPMENT (quarkus:dev) ==========
|
||||||
|
%dev.quarkus.http.host=127.0.0.1
|
||||||
|
# ========== PRODUCTION (Docker/JAR) ==========
|
||||||
|
%prod.quarkus.http.host=0.0.0.0
|
||||||
|
# ========== TEST PROFILE ==========
|
||||||
|
%test.quarkus.http.host=localhost
|
||||||
|
|
||||||
|
# Enable CORS for frontend development
|
||||||
|
quarkus.http.cors=true
|
||||||
|
quarkus.http.cors.origins=*
|
||||||
|
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
|
||||||
|
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
|
||||||
|
quarkus.log.console.level=INFO
|
||||||
|
|
||||||
|
# Development mode settings
|
||||||
|
%dev.quarkus.log.console.level=DEBUG
|
||||||
|
%dev.quarkus.live-reload.instrumentation=true
|
||||||
|
|
||||||
|
# JVM Arguments for native access (Jansi, OpenCV, etc.)
|
||||||
|
quarkus.native.additional-build-args=--enable-native-access=ALL-UNNAMED
|
||||||
|
|
||||||
|
# Production optimizations
|
||||||
|
%prod.quarkus.package.type=fast-jar
|
||||||
|
%prod.quarkus.http.enable-compression=true
|
||||||
|
|
||||||
|
# Static resources
|
||||||
|
quarkus.http.enable-compression=true
|
||||||
|
quarkus.rest.path=/
|
||||||
|
quarkus.http.root-path=/
|
||||||
|
|
||||||
|
# Auction Monitor Configuration
|
||||||
|
auction.database.path=/mnt/okcomputer/output/cache.db
|
||||||
|
auction.images.path=/mnt/okcomputer/output/images
|
||||||
|
# auction.notification.config=desktop
|
||||||
|
# Format: smtp:username:password:recipient_email
|
||||||
|
auction.notification.config=smtp:michael.bakker1986@gmail.com:agrepolhlnvhipkv:michael.bakker1986@gmail.com
|
||||||
|
|
||||||
|
auction.yolo.config=/mnt/okcomputer/output/models/yolov4.cfg
|
||||||
|
auction.yolo.weights=/mnt/okcomputer/output/models/yolov4.weights
|
||||||
|
auction.yolo.classes=/mnt/okcomputer/output/models/coco.names
|
||||||
|
|
||||||
|
# HTTP Rate Limiting Configuration
|
||||||
|
# Prevents overloading external services and getting blocked
|
||||||
|
auction.http.rate-limit.default-max-rps=2
|
||||||
|
auction.http.rate-limit.troostwijk-max-rps=1
|
||||||
|
auction.http.timeout-seconds=30
|
||||||
|
|
||||||
|
# Health Check Configuration
|
||||||
|
quarkus.smallrye-health.root-path=/health
|
||||||
|
|
||||||
Reference in New Issue
Block a user