test: add service and controller tests for vehicle lookup
- Add real HTML fixture from biluppgifter.se/fordon/hdo732/ containing: summary cards (.info > em + span) for Modellar, Typ, Farg, Bransle Fordonsdata section (ul.list with span.label/span.value) for Fabrikat, Modell, Variant, Fordonsar/Modellar - Add VehicleLookupServiceTest with 6 cases: shouldParseAllFieldsFromFixture, shouldParseSummaryFields, shouldParseDataSectionFields, shouldReturnEmptyFieldsForEmptyDocument, shouldBuildModelWithoutVariant, shouldFallbackToModellarWhenNoFordonsar - Add VehicleControllerTest with 4 cases: shouldReturnVehicleInfoForValidPlate (200 with all fields), shouldReturn404WhenVehicleNotFound, shouldBeAccessibleWithoutAuthentication, shouldReturnVehicleInfoWithFuelField
This commit is contained in:
parent
18f462c5c1
commit
3792fdec82
3 changed files with 263 additions and 0 deletions
|
|
@ -0,0 +1,70 @@
|
|||
package se.bilhalsning.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import se.bilhalsning.dto.VehicleInfoResponse;
|
||||
import se.bilhalsning.exception.VehicleNotFoundException;
|
||||
import se.bilhalsning.service.VehicleLookupService;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class VehicleControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private VehicleLookupService vehicleLookupService;
|
||||
|
||||
@Test
|
||||
void shouldReturnVehicleInfoForValidPlate() throws Exception {
|
||||
when(vehicleLookupService.lookup("ABC123"))
|
||||
.thenReturn(new VehicleInfoResponse("Volvo", "V70", 2009, "Silver", "Bensin"));
|
||||
|
||||
mockMvc.perform(get("/api/vehicles/ABC123"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.make").value("Volvo"))
|
||||
.andExpect(jsonPath("$.model").value("V70"))
|
||||
.andExpect(jsonPath("$.year").value(2009))
|
||||
.andExpect(jsonPath("$.color").value("Silver"))
|
||||
.andExpect(jsonPath("$.fuel").value("Bensin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturn404WhenVehicleNotFound() throws Exception {
|
||||
when(vehicleLookupService.lookup("ZZZ999"))
|
||||
.thenThrow(new VehicleNotFoundException("ZZZ999"));
|
||||
|
||||
mockMvc.perform(get("/api/vehicles/ZZZ999"))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.message").value("Inget fordon hittades"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeAccessibleWithoutAuthentication() throws Exception {
|
||||
when(vehicleLookupService.lookup("ABC123"))
|
||||
.thenReturn(new VehicleInfoResponse("Volvo", "V70", 2009, "Silver", "Bensin"));
|
||||
|
||||
mockMvc.perform(get("/api/vehicles/ABC123"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnVehicleInfoWithFuelField() throws Exception {
|
||||
when(vehicleLookupService.lookup("DEF456"))
|
||||
.thenReturn(new VehicleInfoResponse("Saab", "9-3", 2005, "Röd", "Diesel"));
|
||||
|
||||
mockMvc.perform(get("/api/vehicles/DEF456"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.fuel").value("Diesel"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package se.bilhalsning.service;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import se.bilhalsning.dto.VehicleInfoResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class VehicleLookupServiceTest {
|
||||
|
||||
private final VehicleLookupService service = new VehicleLookupService();
|
||||
|
||||
@Test
|
||||
void shouldParseAllFieldsFromFixture() throws Exception {
|
||||
Document doc = loadFixture("biluppgifter-hdo732.html");
|
||||
|
||||
VehicleLookupService spy = new VehicleLookupService() {
|
||||
@Override
|
||||
Document fetchPage(String plate) {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
|
||||
VehicleInfoResponse result = spy.lookup("hdo732");
|
||||
|
||||
assertEquals("Peugeot", result.make());
|
||||
assertEquals("107 1.0", result.model());
|
||||
assertEquals(2011, result.year());
|
||||
assertEquals("Gul", result.color());
|
||||
assertEquals("Bensin", result.fuel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseSummaryFields() {
|
||||
Document doc = loadFixture("biluppgifter-hdo732.html");
|
||||
var fields = new java.util.LinkedHashMap<String, String>();
|
||||
service.extractSummaryFields(doc, fields);
|
||||
|
||||
assertEquals("Gul", fields.get("Färg"));
|
||||
assertEquals("Bensin", fields.get("Bränsle"));
|
||||
assertEquals("2011", fields.get("Modellår"));
|
||||
assertEquals("Personbil", fields.get("Typ"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseDataSectionFields() {
|
||||
Document doc = loadFixture("biluppgifter-hdo732.html");
|
||||
var fields = new java.util.LinkedHashMap<String, String>();
|
||||
service.extractDataSectionFields(doc, fields);
|
||||
|
||||
assertEquals("Peugeot", fields.get("Fabrikat"));
|
||||
assertEquals("107", fields.get("Modell"));
|
||||
assertEquals("1.0", fields.get("Variant"));
|
||||
assertEquals("2011 / 2011", fields.get("Fordonsår / Modellår"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyFieldsForEmptyDocument() {
|
||||
Document doc = Jsoup.parse("<html><body><p>No data</p></body></html>");
|
||||
var fields = service.extractFields(doc);
|
||||
|
||||
assertTrue(fields.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildModelWithoutVariant() {
|
||||
Document doc = Jsoup.parse(
|
||||
"<html><body><section><h2>Fordonsdata</h2>" +
|
||||
"<ul class='list'>" +
|
||||
"<li><span class='label'>Fabrikat</span><span class='value'>Volvo</span></li>" +
|
||||
"<li><span class='label'>Modell</span><span class='value'>V70</span></li>" +
|
||||
"</ul></section></body></html>"
|
||||
);
|
||||
|
||||
VehicleLookupService spy = new VehicleLookupService() {
|
||||
@Override
|
||||
Document fetchPage(String plate) {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
|
||||
var result = spy.lookup("test");
|
||||
assertEquals("V70", result.model());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToModellårWhenNoFordonsår() {
|
||||
Document doc = Jsoup.parse(
|
||||
"<html><body>" +
|
||||
"<div class='info'><em>2009</em><span>Modellår</span></div>" +
|
||||
"<section><h2>Fordonsdata</h2>" +
|
||||
"<ul class='list'>" +
|
||||
"<li><span class='label'>Fabrikat</span><span class='value'>Volvo</span></li>" +
|
||||
"</ul></section></body></html>"
|
||||
);
|
||||
|
||||
VehicleLookupService spy = new VehicleLookupService() {
|
||||
@Override
|
||||
Document fetchPage(String plate) {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
|
||||
var result = spy.lookup("test");
|
||||
assertEquals(2009, result.year());
|
||||
}
|
||||
|
||||
private Document loadFixture(String name) {
|
||||
try {
|
||||
String path = "/fixtures/" + name;
|
||||
var stream = getClass().getResourceAsStream(path);
|
||||
assertNotNull(stream, "Fixture not found: " + path);
|
||||
String html = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
return Jsoup.parse(html);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load fixture: " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
69
backend/src/test/resources/fixtures/biluppgifter-hdo732.html
Normal file
69
backend/src/test/resources/fixtures/biluppgifter-hdo732.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<html>
|
||||
<body>
|
||||
<ul class="icon-grid list-view" id="vehicle-icon-grid">
|
||||
<li onclick="location.href='#fordonsdata';forceOpenSection('vehicle-data')" style="cursor: pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" color="#fff" fill="none"><path opacity=".2" d="M21.5 12.757v-.514c0-1.73 0-3.115-.078-4.243H2.578C2.5 9.128 2.5 10.514 2.5 12.243v.514c0 4.357 0 6.536 1.252 7.89C5.004 22 7.02 22 11.05 22h1.9c4.03 0 6.046 0 7.298-1.354 1.252-1.353 1.252-3.532 1.252-7.89" fill="currentColor"/><path d="M18 2v2M6 2v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M11.996 13h.008m-.008 4h.008m3.987-4H16m-8 0h.009M8 17h.009" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.5 8h17m-18 4.243c0-4.357 0-6.536 1.252-7.89C5.004 3 7.02 3 11.05 3h1.9c4.03 0 6.046 0 7.298 1.354C21.5 5.707 21.5 7.886 21.5 12.244v.513c0 4.357 0 6.536-1.252 7.89C18.996 22 16.98 22 12.95 22h-1.9c-4.03 0-6.046 0-7.298-1.354C2.5 19.293 2.5 17.114 2.5 12.756zM3 8h18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<div class="info">
|
||||
<em>2011</em>
|
||||
<span>Modellår</span>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="location.href='#fordonsdata';forceOpenSection('vehicle-data')" style="cursor: pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" color="#fff" fill="none"><path opacity=".2" d="M20.251 17.756h-1v-.003a2 2 0 0 0-4 0v.003h-6v-.003a2 2 0 0 0-4 0v.003c-.172-.041-.373-.076-.589-.114-.825-.146-1.86-.328-2.262-.979-.149-.241-.149-.542-.149-1.143v-4.764l14.55-.319c.81-.018 1.59.324 2.331.652.233.102.474.193.715.283.927.348 1.843.692 2.238 1.72.166.433.166.936.166 1.943v.72c0 .943 0 1.415-.293 1.708s-.764.293-1.707.293" fill="currentColor"/><path d="M9.251 17.753a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm10 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z" stroke="currentColor" stroke-width="1.5"/><path d="M2.258 10.753h16m-16 0c0 .78-.02 3.04.004 5.26.036.72.156 1.32 2.997 1.74m-3.001-7c.216-1.74 1.155-3.8 1.634-4.58m5.366 4.58v-5m5.99 12H9.252m-6.978-12H12.49s.54 0 1.02.048c.898.084 1.654.492 2.41 1.512.8 1.08 1.414 2.448 2.23 3.18 1.355 1.216 3.933.84 4.076 3.42.036 1.32.036 2.76-.023 3-.097.707-.642.822-1.32.84-.588.015-1.297-.028-1.642 0" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
<div class="info">
|
||||
<em>Personbil</em>
|
||||
<span>Typ</span>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="location.href='#tekniskdata';forceOpenSection('technical-data')" style="cursor: pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" color="#fff" fill="none"><path opacity=".2" d="m14.162 12.75-2.977-2.958.249-.246c4.384-4.355 8.604-7.224 9.426-6.407s-2.066 5.01-6.45 9.364zM3 20.882s3.72.74 5.953-1.479a3.12 3.12 0 0 0 0-4.435 3.17 3.17 0 0 0-4.465 0c-1.86 1.848-.372 3.326-1.488 5.914" fill="currentColor"/><path d="m11.186 9.792 2.977 2.957m-4.293 3.87c1.294-1.034 3.094-2.678 4.54-4.115 4.384-4.355 7.271-8.547 6.45-9.364-.823-.817-5.043 2.052-9.427 6.407-1.446 1.436-3.101 3.225-4.142 4.51m1.661 5.347C6.721 21.621 3 20.882 3 20.882c1.116-2.588-.372-4.066 1.488-5.915a3.17 3.17 0 0 1 4.465 0 3.12 3.12 0 0 1 0 4.436Z" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
<div class="info">
|
||||
<em>Gul <div tooltip="Gul" class="color " style="background-color: #FFFF00"></div></em>
|
||||
<span>Färg</span>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="location.href='#tekniskdata';forceOpenSection('technical-data')" style="cursor: pointer;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" color="#fff" fill="none"><path opacity=".2" d="M4 10v11.002h12v-11z" fill="currentColor"/><path d="m10.463 13-1.394 1.812a.33.33 0 0 0 .2.526l1.461.31a.33.33 0 0 1 .177.553L9.177 18M4 10h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 21V9c0-2.828 0-4.243.879-5.121C5.757 3 7.172 3 10 3s4.243 0 5.121.879C16 4.757 16 6.172 16 9v12z" stroke="currentColor" stroke-width="1.5"/><path d="M2 21h16m-2-7h1.667c.31 0 .465 0 .592.034a1 1 0 0 1 .707.707c.034.127.034.282.034.592V16.5a1.5 1.5 0 0 0 3 0v-6.289c0-.601 0-.902-.086-1.185s-.252-.534-.586-1.034l-.773-1.16A1.87 1.87 0 0 0 19 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<div class="info">
|
||||
<em>Bensin</em>
|
||||
<span>Bränsle</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<section id="vehicle-data" class="">
|
||||
<a id="fordonsdata" class="sub"></a>
|
||||
<div class="bar fancy" onclick="toggleSection('vehicle-data')">
|
||||
<div class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" color="#fff" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.137 1.417c-.475-.168-.98-.168-1.55-.167-1.525 0-2.933 0-3.912.115-1.013.119-1.88.372-2.615.968q-.406.328-.733.736c-.593.74-.845 1.612-.963 2.63-.114.983-.114 2.22-.114 3.753v4.574c0 1.782 0 3.218.15 4.348.158 1.173.493 2.16 1.274 2.945.78.784 1.762 1.121 2.929 1.28 1.124.151 2.695.151 4.468.15 1.773.001 3.201.001 4.325-.15 1.167-.159 2.15-.496 2.93-1.28s1.116-1.772 1.272-2.945c.152-1.13.152-2.566.152-4.348v-3.474c0-.664.002-1.252-.223-1.796-.224-.544-.638-.96-1.106-1.428L14.638 2.52c-.402-.405-.758-.764-1.213-.983a3 3 0 0 0-.288-.12m4.814 7.197c.618.622.723.752.78.89-1.368 0-2.016 0-2.883-.117-.9-.121-1.658-.38-2.26-.982s-.86-1.36-.982-2.26c-.116-.865-.116-1.513-.116-2.875v-.008c.182.054.323.188.856.723zM16.75 17a.75.75 0 0 0-.75-.75H8a.75.75 0 0 0 0 1.5h8a.75.75 0 0 0 .75-.75M12 12.25a.75.75 0 0 1 0 1.5H8a.75.75 0 1 1 0-1.5z" fill="currentColor"/></svg>
|
||||
</div>
|
||||
<h2>Fordonsdata</h2>
|
||||
<div class="chevron">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" color="#fff" fill="none"><path d="m6 9 6 6 6-6" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="16" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner">
|
||||
<ul class="list">
|
||||
<li>
|
||||
<span class="label">Fabrikat</span>
|
||||
<span class="value">Peugeot</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">Modell</span>
|
||||
<span class="value">107</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">Variant</span>
|
||||
<span class="value">1.0</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list top-30">
|
||||
<li>
|
||||
<span class="label">Fordonsår / Modellår</span>
|
||||
<span class="value">2011 / 2011</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue