package botlink import ( "context" "errors" "net" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" botlinkv1 "scrabble/pkg/proto/botlink/v1" telegramv1 "scrabble/pkg/proto/telegram/v1" ) // fakeBot is a test bot that dials the hub, registers, and acks commands with a // fixed delivered flag (or never, when ack is false). type fakeBot struct { delivered bool ack bool received chan *botlinkv1.Command } // startHub registers a Hub on an in-memory gRPC server and returns the hub plus a // dialer for fake bots. func startHub(t *testing.T) (*Hub, func(t *testing.T) botlinkv1.BotLinkClient) { t.Helper() lis := bufconn.Listen(1 << 20) hub := NewHub(nil, nil) srv := grpc.NewServer() botlinkv1.RegisterBotLinkServer(srv, hub) go func() { _ = srv.Serve(lis) }() t.Cleanup(srv.Stop) dial := func(t *testing.T) botlinkv1.BotLinkClient { t.Helper() conn, err := grpc.NewClient("passthrough:///bufnet", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis.Dial() }), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { t.Fatalf("dial: %v", err) } t.Cleanup(func() { _ = conn.Close() }) return botlinkv1.NewBotLinkClient(conn) } return hub, dial } // connect runs a fake bot against client until ctx is cancelled, returning once the // hub has registered it. func (f *fakeBot) connect(t *testing.T, ctx context.Context, hub *Hub, client botlinkv1.BotLinkClient) { t.Helper() f.received = make(chan *botlinkv1.Command, 8) stream, err := client.Link(ctx) if err != nil { t.Fatalf("link: %v", err) } if err := stream.Send(&botlinkv1.FromBot{Msg: &botlinkv1.FromBot_Hello{Hello: &botlinkv1.Hello{InstanceId: "test"}}}); err != nil { t.Fatalf("hello: %v", err) } go func() { for { msg, err := stream.Recv() if err != nil { return } cmd := msg.GetCommand() f.received <- cmd if !f.ack { continue } _ = stream.Send(&botlinkv1.FromBot{Msg: &botlinkv1.FromBot_Ack{Ack: &botlinkv1.Ack{ CommandId: cmd.GetCommandId(), Delivered: f.delivered, }}}) } }() waitConnected(t, hub, 1) } // waitConnected blocks until the hub reports n connected bots. func waitConnected(t *testing.T, hub *Hub, n int) { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { hub.mu.Lock() got := len(hub.links) hub.mu.Unlock() if got == n { return } time.Sleep(5 * time.Millisecond) } t.Fatalf("hub did not reach %d connected bots", n) } func TestHubSendAwaitDelivered(t *testing.T) { hub, dial := startHub(t) ctx := t.Context() bot := &fakeBot{delivered: true, ack: true} bot.connect(t, ctx, hub, dial(t)) delivered, err := hub.SendAwait(ctx, SendToUserCommand("42", "hi")) if err != nil { t.Fatalf("SendAwait: %v", err) } if !delivered { t.Fatal("delivered = false, want true") } select { case cmd := <-bot.received: if cmd.GetSendToUser().GetExternalId() != "42" { t.Errorf("received external_id = %q, want 42", cmd.GetSendToUser().GetExternalId()) } case <-time.After(time.Second): t.Fatal("bot received no command") } } func TestHubSendAwaitNoBot(t *testing.T) { hub, _ := startHub(t) _, err := hub.SendAwait(context.Background(), SendToUserCommand("42", "hi")) if !errors.Is(err, ErrNoBot) { t.Errorf("err = %v, want ErrNoBot", err) } } func TestHubSendAwaitDeadline(t *testing.T) { hub, dial := startHub(t) ctx := t.Context() bot := &fakeBot{ack: false} // receives but never acks bot.connect(t, ctx, hub, dial(t)) awaitCtx, awaitCancel := context.WithTimeout(ctx, 100*time.Millisecond) defer awaitCancel() delivered, err := hub.SendAwait(awaitCtx, SendToUserCommand("42", "hi")) if err != nil { t.Fatalf("SendAwait err = %v, want nil on deadline", err) } if delivered { t.Error("delivered = true, want false on deadline") } } func TestHubSendAsync(t *testing.T) { hub, dial := startHub(t) ctx := t.Context() bot := &fakeBot{ack: false} bot.connect(t, ctx, hub, dial(t)) hub.Send(NotifyCommand("42", "your_turn", nil, "en")) select { case cmd := <-bot.received: if cmd.GetNotify().GetExternalId() != "42" { t.Errorf("received external_id = %q, want 42", cmd.GetNotify().GetExternalId()) } case <-time.After(time.Second): t.Fatal("bot received no command") } } func TestRelayServerNoBot(t *testing.T) { hub, _ := startHub(t) relay := NewRelayServer(hub, 200*time.Millisecond) if _, err := relay.SendToUser(context.Background(), &telegramv1.SendToUserRequest{ExternalId: "42", Text: "hi"}); err == nil { t.Fatal("expected an error with no bot connected") } }