Skip to content

Commit

Permalink
Implement basic support for Neo4j container
Browse files Browse the repository at this point in the history
Fixes #921
  • Loading branch information
fbiville committed Mar 10, 2023
1 parent 5a555da commit f1bdefa
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 19 deletions.
78 changes: 78 additions & 0 deletions modules/neo4j/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package neo4j

import (
"fmt"
"strings"
)

type Option func(*config)

type config struct {
imageCoordinates string
adminPassword string
labsPlugins []string
}

type LabsPlugin string

const (
Apoc LabsPlugin = "apoc"
ApocCore LabsPlugin = "apoc-core"
Bloom LabsPlugin = "bloom"
GraphDataScience LabsPlugin = "graph-data-science"
NeoSemantics LabsPlugin = "n10s"
Streams LabsPlugin = "streams"
)

// WithoutAuthentication disables authentication.
func WithoutAuthentication() Option {
return WithAdminPassword("")
}

// WithAdminPassword sets the admin password for the default account
// An empty string disables authentication.
// The default password is "password".
func WithAdminPassword(adminPassword string) Option {
return func(c *config) {
c.adminPassword = adminPassword
}
}

// WithImageCoordinates sets the image coordinates of the Neo4j container.
func WithImageCoordinates(imageCoordinates string) Option {
return func(c *config) {
c.imageCoordinates = imageCoordinates
}
}

// WithLabsPlugin registers one or more Neo4jLabsPlugin for download and server startup.
// There might be plugins not supported by your selected version of Neo4j.
func WithLabsPlugin(plugins ...LabsPlugin) Option {
return func(c *config) {
rawPluginValues := make([]string, len(plugins))
for i := 0; i < len(plugins); i++ {
rawPluginValues[i] = string(plugins[i])
}
c.labsPlugins = rawPluginValues
}
}

func (c config) exportEnv() map[string]string {
env := make(map[string]string)
env["NEO4J_AUTH"] = c.authEnvVar()
if len(c.labsPlugins) > 0 {
env["NEO4JLABS_PLUGINS"] = c.labsPluginsEnvVar()
}
return env
}

func (c config) authEnvVar() string {
if c.adminPassword == "" {
return "none"
}
return fmt.Sprintf("neo4j/%s", c.adminPassword)
}

func (c config) labsPluginsEnvVar() string {
return fmt.Sprintf(`["%s"]`, strings.Join(c.labsPlugins, `","`))
}
3 changes: 2 additions & 1 deletion modules/neo4j/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module github.com/testcontainers/testcontainers-go/modules/neo4j
go 1.19

require (
github.com/docker/go-connections v0.4.0
github.com/neo4j/neo4j-go-driver/v5 v5.6.0
github.com/testcontainers/testcontainers-go v0.19.0
gotest.tools/gotestsum v1.9.0
)
Expand All @@ -16,7 +18,6 @@ require (
github.com/dnephin/pflag v1.0.7 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions modules/neo4j/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f/go.mod h1:15ce4BGCFxt7I5
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/neo4j/neo4j-go-driver/v5 v5.6.0 h1:+LxOHCyDWGjtD8qHhb20GUpvwCFcJm1wqSEyo2MiehE=
github.com/neo4j/neo4j-go-driver/v5 v5.6.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
Expand Down
49 changes: 38 additions & 11 deletions modules/neo4j/neo4j.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,60 @@ package neo4j

import (
"context"
"fmt"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go/wait"
"net/http"

"github.com/testcontainers/testcontainers-go"
)

const defaultImageName = "neo4j"
const defaultTag = "4.4"
const defaultBoltPort = "7687"
const defaultHttpPort = "7474"
const defaultHttpsPort = "7473"

// Neo4jContainer represents the Neo4j container type used in the module
type Neo4jContainer struct {
testcontainers.Container
}

// TODO:
// - ✅ wait strategies
// - auth via password or no auth
// - EE
// - Neo4jConfig
// - labsPlugins
// - custom plugins
func (c Neo4jContainer) BoltUrl(ctx context.Context) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", err
}
containerPort, err := nat.NewPort("tcp", defaultBoltPort)
if err != nil {
return "", err
}
mappedPort, err := c.MappedPort(ctx, containerPort)
if err != nil {
return "", err
}
return fmt.Sprintf("neo4j://%s:%d", host, mappedPort.Int()), nil
}

// StartContainer creates an instance of the Neo4j container type
func StartContainer(ctx context.Context) (*Neo4jContainer, error) {
func StartContainer(ctx context.Context, options ...Option) (*Neo4jContainer, error) {
settings := config{
imageCoordinates: fmt.Sprintf("%s:%s", defaultImageName, defaultTag),
adminPassword: "password",
}
for _, option := range options {
option(&settings)
}

httpPort, _ := nat.NewPort("tcp", defaultHttpPort)
req := testcontainers.ContainerRequest{
Image: "docker.io/neo4j:5.5",
request := testcontainers.ContainerRequest{
Image: settings.imageCoordinates,
Env: settings.exportEnv(),
ExposedPorts: []string{
fmt.Sprintf("%s/tcp", defaultBoltPort),
fmt.Sprintf("%s/tcp", defaultHttpPort),
fmt.Sprintf("%s/tcp", defaultHttpsPort),
},
WaitingFor: &wait.MultiStrategy{
Strategies: []wait.Strategy{
wait.NewLogStrategy("Bolt enabled on"),
Expand All @@ -40,7 +67,7 @@ func StartContainer(ctx context.Context) (*Neo4jContainer, error) {
},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
ContainerRequest: request,
Started: true,
})
if err != nil {
Expand Down
65 changes: 58 additions & 7 deletions modules/neo4j/neo4j_test.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,75 @@
package neo4j
package neo4j_test

import (
"context"
neo "github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/testcontainers/testcontainers-go/modules/neo4j"
"testing"
)

func TestNeo4j(t *testing.T) {
const testPassword = "letmein!"

func TestNeo4j(outer *testing.T) {
ctx := context.Background()

container, err := setupNeo4j(ctx)
if err != nil {
t.Fatal(err)
outer.Fatal(err)
}

// Clean up the container after the test is complete
t.Cleanup(func() {
outer.Cleanup(func() {
if err := container.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
outer.Fatalf("failed to terminate container: %s", err)
}
})

outer.Run("connects via Bolt", func(t *testing.T) {
driver := createDriver(t, ctx, container)

err = driver.VerifyConnectivity(ctx)

if err != nil {
t.Fatalf("should have successfully connected to server but did not: %s", err)
}
})

outer.Run("exercises APOC plugin", func(t *testing.T) {
driver := createDriver(t, ctx, container)

result, err := neo.ExecuteQuery(ctx, driver,
"RETURN apoc.number.arabicToRoman(1986) AS output", nil,
neo.EagerResultTransformer)

if err != nil {
t.Fatalf("expected APOC query to successfully run but did not: %s", err)
}
if value, _ := result.Records[0].Get("output"); value != "MCMLXXXVI" {
t.Fatalf("did not get expected roman number: %s", value)
}
})

// perform assertions
}

func setupNeo4j(ctx context.Context) (*neo4j.Neo4jContainer, error) {
return neo4j.StartContainer(ctx,
neo4j.WithAdminPassword(testPassword),
neo4j.WithLabsPlugin(neo4j.Apoc),
)
}

func createDriver(t *testing.T, ctx context.Context, container *neo4j.Neo4jContainer) neo.DriverWithContext {
boltUrl, err := container.BoltUrl(ctx)
if err != nil {
t.Fatal(err)
}
driver, err := neo.NewDriverWithContext(boltUrl, neo.BasicAuth("neo4j", testPassword, ""))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := driver.Close(ctx); err != nil {
t.Fatalf("failed to close neo: %s", err)
}
})
return driver
}

0 comments on commit f1bdefa

Please sign in to comment.