Skip to content

Commit

Permalink
Add tests for the enter function (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
ForNeVeR committed Sep 21, 2019
1 parent 5efa3e1 commit 1afff5a
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 33 deletions.
111 changes: 111 additions & 0 deletions Emulsion.Tests/Xmpp/SharpXmppClientTests.fs
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
namespace Emulsion.Tests.Xmpp

open System

open JetBrains.Lifetimes
open SharpXMPP
open SharpXMPP.XMPP
open SharpXMPP.XMPP.Client.Elements
open Xunit
open Xunit.Abstractions

open System.Xml.Linq
open Emulsion.Tests.TestUtils
open Emulsion.Xmpp
open Emulsion.Xmpp.SharpXmppClient
open Emulsion.Xmpp.SharpXmppHelper.Attributes
open Emulsion.Xmpp.SharpXmppHelper.Elements

type SharpXmppClientTests(testOutput: ITestOutputHelper) =
let logger = Logging.xunitLogger testOutput

let createPresenceFor (roomJid: JID) nickname =
let presence = XMPPPresence()
let participantJid = JID(roomJid.FullJid)
participantJid.Resource <- nickname
presence.SetAttributeValue(From, participantJid.FullJid)
presence

let createSelfPresence roomJid nickname =
let presence = createPresenceFor roomJid nickname
let x = XElement X
let status = XElement Status
status.SetAttributeValue(Code, "110")
x.Add status
presence.Add x
presence

let createErrorPresence roomJid nickname errorXml =
let presence = createPresenceFor roomJid nickname
presence.SetAttributeValue(Type, "error")
let error = XElement Error
let errorChild = XElement.Parse errorXml
error.Add errorChild
presence.Add error
presence

let createLeavePresence roomJid nickname =
let presence = createSelfPresence roomJid nickname
presence.SetAttributeValue(Type, "unavailable")
presence

let sendPresence presence handlers =
Seq.iter (fun h -> h presence) handlers

[<Fact>]
member __.``connect function calls the Connect method of the client passed``(): unit =
let mutable connectCalled = false
Expand All @@ -26,3 +68,72 @@ type SharpXmppClientTests(testOutput: ITestOutputHelper) =
Assert.True lt.IsAlive
callback(ConnFailedArgs())
Assert.False lt.IsAlive

[<Fact>]
member __.``enter function calls JoinMultiUserChat``(): unit =
let mutable called = false
let mutable presenceHandlers = ResizeArray()
let client =
XmppClientFactory.create(
addPresenceHandler = (fun _ h -> presenceHandlers.Add h),
joinMultiUserChat = fun roomJid nickname ->
called <- true
Seq.iter (fun h -> h (createSelfPresence roomJid nickname)) presenceHandlers
)
let roomInfo = { RoomJid = JID("room@conference.example.org"); Nickname = "testuser" }
Lifetime.Using(fun lt ->
Async.RunSynchronously <| SharpXmppClient.enterRoom client lt roomInfo |> ignore
Assert.True called
)

[<Fact>]
member __.``enter throws an exception in case of an error presence``(): unit =
let mutable presenceHandlers = ResizeArray()
let client =
XmppClientFactory.create(
addPresenceHandler = (fun _ h -> presenceHandlers.Add h),
joinMultiUserChat = fun roomJid nickname ->
sendPresence (createErrorPresence roomJid nickname "<test />") presenceHandlers
)
let roomInfo = { RoomJid = JID("room@conference.example.org"); Nickname = "testuser" }
Lifetime.Using(fun lt ->
let ae = Assert.Throws<AggregateException>(fun () ->
Async.RunSynchronously <| SharpXmppClient.enterRoom client lt roomInfo |> ignore
)
let ex = Seq.exactlyOne ae.InnerExceptions
Assert.Contains("<test />", ex.Message)
)

[<Fact>]
member __.``Lifetime returned from enter terminates by a room leave presence``(): unit =
let mutable presenceHandlers = ResizeArray()
let client =
XmppClientFactory.create(
addPresenceHandler = (fun _ h -> presenceHandlers.Add h),
joinMultiUserChat = fun roomJid nickname ->
sendPresence (createSelfPresence roomJid nickname) presenceHandlers
)
let roomInfo = { RoomJid = JID("room@conference.example.org"); Nickname = "testuser" }
Lifetime.Using(fun lt ->
let roomLt = Async.RunSynchronously <| SharpXmppClient.enterRoom client lt roomInfo
Assert.True roomLt.IsAlive
sendPresence (createLeavePresence roomInfo.RoomJid roomInfo.Nickname) presenceHandlers
Assert.False roomLt.IsAlive
)

[<Fact>]
member __.``Lifetime returned from enter terminates by an external lifetime termination``(): unit =
let mutable presenceHandlers = ResizeArray()
let client =
XmppClientFactory.create(
addPresenceHandler = (fun _ h -> presenceHandlers.Add h),
joinMultiUserChat = fun roomJid nickname ->
sendPresence (createSelfPresence roomJid nickname) presenceHandlers
)
let roomInfo = { RoomJid = JID("room@conference.example.org"); Nickname = "testuser" }
use ld = Lifetime.Define()
let lt = ld.Lifetime
let roomLt = Async.RunSynchronously <| SharpXmppClient.enterRoom client lt roomInfo
Assert.True roomLt.IsAlive
ld.Terminate()
Assert.False roomLt.IsAlive
6 changes: 5 additions & 1 deletion Emulsion.Tests/Xmpp/XmppClientFactory.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ namespace Emulsion.Tests.Xmpp
open Emulsion.Xmpp.XmppClient

type XmppClientFactory =
static member create(?connect, ?addConnectionFailedHandler): IXmppClient =
static member create(?connect, ?addConnectionFailedHandler, ?addPresenceHandler, ?joinMultiUserChat): IXmppClient =
let connect = defaultArg connect <| fun () -> async { return () }
let addConnectionFailedHandler = defaultArg addConnectionFailedHandler <| fun _ _ -> ()
let addPresenceHandler = defaultArg addPresenceHandler <| fun _ _ -> ()
let joinMultiUserChat = defaultArg joinMultiUserChat <| fun _ _ -> ()
{ new IXmppClient with
member __.Connect() = connect()
member __.AddConnectionFailedHandler lt handler = addConnectionFailedHandler lt handler
member __.AddPresenceHandler lt handler = addPresenceHandler lt handler
member __.JoinMultiUserChat roomJid nickname = joinMultiUserChat roomJid nickname
}
50 changes: 20 additions & 30 deletions Emulsion/Xmpp/SharpXmppClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ open Emulsion
open Emulsion.Lifetimes
open Emulsion.Xmpp
open Emulsion.Xmpp.XmppClient

type Jid = string
open SharpXMPP.XMPP

type RoomInfo = {
RoomJid: Jid
RoomJid: JID
Nickname: string
}

type MessageInfo = {
RecipientJid: Jid
RecipientJid: JID
Text: string
}

Expand All @@ -43,24 +42,19 @@ let connect (logger: ILogger) (client: IXmppClient): Async<Lifetime> = async {
return connectionLifetime.Lifetime
}

let private addPresenceHandler (lifetime: Lifetime) (client: XmppClient) handler =
let handlerDelegate = XmppConnection.PresenceHandler(fun _ p -> handler p)
client.add_Presence handlerDelegate
lifetime.OnTermination (fun () -> client.remove_Presence handlerDelegate) |> ignore

let private isSelfPresence (roomInfo: RoomInfo) (presence: XMPPPresence) =
let presence = SharpXmppHelper.parsePresence presence
let expectedJid = sprintf "%s/%s" roomInfo.RoomJid roomInfo.Nickname
presence.From = expectedJid && Array.contains 110 presence.States
let expectedJid = sprintf "%s/%s" roomInfo.RoomJid.BareJid roomInfo.Nickname
presence.Type = None && presence.From = expectedJid && Array.contains 110 presence.States

let private isLeavePresence (roomInfo: RoomInfo) (presence: XMPPPresence) =
let presence = SharpXmppHelper.parsePresence presence
let expectedJid = sprintf "%s/%s" roomInfo.RoomJid roomInfo.Nickname
presence.From = expectedJid && Array.contains 110 presence.States && presence.Type = "unavailable"
let expectedJid = sprintf "%s/%s" roomInfo.RoomJid.BareJid roomInfo.Nickname
presence.From = expectedJid && Array.contains 110 presence.States && presence.Type = Some "unavailable"

let private extractException (roomInfo: RoomInfo) (presence: XMPPPresence) =
let presence = SharpXmppHelper.parsePresence presence
let expectedJid = sprintf "%s/%s" roomInfo.RoomJid roomInfo.Nickname
let expectedJid = sprintf "%s/%s" roomInfo.RoomJid.BareJid roomInfo.Nickname
if presence.From = expectedJid then
presence.Error
|> Option.map (fun e -> Exception(sprintf "Error: %A" e))
Expand All @@ -72,8 +66,7 @@ let private addMessageHandler (lifetime: Lifetime) (client: XmppClient) handler
lifetime.OnTermination(fun () -> client.remove_Message handlerDelegate) |> ignore

/// Enter the room, returning the in-room lifetime. Will terminate if kicked or left the room.
/// TODO[F]: Write tests for this function.
let enterRoom (client: XmppClient) (lifetime: Lifetime) (roomInfo: RoomInfo): Async<Lifetime> = async {
let enterRoom (client: IXmppClient) (lifetime: Lifetime) (roomInfo: RoomInfo): Async<Lifetime> = async {
use connectionLifetimeDefinition = lifetime.CreateNested()
let connectionLifetime = connectionLifetimeDefinition.Lifetime

Expand All @@ -82,33 +75,30 @@ let enterRoom (client: XmppClient) (lifetime: Lifetime) (roomInfo: RoomInfo): As

let tcs = nestedTaskCompletionSource connectionLifetime

// Enter room successfully handler:
addPresenceHandler connectionLifetime client (fun presence ->
// Success and error handlers:
client.AddPresenceHandler connectionLifetime (fun presence ->
if isSelfPresence roomInfo presence
then tcs.SetResult()
)

// Error handler:
addPresenceHandler connectionLifetime client (fun presence ->
match extractException roomInfo presence with
| Some ex -> tcs.SetException ex
| None -> ()
else
match extractException roomInfo presence with
| Some ex -> tcs.SetException ex
| None -> ()
)

// Room leave handler:
addPresenceHandler roomLifetime client (fun presence ->
client.AddPresenceHandler roomLifetime (fun presence ->
if isLeavePresence roomInfo presence
then roomLifetimeDefinition.Terminate()
)

try
// Start the enter process, wait for a result:
SharpXmppHelper.joinRoom client roomInfo.RoomJid roomInfo.Nickname
// Start the join process, wait for a result:
client.JoinMultiUserChat roomInfo.RoomJid roomInfo.Nickname
do! Async.AwaitTask tcs.Task
return roomLifetime
with
| ex ->
// In case of an error, terminate the room lifetime:
// In case of an error, terminate the room lifetime (but leave it intact in case of success):
roomLifetimeDefinition.Terminate()
return ExceptionUtils.reraise ex
}
Expand All @@ -133,7 +123,7 @@ let private awaitMessageReceival (lifetime: Lifetime) client messageId = async {
let sendRoomMessage (lifetime: Lifetime) (client: XmppClient) (messageInfo: MessageInfo): Async<MessageDeliveryInfo> =
async {
let messageId = Guid.NewGuid().ToString() // TODO[F]: Move to a new function
let message = SharpXmppHelper.message (Some messageId) messageInfo.RecipientJid messageInfo.Text
let message = SharpXmppHelper.message (Some messageId) messageInfo.RecipientJid.FullJid messageInfo.Text
let! delivery = Async.StartChild <| awaitMessageReceival lifetime client messageId
client.Send message
return {
Expand Down
2 changes: 1 addition & 1 deletion Emulsion/Xmpp/SharpXmppHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ let parseMessage (message: XMPPMessage): Message =

let parsePresence(presence: XMPPPresence): Presence =
let from = getAttributeValue presence From |> Option.defaultValue ""
let presenceType = getAttributeValue presence Type |> Option.defaultValue ""
let presenceType = getAttributeValue presence Type
let states =
presence.Element X
|> Option.ofObj
Expand Down
3 changes: 3 additions & 0 deletions Emulsion/Xmpp/XmppClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ open SharpXMPP.XMPP

open Emulsion
open Emulsion.Settings
open SharpXMPP.XMPP.Client.Elements

// TODO[F]: Create an XmppClient-based implementation of this interface
type IXmppClient =
abstract member Connect: unit -> Async<unit>
abstract member AddConnectionFailedHandler: Lifetime -> (ConnFailedArgs -> unit) -> unit
abstract member AddPresenceHandler: Lifetime -> (XMPPPresence -> unit) -> unit
abstract member JoinMultiUserChat: roomJid: JID -> nickname: string -> unit

// TODO[F]: This client should be removed
// TODO[F]: But preserve the logging routines; they're good
Expand Down
2 changes: 1 addition & 1 deletion Emulsion/Xmpp/XmppElements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ type Presence = {
From: string
States: int[]
Error: XElement option
Type: string
Type: string option
}

0 comments on commit 1afff5a

Please sign in to comment.