github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/elasticsearch_integration_test.go (about)

     1  package writer
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	"net/http"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/Jeffail/benthos/v3/lib/log"
    16  	"github.com/Jeffail/benthos/v3/lib/message"
    17  	"github.com/Jeffail/benthos/v3/lib/metrics"
    18  	"github.com/olivere/elastic/v7"
    19  	"github.com/ory/dockertest/v3"
    20  )
    21  
    22  func TestElasticIntegration(t *testing.T) {
    23  	if m := flag.Lookup("test.run").Value.String(); m == "" || regexp.MustCompile(strings.Split(m, "/")[0]).FindString(t.Name()) == "" {
    24  		t.Skip("Skipping as execution was not requested explicitly using go test -run ^TestIntegration$")
    25  	}
    26  
    27  	if testing.Short() {
    28  		t.Skip("Skipping integration test in short mode")
    29  	}
    30  
    31  	pool, err := dockertest.NewPool("")
    32  	if err != nil {
    33  		t.Skipf("Could not connect to docker: %s", err)
    34  	}
    35  	pool.MaxWait = time.Second * 30
    36  
    37  	resource, err := pool.Run("elasticsearch", "7.17.0", []string{
    38  		"discovery.type=single-node",
    39  	})
    40  	if err != nil {
    41  		t.Fatalf("Could not start resource: %s", err)
    42  	}
    43  
    44  	urls := []string{fmt.Sprintf("http://127.0.0.1:%v", resource.GetPort("9200/tcp"))}
    45  
    46  	var client *elastic.Client
    47  
    48  	if err = pool.Retry(func() error {
    49  		opts := []elastic.ClientOptionFunc{
    50  			elastic.SetURL(urls...),
    51  			elastic.SetHttpClient(&http.Client{
    52  				Timeout: time.Second,
    53  			}),
    54  			elastic.SetSniff(false),
    55  		}
    56  
    57  		var cerr error
    58  		client, cerr = elastic.NewClient(opts...)
    59  
    60  		if cerr == nil {
    61  			index := `{
    62  	"settings":{
    63  		"number_of_shards": 1,
    64  		"number_of_replicas": 0
    65  	},
    66  	"mappings":{
    67  		"properties": {
    68  			"user":{
    69  				"type":"keyword"
    70  			},
    71  			"message":{
    72  				"type":"text",
    73  				"store": true,
    74  				"fielddata": true
    75  			}
    76  		}
    77  	}
    78  }`
    79  			_, cerr = client.
    80  				CreateIndex("test_conn_index").
    81  				Timeout("20s").
    82  				Body(index).
    83  				Do(context.Background())
    84  			if cerr == nil {
    85  				_, cerr = client.
    86  					CreateIndex("test_conn_index_2").
    87  					Timeout("20s").
    88  					Body(index).
    89  					Do(context.Background())
    90  			}
    91  
    92  		}
    93  		return cerr
    94  	}); err != nil {
    95  		t.Fatalf("Could not connect to docker resource: %s", err)
    96  	}
    97  
    98  	defer func() {
    99  		if err = pool.Purge(resource); err != nil {
   100  			t.Logf("Failed to clean up docker resource: %v", err)
   101  		}
   102  	}()
   103  
   104  	t.Run("TestElasticNoIndex", func(te *testing.T) {
   105  		testElasticNoIndex(urls, client, te)
   106  	})
   107  
   108  	t.Run("TestElasticParallelWrites", func(te *testing.T) {
   109  		testElasticParallelWrites(urls, client, te)
   110  	})
   111  
   112  	t.Run("TestElasticErrorHandling", func(te *testing.T) {
   113  		testElasticErrorHandling(urls, client, te)
   114  	})
   115  
   116  	t.Run("TestElasticConnect", func(te *testing.T) {
   117  		testElasticConnect(urls, client, te)
   118  	})
   119  
   120  	t.Run("TestElasticIndexInterpolation", func(te *testing.T) {
   121  		testElasticIndexInterpolation(urls, client, te)
   122  	})
   123  
   124  	t.Run("TestElasticBatch", func(te *testing.T) {
   125  		testElasticBatch(urls, client, te)
   126  	})
   127  
   128  	t.Run("TestElasticBatchDelete", func(te *testing.T) {
   129  		testElasticBatchDelete(urls, client, te)
   130  	})
   131  
   132  	t.Run("TestElasticBatchIDCollision", func(te *testing.T) {
   133  		testElasticBatchIDCollision(urls, client, te)
   134  	})
   135  }
   136  
   137  func testElasticNoIndex(urls []string, client *elastic.Client, t *testing.T) {
   138  	conf := NewElasticsearchConfig()
   139  	conf.Index = "does_not_exist"
   140  	conf.ID = "foo-${!count(\"noIndexTest\")}"
   141  	conf.URLs = urls
   142  	conf.MaxRetries = 1
   143  	conf.Backoff.MaxElapsedTime = "1s"
   144  	conf.Sniff = false
   145  
   146  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   147  	if err != nil {
   148  		t.Fatal(err)
   149  	}
   150  
   151  	if err = m.Connect(); err != nil {
   152  		t.Error(err)
   153  	}
   154  
   155  	defer func() {
   156  		m.CloseAsync()
   157  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   158  			t.Error(cErr)
   159  		}
   160  	}()
   161  
   162  	if err = m.Write(message.New([][]byte{[]byte(`{"message":"hello world","user":"1"}`)})); err != nil {
   163  		t.Error(err)
   164  	}
   165  
   166  	if err = m.Write(message.New([][]byte{
   167  		[]byte(`{"message":"hello world","user":"2"}`),
   168  		[]byte(`{"message":"hello world","user":"3"}`),
   169  	})); err != nil {
   170  		t.Error(err)
   171  	}
   172  
   173  	for i := 0; i < 3; i++ {
   174  		id := fmt.Sprintf("foo-%v", i+1)
   175  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   176  		get, err := client.Get().
   177  			Index("does_not_exist").
   178  			Id(id).
   179  			Do(context.Background())
   180  		if err != nil {
   181  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   182  		}
   183  		if !get.Found {
   184  			t.Errorf("document %v not found", i)
   185  		}
   186  	}
   187  }
   188  
   189  func testElasticParallelWrites(urls []string, client *elastic.Client, t *testing.T) {
   190  	conf := NewElasticsearchConfig()
   191  	conf.Index = "new_index_parallel_writes"
   192  	conf.ID = "${!json(\"key\")}"
   193  	conf.URLs = urls
   194  	conf.MaxRetries = 1
   195  	conf.Backoff.MaxElapsedTime = "1s"
   196  	conf.Sniff = false
   197  
   198  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   199  	if err != nil {
   200  		t.Fatal(err)
   201  	}
   202  
   203  	if err = m.Connect(); err != nil {
   204  		t.Error(err)
   205  	}
   206  
   207  	defer func() {
   208  		m.CloseAsync()
   209  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   210  			t.Error(cErr)
   211  		}
   212  	}()
   213  
   214  	N := 10
   215  
   216  	startChan := make(chan struct{})
   217  	wg := sync.WaitGroup{}
   218  	wg.Add(N)
   219  
   220  	docs := map[string]string{}
   221  
   222  	for i := 0; i < N; i++ {
   223  		str := fmt.Sprintf(`{"key":"doc-%v","message":"foobar"}`, i)
   224  		docs[fmt.Sprintf("doc-%v", i)] = str
   225  		go func(content string) {
   226  			<-startChan
   227  			if lerr := m.Write(message.New([][]byte{[]byte(content)})); lerr != nil {
   228  				t.Error(lerr)
   229  			}
   230  			wg.Done()
   231  		}(str)
   232  	}
   233  
   234  	close(startChan)
   235  	wg.Wait()
   236  
   237  	for id, exp := range docs {
   238  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   239  		get, err := client.Get().
   240  			Index("new_index_parallel_writes").
   241  			Type("_doc").
   242  			Id(id).
   243  			Do(context.Background())
   244  		if err != nil {
   245  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   246  		}
   247  		if !get.Found {
   248  			t.Errorf("document %v not found", id)
   249  		} else {
   250  			rawBytes, err := get.Source.MarshalJSON()
   251  			if err != nil {
   252  				t.Error(err)
   253  			} else if act := string(rawBytes); act != exp {
   254  				t.Errorf("Wrong result: %v != %v", act, exp)
   255  			}
   256  		}
   257  	}
   258  }
   259  
   260  func testElasticErrorHandling(urls []string, client *elastic.Client, t *testing.T) {
   261  	conf := NewElasticsearchConfig()
   262  	conf.Index = "test_conn_index?"
   263  	conf.ID = "foo-static"
   264  	conf.URLs = urls
   265  	conf.Backoff.MaxInterval = "1s"
   266  	conf.Sniff = false
   267  
   268  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   269  	if err != nil {
   270  		t.Fatal(err)
   271  	}
   272  
   273  	if err = m.Connect(); err != nil {
   274  		t.Fatal(err)
   275  	}
   276  
   277  	defer func() {
   278  		m.CloseAsync()
   279  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   280  			t.Error(cErr)
   281  		}
   282  	}()
   283  
   284  	if err = m.Write(message.New([][]byte{[]byte(`{"message":true}`)})); err == nil {
   285  		t.Error("Expected error")
   286  	}
   287  
   288  	if err = m.Write(message.New([][]byte{[]byte(`{"message":"foo"}`), []byte(`{"message":"bar"}`)})); err == nil {
   289  		t.Error("Expected error")
   290  	}
   291  }
   292  
   293  func testElasticConnect(urls []string, client *elastic.Client, t *testing.T) {
   294  	conf := NewElasticsearchConfig()
   295  	conf.Index = "test_conn_index"
   296  	conf.ID = "foo-${!count(\"foo\")}"
   297  	conf.URLs = urls
   298  	conf.Type = "_doc"
   299  	conf.Sniff = false
   300  
   301  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   302  	if err != nil {
   303  		t.Fatal(err)
   304  	}
   305  
   306  	if err = m.Connect(); err != nil {
   307  		t.Fatal(err)
   308  	}
   309  
   310  	defer func() {
   311  		m.CloseAsync()
   312  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   313  			t.Error(cErr)
   314  		}
   315  	}()
   316  
   317  	N := 10
   318  
   319  	testMsgs := [][][]byte{}
   320  	for i := 0; i < N; i++ {
   321  		testMsgs = append(testMsgs, [][]byte{
   322  			[]byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)),
   323  		})
   324  	}
   325  	for i := 0; i < N; i++ {
   326  		if err = m.Write(message.New(testMsgs[i])); err != nil {
   327  			t.Fatal(err)
   328  		}
   329  	}
   330  	for i := 0; i < N; i++ {
   331  		id := fmt.Sprintf("foo-%v", i+1)
   332  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   333  		get, err := client.Get().
   334  			Index("test_conn_index").
   335  			Type("_doc").
   336  			Id(id).
   337  			Do(context.Background())
   338  		if err != nil {
   339  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   340  		}
   341  		if !get.Found {
   342  			t.Errorf("document %v not found", i)
   343  		}
   344  
   345  		var sourceBytes []byte
   346  		sourceBytes, err = get.Source.MarshalJSON()
   347  		if err != nil {
   348  			t.Error(err)
   349  		} else if exp, act := string(testMsgs[i][0]), string(sourceBytes); exp != act {
   350  			t.Errorf("wrong user field returned: %v != %v", act, exp)
   351  		}
   352  	}
   353  }
   354  
   355  func testElasticIndexInterpolation(urls []string, client *elastic.Client, t *testing.T) {
   356  	conf := NewElasticsearchConfig()
   357  	conf.Index = "${!meta(\"index\")}"
   358  	conf.ID = "bar-${!count(\"bar\")}"
   359  	conf.URLs = urls
   360  	conf.Type = "_doc"
   361  	conf.Sniff = false
   362  
   363  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   364  	if err != nil {
   365  		t.Fatal(err)
   366  	}
   367  
   368  	if err = m.Connect(); err != nil {
   369  		t.Fatal(err)
   370  	}
   371  
   372  	defer func() {
   373  		m.CloseAsync()
   374  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   375  			t.Error(cErr)
   376  		}
   377  	}()
   378  
   379  	N := 10
   380  
   381  	testMsgs := [][][]byte{}
   382  	for i := 0; i < N; i++ {
   383  		testMsgs = append(testMsgs, [][]byte{
   384  			[]byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)),
   385  		})
   386  	}
   387  	for i := 0; i < N; i++ {
   388  		msg := message.New(testMsgs[i])
   389  		msg.Get(0).Metadata().Set("index", "test_conn_index")
   390  		if err = m.Write(msg); err != nil {
   391  			t.Fatal(err)
   392  		}
   393  	}
   394  	for i := 0; i < N; i++ {
   395  		id := fmt.Sprintf("bar-%v", i+1)
   396  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   397  		get, err := client.Get().
   398  			Index("test_conn_index").
   399  			Type("_doc").
   400  			Id(id).
   401  			Do(context.Background())
   402  		if err != nil {
   403  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   404  		}
   405  		if !get.Found {
   406  			t.Errorf("document %v not found", i)
   407  		}
   408  
   409  		var sourceBytes []byte
   410  		sourceBytes, err = get.Source.MarshalJSON()
   411  		if err != nil {
   412  			t.Error(err)
   413  		} else if exp, act := string(testMsgs[i][0]), string(sourceBytes); exp != act {
   414  			t.Errorf("wrong user field returned: %v != %v", act, exp)
   415  		}
   416  	}
   417  }
   418  
   419  func testElasticBatch(urls []string, client *elastic.Client, t *testing.T) {
   420  	conf := NewElasticsearchConfig()
   421  	conf.Index = "${!meta(\"index\")}"
   422  	conf.ID = "bar-${!count(\"bar\")}"
   423  	conf.URLs = urls
   424  	conf.Sniff = false
   425  	conf.Type = "_doc"
   426  
   427  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   428  	if err != nil {
   429  		t.Fatal(err)
   430  	}
   431  
   432  	if err = m.Connect(); err != nil {
   433  		t.Fatal(err)
   434  	}
   435  
   436  	defer func() {
   437  		m.CloseAsync()
   438  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   439  			t.Error(cErr)
   440  		}
   441  	}()
   442  
   443  	N := 10
   444  
   445  	testMsg := [][]byte{}
   446  	for i := 0; i < N; i++ {
   447  		testMsg = append(testMsg,
   448  			[]byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)),
   449  		)
   450  	}
   451  	msg := message.New(testMsg)
   452  	for i := 0; i < N; i++ {
   453  		msg.Get(i).Metadata().Set("index", "test_conn_index")
   454  	}
   455  	if err = m.Write(msg); err != nil {
   456  		t.Fatal(err)
   457  	}
   458  	for i := 0; i < N; i++ {
   459  		id := fmt.Sprintf("bar-%v", i+1)
   460  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   461  		get, err := client.Get().
   462  			Index("test_conn_index").
   463  			Type("_doc").
   464  			Id(id).
   465  			Do(context.Background())
   466  		if err != nil {
   467  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   468  		}
   469  		if !get.Found {
   470  			t.Errorf("document %v not found", i)
   471  		}
   472  
   473  		var sourceBytes []byte
   474  		sourceBytes, err = get.Source.MarshalJSON()
   475  		if err != nil {
   476  			t.Error(err)
   477  		} else if exp, act := string(testMsg[i]), string(sourceBytes); exp != act {
   478  			t.Errorf("wrong user field returned: %v != %v", act, exp)
   479  		}
   480  	}
   481  }
   482  
   483  func testElasticBatchDelete(urls []string, client *elastic.Client, t *testing.T) {
   484  	conf := NewElasticsearchConfig()
   485  	conf.Index = "${!meta(\"index\")}"
   486  	conf.ID = "bar-${!count(\"elasticBatchDeleteMessages\")}"
   487  	conf.Action = "${!meta(\"elastic_action\")}"
   488  	conf.URLs = urls
   489  	conf.Sniff = false
   490  	conf.Type = "_doc"
   491  
   492  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   493  	if err != nil {
   494  		t.Fatal(err)
   495  	}
   496  
   497  	if err = m.Connect(); err != nil {
   498  		t.Fatal(err)
   499  	}
   500  
   501  	defer func() {
   502  		m.CloseAsync()
   503  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   504  			t.Error(cErr)
   505  		}
   506  	}()
   507  
   508  	N := 10
   509  
   510  	testMsg := [][]byte{}
   511  	for i := 0; i < N; i++ {
   512  		testMsg = append(testMsg,
   513  			[]byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)),
   514  		)
   515  	}
   516  	msg := message.New(testMsg)
   517  	for i := 0; i < N; i++ {
   518  		msg.Get(i).Metadata().Set("index", "test_conn_index")
   519  		msg.Get(i).Metadata().Set("elastic_action", "index")
   520  	}
   521  	if err = m.Write(msg); err != nil {
   522  		t.Fatal(err)
   523  	}
   524  	for i := 0; i < N; i++ {
   525  		id := fmt.Sprintf("bar-%v", i+1)
   526  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   527  		get, err := client.Get().
   528  			Index("test_conn_index").
   529  			Type("_doc").
   530  			Id(id).
   531  			Do(context.Background())
   532  		if err != nil {
   533  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   534  		}
   535  		if !get.Found {
   536  			t.Errorf("document %v not found", i)
   537  		}
   538  
   539  		var sourceBytes []byte
   540  		sourceBytes, err = get.Source.MarshalJSON()
   541  		if err != nil {
   542  			t.Error(err)
   543  		} else if exp, act := string(testMsg[i]), string(sourceBytes); exp != act {
   544  			t.Errorf("wrong user field returned: %v != %v", act, exp)
   545  		}
   546  	}
   547  
   548  	// Set elastic_action to deleted for some message parts
   549  	for i := N / 2; i < N; i++ {
   550  		msg.Get(i).Metadata().Set("elastic_action", "delete")
   551  	}
   552  
   553  	if err = m.Write(msg); err != nil {
   554  		t.Fatal(err)
   555  	}
   556  
   557  	for i := 0; i < N; i++ {
   558  		id := fmt.Sprintf("bar-%v", i+1)
   559  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   560  		get, err := client.Get().
   561  			Index("test_conn_index").
   562  			Type("_doc").
   563  			Id(id).
   564  			Do(context.Background())
   565  		if err != nil {
   566  			t.Fatalf("Failed to get doc '%v': %v", id, err)
   567  		}
   568  		partAction := msg.Get(i).Metadata().Get("elastic_action")
   569  		if partAction == "deleted" && get.Found {
   570  			t.Errorf("document %v found when it should have been deleted", i)
   571  		} else if partAction != "deleted" && !get.Found {
   572  			t.Errorf("document %v was not found", i)
   573  		}
   574  	}
   575  }
   576  
   577  func testElasticBatchIDCollision(urls []string, client *elastic.Client, t *testing.T) {
   578  	conf := NewElasticsearchConfig()
   579  	conf.Index = `${!meta("index")}`
   580  	conf.ID = "bar-id"
   581  	conf.URLs = urls
   582  	conf.Sniff = false
   583  	conf.Type = "_doc"
   584  
   585  	m, err := NewElasticsearch(conf, log.Noop(), metrics.Noop())
   586  	if err != nil {
   587  		t.Fatal(err)
   588  	}
   589  
   590  	if err = m.Connect(); err != nil {
   591  		t.Fatal(err)
   592  	}
   593  
   594  	defer func() {
   595  		m.CloseAsync()
   596  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   597  			t.Error(cErr)
   598  		}
   599  	}()
   600  
   601  	N := 2
   602  
   603  	testMsg := [][]byte{}
   604  	for i := 0; i < N; i++ {
   605  		testMsg = append(testMsg,
   606  			[]byte(fmt.Sprintf(`{"message":"hello world","user":"%v"}`, i)),
   607  		)
   608  	}
   609  
   610  	msg := message.New(testMsg)
   611  	msg.Get(0).Metadata().Set("index", "test_conn_index")
   612  	msg.Get(1).Metadata().Set("index", "test_conn_index_2")
   613  
   614  	if err = m.Write(msg); err != nil {
   615  		t.Fatal(err)
   616  	}
   617  	for i := 0; i < N; i++ {
   618  		// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   619  		get, err := client.Get().
   620  			Index(msg.Get(i).Metadata().Get("index")).
   621  			Type("_doc").
   622  			Id(conf.ID).
   623  			Do(context.Background())
   624  		if err != nil {
   625  			t.Fatalf("Failed to get doc '%v': %v", conf.ID, err)
   626  		}
   627  		if !get.Found {
   628  			t.Errorf("document %v not found", i)
   629  		}
   630  
   631  		var sourceBytes []byte
   632  		sourceBytes, err = get.Source.MarshalJSON()
   633  		if err != nil {
   634  			t.Error(err)
   635  		} else if exp, act := string(testMsg[i]), string(sourceBytes); exp != act {
   636  			t.Errorf("wrong user field returned: %v != %v", act, exp)
   637  		}
   638  	}
   639  
   640  	// testing sequential updates to a document created above
   641  	conf.Action = "update"
   642  	conf.Index = "test_conn_index"
   643  	conf.ID = "bar-id"
   644  
   645  	m, err = NewElasticsearch(conf, log.Noop(), metrics.Noop())
   646  	if err != nil {
   647  		t.Fatal(err)
   648  	}
   649  
   650  	if err = m.Connect(); err != nil {
   651  		t.Fatal(err)
   652  	}
   653  
   654  	defer func() {
   655  		m.CloseAsync()
   656  		if cErr := m.WaitForClose(time.Second); cErr != nil {
   657  			t.Error(cErr)
   658  		}
   659  	}()
   660  
   661  	testMsg = [][]byte{
   662  		[]byte(`{"message":"goodbye"}`),
   663  		[]byte(`{"user": "updated"}`),
   664  	}
   665  	msg = message.New(testMsg)
   666  	if err = m.Write(msg); err != nil {
   667  		t.Fatal(err)
   668  	}
   669  
   670  	// nolint:staticcheck // Ignore SA1019 Type is deprecated warning for .Index()
   671  	get, err := client.Get().
   672  		Index("test_conn_index").
   673  		Type("_doc").
   674  		Id(conf.ID).
   675  		Do(context.Background())
   676  
   677  	if err != nil {
   678  		t.Fatalf("Failed to get doc '%v': %v", conf.ID, err)
   679  	}
   680  	if !get.Found {
   681  		t.Errorf("document not found")
   682  	}
   683  
   684  	var doc struct {
   685  		Message string `json:"message"`
   686  		User    string `json:"user"`
   687  	}
   688  	err = json.Unmarshal(get.Source, &doc)
   689  	if err != nil {
   690  		t.Error(err)
   691  	} else if doc.User != "updated" {
   692  		t.Errorf("wrong user field returned: %v != %v", doc.User, "updated")
   693  	} else if doc.Message != "goodbye" {
   694  		t.Errorf("wrong message field returned: %v != %v", doc.Message, "goodbye")
   695  	}
   696  }