Skip to content
This repository was archived by the owner on Jul 1, 2025. It is now read-only.

Commit df5b01e

Browse files
dfcoffinclaude
andcommitted
Implement comprehensive TestContainers MySQL integration tests for ESPI database validation
- Create TestContainersIntegrationTestBase with MySQL container setup and Flyway migration support - Add MySQLFlywayMigrationIntegrationTest to validate all migration scripts execute correctly - Add MySQLEntityCrudIntegrationTest for basic entity persistence and relationship testing - Add MySQLAtomHrefIntegrationTest to validate ESPI-compliant ATOM href URL relationships - Add MySQLSchemaCouplingIntegrationTest to verify loose coupling between usage and customer schemas - Create testcontainers-mysql Spring profile configuration - Add comprehensive documentation in TESTCONTAINERS_INTEGRATION_TESTS.md - Implement real MySQL database testing (not H2 in-memory) for accurate validation - Validate NAESB ESPI 1.0 specification compliance throughout database layer - Support microservices architecture with independent schema operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 43c132c commit df5b01e

8 files changed

+2234
-0
lines changed

src/test/java/org/greenbuttonalliance/espi/common/integration/MySQLAtomHrefIntegrationTest.java

Lines changed: 335 additions & 0 deletions
Large diffs are not rendered by default.

src/test/java/org/greenbuttonalliance/espi/common/integration/MySQLEntityCrudIntegrationTest.java

Lines changed: 355 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
/*
2+
*
3+
* Copyright (c) 2018-2025 Green Button Alliance, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package org.greenbuttonalliance.espi.common.integration;
20+
21+
import org.junit.jupiter.api.DisplayName;
22+
import org.junit.jupiter.api.Test;
23+
import org.springframework.test.context.jdbc.Sql;
24+
25+
import java.sql.Connection;
26+
import java.sql.DatabaseMetaData;
27+
import java.sql.ResultSet;
28+
import java.sql.SQLException;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
32+
import static org.junit.jupiter.api.Assertions.*;
33+
34+
/**
35+
* Integration tests for MySQL Flyway migrations using TestContainers.
36+
*
37+
* This test class validates:
38+
* - Flyway migration scripts execute successfully
39+
* - Database schema structure matches expectations
40+
* - All required tables are created with correct structure
41+
* - Indexes and constraints are properly applied
42+
* - Usage and customer schemas are properly separated
43+
*/
44+
@DisplayName("MySQL Flyway Migration Integration Tests")
45+
class MySQLFlywayMigrationIntegrationTest extends TestContainersIntegrationTestBase {
46+
47+
@Test
48+
@DisplayName("Should successfully execute all Flyway migrations")
49+
void shouldExecuteAllFlywayMigrations() throws SQLException {
50+
// Test verifies that setUp() method ran successfully without exceptions
51+
// and that database connections are working
52+
53+
try (Connection usageConn = createSchemaDataSource("openespi_usage").getConnection();
54+
Connection customerConn = createSchemaDataSource("openespi_customer").getConnection()) {
55+
56+
assertNotNull(usageConn, "Usage schema connection should be available");
57+
assertNotNull(customerConn, "Customer schema connection should be available");
58+
59+
assertTrue(usageConn.isValid(5), "Usage schema connection should be valid");
60+
assertTrue(customerConn.isValid(5), "Customer schema connection should be valid");
61+
}
62+
}
63+
64+
@Test
65+
@DisplayName("Should create all required tables in usage schema")
66+
void shouldCreateUsageSchemaTables() throws SQLException {
67+
List<String> expectedTables = List.of(
68+
// V1 - Base tables
69+
"application_information",
70+
"subscriptions",
71+
"authorizations",
72+
"retail_customers",
73+
"usage_points",
74+
"service_delivery_points",
75+
76+
// V2 - Meter and reading tables
77+
"meter_readings",
78+
"interval_blocks",
79+
"interval_readings",
80+
"reading_types",
81+
82+
// V3 - Quality and summary tables
83+
"reading_qualities",
84+
"electric_power_quality_summaries",
85+
"usage_summaries",
86+
"summary_measurements"
87+
);
88+
89+
List<String> actualTables = getTablesInSchema("openespi_usage");
90+
91+
for (String expectedTable : expectedTables) {
92+
assertTrue(actualTables.contains(expectedTable),
93+
"Usage schema should contain table: " + expectedTable);
94+
}
95+
}
96+
97+
@Test
98+
@DisplayName("Should create all required tables in customer schema")
99+
void shouldCreateCustomerSchemaTables() throws SQLException {
100+
List<String> expectedTables = List.of(
101+
// V4 - Base customer tables
102+
"customers",
103+
"customer_agreements",
104+
"customer_accounts",
105+
"pricing_structures",
106+
"tariff_profiles",
107+
108+
// V5 - Device and asset tables
109+
"end_devices",
110+
"assets",
111+
"asset_properties",
112+
"device_functions",
113+
114+
// V6 - Organization and support tables
115+
"organisations",
116+
"service_locations",
117+
"postal_addresses",
118+
"electronic_addresses",
119+
"phone_numbers",
120+
"names",
121+
"statements"
122+
);
123+
124+
List<String> actualTables = getTablesInSchema("openespi_customer");
125+
126+
for (String expectedTable : expectedTables) {
127+
assertTrue(actualTables.contains(expectedTable),
128+
"Customer schema should contain table: " + expectedTable);
129+
}
130+
}
131+
132+
@Test
133+
@DisplayName("Should create proper table structure for application_information")
134+
void shouldCreateApplicationInformationTableStructure() throws SQLException {
135+
List<String> expectedColumns = List.of(
136+
"id", "uuid", "uuid_msb", "uuid_lsb", "description",
137+
"created", "updated", "published",
138+
"up_link_rel", "up_link_href", "self_link_rel", "self_link_href",
139+
"client_name", "client_id", "client_secret", "client_id_issued_at",
140+
"client_secret_expires_at", "registration_client_uri",
141+
"registration_access_token", "redirect_uris", "software_id",
142+
"software_version", "token_endpoint_auth_method", "response_types",
143+
"grant_types", "application_type", "contacts", "logo_uri",
144+
"policy_uri", "tos_uri", "jwks_uri", "scope"
145+
);
146+
147+
List<String> actualColumns = getColumnsInTable("openespi_usage", "application_information");
148+
149+
for (String expectedColumn : expectedColumns) {
150+
assertTrue(actualColumns.contains(expectedColumn),
151+
"application_information table should contain column: " + expectedColumn);
152+
}
153+
}
154+
155+
@Test
156+
@DisplayName("Should create proper table structure for retail_customers")
157+
void shouldCreateRetailCustomersTableStructure() throws SQLException {
158+
List<String> expectedColumns = List.of(
159+
"id", "uuid", "uuid_msb", "uuid_lsb", "description",
160+
"created", "updated", "published",
161+
"up_link_rel", "up_link_href", "self_link_rel", "self_link_href",
162+
"enabled", "first_name", "last_name", "password", "role", "username"
163+
);
164+
165+
List<String> actualColumns = getColumnsInTable("openespi_usage", "retail_customers");
166+
167+
for (String expectedColumn : expectedColumns) {
168+
assertTrue(actualColumns.contains(expectedColumn),
169+
"retail_customers table should contain column: " + expectedColumn);
170+
}
171+
}
172+
173+
@Test
174+
@DisplayName("Should create proper table structure for customers with PII fields")
175+
void shouldCreateCustomersTableStructure() throws SQLException {
176+
List<String> expectedColumns = List.of(
177+
"id", "uuid", "uuid_msb", "uuid_lsb", "description",
178+
"created", "updated", "published",
179+
"up_link_rel", "up_link_href", "self_link_rel", "self_link_href",
180+
"kind", "special_need", "vip", "puc_number", "status", "priority",
181+
"locale", "customer_name", "retail_customer_href"
182+
);
183+
184+
List<String> actualColumns = getColumnsInTable("openespi_customer", "customers");
185+
186+
for (String expectedColumn : expectedColumns) {
187+
assertTrue(actualColumns.contains(expectedColumn),
188+
"customers table should contain column: " + expectedColumn);
189+
}
190+
}
191+
192+
@Test
193+
@DisplayName("Should create proper indexes for performance")
194+
void shouldCreateProperIndexes() throws SQLException {
195+
// Test critical indexes exist for performance
196+
assertTrue(indexExists("openespi_usage", "application_information", "idx_app_info_uuid"),
197+
"application_information should have UUID index");
198+
assertTrue(indexExists("openespi_usage", "application_information", "idx_app_info_client_id"),
199+
"application_information should have client_id index");
200+
201+
assertTrue(indexExists("openespi_usage", "retail_customers", "idx_retail_customer_uuid"),
202+
"retail_customers should have UUID index");
203+
assertTrue(indexExists("openespi_usage", "retail_customers", "idx_retail_customer_username"),
204+
"retail_customers should have username index");
205+
206+
assertTrue(indexExists("openespi_customer", "customers", "idx_customer_uuid"),
207+
"customers should have UUID index");
208+
assertTrue(indexExists("openespi_customer", "customers", "idx_customer_puc_number"),
209+
"customers should have PUC number index");
210+
}
211+
212+
@Test
213+
@DisplayName("Should enforce unique constraints")
214+
void shouldEnforceUniqueConstraints() throws SQLException {
215+
// Test that unique constraints are properly applied
216+
assertTrue(hasUniqueConstraint("openespi_usage", "application_information", "uuid"),
217+
"application_information.uuid should have unique constraint");
218+
assertTrue(hasUniqueConstraint("openespi_usage", "application_information", "client_id"),
219+
"application_information.client_id should have unique constraint");
220+
221+
assertTrue(hasUniqueConstraint("openespi_usage", "retail_customers", "uuid"),
222+
"retail_customers.uuid should have unique constraint");
223+
assertTrue(hasUniqueConstraint("openespi_usage", "retail_customers", "username"),
224+
"retail_customers.username should have unique constraint");
225+
226+
assertTrue(hasUniqueConstraint("openespi_customer", "customers", "uuid"),
227+
"customers.uuid should have unique constraint");
228+
}
229+
230+
@Test
231+
@DisplayName("Should use proper column types for ESPI compliance")
232+
void shouldUseProperColumnTypes() throws SQLException {
233+
// Test critical column types for ESPI compliance
234+
assertEquals("VARCHAR", getColumnType("openespi_usage", "application_information", "uuid"),
235+
"UUID column should be VARCHAR(36)");
236+
assertEquals("DATETIME", getColumnType("openespi_usage", "application_information", "created"),
237+
"Created timestamp should be DATETIME(6)");
238+
assertEquals("TEXT", getColumnType("openespi_usage", "application_information", "redirect_uris"),
239+
"Redirect URIs should be TEXT for multiple URLs");
240+
241+
assertEquals("VARCHAR", getColumnType("openespi_customer", "customers", "uuid"),
242+
"Customer UUID should be VARCHAR(36)");
243+
assertEquals("TINYINT", getColumnType("openespi_customer", "customers", "vip"),
244+
"VIP flag should be BOOLEAN (TINYINT in MySQL)");
245+
}
246+
247+
@Test
248+
@DisplayName("Should verify schema separation between usage and customer data")
249+
void shouldVerifySchemaSeparation() throws SQLException {
250+
// Verify that usage schema doesn't contain customer tables
251+
List<String> usageTables = getTablesInSchema("openespi_usage");
252+
assertFalse(usageTables.contains("customers"),
253+
"Usage schema should not contain customers table");
254+
assertFalse(usageTables.contains("organisations"),
255+
"Usage schema should not contain organisations table");
256+
257+
// Verify that customer schema doesn't contain usage tables
258+
List<String> customerTables = getTablesInSchema("openespi_customer");
259+
assertFalse(customerTables.contains("meter_readings"),
260+
"Customer schema should not contain meter_readings table");
261+
assertFalse(customerTables.contains("interval_readings"),
262+
"Customer schema should not contain interval_readings table");
263+
}
264+
265+
// Helper methods for database introspection
266+
267+
private List<String> getTablesInSchema(String schema) throws SQLException {
268+
List<String> tables = new ArrayList<>();
269+
try (Connection conn = createSchemaDataSource(schema).getConnection()) {
270+
DatabaseMetaData metaData = conn.getMetaData();
271+
try (ResultSet rs = metaData.getTables(schema, null, null, new String[]{"TABLE"})) {
272+
while (rs.next()) {
273+
tables.add(rs.getString("TABLE_NAME"));
274+
}
275+
}
276+
}
277+
return tables;
278+
}
279+
280+
private List<String> getColumnsInTable(String schema, String table) throws SQLException {
281+
List<String> columns = new ArrayList<>();
282+
try (Connection conn = createSchemaDataSource(schema).getConnection()) {
283+
DatabaseMetaData metaData = conn.getMetaData();
284+
try (ResultSet rs = metaData.getColumns(schema, null, table, null)) {
285+
while (rs.next()) {
286+
columns.add(rs.getString("COLUMN_NAME"));
287+
}
288+
}
289+
}
290+
return columns;
291+
}
292+
293+
private boolean indexExists(String schema, String table, String indexName) throws SQLException {
294+
try (Connection conn = createSchemaDataSource(schema).getConnection()) {
295+
DatabaseMetaData metaData = conn.getMetaData();
296+
try (ResultSet rs = metaData.getIndexInfo(schema, null, table, false, false)) {
297+
while (rs.next()) {
298+
if (indexName.equals(rs.getString("INDEX_NAME"))) {
299+
return true;
300+
}
301+
}
302+
}
303+
}
304+
return false;
305+
}
306+
307+
private boolean hasUniqueConstraint(String schema, String table, String column) throws SQLException {
308+
try (Connection conn = createSchemaDataSource(schema).getConnection()) {
309+
DatabaseMetaData metaData = conn.getMetaData();
310+
try (ResultSet rs = metaData.getIndexInfo(schema, null, table, true, false)) {
311+
while (rs.next()) {
312+
if (column.equals(rs.getString("COLUMN_NAME"))) {
313+
return true;
314+
}
315+
}
316+
}
317+
}
318+
return false;
319+
}
320+
321+
private String getColumnType(String schema, String table, String column) throws SQLException {
322+
try (Connection conn = createSchemaDataSource(schema).getConnection()) {
323+
DatabaseMetaData metaData = conn.getMetaData();
324+
try (ResultSet rs = metaData.getColumns(schema, null, table, column)) {
325+
if (rs.next()) {
326+
return rs.getString("TYPE_NAME");
327+
}
328+
}
329+
}
330+
return null;
331+
}
332+
333+
private javax.sql.DataSource createSchemaDataSource(String schema) {
334+
org.springframework.boot.jdbc.DataSourceBuilder<?> builder =
335+
org.springframework.boot.jdbc.DataSourceBuilder.create();
336+
337+
return builder
338+
.url(getMySQLContainer().getJdbcUrl().replace("/openespi_test", "/" + schema))
339+
.username(getMySQLContainer().getUsername())
340+
.password(getMySQLContainer().getPassword())
341+
.driverClassName("com.mysql.cj.jdbc.Driver")
342+
.build();
343+
}
344+
}

0 commit comments

Comments
 (0)