github.com/etecs-ru/gnomock@v0.13.2/preset/mongo/preset.go (about)

     1  // Package mongo includes mongo implementation of Gnomock Preset interface.
     2  // This Preset can be passed to gnomock.StartPreset function to create a
     3  // configured mongo container to use in tests
     4  package mongo
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"path"
    14  	"strings"
    15  
    16  	"github.com/etecs-ru/gnomock"
    17  	"github.com/etecs-ru/gnomock/internal/registry"
    18  	"go.mongodb.org/mongo-driver/bson"
    19  	"go.mongodb.org/mongo-driver/bson/bsonrw"
    20  	mongodb "go.mongodb.org/mongo-driver/mongo"
    21  	mongooptions "go.mongodb.org/mongo-driver/mongo/options"
    22  )
    23  
    24  const defaultVersion = "4.4"
    25  
    26  func init() {
    27  	registry.Register("mongo", func() gnomock.Preset { return &P{} })
    28  }
    29  
    30  // Preset creates a new Gmomock MongoDB preset. This preset includes a MongoDB
    31  // specific healthcheck function, default MongoDB image and port, and allows to
    32  // optionally set up initial state.
    33  //
    34  // By default, this preset uses MongoDB 4.4.
    35  func Preset(opts ...Option) gnomock.Preset {
    36  	p := &P{}
    37  
    38  	for _, opt := range opts {
    39  		opt(p)
    40  	}
    41  
    42  	return p
    43  }
    44  
    45  // P is a Gnomock Preset implementation of MongoDB
    46  type P struct {
    47  	DataPath string `json:"data_path"`
    48  	User     string `json:"user"`
    49  	Password string `json:"password"`
    50  	Version  string `json:"version"`
    51  }
    52  
    53  // Image returns an image that should be pulled to create this container
    54  func (p *P) Image() string {
    55  	return fmt.Sprintf("docker.io/library/mongo:%s", p.Version)
    56  }
    57  
    58  // Ports returns ports that should be used to access this container
    59  func (p *P) Ports() gnomock.NamedPorts {
    60  	return gnomock.DefaultTCP(27017)
    61  }
    62  
    63  // Options returns a list of options to configure this container
    64  func (p *P) Options() []gnomock.Option {
    65  	p.setDefaults()
    66  
    67  	opts := []gnomock.Option{
    68  		gnomock.WithHealthCheck(healthcheck),
    69  	}
    70  
    71  	if p.DataPath != "" {
    72  		opts = append(opts, gnomock.WithInit(p.initf))
    73  	}
    74  
    75  	if p.User != "" && p.Password != "" {
    76  		opts = append(
    77  			opts,
    78  			gnomock.WithEnv("MONGO_INITDB_ROOT_USERNAME="+p.User),
    79  			gnomock.WithEnv("MONGO_INITDB_ROOT_PASSWORD="+p.Password),
    80  		)
    81  	}
    82  
    83  	return opts
    84  }
    85  
    86  func (p *P) setDefaults() {
    87  	if p.Version == "" {
    88  		p.Version = defaultVersion
    89  	}
    90  }
    91  
    92  func (p *P) initf(ctx context.Context, c *gnomock.Container) error {
    93  	addr := c.Address(gnomock.DefaultPort)
    94  	uri := "mongodb://" + addr
    95  
    96  	if p.useCustomUser() {
    97  		uri = fmt.Sprintf("mongodb://%s:%s@%s", p.User, p.Password, addr)
    98  	}
    99  
   100  	clientOptions := mongooptions.Client().ApplyURI(uri)
   101  
   102  	client, err := mongodb.NewClient(clientOptions)
   103  	if err != nil {
   104  		return fmt.Errorf("can't create mongo client: %w", err)
   105  	}
   106  
   107  	err = client.Connect(context.Background())
   108  	if err != nil {
   109  		return fmt.Errorf("can't connect: %w", err)
   110  	}
   111  
   112  	topLevelDirs, err := ioutil.ReadDir(p.DataPath)
   113  	if err != nil {
   114  		return fmt.Errorf("can't read test data path: %w", err)
   115  	}
   116  
   117  	for _, topLevelDir := range topLevelDirs {
   118  		if !topLevelDir.IsDir() {
   119  			continue
   120  		}
   121  
   122  		err = p.setupDB(client, topLevelDir.Name())
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func (p *P) useCustomUser() bool {
   132  	return p.User != "" && p.Password != ""
   133  }
   134  
   135  func (p *P) setupDB(client *mongodb.Client, dirName string) error {
   136  	dataFiles, err := ioutil.ReadDir(path.Join(p.DataPath, dirName))
   137  	if err != nil {
   138  		return fmt.Errorf("can't read test data sub path '%s', %w", dirName, err)
   139  	}
   140  
   141  	for _, dataFile := range dataFiles {
   142  		if dataFile.IsDir() {
   143  			continue
   144  		}
   145  
   146  		fName := dataFile.Name()
   147  
   148  		err = p.setupCollection(client, dirName, fName)
   149  		if err != nil {
   150  			return fmt.Errorf("can't setup collection from file '%s': %w", fName, err)
   151  		}
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  func (p *P) setupCollection(client *mongodb.Client, dirName, dataFileName string) error {
   158  	collectionName := strings.TrimSuffix(dataFileName, path.Ext(dataFileName))
   159  
   160  	file, err := os.Open(path.Join(p.DataPath, dirName, dataFileName)) //nolint:gosec
   161  	if err != nil {
   162  		return fmt.Errorf("can't open file '%s': %w", dataFileName, err)
   163  	}
   164  
   165  	vr, err := bsonrw.NewExtJSONValueReader(file, false)
   166  	if err != nil {
   167  		return fmt.Errorf("can't read file '%s': %w", dataFileName, err)
   168  	}
   169  
   170  	dec, err := bson.NewDecoder(vr)
   171  	if err != nil {
   172  		return fmt.Errorf("can't create BSON decoder for '%s': %w", dataFileName, err)
   173  	}
   174  
   175  	ctx := context.Background()
   176  
   177  	for {
   178  		var val interface{}
   179  
   180  		err = dec.Decode(&val)
   181  		if errors.Is(err, io.EOF) {
   182  			return nil
   183  		}
   184  
   185  		if err != nil {
   186  			return fmt.Errorf("can't decode file '%s': %w", dataFileName, err)
   187  		}
   188  
   189  		_, err = client.Database(dirName).Collection(collectionName).InsertOne(ctx, val)
   190  		if err != nil {
   191  			return fmt.Errorf("can't insert value from '%s' (%v): %w", dataFileName, val, err)
   192  		}
   193  	}
   194  }
   195  
   196  func healthcheck(ctx context.Context, c *gnomock.Container) error {
   197  	addr := c.Address(gnomock.DefaultPort)
   198  	clientOptions := mongooptions.Client().ApplyURI("mongodb://" + addr)
   199  
   200  	client, err := mongodb.NewClient(clientOptions)
   201  	if err != nil {
   202  		return fmt.Errorf("can't create mongo client: %w", err)
   203  	}
   204  
   205  	err = client.Connect(context.Background())
   206  	if err != nil {
   207  		return fmt.Errorf("can't connect: %w", err)
   208  	}
   209  
   210  	return client.Ping(context.Background(), nil)
   211  }