Repository: ably/ably-go (go)

Issue:
RTL17: Only dispatch messages when ATTACHED
Write spec tests for [RTL17](https://docs.ably.io/client-lib-development-guide/features/#RTL17), ensure they pass and that the affected API conforms to [the 1.2 API design](https://github.com/ably/ably-go/issues/197).



Pull request description:
Channel message dispatch only when it's attached
- Added missing channel attached check
- Updated tests for the same
- Fixes #240 

Patch:
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index b99feb4..3c03936 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -563,8 +563,10 @@ func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 		c.setState(ChannelStateFailed, newErrorFromProto(msg.Error), false)
 		c.queue.Fail(newErrorFromProto(msg.Error))
 	case proto.ActionMessage:
-		for _, msg := range msg.Messages {
-			c.messageEmitter.Emit(subscriptionName(msg.Name), (*subscriptionMessage)(msg))
+		if c.State() == ChannelStateAttached {
+			for _, msg := range msg.Messages {
+				c.messageEmitter.Emit(subscriptionName(msg.Name), (*subscriptionMessage)(msg))
+			}
 		}
 	default:
 	}


Test patch:
diff --git a/ably/realtime_channel_spec_test.go b/ably/realtime_channel_spec_test.go
index fe74831..0355a36 100644
--- a/ably/realtime_channel_spec_test.go
+++ b/ably/realtime_channel_spec_test.go
@@ -704,7 +704,7 @@ func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 			t.Fatalf("expected %v; got %v (message: %+v)", expected, got, msg)
 		}
 
-		// TODO: Test attach failure too, which requires RTL4ef.
+		// TODO: Test attach failure too, which requires RTL4e.
 
 		in <- &proto.ProtocolMessage{
 			Action:  proto.ActionAttached,
@@ -849,6 +849,145 @@ func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 	})
 }
 
+func TestRealtimeChannel_RTL17_IgnoreMessagesWhenNotAttached(t *testing.T) {
+	t.Parallel()
+
+	const channelRetryTimeout = 123 * time.Millisecond
+
+	setup := func(t *testing.T) (
+		in, out chan *proto.ProtocolMessage,
+		msg chan *proto.Message,
+		c *ably.Realtime,
+		channel *ably.RealtimeChannel,
+		stateChanges ably.ChannelStateChanges,
+	) {
+		in = make(chan *proto.ProtocolMessage, 1)
+		out = make(chan *proto.ProtocolMessage, 16)
+		msg = make(chan *proto.Message, 1)
+		afterCalls := make(chan ablytest.AfterCall, 1)
+		now, after := ablytest.TimeFuncs(afterCalls)
+
+		c, _ = ably.NewRealtime(
+			ably.WithToken("fake:token"),
+			ably.WithAutoConnect(false),
+			ably.WithNow(now),
+			ably.WithAfter(after),
+			ably.WithChannelRetryTimeout(channelRetryTimeout),
+			ably.WithDial(ablytest.MessagePipe(in, out)),
+		)
+
+		in <- &proto.ProtocolMessage{
+			Action:            proto.ActionConnected,
+			ConnectionID:      "connection-id",
+			ConnectionDetails: &proto.ConnectionDetails{},
+		}
+
+		err := ablytest.Wait(ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected), nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		channel = c.Channels.Get("test")
+
+		stateChanges = make(ably.ChannelStateChanges, 10)
+		channel.OnAll(stateChanges.Receive)
+
+		ctx, cancel := context.WithCancel(context.Background())
+		cancel()
+
+		channel.SubscribeAll(ctx, func(message *ably.Message) {
+			msg <- message
+		})
+
+		channel.Attach(ctx)
+		return
+	}
+
+	t.Run("Shouldn't receive message when not attached", func(t *testing.T) {
+		t.Parallel()
+
+		in, out, msg, _, channel, stateChanges := setup(t)
+
+		receiveMessage := func() {
+			message := &ably.Message{
+				ID:           "Id",
+				ClientID:     "clientId",
+				ConnectionID: "connectionId",
+				Name:         "Sample Name",
+				Data:         "Sample Data",
+				Encoding:     "encoding",
+				Timestamp:    0,
+				Extras:       nil,
+			}
+			in <- &proto.ProtocolMessage{
+				Action:        proto.ActionMessage,
+				Channel:       channel.Name,
+				ID:            "uniqueId",
+				MsgSerial:     3,
+				ChannelSerial: "channelSerial",
+				Messages:      append(make([]*ably.Message, 0), message),
+			}
+		}
+
+		var change ably.ChannelStateChange
+
+		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // Consume ATTACHING
+		if expected, got := ably.ChannelStateAttaching, change.Current; expected != got {
+			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
+		}
+
+		// Shouldn't receive message when state is ATTACHING
+		receiveMessage()
+		ablytest.Instantly.NoRecv(t, nil, msg, t.Fatalf)
+
+		// Get the channel to ATTACHED.
+		in <- &proto.ProtocolMessage{
+			Action:  proto.ActionAttached,
+			Channel: channel.Name,
+		}
+
+		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // Consume ATTACHED
+		if expected, got := ably.ChannelStateAttached, change.Current; expected != got {
+			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
+		}
+
+		channel.SubscribeAll(context.Background(), func(message *ably.Message) {
+			msg <- message
+		})
+
+		// receive message when state is ATTACHED
+		receiveMessage()
+		ablytest.Instantly.Recv(t, nil, msg, t.Fatalf)
+
+		ctx, cancel := context.WithCancel(context.Background())
+		cancel()
+		channel.Detach(ctx)
+		// Get the channel to DETACHED.
+
+		ablytest.Instantly.Recv(t, nil, out, t.Fatalf) // Consume DETACHING
+
+		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // DETACHING channel state
+		if expected, got := ably.ChannelStateDetaching, change.Current; expected != got {
+			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
+		}
+
+		in <- &proto.ProtocolMessage{
+			Action:  proto.ActionDetached,
+			Channel: channel.Name,
+		}
+
+		ablytest.Instantly.Recv(t, &change, stateChanges, t.Fatalf) // Consume DETACHED
+		if expected, got := ably.ChannelStateDetached, change.Current; expected != got {
+			t.Fatalf("expected %v; got %v (event: %+v)", expected, got, change)
+		}
+
+		// Shouldn't receive message when state is DETACHED
+		receiveMessage()
+		ablytest.Instantly.NoRecv(t, nil, msg, t.Fatalf)
+
+	})
+}
+
 func TestRealtimeChannel_RTL14_HandleChannelError(t *testing.T) {
 	t.Parallel()