github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/integration/client_server_test.go (about)

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	dockercontainer "github.com/docker/docker/api/types/container"
    15  	"github.com/docker/go-connections/nat"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	testcontainers "github.com/testcontainers/testcontainers-go"
    19  
    20  	"github.com/devseccon/trivy/pkg/clock"
    21  	"github.com/devseccon/trivy/pkg/report"
    22  	"github.com/devseccon/trivy/pkg/uuid"
    23  )
    24  
    25  type csArgs struct {
    26  	Command           string
    27  	RemoteAddrOption  string
    28  	Format            string
    29  	TemplatePath      string
    30  	IgnoreUnfixed     bool
    31  	Severity          []string
    32  	IgnoreIDs         []string
    33  	Input             string
    34  	ClientToken       string
    35  	ClientTokenHeader string
    36  	ListAllPackages   bool
    37  	Target            string
    38  	secretConfig      string
    39  }
    40  
    41  func TestClientServer(t *testing.T) {
    42  	tests := []struct {
    43  		name    string
    44  		args    csArgs
    45  		golden  string
    46  		wantErr string
    47  	}{
    48  		{
    49  			name: "alpine 3.9",
    50  			args: csArgs{
    51  				Input: "testdata/fixtures/images/alpine-39.tar.gz",
    52  			},
    53  			golden: "testdata/alpine-39.json.golden",
    54  		},
    55  		{
    56  			name: "alpine 3.9 with high and critical severity",
    57  			args: csArgs{
    58  				IgnoreUnfixed: true,
    59  				Severity: []string{
    60  					"HIGH",
    61  					"CRITICAL",
    62  				},
    63  				Input: "testdata/fixtures/images/alpine-39.tar.gz",
    64  			},
    65  			golden: "testdata/alpine-39-high-critical.json.golden",
    66  		},
    67  		{
    68  			name: "alpine 3.9 with .trivyignore",
    69  			args: csArgs{
    70  				IgnoreUnfixed: false,
    71  				IgnoreIDs: []string{
    72  					"CVE-2019-1549",
    73  					"CVE-2019-14697",
    74  				},
    75  				Input: "testdata/fixtures/images/alpine-39.tar.gz",
    76  			},
    77  			golden: "testdata/alpine-39-ignore-cveids.json.golden",
    78  		},
    79  		{
    80  			name: "alpine 3.10",
    81  			args: csArgs{
    82  				Input: "testdata/fixtures/images/alpine-310.tar.gz",
    83  			},
    84  			golden: "testdata/alpine-310.json.golden",
    85  		},
    86  		{
    87  			name: "alpine distroless",
    88  			args: csArgs{
    89  				Input: "testdata/fixtures/images/alpine-distroless.tar.gz",
    90  			},
    91  			golden: "testdata/alpine-distroless.json.golden",
    92  		},
    93  		{
    94  			name: "debian buster/10",
    95  			args: csArgs{
    96  				Input: "testdata/fixtures/images/debian-buster.tar.gz",
    97  			},
    98  			golden: "testdata/debian-buster.json.golden",
    99  		},
   100  		{
   101  			name: "debian buster/10 with --ignore-unfixed option",
   102  			args: csArgs{
   103  				IgnoreUnfixed: true,
   104  				Input:         "testdata/fixtures/images/debian-buster.tar.gz",
   105  			},
   106  			golden: "testdata/debian-buster-ignore-unfixed.json.golden",
   107  		},
   108  		{
   109  			name: "debian stretch/9",
   110  			args: csArgs{
   111  				Input: "testdata/fixtures/images/debian-stretch.tar.gz",
   112  			},
   113  			golden: "testdata/debian-stretch.json.golden",
   114  		},
   115  		{
   116  			name: "ubuntu 18.04",
   117  			args: csArgs{
   118  				Input: "testdata/fixtures/images/ubuntu-1804.tar.gz",
   119  			},
   120  			golden: "testdata/ubuntu-1804.json.golden",
   121  		},
   122  		{
   123  			name: "centos 7",
   124  			args: csArgs{
   125  				Input: "testdata/fixtures/images/centos-7.tar.gz",
   126  			},
   127  			golden: "testdata/centos-7.json.golden",
   128  		},
   129  		{
   130  			name: "centos 7 with --ignore-unfixed option",
   131  			args: csArgs{
   132  				IgnoreUnfixed: true,
   133  				Input:         "testdata/fixtures/images/centos-7.tar.gz",
   134  			},
   135  			golden: "testdata/centos-7-ignore-unfixed.json.golden",
   136  		},
   137  		{
   138  			name: "centos 7 with medium severity",
   139  			args: csArgs{
   140  				IgnoreUnfixed: true,
   141  				Severity:      []string{"MEDIUM"},
   142  				Input:         "testdata/fixtures/images/centos-7.tar.gz",
   143  			},
   144  			golden: "testdata/centos-7-medium.json.golden",
   145  		},
   146  		{
   147  			name: "centos 6",
   148  			args: csArgs{
   149  				Input: "testdata/fixtures/images/centos-6.tar.gz",
   150  			},
   151  			golden: "testdata/centos-6.json.golden",
   152  		},
   153  		{
   154  			name: "ubi 7",
   155  			args: csArgs{
   156  				Input: "testdata/fixtures/images/ubi-7.tar.gz",
   157  			},
   158  			golden: "testdata/ubi-7.json.golden",
   159  		},
   160  		{
   161  			name: "almalinux 8",
   162  			args: csArgs{
   163  				Input: "testdata/fixtures/images/almalinux-8.tar.gz",
   164  			},
   165  			golden: "testdata/almalinux-8.json.golden",
   166  		},
   167  		{
   168  			name: "rocky linux 8",
   169  			args: csArgs{
   170  				Input: "testdata/fixtures/images/rockylinux-8.tar.gz",
   171  			},
   172  			golden: "testdata/rockylinux-8.json.golden",
   173  		},
   174  		{
   175  			name: "distroless base",
   176  			args: csArgs{
   177  				Input: "testdata/fixtures/images/distroless-base.tar.gz",
   178  			},
   179  			golden: "testdata/distroless-base.json.golden",
   180  		},
   181  		{
   182  			name: "distroless python27",
   183  			args: csArgs{
   184  				Input: "testdata/fixtures/images/distroless-python27.tar.gz",
   185  			},
   186  			golden: "testdata/distroless-python27.json.golden",
   187  		},
   188  		{
   189  			name: "amazon 1",
   190  			args: csArgs{
   191  				Input: "testdata/fixtures/images/amazon-1.tar.gz",
   192  			},
   193  			golden: "testdata/amazon-1.json.golden",
   194  		},
   195  		{
   196  			name: "amazon 2",
   197  			args: csArgs{
   198  				Input: "testdata/fixtures/images/amazon-2.tar.gz",
   199  			},
   200  			golden: "testdata/amazon-2.json.golden",
   201  		},
   202  		{
   203  			name: "oracle 8",
   204  			args: csArgs{
   205  				Input: "testdata/fixtures/images/oraclelinux-8.tar.gz",
   206  			},
   207  			golden: "testdata/oraclelinux-8.json.golden",
   208  		},
   209  		{
   210  			name: "opensuse leap 15.1",
   211  			args: csArgs{
   212  				Input: "testdata/fixtures/images/opensuse-leap-151.tar.gz",
   213  			},
   214  			golden: "testdata/opensuse-leap-151.json.golden",
   215  		},
   216  		{
   217  			name: "photon 3.0",
   218  			args: csArgs{
   219  				Input: "testdata/fixtures/images/photon-30.tar.gz",
   220  			},
   221  			golden: "testdata/photon-30.json.golden",
   222  		},
   223  		{
   224  			name: "CBL-Mariner 1.0",
   225  			args: csArgs{
   226  				Input: "testdata/fixtures/images/mariner-1.0.tar.gz",
   227  			},
   228  			golden: "testdata/mariner-1.0.json.golden",
   229  		},
   230  		{
   231  			name: "busybox with Cargo.lock",
   232  			args: csArgs{
   233  				Input: "testdata/fixtures/images/busybox-with-lockfile.tar.gz",
   234  			},
   235  			golden: "testdata/busybox-with-lockfile.json.golden",
   236  		},
   237  		{
   238  			name: "scan pox.xml with repo command in client/server mode",
   239  			args: csArgs{
   240  				Command:          "repo",
   241  				RemoteAddrOption: "--server",
   242  				Target:           "testdata/fixtures/repo/pom/",
   243  			},
   244  			golden: "testdata/pom.json.golden",
   245  		},
   246  		{
   247  			name: "scan sample.pem with repo command in client/server mode",
   248  			args: csArgs{
   249  				Command:          "repo",
   250  				RemoteAddrOption: "--server",
   251  				secretConfig:     "testdata/fixtures/repo/secrets/trivy-secret.yaml",
   252  				Target:           "testdata/fixtures/repo/secrets/",
   253  			},
   254  			golden: "testdata/secrets.json.golden",
   255  		},
   256  		{
   257  			name: "scan remote repository with repo command in client/server mode",
   258  			args: csArgs{
   259  				Command:          "repo",
   260  				RemoteAddrOption: "--server",
   261  				Target:           "https://github.com/knqyf263/trivy-ci-test",
   262  			},
   263  			golden: "testdata/test-repo.json.golden",
   264  		},
   265  	}
   266  
   267  	addr, cacheDir := setup(t, setupOptions{})
   268  
   269  	for _, c := range tests {
   270  		t.Run(c.name, func(t *testing.T) {
   271  			osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden)
   272  
   273  			if c.args.secretConfig != "" {
   274  				osArgs = append(osArgs, "--secret-config", c.args.secretConfig)
   275  			}
   276  
   277  			//
   278  			err := execute(osArgs)
   279  			require.NoError(t, err)
   280  
   281  			compareReports(t, c.golden, outputFile, nil)
   282  		})
   283  	}
   284  }
   285  
   286  func TestClientServerWithFormat(t *testing.T) {
   287  	tests := []struct {
   288  		name   string
   289  		args   csArgs
   290  		golden string
   291  	}{
   292  		{
   293  			name: "alpine 3.10 with gitlab template",
   294  			args: csArgs{
   295  				Format:       "template",
   296  				TemplatePath: "@../contrib/gitlab.tpl",
   297  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   298  			},
   299  			golden: "testdata/alpine-310.gitlab.golden",
   300  		},
   301  		{
   302  			name: "alpine 3.10 with gitlab-codequality template",
   303  			args: csArgs{
   304  				Format:       "template",
   305  				TemplatePath: "@../contrib/gitlab-codequality.tpl",
   306  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   307  			},
   308  			golden: "testdata/alpine-310.gitlab-codequality.golden",
   309  		},
   310  		{
   311  			name: "alpine 3.10 with sarif format",
   312  			args: csArgs{
   313  				Format: "sarif",
   314  				Input:  "testdata/fixtures/images/alpine-310.tar.gz",
   315  			},
   316  			golden: "testdata/alpine-310.sarif.golden",
   317  		},
   318  		{
   319  			name: "alpine 3.10 with ASFF template",
   320  			args: csArgs{
   321  				Format:       "template",
   322  				TemplatePath: "@../contrib/asff.tpl",
   323  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   324  			},
   325  			golden: "testdata/alpine-310.asff.golden",
   326  		},
   327  		{
   328  			name: "scan secrets with ASFF template",
   329  			args: csArgs{
   330  				Command:          "repo",
   331  				RemoteAddrOption: "--server",
   332  				Format:           "template",
   333  				TemplatePath:     "@../contrib/asff.tpl",
   334  				Target:           "testdata/fixtures/repo/secrets/",
   335  			},
   336  			golden: "testdata/secrets.asff.golden",
   337  		},
   338  		{
   339  			name: "alpine 3.10 with html template",
   340  			args: csArgs{
   341  				Format:       "template",
   342  				TemplatePath: "@../contrib/html.tpl",
   343  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   344  			},
   345  			golden: "testdata/alpine-310.html.golden",
   346  		},
   347  		{
   348  			name: "alpine 3.10 with github dependency snapshots format",
   349  			args: csArgs{
   350  				Format: "github",
   351  				Input:  "testdata/fixtures/images/alpine-310.tar.gz",
   352  			},
   353  			golden: "testdata/alpine-310.gsbom.golden",
   354  		},
   355  	}
   356  
   357  	fakeTime := time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)
   358  	clock.SetFakeTime(t, fakeTime)
   359  
   360  	report.CustomTemplateFuncMap = map[string]interface{}{
   361  		"now": func() time.Time {
   362  			return fakeTime
   363  		},
   364  		"date": func(format string, t time.Time) string {
   365  			return t.Format(format)
   366  		},
   367  	}
   368  
   369  	// For GitHub Dependency Snapshots
   370  	t.Setenv("GITHUB_REF", "/ref/feature-1")
   371  	t.Setenv("GITHUB_SHA", "39da54a1ff04120a31df8cbc94ce9ede251d21a3")
   372  	t.Setenv("GITHUB_JOB", "integration")
   373  	t.Setenv("GITHUB_RUN_ID", "1910764383")
   374  	t.Setenv("GITHUB_WORKFLOW", "workflow-name")
   375  
   376  	t.Cleanup(func() {
   377  		report.CustomTemplateFuncMap = map[string]interface{}{}
   378  	})
   379  
   380  	addr, cacheDir := setup(t, setupOptions{})
   381  
   382  	for _, tt := range tests {
   383  		t.Run(tt.name, func(t *testing.T) {
   384  			t.Setenv("AWS_REGION", "test-region")
   385  			t.Setenv("AWS_ACCOUNT_ID", "123456789012")
   386  			osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden)
   387  
   388  			// Run Trivy client
   389  			err := execute(osArgs)
   390  			require.NoError(t, err)
   391  
   392  			want, err := os.ReadFile(tt.golden)
   393  			require.NoError(t, err)
   394  
   395  			got, err := os.ReadFile(outputFile)
   396  			require.NoError(t, err)
   397  
   398  			assert.EqualValues(t, string(want), string(got))
   399  		})
   400  	}
   401  }
   402  
   403  func TestClientServerWithCycloneDX(t *testing.T) {
   404  	tests := []struct {
   405  		name   string
   406  		args   csArgs
   407  		golden string
   408  	}{
   409  		{
   410  			name: "fluentd with RubyGems with CycloneDX format",
   411  			args: csArgs{
   412  				Format: "cyclonedx",
   413  				Input:  "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz",
   414  			},
   415  			golden: "testdata/fluentd-multiple-lockfiles.cdx.json.golden",
   416  		},
   417  	}
   418  
   419  	addr, cacheDir := setup(t, setupOptions{})
   420  	for _, tt := range tests {
   421  		t.Run(tt.name, func(t *testing.T) {
   422  			clock.SetFakeTime(t, time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
   423  			uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d")
   424  
   425  			osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden)
   426  
   427  			// Run Trivy client
   428  			err := execute(osArgs)
   429  			require.NoError(t, err)
   430  
   431  			compareCycloneDX(t, tt.golden, outputFile)
   432  		})
   433  	}
   434  }
   435  
   436  func TestClientServerWithToken(t *testing.T) {
   437  	cases := []struct {
   438  		name    string
   439  		args    csArgs
   440  		golden  string
   441  		wantErr string
   442  	}{
   443  		{
   444  			name: "alpine 3.9 with token",
   445  			args: csArgs{
   446  				Input:             "testdata/fixtures/images/alpine-39.tar.gz",
   447  				ClientToken:       "token",
   448  				ClientTokenHeader: "Trivy-Token",
   449  			},
   450  			golden: "testdata/alpine-39.json.golden",
   451  		},
   452  		{
   453  			name: "invalid token",
   454  			args: csArgs{
   455  				Input:             "testdata/fixtures/images/distroless-base.tar.gz",
   456  				ClientToken:       "invalidtoken",
   457  				ClientTokenHeader: "Trivy-Token",
   458  			},
   459  			wantErr: "twirp error unauthenticated: invalid token",
   460  		},
   461  		{
   462  			name: "invalid token header",
   463  			args: csArgs{
   464  				Input:             "testdata/fixtures/images/distroless-base.tar.gz",
   465  				ClientToken:       "token",
   466  				ClientTokenHeader: "Unknown-Header",
   467  			},
   468  			wantErr: "twirp error unauthenticated: invalid token",
   469  		},
   470  	}
   471  
   472  	serverToken := "token"
   473  	serverTokenHeader := "Trivy-Token"
   474  	addr, cacheDir := setup(t, setupOptions{
   475  		token:       serverToken,
   476  		tokenHeader: serverTokenHeader,
   477  	})
   478  
   479  	for _, c := range cases {
   480  		t.Run(c.name, func(t *testing.T) {
   481  			osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden)
   482  
   483  			// Run Trivy client
   484  			err := execute(osArgs)
   485  			if c.wantErr != "" {
   486  				require.Error(t, err, c.name)
   487  				assert.Contains(t, err.Error(), c.wantErr, c.name)
   488  				return
   489  			}
   490  
   491  			require.NoError(t, err, c.name)
   492  			compareReports(t, c.golden, outputFile, nil)
   493  		})
   494  	}
   495  }
   496  
   497  func TestClientServerWithRedis(t *testing.T) {
   498  	// Set up a Redis container
   499  	ctx := context.Background()
   500  	// This test includes 2 checks
   501  	// redisC container will terminate after first check
   502  	redisC, addr := setupRedis(t, ctx)
   503  
   504  	// Set up Trivy server
   505  	addr, cacheDir := setup(t, setupOptions{cacheBackend: addr})
   506  	t.Cleanup(func() { os.RemoveAll(cacheDir) })
   507  
   508  	// Test parameters
   509  	testArgs := csArgs{
   510  		Input: "testdata/fixtures/images/alpine-39.tar.gz",
   511  	}
   512  	golden := "testdata/alpine-39.json.golden"
   513  
   514  	t.Run("alpine 3.9", func(t *testing.T) {
   515  		osArgs, outputFile := setupClient(t, testArgs, addr, cacheDir, golden)
   516  
   517  		// Run Trivy client
   518  		err := execute(osArgs)
   519  		require.NoError(t, err)
   520  
   521  		compareReports(t, golden, outputFile, nil)
   522  	})
   523  
   524  	// Terminate the Redis container
   525  	require.NoError(t, redisC.Terminate(ctx))
   526  
   527  	t.Run("sad path", func(t *testing.T) {
   528  		osArgs, _ := setupClient(t, testArgs, addr, cacheDir, golden)
   529  
   530  		// Run Trivy client
   531  		err := execute(osArgs)
   532  		require.Error(t, err)
   533  		assert.Contains(t, err.Error(), "connect: connection refused")
   534  	})
   535  }
   536  
   537  type setupOptions struct {
   538  	token        string
   539  	tokenHeader  string
   540  	cacheBackend string
   541  }
   542  
   543  func setup(t *testing.T, options setupOptions) (string, string) {
   544  	t.Helper()
   545  
   546  	// Set up testing DB
   547  	cacheDir := initDB(t)
   548  
   549  	// Set a temp dir so that modules will not be loaded
   550  	t.Setenv("XDG_DATA_HOME", cacheDir)
   551  
   552  	port, err := getFreePort()
   553  	assert.NoError(t, err)
   554  	addr := fmt.Sprintf("localhost:%d", port)
   555  
   556  	go func() {
   557  		osArgs := setupServer(addr, options.token, options.tokenHeader, cacheDir, options.cacheBackend)
   558  
   559  		// Run Trivy server
   560  		require.NoError(t, execute(osArgs))
   561  	}()
   562  
   563  	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
   564  	err = waitPort(ctx, addr)
   565  	assert.NoError(t, err)
   566  
   567  	return addr, cacheDir
   568  }
   569  
   570  func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []string {
   571  	osArgs := []string{
   572  		"--cache-dir",
   573  		cacheDir,
   574  		"server",
   575  		"--skip-update",
   576  		"--listen",
   577  		addr,
   578  	}
   579  	if token != "" {
   580  		osArgs = append(osArgs, []string{
   581  			"--token",
   582  			token,
   583  			"--token-header",
   584  			tokenHeader,
   585  		}...)
   586  	}
   587  	if cacheBackend != "" {
   588  		osArgs = append(osArgs, "--cache-backend", cacheBackend)
   589  	}
   590  	return osArgs
   591  }
   592  
   593  func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden string) ([]string, string) {
   594  	if c.Command == "" {
   595  		c.Command = "image"
   596  	}
   597  	if c.RemoteAddrOption == "" {
   598  		c.RemoteAddrOption = "--server"
   599  	}
   600  	t.Helper()
   601  	osArgs := []string{
   602  		"--cache-dir",
   603  		cacheDir,
   604  		c.Command,
   605  		c.RemoteAddrOption,
   606  		"http://" + addr,
   607  	}
   608  
   609  	if c.Format != "" {
   610  		osArgs = append(osArgs, "--format", c.Format)
   611  		if c.TemplatePath != "" {
   612  			osArgs = append(osArgs, "--template", c.TemplatePath)
   613  		}
   614  	} else {
   615  		osArgs = append(osArgs, "--format", "json")
   616  	}
   617  
   618  	if c.IgnoreUnfixed {
   619  		osArgs = append(osArgs, "--ignore-unfixed")
   620  	}
   621  	if len(c.Severity) != 0 {
   622  		osArgs = append(osArgs,
   623  			"--severity", strings.Join(c.Severity, ","),
   624  		)
   625  	}
   626  
   627  	if len(c.IgnoreIDs) != 0 {
   628  		trivyIgnore := filepath.Join(t.TempDir(), ".trivyignore")
   629  		err := os.WriteFile(trivyIgnore, []byte(strings.Join(c.IgnoreIDs, "\n")), 0444)
   630  		require.NoError(t, err, "failed to write .trivyignore")
   631  		osArgs = append(osArgs, "--ignorefile", trivyIgnore)
   632  	}
   633  	if c.ClientToken != "" {
   634  		osArgs = append(osArgs, "--token", c.ClientToken, "--token-header", c.ClientTokenHeader)
   635  	}
   636  	if c.Input != "" {
   637  		osArgs = append(osArgs, "--input", c.Input)
   638  	}
   639  
   640  	// Set up the output file
   641  	outputFile := filepath.Join(t.TempDir(), "output.json")
   642  	if *update {
   643  		outputFile = golden
   644  	}
   645  
   646  	osArgs = append(osArgs, "--output", outputFile)
   647  
   648  	if c.Target != "" {
   649  		osArgs = append(osArgs, c.Target)
   650  	}
   651  
   652  	return osArgs, outputFile
   653  }
   654  
   655  func setupRedis(t *testing.T, ctx context.Context) (testcontainers.Container, string) {
   656  	t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
   657  	t.Helper()
   658  	imageName := "redis:5.0"
   659  	port := "6379/tcp"
   660  	req := testcontainers.ContainerRequest{
   661  		Name:         "redis",
   662  		Image:        imageName,
   663  		ExposedPorts: []string{port},
   664  		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
   665  			hostConfig.AutoRemove = true
   666  		},
   667  	}
   668  
   669  	redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
   670  		ContainerRequest: req,
   671  		Started:          true,
   672  	})
   673  	require.NoError(t, err)
   674  
   675  	ip, err := redis.Host(ctx)
   676  	require.NoError(t, err)
   677  
   678  	p, err := redis.MappedPort(ctx, nat.Port(port))
   679  	require.NoError(t, err)
   680  
   681  	addr := fmt.Sprintf("redis://%s:%s", ip, p.Port())
   682  	return redis, addr
   683  }