44import org .junit .jupiter .api .BeforeEach ;
55import org .junit .jupiter .api .AfterEach ;
66import org .junit .jupiter .api .AfterAll ;
7+ import org .testcontainers .containers .GenericContainer ;
8+ import org .testcontainers .containers .wait .strategy .Wait ;
9+ import org .testcontainers .junit .jupiter .Container ;
710import org .testcontainers .junit .jupiter .Testcontainers ;
811
12+ import java .time .Duration ;
13+ import java .util .regex .Matcher ;
14+ import java .util .regex .Pattern ;
15+
916/**
1017 * Base class for integration tests that provides common setup and teardown functionality.
11- * This class would normally set up a TeamSpeak 3 server using TestContainers for integration testing.
12- *
13- * Note: This is a demonstration of the integration test infrastructure.
14- * A real implementation would require a TeamSpeak 3 server Docker image.
18+ * This class sets up a TeamSpeak 3 server using TestContainers for integration testing.
1519 */
1620@ Testcontainers
1721public abstract class BaseIntegrationTest {
1822
19- // Note: In a real implementation, this would be a TeamSpeak 3 server container
20- // protected static GenericContainer<?> teamspeakServer;
21-
23+ /**
24+ * TeamSpeak 3 server container for integration testing.
25+ * Uses the official TeamSpeak Docker image with proper configuration.
26+ */
27+ @ Container
28+ protected static final GenericContainer <?> teamspeakServer = new GenericContainer <>("teamspeak:latest" )
29+ .withExposedPorts (9987 , 10011 , 30033 )
30+ .withEnv ("TS3SERVER_LICENSE" , "accept" )
31+ .withEnv ("TS3SERVER_QUERY_PROTOCOLS" , "raw,ssh" )
32+ .withEnv ("TS3SERVER_DB_PLUGIN" , "ts3db_sqlite3" )
33+ .withEnv ("TS3SERVER_DB_SQLCREATEPATH" , "create_sqlite" )
34+ .withEnv ("TS3SERVER_MACHINE_ID" , "test-machine" ).withStartupTimeout (Duration .ofMinutes (2 ));
35+ // .waitingFor(Wait.forLogMessage(".*listening on.*", 1)
36+ // .withStartupTimeout(Duration.ofMinutes(2)));
37+
2238 protected static String serverHost ;
23- protected static int serverPort ;
39+ protected static int serverQueryPort ;
40+ protected static int serverVoicePort ;
41+ protected static int serverFileTransferPort ;
2442 protected static String serverQueryUsername ;
2543 protected static String serverQueryPassword ;
44+ protected static String serverAdminToken ;
45+ protected static TestLifecycleManager lifecycleManager ;
2646
2747 @ BeforeAll
2848 public static void setUpClass () {
29- // In a real implementation, this would start a TeamSpeak 3 server container
30- // teamspeakServer = new GenericContainer<>("teamspeak:latest")
31- // .withExposedPorts(9987, 10011, 30033)
32- // .withEnv("TS3SERVER_LICENSE", "accept")
33- // .waitingFor(Wait.forLogMessage(".*listening on.*", 1));
34- // teamspeakServer.start();
35-
36- // For demonstration purposes, we'll use mock values
37- serverHost = "localhost" ;
38- serverPort = 10011 ;
49+ teamspeakServer .followOutput (c -> System .out .println ("Container: " + c .getUtf8String ()));
50+ // Get connection details from the started container
51+ serverHost = teamspeakServer .getHost ();
52+ serverQueryPort = teamspeakServer .getMappedPort (10011 );
53+ serverVoicePort = teamspeakServer .getMappedPort (9987 );
54+ serverFileTransferPort = teamspeakServer .getMappedPort (30033 );
55+
56+ // Default credentials for TeamSpeak server
3957 serverQueryUsername = "serveradmin" ;
40- serverQueryPassword = "test123" ;
41-
58+ serverQueryPassword = extractServerAdminPassword ();
59+
60+ // Initialize the lifecycle manager
61+ lifecycleManager = new TestLifecycleManager (serverHost , serverQueryPort ,
62+ serverQueryUsername , serverQueryPassword );
63+
64+ // Wait for server to be ready
65+ try {
66+ lifecycleManager .waitForServerReady ();
67+ } catch (Exception e ) {
68+ throw new RuntimeException ("Failed to initialize TeamSpeak server for testing" , e );
69+ }
70+
4271 System .out .println ("Integration test environment set up" );
43- System .out .println ("Server: " + serverHost + ":" + serverPort );
72+ System .out .println ("Server Host: " + serverHost );
73+ System .out .println ("Query Port: " + serverQueryPort );
74+ System .out .println ("Voice Port: " + serverVoicePort );
75+ System .out .println ("File Transfer Port: " + serverFileTransferPort );
76+ }
77+
78+ /**
79+ * Extract the server admin password from the container logs.
80+ * TeamSpeak server generates a random password on first startup.
81+ * @return the server admin password
82+ */
83+ private static String extractServerAdminPassword () {
84+ try {
85+ String logs = teamspeakServer .getLogs ();
86+ Pattern pattern = Pattern .compile ("ServerAdmin password= \" ([^\" ]+)\" " );
87+ Matcher matcher = pattern .matcher (logs );
88+ if (matcher .find ()) {
89+ return matcher .group (1 );
90+ }
91+ // Fallback to a default password if extraction fails
92+ System .out .println ("Warning: Could not extract server admin password from logs, using default" );
93+ return "test123" ;
94+ } catch (Exception e ) {
95+ System .out .println ("Warning: Error extracting server admin password: " + e .getMessage ());
96+ return "test123" ;
97+ }
4498 }
4599
46100 @ AfterAll
47101 public static void tearDownClass () {
48- // In a real implementation, this would stop the container
49- // if (teamspeakServer != null) {
50- // teamspeakServer.stop();
51- // }
52-
102+ if (lifecycleManager != null ) {
103+ lifecycleManager .cleanupAll ();
104+ }
53105 System .out .println ("Integration test environment torn down" );
106+ // Container will be automatically stopped by TestContainers
54107 }
55108
56109 @ BeforeEach
@@ -61,10 +114,34 @@ public void setUp() {
61114
62115 @ AfterEach
63116 public void tearDown () {
64- // Common cleanup for each test
117+ // Clean up test-specific resources
118+ String testName = getCurrentTestName ();
119+ if (lifecycleManager != null && testName != null ) {
120+ lifecycleManager .cleanupTest (testName );
121+ lifecycleManager .disconnectTestQuery (testName );
122+ }
65123 System .out .println ("Cleaning up individual test" );
66124 }
67125
126+ /**
127+ * Gets the current test name for resource tracking.
128+ * This is a simple implementation - in a real scenario you might want to use
129+ * JUnit 5's TestInfo parameter injection.
130+ *
131+ * @return the current test name or null if not available
132+ */
133+ protected String getCurrentTestName () {
134+ // Simple implementation using stack trace
135+ StackTraceElement [] stackTrace = Thread .currentThread ().getStackTrace ();
136+ for (StackTraceElement element : stackTrace ) {
137+ if (element .getMethodName ().startsWith ("test" ) &&
138+ element .getClassName ().contains ("Test" )) {
139+ return element .getClassName () + "." + element .getMethodName ();
140+ }
141+ }
142+ return null ;
143+ }
144+
68145 /**
69146 * Get the server host for testing.
70147 * @return the server host
@@ -74,11 +151,43 @@ protected String getServerHost() {
74151 }
75152
76153 /**
77- * Get the server port for testing.
78- * @return the server port
154+ * Get the server query port for testing.
155+ * @return the server query port
79156 */
80157 protected int getServerPort () {
81- return serverPort ;
158+ return serverQueryPort ;
159+ }
160+
161+ /**
162+ * Get the server query port for testing.
163+ * @return the server query port
164+ */
165+ protected int getServerQueryPort () {
166+ return serverQueryPort ;
167+ }
168+
169+ /**
170+ * Get the server voice port for testing.
171+ * @return the server voice port
172+ */
173+ protected int getServerVoicePort () {
174+ return serverVoicePort ;
175+ }
176+
177+ /**
178+ * Get the server file transfer port for testing.
179+ * @return the server file transfer port
180+ */
181+ protected int getServerFileTransferPort () {
182+ return serverFileTransferPort ;
183+ }
184+
185+ /**
186+ * Get the server admin token for testing.
187+ * @return the server admin token
188+ */
189+ protected String getServerAdminToken () {
190+ return serverAdminToken ;
82191 }
83192
84193 /**
@@ -99,53 +208,124 @@ protected String getServerQueryPassword() {
99208
100209 /**
101210 * Wait for the server to be ready for connections.
102- * In a real implementation, this would check server status.
103211 */
104212 protected void waitForServerReady () {
105213 try {
106- Thread .sleep (1000 ); // Simulate waiting for server
107- } catch (InterruptedException e ) {
108- Thread .currentThread ().interrupt ();
214+ lifecycleManager .waitForServerReady ();
215+ } catch (Exception e ) {
216+ throw new RuntimeException ("Server not ready for testing" , e );
217+ }
218+ }
219+
220+ /**
221+ * Create a test query connection for the current test.
222+ * The connection will be automatically cleaned up after the test.
223+ *
224+ * @return a connected TS3Query instance
225+ */
226+ protected com .github .theholywaffle .teamspeak3 .TS3Query createTestQuery () {
227+ try {
228+ String testName = getCurrentTestName ();
229+ return lifecycleManager .createTestQuery (testName != null ? testName : "unknown-test" );
230+ } catch (Exception e ) {
231+ throw new RuntimeException ("Failed to create test query" , e );
109232 }
110233 }
111234
112235 /**
113236 * Create a test channel for testing purposes.
237+ * The channel will be automatically cleaned up after the test.
238+ *
114239 * @param channelName the name of the channel to create
115- * @return the channel ID (mocked for demonstration)
240+ * @param api the TS3Api instance to use
241+ * @return the channel ID
242+ */
243+ protected int createTestChannel (String channelName , com .github .theholywaffle .teamspeak3 .TS3Api api ) {
244+ String testName = getCurrentTestName ();
245+ return lifecycleManager .createTestChannel (testName != null ? testName : "unknown-test" , channelName , api );
246+ }
247+
248+ /**
249+ * Track a test client for cleanup.
250+ * The client will be automatically cleaned up after the test.
251+ *
252+ * @param clientId the ID of the client to track
253+ */
254+ protected void trackTestClient (int clientId ) {
255+ String testName = getCurrentTestName ();
256+ lifecycleManager .trackTestClient (testName != null ? testName : "unknown-test" , clientId );
257+ }
258+
259+ /**
260+ * Get the lifecycle manager for advanced test resource management.
261+ *
262+ * @return the test lifecycle manager
116263 */
117- protected int createTestChannel (String channelName ) {
118- // In a real implementation, this would create an actual channel
119- System .out .println ("Creating test channel: " + channelName );
120- return 1 ; // Mock channel ID
264+ protected TestLifecycleManager getLifecycleManager () {
265+ return lifecycleManager ;
121266 }
122267
123268 /**
124- * Clean up test channels after testing.
125- * @param channelId the ID of the channel to delete
269+ * Creates a test query and API instance for the current test.
270+ * Convenience method that combines query creation with API access.
271+ *
272+ * @return a TS3Api instance ready for testing
126273 */
127- protected void deleteTestChannel (int channelId ) {
128- // In a real implementation, this would delete the actual channel
129- System .out .println ("Deleting test channel: " + channelId );
274+ protected com .github .theholywaffle .teamspeak3 .TS3Api createTestApi () {
275+ return createTestQuery ().getApi ();
276+ }
277+
278+ /**
279+ * Verifies that the server is ready and in a clean state for testing.
280+ * Should be called at the beginning of tests that require a clean server state.
281+ *
282+ * @throws AssertionError if server is not in a clean state
283+ */
284+ protected void assertServerReady () {
285+ try {
286+ com .github .theholywaffle .teamspeak3 .TS3Api api = createTestApi ();
287+ boolean isClean = IntegrationTestUtils .verifyCleanServerState (api );
288+ if (!isClean ) {
289+ throw new AssertionError ("Server is not in a clean state for testing" );
290+ }
291+ } catch (Exception e ) {
292+ throw new AssertionError ("Failed to verify server state: " + e .getMessage (), e );
293+ }
130294 }
131295
132296 /**
133- * Create a test client for testing purposes.
134- * @param nickname the nickname of the client
135- * @return the client ID (mocked for demonstration)
297+ * Waits for a condition to be true with a default timeout.
298+ * Convenience method for common wait operations in tests.
299+ *
300+ * @param condition the condition to wait for
301+ * @return true if condition became true within timeout
136302 */
137- protected int createTestClient (String nickname ) {
138- // In a real implementation, this would create an actual client connection
139- System .out .println ("Creating test client: " + nickname );
140- return 1 ; // Mock client ID
303+ protected boolean waitForCondition (IntegrationTestUtils .BooleanSupplier condition ) {
304+ return IntegrationTestUtils .waitForCondition (condition );
141305 }
142306
143307 /**
144- * Disconnect and clean up test clients.
145- * @param clientId the ID of the client to disconnect
308+ * Waits for a condition to be true with a specified timeout.
309+ *
310+ * @param condition the condition to wait for
311+ * @param timeoutSeconds the timeout in seconds
312+ * @return true if condition became true within timeout
146313 */
147- protected void disconnectTestClient (int clientId ) {
148- // In a real implementation, this would disconnect the actual client
149- System .out .println ("Disconnecting test client: " + clientId );
314+ protected boolean waitForCondition (IntegrationTestUtils .BooleanSupplier condition , int timeoutSeconds ) {
315+ return IntegrationTestUtils .waitForCondition (condition , timeoutSeconds , 500 );
316+ }
317+
318+ /**
319+ * Sleeps for a short duration. Useful for waiting between operations.
320+ *
321+ * @param milliseconds the duration to sleep in milliseconds
322+ */
323+ protected void sleep (long milliseconds ) {
324+ try {
325+ Thread .sleep (milliseconds );
326+ } catch (InterruptedException e ) {
327+ Thread .currentThread ().interrupt ();
328+ throw new RuntimeException ("Sleep interrupted" , e );
329+ }
150330 }
151331}
0 commit comments