View Javadoc
1   /*
2    * Copyright (c) 2012-2023, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.mysql.maven.plugin;
31  
32  import com.jcabi.jdbc.JdbcSession;
33  import com.jcabi.jdbc.UrlSource;
34  import java.io.File;
35  import java.net.ServerSocket;
36  import java.nio.file.Files;
37  import java.util.Collections;
38  import java.util.concurrent.TimeUnit;
39  import javax.sql.DataSource;
40  import org.hamcrest.MatcherAssert;
41  import org.junit.jupiter.api.Disabled;
42  import org.junit.jupiter.api.Test;
43  
44  /**
45   * Test case for {@link Instances}.
46   * @checkstyle ClassDataAbstractionCoupling (500 lines)
47   * @checkstyle MultipleStringLiterals (500 lines)
48   * @since 0.6
49   */
50  @SuppressWarnings("PMD.AvoidDuplicateLiterals")
51  final class InstancesTest {
52  
53      /**
54       * User.
55       */
56      public static final String USER = "u13";
57  
58      /**
59       * Password.
60       */
61      public static final String PASSWORD = "swordfish";
62  
63      /**
64       * Database name.
65       */
66      public static final String DBNAME = "papamama";
67  
68      /**
69       * Time to sleep in between instances.
70       */
71      private static final long SLEEP_SECONDS = 5L;
72  
73      /**
74       * Location of MySQL dist.
75       */
76      private static final String DIST = getDist();
77  
78      /**
79       * MySQL connection string format.
80       */
81      private static final String CONNECTION_STRING =
82          "jdbc:mysql://localhost:%d/%s?user=%s&password=%s";
83  
84      /**
85       * Instances can start and stop.
86       * @throws Exception If something is wrong
87       */
88      @Test
89      void startsAndStops() throws Exception {
90          final int port = this.reserve();
91          final Instances instances = new Instances();
92          instances.start(
93              new Config(
94                  port,
95                  InstancesTest.USER,
96                  InstancesTest.PASSWORD,
97                  InstancesTest.DBNAME,
98                  Collections.emptyList()
99              ),
100             new File(InstancesTest.DIST),
101             Files.createTempDirectory("").toFile(),
102             true,
103             null
104         );
105         final DataSource source = new UrlSource(
106             String.format(
107                 InstancesTest.CONNECTION_STRING,
108                 port,
109                 InstancesTest.DBNAME,
110                 InstancesTest.USER,
111                 InstancesTest.PASSWORD
112             )
113         );
114         try {
115             new JdbcSession(source)
116                 .autocommit(false)
117                 .sql("CREATE TABLE foo (id INT)")
118                 .execute()
119                 .sql("INSERT INTO foo VALUES (1)")
120                 .execute()
121                 .sql("SELECT COUNT(*) FROM foo")
122                 .execute()
123                 .sql("DROP TABLE foo")
124                 .execute();
125         } finally {
126             instances.stop(port);
127         }
128     }
129 
130     /**
131      * Instances can use option.
132      * Test creates and inserts incorrect date in it
133      * Without option "--sql-mode=ALLOW_INVALID_DATES" it produces
134      * invalid date error.
135      * @throws Exception If something is wrong
136      */
137     @Test
138     void useOptions() throws Exception {
139         final int port = this.reserve();
140         final Instances instances = new Instances();
141         instances.start(
142             new Config(
143                 port,
144                 InstancesTest.USER,
145                 InstancesTest.PASSWORD,
146                 InstancesTest.DBNAME,
147                 Collections.singletonList("sql-mode=ALLOW_INVALID_DATES")
148             ),
149             new File(InstancesTest.DIST),
150             Files.createTempDirectory("").toFile(),
151             true,
152             null
153         );
154         try {
155             final DataSource source = new UrlSource(
156                 String.format(
157                     InstancesTest.CONNECTION_STRING,
158                     port,
159                     InstancesTest.DBNAME,
160                     InstancesTest.USER,
161                     InstancesTest.PASSWORD
162                 )
163             );
164             new JdbcSession(source)
165                 .autocommit(false)
166                 .sql("CREATE TABLE foo (date DATE)")
167                 .execute()
168                 .sql("INSERT INTO foo VALUES ('2004-04-31')")
169                 .execute()
170                 .sql("SELECT * FROM foo")
171                 .execute()
172                 .sql("DROP TABLE foo")
173                 .execute();
174         } finally {
175             instances.stop(port);
176         }
177     }
178 
179     /**
180      * Instances can use custom db user name.
181      * @throws Exception If something is wrong
182      * @todo #8 Create integration tests for Config.
183      *  Integration tests 'WithConfigITCase' should be created to test
184      *  that user name, password and dbname are set properly.
185      *  This issue should be done after non root user name is set properly
186      */
187     @Test
188     void canUseCustomDbUserName() throws Exception {
189         final int port = this.reserve();
190         final String user = "notRoot";
191         final Instances instances = new Instances();
192         instances.start(
193             new Config(
194                 port,
195                 user,
196                 InstancesTest.PASSWORD,
197                 InstancesTest.DBNAME,
198                 Collections.<String>emptyList()
199             ),
200             new File(InstancesTest.DIST),
201             Files.createTempDirectory("").toFile(),
202             true,
203             null
204         );
205         final DataSource source = new UrlSource(
206             String.format(
207                 InstancesTest.CONNECTION_STRING,
208                 port,
209                 InstancesTest.DBNAME,
210                 user,
211                 InstancesTest.PASSWORD
212             )
213         );
214         try {
215             new JdbcSession(source)
216                 .autocommit(false)
217                 .sql("CREATE TABLE foo (id INT)")
218                 .execute()
219                 .sql("INSERT INTO foo VALUES (1)")
220                 .execute()
221                 .sql("SELECT COUNT(*) FROM foo")
222                 .execute()
223                 .sql("DROP TABLE foo")
224                 .execute();
225         } finally {
226             instances.stop(port);
227         }
228     }
229 
230     /**
231      * Instances can use custom db password.
232      * Password changed with username, because we don't support
233      * changing password for existing user
234      * @throws Exception If something is wrong
235      */
236     @Test
237     void canUseCustomDbPassword() throws Exception {
238         final int port = this.reserve();
239         final String user = "notRoot";
240         final String password = "notRoot";
241         final Instances instances = new Instances();
242         instances.start(
243             new Config(
244                 port,
245                 user,
246                 password,
247                 InstancesTest.DBNAME,
248                 Collections.<String>emptyList()
249             ),
250             new File(InstancesTest.DIST),
251             Files.createTempDirectory("").toFile(),
252             true,
253             null
254         );
255         final DataSource source = new UrlSource(
256             String.format(
257                 InstancesTest.CONNECTION_STRING,
258                 port,
259                 InstancesTest.DBNAME,
260                 user,
261                 password
262             )
263         );
264         try {
265             new JdbcSession(source)
266                 .autocommit(false)
267                 .sql("CREATE TABLE foo (id INT)")
268                 .execute()
269                 .sql("INSERT INTO foo VALUES (1)")
270                 .execute()
271                 .sql("SELECT COUNT(*) FROM foo")
272                 .execute()
273                 .sql("DROP TABLE foo")
274                 .execute();
275         } finally {
276             instances.stop(port);
277         }
278     }
279 
280     /**
281      * Instances can use custom db name.
282      * @throws Exception If something is wrong
283      */
284     @Test
285     void canUseCustomDbDbName() throws Exception {
286         final int port = this.reserve();
287         final String dbname = "notRoot";
288         final Instances instances = new Instances();
289         instances.start(
290             new Config(
291                 port,
292                 InstancesTest.USER,
293                 InstancesTest.PASSWORD,
294                 dbname,
295                 Collections.<String>emptyList()
296             ),
297             new File(InstancesTest.DIST),
298             Files.createTempDirectory("").toFile(),
299             true,
300             null
301         );
302         final DataSource source = new UrlSource(
303             String.format(
304                 InstancesTest.CONNECTION_STRING,
305                 port,
306                 dbname,
307                 InstancesTest.USER,
308                 InstancesTest.PASSWORD
309             )
310         );
311         try {
312             new JdbcSession(source)
313                 .autocommit(false)
314                 .sql("CREATE TABLE foo (id INT)")
315                 .execute()
316                 .sql("INSERT INTO foo VALUES (1)")
317                 .execute()
318                 .sql("SELECT COUNT(*) FROM foo")
319                 .execute()
320                 .sql("DROP TABLE foo")
321                 .execute();
322         } finally {
323             instances.stop(port);
324         }
325     }
326 
327     /**
328      * If no database exists, it will create one even if clear = false.
329      * @throws Exception If something is wrong
330      */
331     @Test
332     void willCreateDatabaseEvenWithoutClear() throws Exception {
333         final int port = this.reserve();
334         final Instances instances = new Instances();
335         instances.start(
336             new Config(
337                 port,
338                 InstancesTest.USER,
339                 InstancesTest.PASSWORD,
340                 InstancesTest.DBNAME,
341                 Collections.emptyList()
342             ),
343             new File(InstancesTest.DIST),
344             Files.createTempDirectory("").toFile(),
345             false,
346             null
347         );
348         MatcherAssert.assertThat(
349             "Instance reusedExistingDatabase should be false.",
350             !instances.reusedExistingDatabase()
351         );
352         final DataSource source = new UrlSource(
353             String.format(
354                 InstancesTest.CONNECTION_STRING,
355                 port,
356                 InstancesTest.DBNAME,
357                 InstancesTest.USER,
358                 InstancesTest.PASSWORD
359             )
360         );
361         try {
362             new JdbcSession(source)
363                 .autocommit(false)
364                 .sql("CREATE TABLE foo (id INT)")
365                 .execute()
366                 .sql("INSERT INTO foo VALUES (1)")
367                 .execute()
368                 .sql("SELECT COUNT(*) FROM foo")
369                 .execute()
370                 .sql("DROP TABLE foo")
371                 .execute();
372         } finally {
373             instances.stop(port);
374         }
375     }
376 
377     /**
378      * Is able to reuse a previously created database.
379      * @throws Exception If something is wrong
380      */
381     @Test
382     @Disabled
383     void canReuseExistingDatabse() throws Exception {
384         final int port = this.reserve();
385         final File target = Files.createTempDirectory("").toFile();
386         final Instances instances = new Instances();
387         instances.start(
388             new Config(
389                 port,
390                 InstancesTest.USER,
391                 InstancesTest.PASSWORD,
392                 InstancesTest.DBNAME,
393                 Collections.emptyList()
394             ),
395             new File(InstancesTest.DIST),
396             target,
397             true,
398             null
399         );
400         MatcherAssert.assertThat(
401             "Instance reusedExistingDatabase should be false.",
402             !instances.reusedExistingDatabase()
403         );
404         final DataSource source = new UrlSource(
405             String.format(
406                 InstancesTest.CONNECTION_STRING,
407                 port,
408                 InstancesTest.DBNAME,
409                 InstancesTest.USER,
410                 InstancesTest.PASSWORD
411             )
412         );
413         try {
414             new JdbcSession(source)
415                 .autocommit(false)
416                 .sql("START TRANSACTION")
417                 .execute()
418                 .sql("CREATE TABLE foo (id INT)")
419                 .execute()
420                 .sql("INSERT INTO foo VALUES (1)")
421                 .execute()
422                 .sql("SELECT COUNT(*) FROM foo")
423                 .execute()
424                 .sql("COMMIT")
425                 .execute();
426         } finally {
427             instances.stop(port);
428         }
429         this.checkExistingDatabase(target);
430     }
431 
432     /**
433      * Helper for canReuseExistingDatabse test.
434      * @param target Directory of existing database
435      * @throws Exception If something is wrong
436      */
437     private void checkExistingDatabase(final File target) throws Exception {
438         final File socket = new File(target, "mysql.sock");
439         while (socket.exists()) {
440             TimeUnit.SECONDS.sleep(InstancesTest.SLEEP_SECONDS);
441         }
442         final int port = this.reserve();
443         final Instances instances = new Instances();
444         instances.start(
445             new Config(
446                 port,
447                 InstancesTest.USER,
448                 InstancesTest.PASSWORD,
449                 InstancesTest.DBNAME,
450                 Collections.emptyList()
451             ),
452             new File(InstancesTest.DIST),
453             target,
454             false,
455             null
456         );
457         MatcherAssert.assertThat(
458             "Instance reusedExistingDatabase should be true.",
459             instances.reusedExistingDatabase()
460         );
461         do {
462             TimeUnit.SECONDS.sleep(InstancesTest.SLEEP_SECONDS);
463         } while (!socket.exists());
464         try {
465             final DataSource source = new UrlSource(
466                 String.format(
467                     InstancesTest.CONNECTION_STRING,
468                     port,
469                     InstancesTest.DBNAME,
470                     InstancesTest.USER,
471                     InstancesTest.PASSWORD
472                 )
473             );
474             new JdbcSession(source)
475                 .autocommit(false)
476                 .sql("SELECT COUNT(*) FROM foo")
477                 .execute()
478                 .sql("DROP TABLE foo")
479                 .execute();
480         } finally {
481             instances.stop(port);
482         }
483     }
484 
485     /**
486      * Find and return the first available port.
487      * @return The port number
488      * @throws Exception If fails
489      */
490     private int reserve() throws Exception {
491         try (ServerSocket socket = new ServerSocket(0)) {
492             return socket.getLocalPort();
493         }
494     }
495 
496     /**
497      * This will be taken from the surefire.dist system property
498      * or defaulted to the target.
499      * @return The MySQL distribution location
500      */
501     private static String getDist() {
502         String dist = System.getProperty("surefire.dist");
503         if (dist == null) {
504             dist = "./target/mysql-dist";
505         }
506         return dist;
507     }
508 }