fix-tests-cleanup

Former-commit-id: be65f4a5e6
This commit is contained in:
Tour
2025-12-08 07:19:50 +01:00
parent aecf32eb19
commit df919abad5
24 changed files with 4524 additions and 2808 deletions

View File

@@ -5,6 +5,7 @@ import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
import nu.pattern.OpenCV;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.opencv.core.Core;
@@ -24,7 +25,7 @@ public class AuctionMonitorProducer {
@PostConstruct void init() {
try {
nu.pattern.OpenCV.loadLocally();
OpenCV.loadLocally();
LOG.info("✓ OpenCV loaded successfully");
} catch (Exception e) {
LOG.warn("⚠️ OpenCV not available - image detection will be disabled: " + e.getMessage());

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ import java.time.LocalDateTime;
/// Data typically populated by the external scraper process.
/// This project enriches the data with image analysis and monitoring.
@With
record Lot(
public record Lot(
long saleId,
long lotId,
String displayId, // Full lot ID string (e.g., "A1-34732-49") for GraphQL queries

View File

@@ -60,25 +60,21 @@ public class TroostwijkMonitor {
for (var lot : activeLots) {
checkAndUpdateLot(lot);
}
} catch (SQLException e) {
} catch (Exception e) {
log.error("Error during scheduled monitoring", e);
}
}
private void checkAndUpdateLot(Lot lot) {
refreshLotBid(lot);
var minutesLeft = lot.minutesUntilClose();
if (minutesLeft < 30) {
if (minutesLeft <= 5 && !lot.closingNotified()) {
notifier.sendNotification(
"Kavel " + lot.lotId() + " sluit binnen " + minutesLeft + " min.",
"Lot nearing closure", 1);
try {
db.updateLotNotificationFlags(lot.withClosingNotified(true));
} catch (SQLException e) {
throw new RuntimeException(e);
}
db.updateLotNotificationFlags(lot.withClosingNotified(true));
}
scheduler.schedule(() -> refreshLotBid(lot), 5, TimeUnit.MINUTES);
}
@@ -109,12 +105,12 @@ public class TroostwijkMonitor {
notifier.sendNotification(msg, "Kavel bieding update", 0);
}
}
} catch (IOException | InterruptedException | SQLException e) {
} catch (IOException | InterruptedException e) {
log.warn("Failed to refresh bid for lot {}", lot.lotId(), e);
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
}
}
public void printDatabaseStats() {
try {
var allLots = db.getAllLots();
@@ -125,7 +121,7 @@ public class TroostwijkMonitor {
var sum = allLots.stream().mapToDouble(Lot::currentBid).sum();
log.info("Total current bids: €{:.2f}", sum);
}
} catch (SQLException e) {
} catch (Exception e) {
log.warn("Could not retrieve database stats", e);
}
}

View File

@@ -0,0 +1,164 @@
package auctiora.db;
import auctiora.AuctionInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Jdbi;
import java.time.LocalDateTime;
import java.util.List;
/**
* Repository for auction-related database operations using JDBI3.
* Handles CRUD operations and queries for auctions.
*/
@Slf4j
@RequiredArgsConstructor
public class AuctionRepository {
private final Jdbi jdbi;
/**
* Inserts or updates an auction record.
* Handles both auction_id conflicts and url uniqueness constraints.
*/
public void upsert(AuctionInfo auction) {
jdbi.useTransaction(handle -> {
try {
// Try INSERT with ON CONFLICT on auction_id
handle.createUpdate("""
INSERT INTO auctions (
auction_id, title, location, city, country, url, type, lot_count, closing_time, discovered_at
) VALUES (
:auctionId, :title, :location, :city, :country, :url, :type, :lotCount, :closingTime, :discoveredAt
)
ON CONFLICT(auction_id) DO UPDATE SET
title = excluded.title,
location = excluded.location,
city = excluded.city,
country = excluded.country,
url = excluded.url,
type = excluded.type,
lot_count = excluded.lot_count,
closing_time = excluded.closing_time
""")
.bind("auctionId", auction.auctionId())
.bind("title", auction.title())
.bind("location", auction.location())
.bind("city", auction.city())
.bind("country", auction.country())
.bind("url", auction.url())
.bind("type", auction.typePrefix())
.bind("lotCount", auction.lotCount())
.bind("closingTime", auction.firstLotClosingTime() != null ? auction.firstLotClosingTime().toString() : null)
.bind("discoveredAt", java.time.Instant.now().getEpochSecond())
.execute();
} catch (Exception e) {
// If UNIQUE constraint on url fails, try updating by url
String errMsg = e.getMessage();
if (errMsg != null && (errMsg.contains("UNIQUE constraint failed") ||
errMsg.contains("PRIMARY KEY constraint failed"))) {
log.debug("Auction conflict detected, attempting update by URL: {}", auction.url());
int updated = handle.createUpdate("""
UPDATE auctions SET
auction_id = :auctionId,
title = :title,
location = :location,
city = :city,
country = :country,
type = :type,
lot_count = :lotCount,
closing_time = :closingTime
WHERE url = :url
""")
.bind("auctionId", auction.auctionId())
.bind("title", auction.title())
.bind("location", auction.location())
.bind("city", auction.city())
.bind("country", auction.country())
.bind("type", auction.typePrefix())
.bind("lotCount", auction.lotCount())
.bind("closingTime", auction.firstLotClosingTime() != null ? auction.firstLotClosingTime().toString() : null)
.bind("url", auction.url())
.execute();
if (updated == 0) {
log.warn("Failed to update auction by URL: {}", auction.url());
}
} else {
log.error("Unexpected error upserting auction: {}", e.getMessage(), e);
throw e;
}
}
});
}
/**
* Retrieves all auctions from the database.
*/
public List<AuctionInfo> getAll() {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT * FROM auctions")
.map((rs, ctx) -> {
String closingStr = rs.getString("closing_time");
LocalDateTime closingTime = null;
if (closingStr != null && !closingStr.isBlank()) {
try {
closingTime = LocalDateTime.parse(closingStr);
} catch (Exception e) {
log.warn("Invalid closing_time format: {}", closingStr);
}
}
return new AuctionInfo(
rs.getLong("auction_id"),
rs.getString("title"),
rs.getString("location"),
rs.getString("city"),
rs.getString("country"),
rs.getString("url"),
rs.getString("type"),
rs.getInt("lot_count"),
closingTime
);
})
.list()
);
}
/**
* Retrieves auctions filtered by country code.
*/
public List<AuctionInfo> getByCountry(String countryCode) {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT * FROM auctions WHERE country = :country")
.bind("country", countryCode)
.map((rs, ctx) -> {
String closingStr = rs.getString("closing_time");
LocalDateTime closingTime = null;
if (closingStr != null && !closingStr.isBlank()) {
try {
closingTime = LocalDateTime.parse(closingStr);
} catch (Exception e) {
log.warn("Invalid closing_time format: {}", closingStr);
}
}
return new AuctionInfo(
rs.getLong("auction_id"),
rs.getString("title"),
rs.getString("location"),
rs.getString("city"),
rs.getString("country"),
rs.getString("url"),
rs.getString("type"),
rs.getInt("lot_count"),
closingTime
);
})
.list()
);
}
}

View File

@@ -0,0 +1,154 @@
package auctiora.db;
import lombok.experimental.UtilityClass;
import org.jdbi.v3.core.Jdbi;
/**
* Database schema DDL definitions for all tables and indexes.
* Uses text blocks (Java 15+) for clean SQL formatting.
*/
@UtilityClass
public class DatabaseSchema {
/**
* Initializes all database tables and indexes if they don't exist.
*/
public void ensureSchema(Jdbi jdbi) {
jdbi.useHandle(handle -> {
// Enable WAL mode for better concurrent access
handle.execute("PRAGMA journal_mode=WAL");
handle.execute("PRAGMA busy_timeout=10000");
handle.execute("PRAGMA synchronous=NORMAL");
createTables(handle);
createIndexes(handle);
});
}
private void createTables(org.jdbi.v3.core.Handle handle) {
// Cache table (for HTTP caching)
handle.execute("""
CREATE TABLE IF NOT EXISTS cache (
url TEXT PRIMARY KEY,
content BLOB,
timestamp REAL,
status_code INTEGER
)""");
// Auctions table (populated by external scraper)
handle.execute("""
CREATE TABLE IF NOT EXISTS auctions (
auction_id TEXT PRIMARY KEY,
url TEXT UNIQUE,
title TEXT,
location TEXT,
lots_count INTEGER,
first_lot_closing_time TEXT,
scraped_at TEXT,
city TEXT,
country TEXT,
type TEXT,
lot_count INTEGER DEFAULT 0,
closing_time TEXT,
discovered_at INTEGER
)""");
// Lots table (populated by external scraper)
handle.execute("""
CREATE TABLE IF NOT EXISTS lots (
lot_id TEXT PRIMARY KEY,
auction_id TEXT,
url TEXT UNIQUE,
title TEXT,
current_bid TEXT,
bid_count INTEGER,
closing_time TEXT,
viewing_time TEXT,
pickup_date TEXT,
location TEXT,
description TEXT,
category TEXT,
scraped_at TEXT,
sale_id INTEGER,
manufacturer TEXT,
type TEXT,
year INTEGER,
currency TEXT DEFAULT 'EUR',
closing_notified INTEGER DEFAULT 0,
starting_bid TEXT,
minimum_bid TEXT,
status TEXT,
brand TEXT,
model TEXT,
attributes_json TEXT,
first_bid_time TEXT,
last_bid_time TEXT,
bid_velocity REAL,
bid_increment REAL,
year_manufactured INTEGER,
condition_score REAL,
condition_description TEXT,
serial_number TEXT,
damage_description TEXT,
followers_count INTEGER DEFAULT 0,
estimated_min_price REAL,
estimated_max_price REAL,
lot_condition TEXT,
appearance TEXT,
estimated_min REAL,
estimated_max REAL,
next_bid_step_cents INTEGER,
condition TEXT,
category_path TEXT,
city_location TEXT,
country_code TEXT,
bidding_status TEXT,
packaging TEXT,
quantity INTEGER,
vat REAL,
buyer_premium_percentage REAL,
remarks TEXT,
reserve_price REAL,
reserve_met INTEGER,
view_count INTEGER,
FOREIGN KEY (auction_id) REFERENCES auctions(auction_id)
)""");
// Images table (populated by external scraper with URLs and local_path)
handle.execute("""
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
lot_id TEXT,
url TEXT,
local_path TEXT,
downloaded INTEGER DEFAULT 0,
labels TEXT,
processed_at INTEGER,
FOREIGN KEY (lot_id) REFERENCES lots(lot_id)
)""");
// Bid history table
handle.execute("""
CREATE TABLE IF NOT EXISTS bid_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
lot_id TEXT NOT NULL,
bid_amount REAL NOT NULL,
bid_time TEXT NOT NULL,
is_autobid INTEGER DEFAULT 0,
bidder_id TEXT,
bidder_number INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (lot_id) REFERENCES lots(lot_id)
)""");
}
private void createIndexes(org.jdbi.v3.core.Handle handle) {
handle.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON cache(timestamp)");
handle.execute("CREATE INDEX IF NOT EXISTS idx_auctions_country ON auctions(country)");
handle.execute("CREATE INDEX IF NOT EXISTS idx_lots_sale_id ON lots(sale_id)");
handle.execute("CREATE INDEX IF NOT EXISTS idx_images_lot_id ON images(lot_id)");
handle.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_lot_url ON images(lot_id, url)");
handle.execute("CREATE INDEX IF NOT EXISTS idx_bid_history_lot_time ON bid_history(lot_id, bid_time)");
handle.execute("CREATE INDEX IF NOT EXISTS idx_bid_history_bidder ON bid_history(bidder_id)");
}
}

View File

@@ -0,0 +1,137 @@
package auctiora.db;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Jdbi;
import java.time.Instant;
import java.util.List;
/**
* Repository for image-related database operations using JDBI3.
* Handles image storage, object detection labels, and processing status.
*/
@Slf4j
@RequiredArgsConstructor
public class ImageRepository {
private final Jdbi jdbi;
/**
* Image record containing all image metadata.
*/
public record ImageRecord(int id, long lotId, String url, String filePath, String labels) {}
/**
* Minimal record for images needing object detection processing.
*/
public record ImageDetectionRecord(int id, long lotId, String filePath) {}
/**
* Inserts a complete image record (for testing/legacy compatibility).
* In production, scraper inserts with local_path, monitor updates labels via updateLabels.
*/
public void insert(long lotId, String url, String filePath, List<String> labels) {
jdbi.useHandle(handle ->
handle.createUpdate("""
INSERT INTO images (lot_id, url, local_path, labels, processed_at, downloaded)
VALUES (:lotId, :url, :localPath, :labels, :processedAt, 1)
""")
.bind("lotId", lotId)
.bind("url", url)
.bind("localPath", filePath)
.bind("labels", String.join(",", labels))
.bind("processedAt", Instant.now().getEpochSecond())
.execute()
);
}
/**
* Updates the labels field for an image after object detection.
*/
public void updateLabels(int imageId, List<String> labels) {
jdbi.useHandle(handle ->
handle.createUpdate("UPDATE images SET labels = :labels, processed_at = :processedAt WHERE id = :id")
.bind("labels", String.join(",", labels))
.bind("processedAt", Instant.now().getEpochSecond())
.bind("id", imageId)
.execute()
);
}
/**
* Gets the labels for a specific image.
*/
public List<String> getLabels(int imageId) {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT labels FROM images WHERE id = :id")
.bind("id", imageId)
.mapTo(String.class)
.findOne()
.map(labelsStr -> {
if (labelsStr != null && !labelsStr.isEmpty()) {
return List.of(labelsStr.split(","));
}
return List.<String>of();
})
.orElse(List.of())
);
}
/**
* Retrieves images for a specific lot.
*/
public List<ImageRecord> getImagesForLot(long lotId) {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT id, lot_id, url, local_path, labels FROM images WHERE lot_id = :lotId")
.bind("lotId", lotId)
.map((rs, ctx) -> new ImageRecord(
rs.getInt("id"),
rs.getLong("lot_id"),
rs.getString("url"),
rs.getString("local_path"),
rs.getString("labels")
))
.list()
);
}
/**
* Gets images that have been downloaded by the scraper but need object detection.
* Only returns images that have local_path set but no labels yet.
*/
public List<ImageDetectionRecord> getImagesNeedingDetection() {
return jdbi.withHandle(handle ->
handle.createQuery("""
SELECT i.id, i.lot_id, i.local_path
FROM images i
WHERE i.local_path IS NOT NULL
AND i.local_path != ''
AND (i.labels IS NULL OR i.labels = '')
""")
.map((rs, ctx) -> {
// Extract numeric lot ID from TEXT field (e.g., "A1-34732-49" -> 3473249)
String lotIdStr = rs.getString("lot_id");
long lotId = auctiora.ScraperDataAdapter.extractNumericId(lotIdStr);
return new ImageDetectionRecord(
rs.getInt("id"),
lotId,
rs.getString("local_path")
);
})
.list()
);
}
/**
* Gets the total number of images in the database.
*/
public int getImageCount() {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT COUNT(*) FROM images")
.mapTo(Integer.class)
.one()
);
}
}

View File

@@ -0,0 +1,275 @@
package auctiora.db;
import auctiora.Lot;
import auctiora.BidHistory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Jdbi;
import java.time.LocalDateTime;
import java.util.List;
import static java.sql.Types.*;
/**
* Repository for lot-related database operations using JDBI3.
* Handles CRUD operations and queries for auction lots.
*/
@Slf4j
@RequiredArgsConstructor
public class LotRepository {
private final Jdbi jdbi;
/**
* Inserts or updates a lot (upsert operation).
* First tries UPDATE, then falls back to INSERT if lot doesn't exist.
*/
public void upsert(Lot lot) {
jdbi.useTransaction(handle -> {
// Try UPDATE first
int updated = handle.createUpdate("""
UPDATE lots SET
sale_id = :saleId,
auction_id = :auctionId,
title = :title,
description = :description,
manufacturer = :manufacturer,
type = :type,
year = :year,
category = :category,
current_bid = :currentBid,
currency = :currency,
url = :url,
closing_time = :closingTime
WHERE lot_id = :lotId
""")
.bind("saleId", String.valueOf(lot.saleId()))
.bind("auctionId", String.valueOf(lot.saleId())) // auction_id = sale_id
.bind("title", lot.title())
.bind("description", lot.description())
.bind("manufacturer", lot.manufacturer())
.bind("type", lot.type())
.bind("year", lot.year())
.bind("category", lot.category())
.bind("currentBid", lot.currentBid())
.bind("currency", lot.currency())
.bind("url", lot.url())
.bind("closingTime", lot.closingTime() != null ? lot.closingTime().toString() : null)
.bind("lotId", String.valueOf(lot.lotId()))
.execute();
if (updated == 0) {
// No rows updated, perform INSERT
handle.createUpdate("""
INSERT OR IGNORE INTO lots (
lot_id, sale_id, auction_id, title, description, manufacturer, type, year,
category, current_bid, currency, url, closing_time, closing_notified
) VALUES (
:lotId, :saleId, :auctionId, :title, :description, :manufacturer, :type, :year,
:category, :currentBid, :currency, :url, :closingTime, :closingNotified
)
""")
.bind("lotId", String.valueOf(lot.lotId()))
.bind("saleId", String.valueOf(lot.saleId()))
.bind("auctionId", String.valueOf(lot.saleId())) // auction_id = sale_id
.bind("title", lot.title())
.bind("description", lot.description())
.bind("manufacturer", lot.manufacturer())
.bind("type", lot.type())
.bind("year", lot.year())
.bind("category", lot.category())
.bind("currentBid", lot.currentBid())
.bind("currency", lot.currency())
.bind("url", lot.url())
.bind("closingTime", lot.closingTime() != null ? lot.closingTime().toString() : null)
.bind("closingNotified", lot.closingNotified() ? 1 : 0)
.execute();
}
});
}
/**
* Updates a lot with full intelligence data from GraphQL enrichment.
* Includes all 24+ intelligence fields from bidding platform.
*/
public void upsertWithIntelligence(Lot lot) {
jdbi.useHandle(handle -> {
var update = handle.createUpdate("""
UPDATE lots SET
sale_id = :saleId,
title = :title,
description = :description,
manufacturer = :manufacturer,
type = :type,
year = :year,
category = :category,
current_bid = :currentBid,
currency = :currency,
url = :url,
closing_time = :closingTime,
followers_count = :followersCount,
estimated_min = :estimatedMin,
estimated_max = :estimatedMax,
next_bid_step_cents = :nextBidStepInCents,
condition = :condition,
category_path = :categoryPath,
city_location = :cityLocation,
country_code = :countryCode,
bidding_status = :biddingStatus,
appearance = :appearance,
packaging = :packaging,
quantity = :quantity,
vat = :vat,
buyer_premium_percentage = :buyerPremiumPercentage,
remarks = :remarks,
starting_bid = :startingBid,
reserve_price = :reservePrice,
reserve_met = :reserveMet,
bid_increment = :bidIncrement,
view_count = :viewCount,
first_bid_time = :firstBidTime,
last_bid_time = :lastBidTime,
bid_velocity = :bidVelocity
WHERE lot_id = :lotId
""")
.bind("saleId", lot.saleId())
.bind("title", lot.title())
.bind("description", lot.description())
.bind("manufacturer", lot.manufacturer())
.bind("type", lot.type())
.bind("year", lot.year())
.bind("category", lot.category())
.bind("currentBid", lot.currentBid())
.bind("currency", lot.currency())
.bind("url", lot.url())
.bind("closingTime", lot.closingTime() != null ? lot.closingTime().toString() : null)
.bind("followersCount", lot.followersCount())
.bind("estimatedMin", lot.estimatedMin())
.bind("estimatedMax", lot.estimatedMax())
.bind("nextBidStepInCents", lot.nextBidStepInCents())
.bind("condition", lot.condition())
.bind("categoryPath", lot.categoryPath())
.bind("cityLocation", lot.cityLocation())
.bind("countryCode", lot.countryCode())
.bind("biddingStatus", lot.biddingStatus())
.bind("appearance", lot.appearance())
.bind("packaging", lot.packaging())
.bind("quantity", lot.quantity())
.bind("vat", lot.vat())
.bind("buyerPremiumPercentage", lot.buyerPremiumPercentage())
.bind("remarks", lot.remarks())
.bind("startingBid", lot.startingBid())
.bind("reservePrice", lot.reservePrice())
.bind("reserveMet", lot.reserveMet() != null && lot.reserveMet() ? 1 : null)
.bind("bidIncrement", lot.bidIncrement())
.bind("viewCount", lot.viewCount())
.bind("firstBidTime", lot.firstBidTime() != null ? lot.firstBidTime().toString() : null)
.bind("lastBidTime", lot.lastBidTime() != null ? lot.lastBidTime().toString() : null)
.bind("bidVelocity", lot.bidVelocity())
.bind("lotId", lot.lotId());
int updated = update.execute();
if (updated == 0) {
log.warn("Failed to update lot {} - lot not found in database", lot.lotId());
}
});
}
/**
* Updates only the current bid for a lot (lightweight update).
*/
public void updateCurrentBid(Lot lot) {
jdbi.useHandle(handle ->
handle.createUpdate("UPDATE lots SET current_bid = :bid WHERE lot_id = :lotId")
.bind("bid", lot.currentBid())
.bind("lotId", String.valueOf(lot.lotId()))
.execute()
);
}
/**
* Updates notification flags for a lot.
*/
public void updateNotificationFlags(Lot lot) {
jdbi.useHandle(handle ->
handle.createUpdate("UPDATE lots SET closing_notified = :notified WHERE lot_id = :lotId")
.bind("notified", lot.closingNotified() ? 1 : 0)
.bind("lotId", String.valueOf(lot.lotId()))
.execute()
);
}
/**
* Retrieves all active lots.
* Note: Despite the name, this returns ALL lots (legacy behavior for backward compatibility).
*/
public List<Lot> getActiveLots() {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT * FROM lots")
.map((rs, ctx) -> auctiora.ScraperDataAdapter.fromScraperLot(rs))
.list()
);
}
/**
* Retrieves all lots from the database.
*/
public List<Lot> getAllLots() {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT * FROM lots")
.map((rs, ctx) -> auctiora.ScraperDataAdapter.fromScraperLot(rs))
.list()
);
}
/**
* Retrieves bid history for a specific lot.
*/
public List<BidHistory> getBidHistory(String lotId) {
return jdbi.withHandle(handle ->
handle.createQuery("""
SELECT id, lot_id, bid_amount, bid_time, is_autobid, bidder_id, bidder_number
FROM bid_history
WHERE lot_id = :lotId
ORDER BY bid_time DESC
""")
.bind("lotId", lotId)
.map((rs, ctx) -> new BidHistory(
rs.getInt("id"),
rs.getString("lot_id"),
rs.getDouble("bid_amount"),
LocalDateTime.parse(rs.getString("bid_time")),
rs.getInt("is_autobid") != 0,
rs.getString("bidder_id"),
(Integer) rs.getObject("bidder_number")
))
.list()
);
}
/**
* Inserts bid history records in batch.
*/
public void insertBidHistory(List<BidHistory> bidHistory) {
jdbi.useHandle(handle -> {
var batch = handle.prepareBatch("""
INSERT OR IGNORE INTO bid_history (
lot_id, bid_amount, bid_time, is_autobid, bidder_id, bidder_number
) VALUES (:lotId, :bidAmount, :bidTime, :isAutobid, :bidderId, :bidderNumber)
""");
bidHistory.forEach(bid ->
batch.bind("lotId", bid.lotId())
.bind("bidAmount", bid.bidAmount())
.bind("bidTime", bid.bidTime().toString())
.bind("isAutobid", bid.isAutobid() ? 1 : 0)
.bind("bidderId", bid.bidderId())
.bind("bidderNumber", bid.bidderNumber())
.add()
);
batch.execute();
});
}
}

View File

@@ -44,12 +44,12 @@ quarkus.rest.path=/
quarkus.http.root-path=/
# Auction Monitor Configuration
auction.database.path=C:\\mnt\\okcomputer\\output\\cache.db
auction.images.path=C:\\mnt\\okcomputer\\output\\images
auction.database.path=/mnt/okcomputer/output/cache.db
auction.images.path=/mnt/okcomputer/output/images
auction.notification.config=desktop
auction.yolo.config=models/yolov4.cfg
auction.yolo.weights=models/yolov4.weights
auction.yolo.classes=models/coco.names
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
# Scheduler Configuration
quarkus.scheduler.enabled=true

View File

@@ -0,0 +1,138 @@
package auctiora;
import org.junit.jupiter.api.*;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for closing time calculations that power the UI
* Tests the minutesUntilClose() logic used in dashboard and alerts
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("Closing Time Calculation Tests")
class ClosingTimeCalculationTest {
@Test
@Order(1)
@DisplayName("Should calculate minutes until close for lot closing in 15 minutes")
void testMinutesUntilClose15Minutes() {
var lot = createLot(LocalDateTime.now().plusMinutes(15));
long minutes = lot.minutesUntilClose();
assertTrue(minutes >= 14 && minutes <= 16,
"Should be approximately 15 minutes, was: " + minutes);
}
@Test
@Order(2)
@DisplayName("Should calculate minutes until close for lot closing in 2 hours")
void testMinutesUntilClose2Hours() {
var lot = createLot(LocalDateTime.now().plusHours(2));
long minutes = lot.minutesUntilClose();
assertTrue(minutes >= 119 && minutes <= 121,
"Should be approximately 120 minutes, was: " + minutes);
}
@Test
@Order(3)
@DisplayName("Should return negative value for already closed lot")
void testMinutesUntilCloseNegative() {
var lot = createLot(LocalDateTime.now().minusHours(1));
long minutes = lot.minutesUntilClose();
assertTrue(minutes < 0,
"Should be negative for closed lots, was: " + minutes);
}
@Test
@Order(4)
@DisplayName("Should return MAX_VALUE when lot has no closing time")
void testMinutesUntilCloseNoTime() {
var lot = Lot.basic(100, 1001, "No closing time", "", "", "", 0, "General",
100.0, "EUR", "http://test.com/1001", null, false);
long minutes = lot.minutesUntilClose();
assertEquals(Long.MAX_VALUE, minutes,
"Should return MAX_VALUE when no closing time set");
}
@Test
@Order(5)
@DisplayName("Should identify lots closing within 5 minutes (critical threshold)")
void testCriticalClosingThreshold() {
var closing4Min = createLot(LocalDateTime.now().plusMinutes(4));
var closing5Min = createLot(LocalDateTime.now().plusMinutes(5));
var closing6Min = createLot(LocalDateTime.now().plusMinutes(6));
assertTrue(closing4Min.minutesUntilClose() < 5,
"Lot closing in 4 min should be < 5 minutes");
assertTrue(closing5Min.minutesUntilClose() >= 5,
"Lot closing in 5 min should be >= 5 minutes");
assertTrue(closing6Min.minutesUntilClose() > 5,
"Lot closing in 6 min should be > 5 minutes");
}
@Test
@Order(6)
@DisplayName("Should identify lots closing within 30 minutes (dashboard threshold)")
void testDashboardClosingThreshold() {
var closing20Min = createLot(LocalDateTime.now().plusMinutes(20));
var closing30Min = createLot(LocalDateTime.now().plusMinutes(30));
var closing40Min = createLot(LocalDateTime.now().plusMinutes(40));
assertTrue(closing20Min.minutesUntilClose() < 30,
"Lot closing in 20 min should be < 30 minutes");
assertTrue(closing30Min.minutesUntilClose() >= 30,
"Lot closing in 30 min should be >= 30 minutes");
assertTrue(closing40Min.minutesUntilClose() > 30,
"Lot closing in 40 min should be > 30 minutes");
}
@Test
@Order(7)
@DisplayName("Should calculate correctly for lots closing soon (boundary cases)")
void testBoundaryCases() {
// Just closed (< 1 minute ago)
var justClosed = createLot(LocalDateTime.now().minusSeconds(30));
assertTrue(justClosed.minutesUntilClose() <= 0, "Just closed should be <= 0");
// Closing very soon (< 1 minute)
var closingVerySoon = createLot(LocalDateTime.now().plusSeconds(30));
assertTrue(closingVerySoon.minutesUntilClose() < 1, "Closing in 30 sec should be < 1 minute");
// Closing in exactly 1 hour
var closing1Hour = createLot(LocalDateTime.now().plusHours(1));
long minutes1Hour = closing1Hour.minutesUntilClose();
assertTrue(minutes1Hour >= 59 && minutes1Hour <= 61,
"Closing in 1 hour should be ~60 minutes, was: " + minutes1Hour);
}
@Test
@Order(8)
@DisplayName("Multiple lots should sort correctly by urgency")
void testSortingByUrgency() {
var lot5Min = createLot(LocalDateTime.now().plusMinutes(5));
var lot30Min = createLot(LocalDateTime.now().plusMinutes(30));
var lot1Hour = createLot(LocalDateTime.now().plusHours(1));
var lot3Hours = createLot(LocalDateTime.now().plusHours(3));
var lots = java.util.List.of(lot3Hours, lot30Min, lot5Min, lot1Hour);
var sorted = lots.stream()
.sorted((a, b) -> Long.compare(a.minutesUntilClose(), b.minutesUntilClose()))
.toList();
assertEquals(lot5Min, sorted.get(0), "Most urgent should be first");
assertEquals(lot30Min, sorted.get(1), "Second most urgent");
assertEquals(lot1Hour, sorted.get(2), "Third most urgent");
assertEquals(lot3Hours, sorted.get(3), "Least urgent should be last");
}
// Helper method
private Lot createLot(LocalDateTime closingTime) {
return Lot.basic(100, 1001, "Test Item", "", "", "", 0, "General",
100.0, "EUR", "http://test.com/1001", closingTime, false);
}
}

View File

@@ -357,7 +357,7 @@ class DatabaseServiceTest {
100.0, "EUR", "https://example.com/" + i, null, false
));
}
} catch (SQLException e) {
} catch (Exception e) {
fail("Thread 1 failed: " + e.getMessage());
}
});
@@ -370,7 +370,7 @@ class DatabaseServiceTest {
200.0, "EUR", "https://example.com/" + i, null, false
));
}
} catch (SQLException e) {
} catch (Exception e) {
fail("Thread 2 failed: " + e.getMessage());
}
});

View File

@@ -72,13 +72,13 @@ class ImageProcessingServiceTest {
@Test
@DisplayName("Should handle database error gracefully")
void testProcessImageDatabaseError() throws SQLException {
void testProcessImageDatabaseError() {
String normalizedPath = testImage.getAbsolutePath().replace('\\', '/');
when(mockDetector.detectObjects(normalizedPath))
.thenReturn(List.of("object"));
doThrow(new SQLException("Database error"))
doThrow(new RuntimeException("Database error"))
.when(mockDb).updateImageLabels(anyInt(), anyList());
// Should return false on error
@@ -162,9 +162,9 @@ class ImageProcessingServiceTest {
@Test
@DisplayName("Should handle database query error in batch processing")
void testProcessPendingImagesDatabaseError() throws SQLException {
void testProcessPendingImagesDatabaseError() {
when(mockDb.getImagesNeedingDetection())
.thenThrow(new SQLException("Database connection failed"));
.thenThrow(new RuntimeException("Database connection failed"));
// Should not throw exception
assertDoesNotThrow(() -> service.processPendingImages());

View File

@@ -370,11 +370,11 @@ class IntegrationTest {
"https://example.com/60" + i, "A1", 5, null
));
}
} catch (SQLException e) {
} catch (Exception e) {
fail("Auction thread failed: " + e.getMessage());
}
});
var lotThread = new Thread(() -> {
try {
for (var i = 0; i < 10; i++) {
@@ -383,7 +383,7 @@ class IntegrationTest {
100.0 * i, "EUR", "https://example.com/70" + i, null, false
));
}
} catch (SQLException e) {
} catch (Exception e) {
fail("Lot thread failed: " + e.getMessage());
}
});

View File

@@ -21,7 +21,6 @@ class ObjectDetectionServiceTest {
@Test
@DisplayName("Should initialize with missing YOLO models (disabled mode)")
void testInitializeWithoutModels() throws IOException {
// When models don't exist, service should initialize in disabled mode
ObjectDetectionService service = new ObjectDetectionService(
"non_existent.cfg",
"non_existent.weights",
@@ -84,7 +83,6 @@ class ObjectDetectionServiceTest {
@Test
@DisplayName("Should throw IOException when model files exist but OpenCV fails to load")
void testInitializeWithValidModels() throws IOException {
// Create dummy model files for testing initialization
var cfgPath = Paths.get(TEST_CFG);
var weightsPath = Paths.get(TEST_WEIGHTS);
var classesPath = Paths.get(TEST_CLASSES);

View File

@@ -16,366 +16,365 @@ import static org.junit.jupiter.api.Assertions.*;
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TroostwijkMonitorTest {
private String testDbPath;
private TroostwijkMonitor monitor;
@BeforeAll
void setUp() throws SQLException, IOException {
testDbPath = "test_monitor_" + System.currentTimeMillis() + ".db";
// Initialize with non-existent YOLO models (disabled mode)
monitor = new TroostwijkMonitor(
testDbPath,
"desktop",
"non_existent.cfg",
"non_existent.weights",
"non_existent.txt"
);
}
@AfterAll
void tearDown() throws Exception {
Files.deleteIfExists(Paths.get(testDbPath));
}
@Test
@DisplayName("Should initialize monitor successfully")
void testMonitorInitialization() {
assertNotNull(monitor);
assertNotNull(monitor.getDb());
}
@Test
@DisplayName("Should print database stats without error")
void testPrintDatabaseStats() {
assertDoesNotThrow(() -> monitor.printDatabaseStats());
}
@Test
@DisplayName("Should process pending images without error")
void testProcessPendingImages() {
assertDoesNotThrow(() -> monitor.processPendingImages());
}
@Test
@DisplayName("Should handle empty database gracefully")
void testEmptyDatabaseHandling() throws SQLException {
var auctions = monitor.getDb().getAllAuctions();
var lots = monitor.getDb().getAllLots();
assertNotNull(auctions);
assertNotNull(lots);
assertTrue(auctions.isEmpty() || auctions.size() >= 0);
}
@Test
@DisplayName("Should track lots in database")
void testLotTracking() throws SQLException {
// Insert test lot
var lot = Lot.basic(
11111, 22222,
"Test Forklift",
"Electric forklift in good condition",
"Toyota",
"Electric",
2020,
"Machinery",
1500.00,
"EUR",
"https://example.com/lot/22222",
LocalDateTime.now().plusDays(1),
false
);
monitor.getDb().upsertLot(lot);
var lots = monitor.getDb().getAllLots();
assertTrue(lots.stream().anyMatch(l -> l.lotId() == 22222));
}
@Test
@DisplayName("Should monitor lots closing soon")
void testClosingSoonMonitoring() throws SQLException {
// Insert lot closing in 4 minutes
var closingSoon = Lot.basic(
33333, 44444,
"Closing Soon Item",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/44444",
LocalDateTime.now().plusMinutes(4),
false
);
monitor.getDb().upsertLot(closingSoon);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 44444)
.findFirst()
.orElse(null);
assertNotNull(found);
assertTrue(found.minutesUntilClose() < 30);
}
@Test
@DisplayName("Should identify lots with time remaining")
void testTimeRemainingCalculation() throws SQLException {
var futureLot = Lot.basic(
55555, 66666,
"Future Lot",
"Description",
"",
"",
0,
"Category",
200.00,
"EUR",
"https://example.com/lot/66666",
LocalDateTime.now().plusHours(2),
false
);
monitor.getDb().upsertLot(futureLot);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 66666)
.findFirst()
.orElse(null);
assertNotNull(found);
assertTrue(found.minutesUntilClose() > 60);
}
@Test
@DisplayName("Should handle lots without closing time")
void testLotsWithoutClosingTime() throws SQLException {
var noClosing = Lot.basic(
77777, 88888,
"No Closing Time",
"Description",
"",
"",
0,
"Category",
150.00,
"EUR",
"https://example.com/lot/88888",
null,
false
);
monitor.getDb().upsertLot(noClosing);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 88888)
.findFirst()
.orElse(null);
assertNotNull(found);
assertNull(found.closingTime());
}
@Test
@DisplayName("Should track notification status")
void testNotificationStatusTracking() throws SQLException {
var lot = Lot.basic(
99999, 11110,
"Test Notification",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/11110",
LocalDateTime.now().plusMinutes(3),
false
);
monitor.getDb().upsertLot(lot);
// Update notification flag
var notified = Lot.basic(
99999, 11110,
"Test Notification",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/11110",
LocalDateTime.now().plusMinutes(3),
true
);
monitor.getDb().updateLotNotificationFlags(notified);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 11110)
.findFirst()
.orElse(null);
assertNotNull(found);
assertTrue(found.closingNotified());
}
@Test
@DisplayName("Should update bid amounts")
void testBidAmountUpdates() throws SQLException {
var lot = Lot.basic(
12121, 13131,
"Bid Update Test",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/13131",
LocalDateTime.now().plusDays(1),
false
);
monitor.getDb().upsertLot(lot);
// Simulate bid increase
var higherBid = Lot.basic(
12121, 13131,
"Bid Update Test",
"Description",
"",
"",
0,
"Category",
250.00,
"EUR",
"https://example.com/lot/13131",
LocalDateTime.now().plusDays(1),
false
);
monitor.getDb().updateLotCurrentBid(higherBid);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 13131)
.findFirst()
.orElse(null);
assertNotNull(found);
assertEquals(250.00, found.currentBid(), 0.01);
}
@Test
@DisplayName("Should handle multiple concurrent lot updates")
void testConcurrentLotUpdates() throws InterruptedException, SQLException {
Thread t1 = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
monitor.getDb().upsertLot(Lot.basic(
20000 + i, 30000 + i, "Concurrent " + i, "Desc", "", "", 0, "Cat",
100.0, "EUR", "https://example.com/" + i, null, false
));
}
} catch (SQLException e) {
fail("Thread 1 failed: " + e.getMessage());
private String testDbPath;
private TroostwijkMonitor monitor;
@BeforeAll
void setUp() throws SQLException, IOException {
testDbPath = "test_monitor_" + System.currentTimeMillis() + ".db";
monitor = new TroostwijkMonitor(
testDbPath,
"desktop",
"non_existent.cfg",
"non_existent.weights",
"non_existent.txt"
);
}
@AfterAll
void tearDown() throws Exception {
Files.deleteIfExists(Paths.get(testDbPath));
}
@Test
@DisplayName("Should initialize monitor successfully")
void testMonitorInitialization() {
assertNotNull(monitor);
assertNotNull(monitor.getDb());
}
@Test
@DisplayName("Should print database stats without error")
void testPrintDatabaseStats() {
assertDoesNotThrow(() -> monitor.printDatabaseStats());
}
@Test
@DisplayName("Should process pending images without error")
void testProcessPendingImages() {
assertDoesNotThrow(() -> monitor.processPendingImages());
}
@Test
@DisplayName("Should handle empty database gracefully")
void testEmptyDatabaseHandling() throws SQLException {
var auctions = monitor.getDb().getAllAuctions();
var lots = monitor.getDb().getAllLots();
assertNotNull(auctions);
assertNotNull(lots);
assertTrue(auctions.isEmpty() || auctions.size() >= 0);
}
@Test
@DisplayName("Should track lots in database")
void testLotTracking() throws SQLException {
// Insert test lot
var lot = Lot.basic(
11111, 22222,
"Test Forklift",
"Electric forklift in good condition",
"Toyota",
"Electric",
2020,
"Machinery",
1500.00,
"EUR",
"https://example.com/lot/22222",
LocalDateTime.now().plusDays(1),
false
);
monitor.getDb().upsertLot(lot);
var lots = monitor.getDb().getAllLots();
assertTrue(lots.stream().anyMatch(l -> l.lotId() == 22222));
}
@Test
@DisplayName("Should monitor lots closing soon")
void testClosingSoonMonitoring() throws SQLException {
// Insert lot closing in 4 minutes
var closingSoon = Lot.basic(
33333, 44444,
"Closing Soon Item",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/44444",
LocalDateTime.now().plusMinutes(4),
false
);
monitor.getDb().upsertLot(closingSoon);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 44444)
.findFirst()
.orElse(null);
assertNotNull(found);
assertTrue(found.minutesUntilClose() < 30);
}
@Test
@DisplayName("Should identify lots with time remaining")
void testTimeRemainingCalculation() throws SQLException {
var futureLot = Lot.basic(
55555, 66666,
"Future Lot",
"Description",
"",
"",
0,
"Category",
200.00,
"EUR",
"https://example.com/lot/66666",
LocalDateTime.now().plusHours(2),
false
);
monitor.getDb().upsertLot(futureLot);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 66666)
.findFirst()
.orElse(null);
assertNotNull(found);
assertTrue(found.minutesUntilClose() > 60);
}
@Test
@DisplayName("Should handle lots without closing time")
void testLotsWithoutClosingTime() throws SQLException {
var noClosing = Lot.basic(
77777, 88888,
"No Closing Time",
"Description",
"",
"",
0,
"Category",
150.00,
"EUR",
"https://example.com/lot/88888",
null,
false
);
monitor.getDb().upsertLot(noClosing);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 88888)
.findFirst()
.orElse(null);
assertNotNull(found);
assertNull(found.closingTime());
}
@Test
@DisplayName("Should track notification status")
void testNotificationStatusTracking() throws SQLException {
var lot = Lot.basic(
99999, 11110,
"Test Notification",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/11110",
LocalDateTime.now().plusMinutes(3),
false
);
monitor.getDb().upsertLot(lot);
// Update notification flag
var notified = Lot.basic(
99999, 11110,
"Test Notification",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/11110",
LocalDateTime.now().plusMinutes(3),
true
);
monitor.getDb().updateLotNotificationFlags(notified);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 11110)
.findFirst()
.orElse(null);
assertNotNull(found);
assertTrue(found.closingNotified());
}
@Test
@DisplayName("Should update bid amounts")
void testBidAmountUpdates() throws SQLException {
var lot = Lot.basic(
12121, 13131,
"Bid Update Test",
"Description",
"",
"",
0,
"Category",
100.00,
"EUR",
"https://example.com/lot/13131",
LocalDateTime.now().plusDays(1),
false
);
monitor.getDb().upsertLot(lot);
// Simulate bid increase
var higherBid = Lot.basic(
12121, 13131,
"Bid Update Test",
"Description",
"",
"",
0,
"Category",
250.00,
"EUR",
"https://example.com/lot/13131",
LocalDateTime.now().plusDays(1),
false
);
monitor.getDb().updateLotCurrentBid(higherBid);
var lots = monitor.getDb().getActiveLots();
var found = lots.stream()
.filter(l -> l.lotId() == 13131)
.findFirst()
.orElse(null);
assertNotNull(found);
assertEquals(250.00, found.currentBid(), 0.01);
}
@Test
@DisplayName("Should handle multiple concurrent lot updates")
void testConcurrentLotUpdates() throws InterruptedException, SQLException {
Thread t1 = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
monitor.getDb().upsertLot(Lot.basic(
20000 + i, 30000 + i, "Concurrent " + i, "Desc", "", "", 0, "Cat",
100.0, "EUR", "https://example.com/" + i, null, false
));
}
});
Thread t2 = new Thread(() -> {
try {
for (int i = 5; i < 10; i++) {
monitor.getDb().upsertLot(Lot.basic(
20000 + i, 30000 + i, "Concurrent " + i, "Desc", "", "", 0, "Cat",
200.0, "EUR", "https://example.com/" + i, null, false
));
}
} catch (SQLException e) {
fail("Thread 2 failed: " + e.getMessage());
} catch (Exception e) {
fail("Thread 1 failed: " + e.getMessage());
}
});
Thread t2 = new Thread(() -> {
try {
for (int i = 5; i < 10; i++) {
monitor.getDb().upsertLot(Lot.basic(
20000 + i, 30000 + i, "Concurrent " + i, "Desc", "", "", 0, "Cat",
200.0, "EUR", "https://example.com/" + i, null, false
));
}
});
t1.start();
t2.start();
t1.join();
t2.join();
var lots = monitor.getDb().getActiveLots();
long count = lots.stream()
.filter(l -> l.lotId() >= 30000 && l.lotId() < 30010)
.count();
assertTrue(count >= 10);
}
@Test
@DisplayName("Should schedule monitoring without error")
void testScheduleMonitoring() {
// This just tests that scheduling doesn't throw
// Actual monitoring would run in background
assertDoesNotThrow(() -> {
// Don't actually start monitoring in test
// Just verify monitor is ready
assertNotNull(monitor);
});
}
@Test
@DisplayName("Should handle database with auctions and lots")
void testDatabaseWithData() throws SQLException {
// Insert auction
var auction = new AuctionInfo(
40000,
"Test Auction",
"Amsterdam, NL",
"Amsterdam",
"NL",
"https://example.com/auction/40000",
"A7",
10,
LocalDateTime.now().plusDays(2)
);
monitor.getDb().upsertAuction(auction);
// Insert related lot
var lot = Lot.basic(
40000, 50000,
"Test Lot",
"Description",
"",
"",
0,
"Category",
500.00,
"EUR",
"https://example.com/lot/50000",
LocalDateTime.now().plusDays(2),
false
);
monitor.getDb().upsertLot(lot);
// Verify
var auctions = monitor.getDb().getAllAuctions();
var lots = monitor.getDb().getAllLots();
assertTrue(auctions.stream().anyMatch(a -> a.auctionId() == 40000));
assertTrue(lots.stream().anyMatch(l -> l.lotId() == 50000));
}
} catch (Exception e) {
fail("Thread 2 failed: " + e.getMessage());
}
});
t1.start();
t2.start();
t1.join();
t2.join();
var lots = monitor.getDb().getActiveLots();
long count = lots.stream()
.filter(l -> l.lotId() >= 30000 && l.lotId() < 30010)
.count();
assertTrue(count >= 10);
}
@Test
@DisplayName("Should schedule monitoring without error")
void testScheduleMonitoring() {
// This just tests that scheduling doesn't throw
// Actual monitoring would run in background
assertDoesNotThrow(() -> {
// Don't actually start monitoring in test
// Just verify monitor is ready
assertNotNull(monitor);
});
}
@Test
@DisplayName("Should handle database with auctions and lots")
void testDatabaseWithData() throws SQLException {
// Insert auction
var auction = new AuctionInfo(
40000,
"Test Auction",
"Amsterdam, NL",
"Amsterdam",
"NL",
"https://example.com/auction/40000",
"A7",
10,
LocalDateTime.now().plusDays(2)
);
monitor.getDb().upsertAuction(auction);
// Insert related lot
var lot = Lot.basic(
40000, 50000,
"Test Lot",
"Description",
"",
"",
0,
"Category",
500.00,
"EUR",
"https://example.com/lot/50000",
LocalDateTime.now().plusDays(2),
false
);
monitor.getDb().upsertLot(lot);
// Verify
var auctions = monitor.getDb().getAllAuctions();
var lots = monitor.getDb().getAllLots();
assertTrue(auctions.stream().anyMatch(a -> a.auctionId() == 40000));
assertTrue(lots.stream().anyMatch(l -> l.lotId() == 50000));
}
}