github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/testserver/datastore/mysql.go (about)

     1  //go:build docker
     2  // +build docker
     3  
     4  package datastore
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"fmt"
    10  	"testing"
    11  
    12  	"github.com/google/uuid"
    13  	"github.com/ory/dockertest/v3"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/authzed/spicedb/internal/datastore/mysql/migrations"
    17  	"github.com/authzed/spicedb/internal/datastore/mysql/version"
    18  	"github.com/authzed/spicedb/pkg/datastore"
    19  	"github.com/authzed/spicedb/pkg/migrate"
    20  	"github.com/authzed/spicedb/pkg/secrets"
    21  )
    22  
    23  const (
    24  	mysqlPort    = 3306
    25  	defaultCreds = "root:secret"
    26  	testDBPrefix = "spicedb_test_"
    27  )
    28  
    29  type mysqlTester struct {
    30  	db       *sql.DB
    31  	hostname string
    32  	creds    string
    33  	port     string
    34  	options  MySQLTesterOptions
    35  }
    36  
    37  // MySQLTesterOptions allows tweaking the behaviour of the builder for the MySQL datastore
    38  type MySQLTesterOptions struct {
    39  	Prefix                 string
    40  	MigrateForNewDatastore bool
    41  	UseV8                  bool
    42  }
    43  
    44  // RunMySQLForTesting returns a RunningEngineForTest for the mysql driver
    45  // backed by a MySQL instance with RunningEngineForTest options - no prefix is added, and datastore migration is run.
    46  func RunMySQLForTesting(t testing.TB, bridgeNetworkName string) RunningEngineForTest {
    47  	return RunMySQLForTestingWithOptions(t, MySQLTesterOptions{Prefix: "", MigrateForNewDatastore: true}, bridgeNetworkName)
    48  }
    49  
    50  // RunMySQLForTestingWithOptions returns a RunningEngineForTest for the mysql driver
    51  // backed by a MySQL instance, while allowing options to be forwarded
    52  func RunMySQLForTestingWithOptions(t testing.TB, options MySQLTesterOptions, bridgeNetworkName string) RunningEngineForTest {
    53  	pool, err := dockertest.NewPool("")
    54  	require.NoError(t, err)
    55  
    56  	containerImageTag := version.MinimumSupportedMySQLVersion
    57  
    58  	name := fmt.Sprintf("mysql-%s", uuid.New().String())
    59  	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
    60  		Name:       name,
    61  		Repository: "mirror.gcr.io/library/mysql",
    62  		Tag:        containerImageTag,
    63  		Env:        []string{"MYSQL_ROOT_PASSWORD=secret"},
    64  		// increase max connections (default 151) to accommodate tests using the same docker container
    65  		Cmd:       []string{"--max-connections=500"},
    66  		NetworkID: bridgeNetworkName,
    67  	})
    68  	require.NoError(t, err)
    69  
    70  	builder := &mysqlTester{
    71  		creds:   defaultCreds,
    72  		options: options,
    73  	}
    74  	t.Cleanup(func() {
    75  		require.NoError(t, pool.Purge(resource))
    76  	})
    77  
    78  	port := resource.GetPort(fmt.Sprintf("%d/tcp", mysqlPort))
    79  	if bridgeNetworkName != "" {
    80  		builder.hostname = name
    81  		builder.port = fmt.Sprintf("%d", mysqlPort)
    82  	} else {
    83  		builder.port = port
    84  	}
    85  
    86  	dsn := fmt.Sprintf("%s@(localhost:%s)/mysql?parseTime=true", builder.creds, port)
    87  	require.NoError(t, pool.Retry(func() error {
    88  		var err error
    89  		builder.db, err = sql.Open("mysql", dsn)
    90  		if err != nil {
    91  			return err
    92  		}
    93  		ctx, cancelPing := context.WithTimeout(context.Background(), dockerBootTimeout)
    94  		defer cancelPing()
    95  		err = builder.db.PingContext(ctx)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		return nil
   100  	}))
   101  
   102  	return builder
   103  }
   104  
   105  func (mb *mysqlTester) NewDatabase(t testing.TB) string {
   106  	uniquePortion, err := secrets.TokenHex(4)
   107  	require.NoError(t, err, "Could not generate unique portion of db name: %s", err)
   108  	dbName := testDBPrefix + uniquePortion
   109  	tx, err := mb.db.Begin()
   110  	require.NoError(t, err, "Could being transaction: %s", err)
   111  	_, err = tx.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName))
   112  	require.NoError(t, err, "failed to create database %s: %s", dbName, err)
   113  	err = tx.Commit()
   114  	require.NoError(t, err, "failed to commit: %s", err)
   115  	return fmt.Sprintf("%s@(%s:%s)/%s?parseTime=true", mb.creds, mb.hostname, mb.port, dbName)
   116  }
   117  
   118  func (mb *mysqlTester) runMigrate(t testing.TB, dsn string) {
   119  	driver, err := migrations.NewMySQLDriverFromDSN(dsn, mb.options.Prefix, datastore.NoCredentialsProvider)
   120  	require.NoError(t, err, "failed to create migration driver: %s", err)
   121  	err = migrations.Manager.Run(context.Background(), driver, migrate.Head, migrate.LiveRun)
   122  	require.NoError(t, err, "failed to run migration: %s", err)
   123  }
   124  
   125  func (mb *mysqlTester) NewDatastore(t testing.TB, initFunc InitFunc) datastore.Datastore {
   126  	dsn := mb.NewDatabase(t)
   127  	if mb.options.MigrateForNewDatastore {
   128  		mb.runMigrate(t, dsn)
   129  	}
   130  	return initFunc("mysql", dsn)
   131  }