github.com/thiagoyeds/go-cloud@v0.26.0/pubsub/azuresb/azuresb_test.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package azuresb
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"os"
    20  	"strings"
    21  	"sync/atomic"
    22  	"testing"
    23  
    24  	"gocloud.dev/internal/testing/setup"
    25  	"gocloud.dev/pubsub"
    26  	"gocloud.dev/pubsub/driver"
    27  	"gocloud.dev/pubsub/drivertest"
    28  
    29  	common "github.com/Azure/azure-amqp-common-go/v3"
    30  	servicebus "github.com/Azure/azure-service-bus-go"
    31  )
    32  
    33  var (
    34  	// See docs below on how to provision an Azure Service Bus Namespace and obtaining the connection string.
    35  	// https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-get-started-with-queues
    36  	connString = os.Getenv("SERVICEBUS_CONNECTION_STRING")
    37  )
    38  
    39  const (
    40  	nonexistentTopicName = "nonexistent-topic"
    41  
    42  	// Try to keep the entity name under Azure limits.
    43  	// https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-quotas
    44  	// says 50, but there appears to be some additional overhead. 40 works.
    45  	maxNameLen = 40
    46  )
    47  
    48  type harness struct {
    49  	ns         *servicebus.Namespace
    50  	numTopics  uint32 // atomic
    51  	numSubs    uint32 // atomic
    52  	closer     func()
    53  	autodelete bool
    54  }
    55  
    56  func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    57  	if connString == "" {
    58  		return nil, fmt.Errorf("azuresb: test harness requires environment variable SERVICEBUS_CONNECTION_STRING to run")
    59  	}
    60  	ns, err := NewNamespaceFromConnectionString(connString)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	noop := func() {}
    65  	return &harness{
    66  		ns:     ns,
    67  		closer: noop,
    68  	}, nil
    69  }
    70  
    71  func newHarnessUsingAutodelete(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
    72  	h, err := newHarness(ctx, t)
    73  	if err == nil {
    74  		h.(*harness).autodelete = true
    75  	}
    76  	return h, err
    77  }
    78  
    79  func (h *harness) CreateTopic(ctx context.Context, testName string) (dt driver.Topic, cleanup func(), err error) {
    80  	topicName := sanitize(fmt.Sprintf("%s-top-%d", testName, atomic.AddUint32(&h.numTopics, 1)))
    81  	if err := createTopic(ctx, topicName, h.ns, nil); err != nil {
    82  		return nil, nil, err
    83  	}
    84  
    85  	sbTopic, err := NewTopic(h.ns, topicName, nil)
    86  	dt, err = openTopic(ctx, sbTopic, nil)
    87  	if err != nil {
    88  		return nil, nil, err
    89  	}
    90  
    91  	cleanup = func() {
    92  		sbTopic.Close(ctx)
    93  		deleteTopic(ctx, topicName, h.ns)
    94  	}
    95  	return dt, cleanup, nil
    96  }
    97  
    98  func (h *harness) MakeNonexistentTopic(ctx context.Context) (driver.Topic, error) {
    99  	sbTopic, err := NewTopic(h.ns, nonexistentTopicName, nil)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	return openTopic(ctx, sbTopic, nil)
   104  }
   105  
   106  func (h *harness) CreateSubscription(ctx context.Context, dt driver.Topic, testName string) (ds driver.Subscription, cleanup func(), err error) {
   107  	subName := sanitize(fmt.Sprintf("%s-sub-%d", testName, atomic.AddUint32(&h.numSubs, 1)))
   108  	t := dt.(*topic)
   109  	err = createSubscription(ctx, t.sbTopic.Name, subName, h.ns, nil)
   110  	if err != nil {
   111  		return nil, nil, err
   112  	}
   113  
   114  	var opts []servicebus.SubscriptionOption
   115  	if h.autodelete {
   116  		opts = append(opts, servicebus.SubscriptionWithReceiveAndDelete())
   117  	}
   118  	sbSub, err := NewSubscription(t.sbTopic, subName, opts)
   119  	if err != nil {
   120  		return nil, nil, err
   121  	}
   122  
   123  	sopts := SubscriptionOptions{}
   124  	if h.autodelete {
   125  		sopts.ReceiveAndDelete = true
   126  	}
   127  	ds, err = openSubscription(ctx, h.ns, t.sbTopic, sbSub, &sopts)
   128  	if err != nil {
   129  		return nil, nil, err
   130  	}
   131  
   132  	cleanup = func() {
   133  		sbSub.Close(ctx)
   134  		deleteSubscription(ctx, t.sbTopic.Name, subName, h.ns)
   135  	}
   136  	return ds, cleanup, nil
   137  }
   138  
   139  func (h *harness) MakeNonexistentSubscription(ctx context.Context) (driver.Subscription, func(), error) {
   140  	dt, cleanup, err := h.CreateTopic(ctx, "topic-for-nonexistent-sub")
   141  	if err != nil {
   142  		return nil, nil, err
   143  	}
   144  	sbTopic := dt.(*topic).sbTopic
   145  	sbSub, err := NewSubscription(sbTopic, "nonexistent-subscription", nil)
   146  	if err != nil {
   147  		return nil, cleanup, err
   148  	}
   149  	sub, err := openSubscription(ctx, h.ns, sbTopic, sbSub, nil)
   150  	return sub, cleanup, err
   151  }
   152  
   153  func (h *harness) Close() {
   154  	h.closer()
   155  }
   156  
   157  func (h *harness) MaxBatchSizes() (int, int) { return sendBatcherOpts.MaxBatchSize, 0 }
   158  
   159  func (h *harness) SupportsMultipleSubscriptions() bool { return true }
   160  
   161  // Please run the TestConformance with an extended timeout since each test needs to perform CRUD for ServiceBus Topics and Subscriptions.
   162  // Example: C:\Go\bin\go.exe test -timeout 60s gocloud.dev/pubsub/azuresb -run ^TestConformance$
   163  func TestConformance(t *testing.T) {
   164  	if !*setup.Record {
   165  		t.Skip("replaying is not yet supported for Azure pubsub")
   166  	}
   167  	asTests := []drivertest.AsTest{sbAsTest{}}
   168  	drivertest.RunConformanceTests(t, newHarness, asTests)
   169  }
   170  
   171  func TestConformanceWithAutodelete(t *testing.T) {
   172  	if !*setup.Record {
   173  		t.Skip("replaying is not yet supported for Azure pubsub")
   174  	}
   175  	asTests := []drivertest.AsTest{sbAsTest{}}
   176  	drivertest.RunConformanceTests(t, newHarnessUsingAutodelete, asTests)
   177  }
   178  
   179  type sbAsTest struct{}
   180  
   181  func (sbAsTest) Name() string {
   182  	return "azure"
   183  }
   184  
   185  func (sbAsTest) TopicCheck(topic *pubsub.Topic) error {
   186  	var t2 servicebus.Topic
   187  	if topic.As(&t2) {
   188  		return fmt.Errorf("cast succeeded for %T, want failure", &t2)
   189  	}
   190  	var t3 *servicebus.Topic
   191  	if !topic.As(&t3) {
   192  		return fmt.Errorf("cast failed for %T", &t3)
   193  	}
   194  	return nil
   195  }
   196  
   197  func (sbAsTest) SubscriptionCheck(sub *pubsub.Subscription) error {
   198  	var s2 servicebus.Subscription
   199  	if sub.As(&s2) {
   200  		return fmt.Errorf("cast succeeded for %T, want failure", &s2)
   201  	}
   202  	var s3 *servicebus.Subscription
   203  	if !sub.As(&s3) {
   204  		return fmt.Errorf("cast failed for %T", &s3)
   205  	}
   206  	return nil
   207  }
   208  
   209  func (sbAsTest) TopicErrorCheck(t *pubsub.Topic, err error) error {
   210  	var sbError common.Retryable
   211  	if !t.ErrorAs(err, &sbError) {
   212  		return fmt.Errorf("failed to convert %v (%T) to a common.Retryable", err, err)
   213  	}
   214  	return nil
   215  }
   216  
   217  func (sbAsTest) SubscriptionErrorCheck(s *pubsub.Subscription, err error) error {
   218  	// We generate our own error for non-existent subscription, so there's no
   219  	// underlying Azure error type.
   220  	return nil
   221  }
   222  
   223  func (sbAsTest) MessageCheck(m *pubsub.Message) error {
   224  	var m2 servicebus.Message
   225  	if m.As(&m2) {
   226  		return fmt.Errorf("cast succeeded for %T, want failure", &m2)
   227  	}
   228  	var m3 *servicebus.Message
   229  	if !m.As(&m3) {
   230  		return fmt.Errorf("cast failed for %T", &m3)
   231  	}
   232  	return nil
   233  }
   234  
   235  func (sbAsTest) BeforeSend(as func(interface{}) bool) error {
   236  	var m *servicebus.Message
   237  	if !as(&m) {
   238  		return fmt.Errorf("cast failed for %T", &m)
   239  	}
   240  	return nil
   241  }
   242  
   243  func (sbAsTest) AfterSend(as func(interface{}) bool) error {
   244  	return nil
   245  }
   246  
   247  func sanitize(s string) string {
   248  	// First trim some not-so-useful strings that are part of all test names.
   249  	s = strings.Replace(s, "TestConformance/Test", "", 1)
   250  	s = strings.Replace(s, "TestConformanceWithAutodelete/Test", "", 1)
   251  	s = strings.Replace(s, "/", "_", -1)
   252  	if len(s) > maxNameLen {
   253  		// Drop prefix, not suffix, because suffix includes something to make
   254  		// entities unique within a test.
   255  		s = s[len(s)-maxNameLen:]
   256  	}
   257  	return s
   258  }
   259  
   260  // createTopic ensures the existence of a Service Bus Topic on a given Namespace.
   261  func createTopic(ctx context.Context, topicName string, ns *servicebus.Namespace, opts []servicebus.TopicManagementOption) error {
   262  	tm := ns.NewTopicManager()
   263  	_, err := tm.Get(ctx, topicName)
   264  	if err == nil {
   265  		_ = tm.Delete(ctx, topicName)
   266  	}
   267  	_, err = tm.Put(ctx, topicName, opts...)
   268  	return err
   269  }
   270  
   271  // deleteTopic removes a Service Bus Topic on a given Namespace.
   272  func deleteTopic(ctx context.Context, topicName string, ns *servicebus.Namespace) error {
   273  	tm := ns.NewTopicManager()
   274  	te, _ := tm.Get(ctx, topicName)
   275  	if te != nil {
   276  		return tm.Delete(ctx, topicName)
   277  	}
   278  	return nil
   279  }
   280  
   281  // createSubscription ensures the existence of a Service Bus Subscription on a given Namespace and Topic.
   282  func createSubscription(ctx context.Context, topicName string, subscriptionName string, ns *servicebus.Namespace, opts []servicebus.SubscriptionManagementOption) error {
   283  	sm, err := ns.NewSubscriptionManager(topicName)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	_, err = sm.Get(ctx, subscriptionName)
   288  	if err == nil {
   289  		_ = sm.Delete(ctx, subscriptionName)
   290  	}
   291  	_, err = sm.Put(ctx, subscriptionName, opts...)
   292  	return err
   293  }
   294  
   295  // deleteSubscription removes a Service Bus Subscription on a given Namespace and Topic.
   296  func deleteSubscription(ctx context.Context, topicName string, subscriptionName string, ns *servicebus.Namespace) error {
   297  	sm, err := ns.NewSubscriptionManager(topicName)
   298  	if err != nil {
   299  		return nil
   300  	}
   301  	se, _ := sm.Get(ctx, subscriptionName)
   302  	if se != nil {
   303  		_ = sm.Delete(ctx, subscriptionName)
   304  	}
   305  	return nil
   306  }
   307  
   308  func BenchmarkAzureServiceBusPubSub(b *testing.B) {
   309  	const (
   310  		benchmarkTopicName        = "benchmark-topic"
   311  		benchmarkSubscriptionName = "benchmark-subscription"
   312  	)
   313  	ctx := context.Background()
   314  
   315  	if connString == "" {
   316  		b.Fatal("azuresb: benchmark requires environment variable SERVICEBUS_CONNECTION_STRING to run")
   317  	}
   318  	ns, err := NewNamespaceFromConnectionString(connString)
   319  	if err != nil {
   320  		b.Fatal(err)
   321  	}
   322  
   323  	// Make topic.
   324  	if err := createTopic(ctx, benchmarkTopicName, ns, nil); err != nil {
   325  		b.Fatal(err)
   326  	}
   327  	defer deleteTopic(ctx, benchmarkTopicName, ns)
   328  
   329  	sbTopic, err := NewTopic(ns, benchmarkTopicName, nil)
   330  	if err != nil {
   331  		b.Fatal(err)
   332  	}
   333  	defer sbTopic.Close(ctx)
   334  	topic, err := OpenTopic(ctx, sbTopic, nil)
   335  	if err != nil {
   336  		b.Fatal(err)
   337  	}
   338  	defer topic.Shutdown(ctx)
   339  
   340  	// Make subscription.
   341  	if err := createSubscription(ctx, benchmarkTopicName, benchmarkSubscriptionName, ns, nil); err != nil {
   342  		b.Fatal(err)
   343  	}
   344  	sbSub, err := NewSubscription(sbTopic, benchmarkSubscriptionName, nil)
   345  	if err != nil {
   346  		b.Fatal(err)
   347  	}
   348  	sub, err := OpenSubscription(ctx, ns, sbTopic, sbSub, nil)
   349  	if err != nil {
   350  		b.Fatal(err)
   351  	}
   352  	defer sub.Shutdown(ctx)
   353  
   354  	drivertest.RunBenchmarks(b, topic, sub)
   355  }
   356  
   357  func fakeConnectionStringInEnv() func() {
   358  	oldEnvVal := os.Getenv("SERVICEBUS_CONNECTION_STRING")
   359  	os.Setenv("SERVICEBUS_CONNECTION_STRING", "Endpoint=sb://foo.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=mykey")
   360  	return func() {
   361  		os.Setenv("SERVICEBUS_CONNECTION_STRING", oldEnvVal)
   362  	}
   363  }
   364  
   365  func TestOpenTopicFromURL(t *testing.T) {
   366  	cleanup := fakeConnectionStringInEnv()
   367  	defer cleanup()
   368  
   369  	tests := []struct {
   370  		URL     string
   371  		WantErr bool
   372  	}{
   373  		// OK.
   374  		{"azuresb://mytopic", false},
   375  		// Invalid parameter.
   376  		{"azuresb://mytopic?param=value", true},
   377  	}
   378  
   379  	ctx := context.Background()
   380  	for _, test := range tests {
   381  		topic, err := pubsub.OpenTopic(ctx, test.URL)
   382  		if (err != nil) != test.WantErr {
   383  			t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
   384  		}
   385  		if topic != nil {
   386  			topic.Shutdown(ctx)
   387  		}
   388  	}
   389  }
   390  
   391  func TestOpenSubscriptionFromURL(t *testing.T) {
   392  	cleanup := fakeConnectionStringInEnv()
   393  	defer cleanup()
   394  
   395  	tests := []struct {
   396  		URL     string
   397  		WantErr bool
   398  	}{
   399  		// OK.
   400  		{"azuresb://mytopic?subscription=mysub", false},
   401  		// Missing subscription.
   402  		{"azuresb://mytopic", true},
   403  		// Invalid parameter.
   404  		{"azuresb://mytopic?subscription=mysub&param=value", true},
   405  	}
   406  
   407  	ctx := context.Background()
   408  	for _, test := range tests {
   409  		sub, err := pubsub.OpenSubscription(ctx, test.URL)
   410  		if (err != nil) != test.WantErr {
   411  			t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
   412  		}
   413  		if sub != nil {
   414  			sub.Shutdown(ctx)
   415  		}
   416  	}
   417  }