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¶m=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 }