Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d51b838
Add Fintraffic parking extension: FintrafficParking with paymentMethods
teppope Jul 15, 2026
1085068
Add GraphQL parking read query integration test for paymentMethods
teppope Jul 16, 2026
3acbd21
Exclude paymentMethods from Orika classmap in FintrafficParkingEntity…
teppope Jul 16, 2026
fc456b6
Fix getMappingExclusions test to reflect paymentMethods exclusion
teppope Jul 16, 2026
adf73b2
Remove dtype DDL from V3 ext migration (now handled by core V62)
teppope Jul 16, 2026
aa7c30a
Fix BeanDefinitionOverrideException in fintraffic integration tests
teppope Jul 16, 2026
de576f5
Fix GaplessIdGeneratorServiceTest failures caused by ApplicationConte…
teppope Jul 17, 2026
80afafc
Merge branch 'master' into feat/ext-fintraffic-parking-entity-factory
teppope Jul 17, 2026
04247ac
DPO-4810 Persist infoLinks on FintrafficParking
teppope Jul 20, 2026
1e35e2d
Persist vehicleEntrances on FintrafficParking
teppope Jul 20, 2026
6a93cee
Persist vehicle entrance width and height from GraphQL input
teppope Jul 20, 2026
5bf9ffc
Persist and expose parking lighting via FintrafficParking extension
teppope Jul 20, 2026
b45e349
Store parking lighting in separate collection table to avoid breaking…
teppope Jul 20, 2026
b210360
Persist and expose parking opening hours (AvailabilityConditions)
teppope Jul 21, 2026
e53e23f
Fix availability condition schema uniqueness, time parsing, and expor…
teppope Jul 21, 2026
769841c
Harden parseLocalTime and improve availability condition test coverage
teppope Jul 21, 2026
6c363b1
Register ext package for entity scanning so FintrafficParking is a re…
teppope Jul 23, 2026
1371674
Merge branch 'feat/ext-fintraffic-parking-entity-factory' into feat/e…
teppope Jul 23, 2026
3b7a9ef
Merge branch 'feat/ext-fintraffic-info-links' into feat/ext-fintraffi…
teppope Jul 23, 2026
2360fc3
Merge branch 'feat/ext-fintraffic-vehicle-entrances' into feat/ext-fi…
teppope Jul 23, 2026
733bff1
Merge branch 'feat/ext-fintraffic-parking-lighting' into feat/ext-fin…
teppope Jul 23, 2026
43db1f0
Map parking lighting through NeTEx import/export
teppope Jul 27, 2026
9d38f17
Merge branch 'feat/ext-fintraffic-parking-lighting' into feat/ext-fin…
teppope Jul 27, 2026
ec12897
Fix NeTEx export failure for parking vehicle entrances
teppope Jul 28, 2026
2af55e0
Fix NeTEx export failure for parking availability conditions
teppope Jul 28, 2026
0424699
Fix Parking entities never appearing in Read API cache
teppope Jul 28, 2026
69eb683
Merge branch 'feat/ext-fintraffic-parking-entity-factory' into feat/e…
teppope Jul 28, 2026
0c340b3
Fix nested test config leaking into unrelated test contexts
teppope Jul 28, 2026
918da73
Merge branch 'feat/ext-fintraffic-parking-entity-factory' into feat/e…
teppope Jul 28, 2026
421dce4
Merge branch 'feat/ext-fintraffic-vehicle-entrances' into feat/ext-fi…
teppope Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by
* the European Commission - subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at:
*
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and
* limitations under the Licence.
*/

package org.rutebanken.tiamat.ext.fintraffic;

import org.junit.AfterClass;
import org.rutebanken.tiamat.ext.fintraffic.config.FintrafficTestContextConfiguration;

/**
* Base class for fintraffic integration tests that start a secondary Spring context.
* <p>
* {@link org.rutebanken.tiamat.config.ApplicationContextProvider} uses a static field to hold
* the active {@code ApplicationContext}, which is a JVM-global singleton. When the fintraffic
* test context starts it overwrites this field via {@code RestoringApplicationContextProvider},
* which saves the previous value. {@link #restoreApplicationContext()} restores it after all
* tests in the subclass complete, so the primary-context tests that follow get the correct beans.
*/
public abstract class FintrafficIntegrationTest {

@AfterClass
public static void restoreApplicationContext() {
FintrafficTestContextConfiguration.restoreContext();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.rutebanken.tiamat.ext.fintraffic;

import org.rutebanken.tiamat.auth.TiamatSecurityConfig;
import org.rutebanken.tiamat.config.ApplicationContextProvider;
import org.rutebanken.tiamat.ext.fintraffic.auth.FintrafficSecurityConfig;
import org.rutebanken.tiamat.ext.fintraffic.config.FintrafficTestContextConfiguration;
import org.rutebanken.tiamat.ext.fintraffic.model.FintrafficParking;
import org.rutebanken.tiamat.model.StopPlace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration;
import org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration;
import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
* Spring Boot test application for Fintraffic extension integration tests.
* Extends the core TiamatTestApplication setup with:
* <ul>
* <li>Entity scan covering {@link FintrafficParking} (ext model)</li>
* <li>Component scan covering the ext package tree</li>
* </ul>
* Security is excluded; authorization is mocked in individual tests.
*/
@SpringBootApplication(exclude = {
SecurityAutoConfiguration.class,
ManagementWebSecurityAutoConfiguration.class,
SecurityFilterAutoConfiguration.class,
ServletWebSecurityAutoConfiguration.class,
UserDetailsServiceAutoConfiguration.class
})
@EnableTransactionManagement
@EnableCaching
@EntityScan(basePackageClasses = {StopPlace.class, FintrafficParking.class, Jsr310JpaConverters.class})
@ComponentScan(
basePackages = "org.rutebanken.tiamat",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = TiamatSecurityConfig.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = FintrafficSecurityConfig.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = ApplicationContextProvider.class)
})
@Import(FintrafficTestContextConfiguration.class)
public class FintrafficTiamatTestApplication {
public static void main(String[] args) {
SpringApplication.run(FintrafficTiamatTestApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package org.rutebanken.tiamat.ext.fintraffic.api;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.rutebanken.tiamat.auth.AuthorizationService;
import org.rutebanken.tiamat.changelog.EntityChangedListener;
import org.rutebanken.tiamat.ext.fintraffic.FintrafficIntegrationTest;
import org.rutebanken.tiamat.ext.fintraffic.FintrafficTiamatTestApplication;
import org.rutebanken.tiamat.model.EmbeddableMultilingualString;
import org.rutebanken.tiamat.model.StopPlace;
import org.rutebanken.tiamat.model.StopTypeEnumeration;
import org.rutebanken.tiamat.repository.ParkingRepository;
import org.rutebanken.tiamat.repository.StopPlaceRepository;
import org.rutebanken.tiamat.rest.graphql.GraphQLNames;
import org.rutebanken.tiamat.versioning.save.StopPlaceVersionedSaverService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Map;

import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.notNullValue;
import static org.rutebanken.tiamat.config.JerseyConfig.SERVICES_STOP_PLACE_PATH;

/**
* Reproduces the reported scenario: a brand-new Parking created via the real GraphQL
* {@code mutateParking} editor path never appears in the Read API cache table, even though
* the incremental sync path ({@code ParkingVersionedSaverService.sendToJMS} →
* {@code ReadApiEntityChangedPublisher.onChange} → {@code ReadApiNetexMarshallingService
* .handleEntityChange}) is invoked for every save. Activates the real {@code
* fintraffic-read-api} profile (not mocked, unlike the other Read API unit tests) so the real
* marshaller, search key service and repository run end to end.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = FintrafficTiamatTestApplication.class
)
@ActiveProfiles({"test", "gcs-blobstore", "fintraffic", "fintraffic-read-api"})
@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true")
public class ReadApiParkingIncrementalSyncIntegrationTest extends FintrafficIntegrationTest {

private static final String BASE_URI_GRAPHQL = SERVICES_STOP_PLACE_PATH + "/graphql/";

/**
* The {@code test} profile activates {@code EntityChangedEventLocalPublisher}
* ({@code @Profile("local-changelog | test")}) alongside {@code fintraffic-read-api}'s
* {@code ReadApiEntityChangedPublisher}, so every bean that autowires the single-bean
* {@code EntityChangedListener} interface fails with a {@code NoUniqueBeanDefinitionException}.
* Real deployments only ever activate one of the two (see {@code
* spring.profiles.group.dev/tst/prd} in the peti-backend config, which never combines
* {@code test}/{@code local-changelog} with {@code fintraffic-read-api}), so this ambiguity
* is a test-only artifact. Marking the real Read API publisher {@code @Primary} here
* reproduces the production wiring for this test without changing any production bean.
* <p>
* This class is a static nested {@code @TestConfiguration}, so the shared
* {@code FintrafficTiamatTestApplication}'s {@code @ComponentScan(basePackages =
* "org.rutebanken.tiamat")} picks it up as a real component in every test that boots that
* application context, not just this one. Gating it with {@code @Profile("fintraffic-read-api")}
* keeps it a no-op wherever that profile isn't active, so it can't break unrelated tests.
*/
@TestConfiguration
@Profile("fintraffic-read-api")
static class PrimaryEntityChangedListenerConfig {
@Bean
@Primary
EntityChangedListener primaryEntityChangedListener(ReadApiEntityChangedPublisher readApiEntityChangedPublisher) {
return readApiEntityChangedPublisher;
}
}

@MockitoBean
private AuthorizationService authorizationService;

@Value("${local.server.port}")
private int port;

@Autowired
private StopPlaceRepository stopPlaceRepository;

@Autowired
private StopPlaceVersionedSaverService stopPlaceVersionedSaverService;

@Autowired
private ParkingRepository parkingRepository;

@Autowired
private JdbcTemplate jdbcTemplate;

@Autowired
private org.rutebanken.tiamat.ext.fintraffic.api.repository.NetexRepository netexRepository;

@Autowired
private org.springframework.transaction.PlatformTransactionManager transactionManager;

@Before
public void configureRestAssured() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = port;
}

@After
public void cleanUp() {
parkingRepository.deleteAll();
stopPlaceRepository.deleteAll();
jdbcTemplate.update("DELETE FROM ext_fintraffic_netex_entity");
}

/**
* Reproduces the exact reported symptom: a brand-new Parking created via the real GraphQL
* editor path must (a) sync a {@code type='Parking'} row into the Read API cache table, and
* (b) actually be returned by the real {@code streamStopPlaces} repository method (the same
* one the {@code GET /api/fintraffic/v1/stops} endpoint uses) — not just be present in the
* table under some other type value. The StopPlace/Parking creation happens over real HTTP
* (a separate thread/transaction/connection), so only the final {@code streamStopPlaces}
* call (which requires an active transaction) is wrapped in one, via a {@code
* TransactionTemplate} — wrapping the whole test method in {@code @Transactional} would hide
* the StopPlace/Parking from the GraphQL server's own connection until commit.
*/
@Test
public void mutateParking_incrementalSync_writesRowToReadApiCacheTable() {
StopPlace stopPlace = new StopPlace(new EmbeddableMultilingualString("Test stop"));
stopPlace.setStopPlaceType(StopTypeEnumeration.ONSTREET_BUS);
stopPlace = stopPlaceVersionedSaverService.saveNewVersion(stopPlace);
String stopNetexId = stopPlace.getNetexId();

String mutation = """
{
"query": "mutation { parking: %s (Parking: { name: { value: \\"Test parking\\" lang: \\"fi\\" } parkingType: parkAndRide parentSiteRef: \\"%s\\" }) { id } }",
"variables": ""
}
""".formatted(GraphQLNames.MUTATE_PARKING, stopNetexId);

String parkingNetexId = given()
.port(port)
.contentType(ContentType.JSON)
.body(mutation)
.when()
.post(BASE_URI_GRAPHQL)
.then()
.statusCode(200)
.body("data.parking[0].id", notNullValue())
.extract()
.path("data.parking[0].id");

Map<String, Object> row = jdbcTemplate.queryForMap(
"SELECT id, type, status FROM ext_fintraffic_netex_entity WHERE id = ?", parkingNetexId);

assertThat(row)
.as("a brand-new Parking created via GraphQL must sync into the Read API cache table")
.containsEntry("id", parkingNetexId)
.containsEntry("type", "Parking")
.containsEntry("status", "CURRENT");

var transactionTemplate = new org.springframework.transaction.support.TransactionTemplate(transactionManager);
java.util.List<String> matchingTypes = transactionTemplate.execute(status -> {
try (var stream = netexRepository.streamStopPlaces(
org.rutebanken.tiamat.ext.fintraffic.api.model.FintrafficReadApiSearchKey.empty())) {
return stream
.filter(r -> r.type().equals("Parking") && r.xml().contains(parkingNetexId))
.map(r -> r.type())
.toList();
}
});
assertThat(matchingTypes)
.as("the new Parking must be returned by the real Read API stream query")
.hasSize(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by
* the European Commission - subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at:
*
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and
* limitations under the Licence.
*/

package org.rutebanken.tiamat.ext.fintraffic.config;

import org.rutebanken.tiamat.config.ApplicationContextProvider;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

/**
* Replaces {@link ApplicationContextProvider} in the fintraffic Spring test context with
* {@link RestoringApplicationContextProvider}, which saves and restores the static context
* field so that primary-context tests running after the fintraffic context is loaded still
* see the correct beans.
*/
@TestConfiguration
public class FintrafficTestContextConfiguration {

@Bean
public ApplicationContextProvider applicationContextProvider() {
return new RestoringApplicationContextProvider();
}

public static void restoreContext() {
RestoringApplicationContextProvider.restoreContext();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by
* the European Commission - subsequent versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at:
*
* https://joinup.ec.europa.eu/software/page/eupl
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and
* limitations under the Licence.
*/

package org.rutebanken.tiamat.ext.fintraffic.config;

import org.rutebanken.tiamat.config.ApplicationContextProvider;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;

/**
* Test-only replacement for {@link ApplicationContextProvider} used in the fintraffic Spring test context.
* <p>
* {@link ApplicationContextProvider} stores the active {@link ApplicationContext} in a static field.
* When the fintraffic test context starts it overwrites this field, causing subsequent tests running
* in the primary test context to get the wrong beans via {@code ApplicationContextProvider}.
* <p>
* This class saves the previous value of the static field when the fintraffic context sets it and
* exposes {@link #restoreContext()} so test teardown can restore the field before the primary-context
* tests that follow continue running.
* <p>
* Registered as a {@code @Bean} in {@link FintrafficTestContextConfiguration} which also excludes
* the component-scanned {@link ApplicationContextProvider} from the fintraffic context.
*/
class RestoringApplicationContextProvider extends ApplicationContextProvider {

private static ApplicationContext previousContext;
private static RestoringApplicationContextProvider instance;

@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
previousContext = ApplicationContextProvider.getApplicationContext();
instance = this;
super.setApplicationContext(ctx);
}

private void applyContext(ApplicationContext ctx) throws BeansException {
super.setApplicationContext(ctx);
}

static void restoreContext() {
if (instance != null && previousContext != null) {
instance.applyContext(previousContext);
previousContext = null;
}
}
}
Loading
Loading