diff --git a/ChatExample/app/build.gradle b/ChatExample/app/build.gradle index 18dd1a7..6ddf2db 100644 --- a/ChatExample/app/build.gradle +++ b/ChatExample/app/build.gradle @@ -1,15 +1,12 @@ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' - android { - compileSdkVersion 30 + compileSdkVersion 33 defaultConfig { applicationId "com.github.dsrees.chatexample" minSdkVersion 19 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -21,6 +18,10 @@ android { } } + buildFeatures { + viewBinding true + } + compileOptions { targetCompatibility = "8" sourceCompatibility = "8" @@ -40,14 +41,10 @@ dependencies { // implementation 'com.github.dsrees:JavaPhoenixClient:0.3.4' - implementation "com.google.code.gson:gson:2.8.5" - implementation "com.squareup.okhttp3:okhttp:3.12.2" - - - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation "com.google.code.gson:gson:2.10.1" + implementation "com.squareup.okhttp3:okhttp:4.11.0" + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.recyclerview:recyclerview:1.3.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' } diff --git a/ChatExample/app/libs/JavaPhoenixClient-0.7.0.jar b/ChatExample/app/libs/JavaPhoenixClient-0.7.0.jar deleted file mode 100644 index 6dbc437..0000000 Binary files a/ChatExample/app/libs/JavaPhoenixClient-0.7.0.jar and /dev/null differ diff --git a/ChatExample/app/libs/JavaPhoenixClient-1.1.5.jar b/ChatExample/app/libs/JavaPhoenixClient-1.1.5.jar new file mode 100644 index 0000000..dae2106 Binary files /dev/null and b/ChatExample/app/libs/JavaPhoenixClient-1.1.5.jar differ diff --git a/ChatExample/app/src/main/AndroidManifest.xml b/ChatExample/app/src/main/AndroidManifest.xml index 15f50d6..5ed4835 100644 --- a/ChatExample/app/src/main/AndroidManifest.xml +++ b/ChatExample/app/src/main/AndroidManifest.xml @@ -14,7 +14,9 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" tools:ignore="GoogleAppIndexingWarning"> - + diff --git a/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MainActivity.kt b/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MainActivity.kt index 6e225dd..9a4caa5 100644 --- a/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MainActivity.kt +++ b/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MainActivity.kt @@ -3,11 +3,8 @@ package com.github.dsrees.chatexample import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log -import android.widget.ArrayAdapter -import android.widget.Button -import android.widget.EditText import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.android.synthetic.main.activity_main.* +import com.github.dsrees.chatexample.databinding.ActivityMainBinding import org.phoenixframework.Channel import org.phoenixframework.Socket @@ -17,44 +14,47 @@ class MainActivity : AppCompatActivity() { const val TAG = "MainActivity" } + private lateinit var binding: ActivityMainBinding + private val messagesAdapter = MessagesAdapter() private val layoutManager = LinearLayoutManager(this) // Use when connecting to https://github.com/dwyl/phoenix-chat-example - // private val socket = Socket("https://phxchat.herokuapp.com/socket/websocket") - // private val topic = "room:lobby" + private val socket = Socket("https://phoenix-chat.fly.dev/socket/websocket") + private val topic = "room:lobby" // Use when connecting to local server - private val socket = Socket("ws://10.0.2.2:4000/socket/websocket") - private val topic = "rooms:lobby" + // private val socket = Socket("ws://10.0.2.2:4000/socket/websocket") + // private val topic = "rooms:lobby" private var lobbyChannel: Channel? = null private val username: String - get() = username_input.text.toString() + get() = binding.usernameInput.text.toString() private val message: String - get() = message_input.text.toString() + get() = binding.messageInput.text.toString() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + this.binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) layoutManager.stackFromEnd = true - messages_recycler_view.layoutManager = layoutManager - messages_recycler_view.adapter = messagesAdapter + binding.messagesRecyclerView.layoutManager = layoutManager + binding.messagesRecyclerView.adapter = messagesAdapter socket.onOpen { this.addText("Socket Opened") - runOnUiThread { connect_button.text = "Disconnect" } + runOnUiThread { binding.connectButton.text = "Disconnect" } } socket.onClose { this.addText("Socket Closed") - runOnUiThread { connect_button.text = "Connect" } + runOnUiThread { binding.connectButton.text = "Connect" } } socket.onError { throwable, response -> @@ -67,7 +67,7 @@ class MainActivity : AppCompatActivity() { } - connect_button.setOnClickListener { + binding.connectButton.setOnClickListener { if (socket.isConnected) { this.disconnectAndLeave() } else { @@ -76,7 +76,7 @@ class MainActivity : AppCompatActivity() { } } - send_button.setOnClickListener { sendMessage() } + binding.sendButton.setOnClickListener { sendMessage() } } private fun sendMessage() { @@ -85,7 +85,7 @@ class MainActivity : AppCompatActivity() { ?.receive("ok") { Log.d(TAG, "success $it") } ?.receive("error") { Log.d(TAG, "error $it") } - message_input.text.clear() + binding.messageInput.text.clear() } private fun disconnectAndLeave() { @@ -132,9 +132,7 @@ class MainActivity : AppCompatActivity() { private fun addText(message: String) { runOnUiThread { this.messagesAdapter.add(message) - layoutManager.smoothScrollToPosition(messages_recycler_view, null, messagesAdapter.itemCount) + layoutManager.smoothScrollToPosition(binding.messagesRecyclerView, null, messagesAdapter.itemCount) } - } - } diff --git a/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MessagesAdapter.kt b/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MessagesAdapter.kt index e99b294..143df58 100644 --- a/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MessagesAdapter.kt +++ b/ChatExample/app/src/main/java/com/github/dsrees/chatexample/MessagesAdapter.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView +import com.github.dsrees.chatexample.databinding.ItemMessageBinding class MessagesAdapter : RecyclerView.Adapter() { @@ -17,8 +18,8 @@ class MessagesAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false) - return ViewHolder(view) + val binding = ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) } override fun getItemCount(): Int = messages.size @@ -27,8 +28,8 @@ class MessagesAdapter : RecyclerView.Adapter() { holder.label.text = messages[position] } - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val label: TextView = itemView.findViewById(R.id.item_message_label) + inner class ViewHolder(binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root) { + val label: TextView = binding.itemMessageLabel } } \ No newline at end of file diff --git a/ChatExample/build.gradle b/ChatExample/build.gradle index 5ff4b70..08b070b 100644 --- a/ChatExample/build.gradle +++ b/ChatExample/build.gradle @@ -1,17 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.31' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files } } diff --git a/ChatExample/gradle/wrapper/gradle-wrapper.properties b/ChatExample/gradle/wrapper/gradle-wrapper.properties index 3c4d55d..5fa6080 100644 --- a/ChatExample/gradle/wrapper/gradle-wrapper.properties +++ b/ChatExample/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue May 14 11:28:07 EDT 2019 +#Wed Oct 04 12:35:15 EDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/README.md b/README.md index 9ca88c2..ecdb961 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ repositories { and then add the library. See [releases](https://github.com/dsrees/JavaPhoenixClient/releases) for the latest version ```$xslt dependencies { - implementation 'com.github.dsrees:JavaPhoenixClient:1.0.1' + implementation 'com.github.dsrees:JavaPhoenixClient:1.3.1' } ``` diff --git a/build.gradle b/build.gradle index 99333e0..78e6a8a 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { plugins { id 'java' id 'jacoco' - id 'org.jetbrains.kotlin.jvm' version '1.3.31' + id 'org.jetbrains.kotlin.jvm' version '1.8.0' } ext { @@ -25,7 +25,7 @@ apply plugin: "org.jetbrains.dokka" apply plugin: "com.vanniktech.maven.publish" group 'com.github.dsrees' -version '1.0.1' +version '1.3.1' sourceCompatibility = 1.8 @@ -47,16 +47,16 @@ test { } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - compile "com.google.code.gson:gson:2.8.5" - compile "com.squareup.okhttp3:okhttp:3.12.2" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "com.google.code.gson:gson:2.10.1" + implementation "com.squareup.okhttp3:okhttp:4.11.0" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' - testCompile group: 'com.google.truth', name: 'truth', version: '1.1.3' - testCompile group: 'org.mockito', name: 'mockito-core', version: '4.0.0' - testCompile group: 'org.mockito.kotlin', name: 'mockito-kotlin', version: '4.0.0' + testImplementation group: 'com.google.truth', name: 'truth', version: '1.1.3' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.0.0' + testImplementation group: 'org.mockito.kotlin', name: 'mockito-kotlin', version: '4.0.0' } jacocoTestReport { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d663d77..aa98239 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 18 20:34:38 EDT 2021 +#Wed Oct 04 12:21:20 EDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip diff --git a/src/main/kotlin/org/phoenixframework/Channel.kt b/src/main/kotlin/org/phoenixframework/Channel.kt index dd594f3..d6103ab 100644 --- a/src/main/kotlin/org/phoenixframework/Channel.kt +++ b/src/main/kotlin/org/phoenixframework/Channel.kt @@ -38,7 +38,7 @@ data class Binding( */ class Channel( val topic: String, - params: Payload, + paramsClosure: PayloadClosure, internal val socket: Socket ) { @@ -94,10 +94,10 @@ class Channel( internal var timeout: Long /** Params passed in through constructions and provided to the JoinPush */ - var params: Payload = params + var params: Payload + get() = joinPush.payload set(value) { joinPush.payload = value - field = value } /** Set to true once the channel has attempted to join */ @@ -121,6 +121,12 @@ class Channel( */ internal var onMessage: (Message) -> Message = { it } + constructor( + topic: String, + params: Payload, + socket: Socket + ) : this(topic, { params }, socket) + init { this.state = State.CLOSED this.bindings = ConcurrentLinkedQueue() @@ -148,7 +154,7 @@ class Channel( this.joinPush = Push( channel = this, event = Event.JOIN.value, - payload = params, + payloadClosure = paramsClosure, timeout = timeout) // Perform once the Channel has joined @@ -224,7 +230,7 @@ class Channel( // Perform when the join reply is received this.on(Event.REPLY) { message -> - this.trigger(replyEventName(message.ref), message.rawPayload, message.ref, message.joinRef) + this.trigger(replyEventName(message.ref), message.rawPayload, message.ref, message.joinRef, message.payloadJson) } } @@ -383,18 +389,20 @@ class Channel( event: Event, payload: Payload = hashMapOf(), ref: String = "", - joinRef: String? = null + joinRef: String? = null, + payloadJson: String = "" ) { - this.trigger(event.value, payload, ref, joinRef) + this.trigger(event.value, payload, ref, joinRef, payloadJson) } internal fun trigger( event: String, payload: Payload = hashMapOf(), ref: String = "", - joinRef: String? = null + joinRef: String? = null, + payloadJson: String = "" ) { - this.trigger(Message(joinRef, ref, topic, event, payload)) + this.trigger(Message(joinRef, ref, topic, event, payload, payloadJson)) } internal fun trigger(message: Message) { diff --git a/src/main/kotlin/org/phoenixframework/Defaults.kt b/src/main/kotlin/org/phoenixframework/Defaults.kt index 8a6a387..06ecc9f 100644 --- a/src/main/kotlin/org/phoenixframework/Defaults.kt +++ b/src/main/kotlin/org/phoenixframework/Defaults.kt @@ -25,8 +25,9 @@ package org.phoenixframework import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.google.gson.JsonObject import com.google.gson.reflect.TypeToken -import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.net.URL object Defaults { @@ -65,18 +66,53 @@ object Defaults { */ @Suppress("UNCHECKED_CAST") val decode: DecodeClosure = { rawMessage -> - val anyType = object : TypeToken>() {}.type - val result = gson.fromJson>(rawMessage, anyType) + val parseValue: (String) -> String? = { value -> + when(value) { + "null" -> null + else -> value.replace("\"", "") + } + } + + var message = rawMessage + message = message.removeRange(0, 1) // remove '[' + + val joinRef = message.takeWhile { it != ',' } // take "join ref", "null" or "\"5\"" + message = message.removeRange(0, joinRef.length) // remove join ref + message = message.removeRange(0, 1) // remove ',' + + val ref = message.takeWhile { it != ',' } // take ref, "null" or "\"5\"" + message = message.removeRange(0, ref.length) // remove ref + message = message.removeRange(0, 1) // remove ',' + + val topic = message.takeWhile { it != ',' } // take topic, "\"topic\"" + message = message.removeRange(0, topic.length) + message = message.removeRange(0, 1) // remove ',' + + val event = message.takeWhile { it != ',' } // take event, "\"phx_reply\"" + message = message.removeRange(0, event.length) + message = message.removeRange(0, 1) // remove ',' + + var remaining = message.removeRange(message.length - 1, message.length) // remove ']' + + // Payload should now just be "{"message":"hello","from":"user_1"}" or + // "{"response": {"message":"hello","from":"user_1"}},"status":"ok"}", flatten. + val jsonObj = gson.fromJson(remaining, JsonObject::class.java) + val response = jsonObj.get("response") + val payload = response?.let { gson.toJson(response) } ?: remaining + + val anyType = object : TypeToken>() {}.type + val result = gson.fromJson>(remaining, anyType) // vsn=2.0.0 message structure // [join_ref, ref, topic, event, payload] Message( - joinRef = result[0] as? String?, - ref = result[1] as? String ?: "", - topic = result[2] as? String ?: "", - event = result[3] as? String ?: "", - rawPayload = result[4] as? Payload ?: mapOf() + joinRef = parseValue(joinRef), + ref = parseValue(ref) ?: "", + topic = parseValue(topic) ?: "", + event = parseValue(event) ?: "", + rawPayload = result, + payloadJson = payload ) } @@ -110,19 +146,17 @@ object Defaults { } // Add the VSN query parameter - var httpUrl = HttpUrl.parse(mutableUrl) + var httpUrl = mutableUrl.toHttpUrlOrNull() ?: throw IllegalArgumentException("invalid url: $endpoint") val httpBuilder = httpUrl.newBuilder() httpBuilder.addQueryParameter("vsn", vsn) // Append any additional query params - paramsClosure.invoke()?.let { - it.forEach { (key, value) -> - httpBuilder.addQueryParameter(key, value.toString()) - } + paramsClosure.invoke().forEach { (key, value) -> + httpBuilder.addQueryParameter(key, value.toString()) } // Return the [URL] that will be used to establish a connection - return httpBuilder.build().url() + return httpBuilder.build().toUrl() } } \ No newline at end of file diff --git a/src/main/kotlin/org/phoenixframework/Message.kt b/src/main/kotlin/org/phoenixframework/Message.kt index 40e7462..ed177b0 100644 --- a/src/main/kotlin/org/phoenixframework/Message.kt +++ b/src/main/kotlin/org/phoenixframework/Message.kt @@ -37,7 +37,10 @@ data class Message( val event: String = "", /** The raw payload of the message. It is recommended that you use `payload` instead. */ - internal val rawPayload: Payload = HashMap() + internal val rawPayload: Payload = HashMap(), + + /** The payload, as a json string */ + val payloadJson: String = "" ) { /** The payload of the message */ diff --git a/src/main/kotlin/org/phoenixframework/Push.kt b/src/main/kotlin/org/phoenixframework/Push.kt index 5234205..75c75f1 100644 --- a/src/main/kotlin/org/phoenixframework/Push.kt +++ b/src/main/kotlin/org/phoenixframework/Push.kt @@ -32,8 +32,8 @@ class Push( val channel: Channel, /** The event the Push is targeting */ val event: String, - /** The message to be sent */ - var payload: Payload = mapOf(), + /** Closure that allows changing parameters sent during push */ + var payloadClosure: PayloadClosure, /** Duration before the message is considered timed out and failed to send */ var timeout: Long = Defaults.TIMEOUT ) { @@ -47,6 +47,9 @@ class Push( /** Hooks into a Push. Where .receive("ok", callback(Payload)) are stored */ var receiveHooks: MutableMap Unit)>> = HashMap() + /** Hooks into a Push. Where .receiveAll(callback(status, message)) are stored */ + private var receiveAllHooks: MutableList<(status: String, message: Message) -> Unit> = mutableListOf() + /** True if the Push has been sent */ var sent: Boolean = false @@ -56,6 +59,23 @@ class Push( /** The event that is associated with the reference ID of the Push */ var refEvent: String? = null + var payload: Payload + get() = payloadClosure.invoke() + set(value) { + payloadClosure = { value } + } + + constructor( + /** The channel the Push is being sent through */ + channel: Channel, + /** The event the Push is targeting */ + event: String, + /** The message to be sent */ + payload: Payload = mapOf(), + /** Duration before the message is considered timed out and failed to send */ + timeout: Long = Defaults.TIMEOUT + ) : this(channel, event, { payload }, timeout) + //------------------------------------------------------------------------------ // Public //------------------------------------------------------------------------------ @@ -100,6 +120,28 @@ class Push( return this } + /** + * Receives any event that was a response to an outbound message. + * + * Example: + * channel + * .send("event", mPayload) + * .receive { status, message -> + * print(status) // "ok" + * } + */ + fun receive(callback: (status: String, message: Message) -> Unit): Push { + // If the message has already been received, pass it to the callback. + receivedMessage?.let { + val status = it.status + if (status != null) { + callback(status, it) + } + } + receiveAllHooks.add(callback) + return this + } + //------------------------------------------------------------------------------ // Internal //------------------------------------------------------------------------------ @@ -165,6 +207,7 @@ class Push( */ private fun matchReceive(status: String, message: Message) { receiveHooks[status]?.forEach { it(message) } + receiveAllHooks.forEach { it(status, message) } } /** Removes receive hook from Channel regarding this Push */ diff --git a/src/main/kotlin/org/phoenixframework/Socket.kt b/src/main/kotlin/org/phoenixframework/Socket.kt index 73f5e23..4d3ddef 100644 --- a/src/main/kotlin/org/phoenixframework/Socket.kt +++ b/src/main/kotlin/org/phoenixframework/Socket.kt @@ -100,7 +100,7 @@ const val WS_CLOSE_ABNORMAL = 1006 /** * A closure that will return an optional Payload */ -typealias PayloadClosure = () -> Payload? +typealias PayloadClosure = () -> Payload /** A closure that will encode a Map into a JSON String */ typealias EncodeClosure = (Any) -> String @@ -242,7 +242,7 @@ class Socket( */ constructor( url: String, - params: Payload? = null, + params: Payload = mapOf(), vsn: String = Defaults.VSN, encode: EncodeClosure = Defaults.encode, decode: DecodeClosure = Defaults.decode, @@ -273,7 +273,7 @@ class Socket( // Create reconnect timer this.reconnectTimer = TimeoutTimer( dispatchQueue = dispatchQueue, - timerCalculation = reconnectAfterMs, + timerCalculation = { reconnectAfterMs(it) }, callback = { this.logItems("Socket attempting to reconnect") this.teardown { this.connect() } @@ -358,9 +358,14 @@ class Socket( fun channel( topic: String, params: Payload = mapOf() + ): Channel = this.channel(topic) { params } + + fun channel( + topic: String, + paramsClosure: PayloadClosure ): Channel { - val channel = Channel(topic, params, this) - this.channels = this.channels + channel + val channel = Channel(topic, paramsClosure, this) + this.channels += channel return channel } diff --git a/src/main/kotlin/org/phoenixframework/Transport.kt b/src/main/kotlin/org/phoenixframework/Transport.kt index 25c8818..738fad6 100644 --- a/src/main/kotlin/org/phoenixframework/Transport.kt +++ b/src/main/kotlin/org/phoenixframework/Transport.kt @@ -155,6 +155,7 @@ class WebSocketTransport( override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { this.readyState = Transport.ReadyState.CLOSING + webSocket.close(code, reason) } override fun onMessage(webSocket: WebSocket, text: String) { diff --git a/src/test/kotlin/org/phoenixframework/ChannelTest.kt b/src/test/kotlin/org/phoenixframework/ChannelTest.kt index bd83c49..9965caa 100644 --- a/src/test/kotlin/org/phoenixframework/ChannelTest.kt +++ b/src/test/kotlin/org/phoenixframework/ChannelTest.kt @@ -28,6 +28,7 @@ class ChannelTest { @Mock lateinit var socket: Socket @Mock lateinit var mockCallback: ((Message) -> Unit) + @Mock lateinit var mockStatusCallback: ((String, Message) -> Unit) private val kDefaultRef = "1" private val kDefaultTimeout = 10_000L @@ -149,6 +150,32 @@ class ChannelTest { /* End JoinParams */ } + + @Nested + @DisplayName("join paramsClosure") + inner class JoinParamsClosure { + @Test + internal fun `updating join params closure`() { + val paramsClosure = { mapOf("value" to 1) } + val change = mapOf("value" to 2) + + channel = Channel("topic", paramsClosure, socket) + val joinPush = channel.joinPush + + assertThat(joinPush.channel).isEqualTo(channel) + assertThat(joinPush.payload["value"]).isEqualTo(1) + assertThat(joinPush.event).isEqualTo("phx_join") + assertThat(joinPush.timeout).isEqualTo(10_000L) + + channel.params = change + assertThat(joinPush.channel).isEqualTo(channel) + assertThat(joinPush.payload["value"]).isEqualTo(2) + assertThat(channel.params["value"]).isEqualTo(2) + assertThat(joinPush.event).isEqualTo("phx_join") + assertThat(joinPush.timeout).isEqualTo(10_000L) + } + } + @Nested @DisplayName("join") inner class Join { @@ -384,6 +411,11 @@ class ChannelTest { joinPush.trigger("error", mapOf("a" to "b")) } + private fun receivesApproved() { + fakeClock.tick(joinPush.timeout / 2) + joinPush.trigger("approved", mapOf("a" to "b")) + } + @Nested @DisplayName("receives 'ok'") inner class ReceivesOk { @@ -626,6 +658,52 @@ class ChannelTest { /* End ReceivesError */ } + + @Nested + @DisplayName("receives 'all status'") + inner class ReceivesAllStatus { + @Test + internal fun `triggers receive('error') callback after error response`() { + assertThat(channel.state).isEqualTo(Channel.State.JOINING) + joinPush.receive(mockStatusCallback) + + receivesError() + joinPush.trigger("error", kEmptyPayload) + verify(mockStatusCallback, times(1)).invoke(any(), any()) + } + + @Test + internal fun `triggers receive('error') callback if error response already received`() { + receivesError() + + joinPush.receive(mockStatusCallback) + + verify(mockStatusCallback).invoke(any(), any()) + } + + @Test + internal fun `triggers receive('approved') callback after approved response`() { + assertThat(channel.state).isEqualTo(Channel.State.JOINING) + joinPush.receive(mockStatusCallback) + + receivesApproved() + joinPush.trigger("approved", kEmptyPayload) + verify(mockStatusCallback, times(1)).invoke(any(), any()) + + } + + @Test + internal fun `triggers receive('approved') callback if approved response already received`() { + receivesApproved() + + joinPush.receive(mockStatusCallback) + + verify(mockStatusCallback).invoke(any(), any()) + } + + /* End ReceivesAllStatus */ + } + /* End JoinPush */ } diff --git a/src/test/kotlin/org/phoenixframework/DefaultsTest.kt b/src/test/kotlin/org/phoenixframework/DefaultsTest.kt index 41efcad..bf2da9b 100644 --- a/src/test/kotlin/org/phoenixframework/DefaultsTest.kt +++ b/src/test/kotlin/org/phoenixframework/DefaultsTest.kt @@ -43,7 +43,6 @@ internal class DefaultsTest { assertThat(reconnect(5)).isEqualTo(10_000) } - @Test internal fun `decoder converts json array into message`() { val v2Json = """ @@ -58,6 +57,82 @@ internal class DefaultsTest { assertThat(message.payload).isEqualTo(mapOf("message" to "Hi", "name" to "Tester")) } + @Test + internal fun `decoder provides raw json payload`() { + val v2Json = """ + ["1","2","room:lobby","shout",{"message":"Hi","name":"Tester","count":15,"ratio":0.2}] + """.trimIndent() + + val message = Defaults.decode(v2Json) + assertThat(message.joinRef).isEqualTo("1") + assertThat(message.ref).isEqualTo("2") + assertThat(message.topic).isEqualTo("room:lobby") + assertThat(message.event).isEqualTo("shout") + assertThat(message.payloadJson).isEqualTo("{\"message\":\"Hi\",\"name\":\"Tester\",\"count\":15,\"ratio\":0.2}") + assertThat(message.payload).isEqualTo(mapOf( + "message" to "Hi", + "name" to "Tester", + "count" to 15.0, // Note that this is a bug and should eventually be removed + "ratio" to 0.2 + )) + } + + @Test + internal fun `decoder decodes a status`() { + val v2Json = """ + ["1","2","room:lobby","phx_reply",{"response":{"message":"Hi","name":"Tester","count":15,"ratio":0.2},"status":"ok"}] + """.trimIndent() + + val message = Defaults.decode(v2Json) + assertThat(message.joinRef).isEqualTo("1") + assertThat(message.ref).isEqualTo("2") + assertThat(message.topic).isEqualTo("room:lobby") + assertThat(message.event).isEqualTo("phx_reply") + assertThat(message.payloadJson).isEqualTo("{\"message\":\"Hi\",\"name\":\"Tester\",\"count\":15,\"ratio\":0.2}") + assertThat(message.payload).isEqualTo(mapOf( + "message" to "Hi", + "name" to "Tester", + "count" to 15.0, // Note that this is a bug and should eventually be removed + "ratio" to 0.2 + )) + } + + + + @Test + internal fun `decoder decodes an error`() { + val v2Json = """ + ["6","8","drivers:self","phx_reply",{"response":{"details":"invalid code specified"},"status":"error"}] + """.trimIndent() + + val message = Defaults.decode(v2Json) + assertThat(message.payloadJson).isEqualTo("{\"details\":\"invalid code specified\"}") + assertThat(message.rawPayload).isEqualTo(mapOf( + "response" to mapOf( + "details" to "invalid code specified" + ), + "status" to "error" + )) + assertThat(message.payload).isEqualTo(mapOf( + "details" to "invalid code specified" + )) + + } + + @Test + internal fun `decoder decodes a non-json payload`() { + val v2Json = """ + ["1","2","room:lobby","phx_reply",{"response":"hello","status":"ok"}] + """.trimIndent() + + val message = Defaults.decode(v2Json) + assertThat(message.payloadJson).isEqualTo("\"hello\"") + assertThat(message.payload).isEqualTo(mapOf( + "response" to "hello", + "status" to "ok" + )) + } + @Test internal fun `encode converts message into json`() { val body = listOf(null, null, "topic", "event", mapOf("one" to "two")) diff --git a/src/test/kotlin/org/phoenixframework/MessageTest.kt b/src/test/kotlin/org/phoenixframework/MessageTest.kt index 0a555d0..2dcd321 100644 --- a/src/test/kotlin/org/phoenixframework/MessageTest.kt +++ b/src/test/kotlin/org/phoenixframework/MessageTest.kt @@ -14,7 +14,7 @@ class MessageTest { @Test internal fun `jsonParsing parses normal message`() { val json = """ - [null, "6", "my-topic", "update", {"user": "James S.", "message": "This is a test"}] + [null,"6","my-topic","update",{"user":"James S.","message":"This is a test"}] """.trimIndent() val message = Defaults.decode.invoke(json) @@ -30,7 +30,7 @@ class MessageTest { @Test internal fun `jsonParsing parses a reply`() { val json = """ - [null, "6", "my-topic", "phx_reply", {"response": {"user": "James S.","message": "This is a test"},"status": "ok"}] + [null,"6","my-topic","phx_reply",{"response":{"user":"James S.","message":"This is a test"},"status": "ok"}] """.trimIndent() val message = Defaults.decode.invoke(json) diff --git a/src/test/kotlin/org/phoenixframework/PresenceTest.kt b/src/test/kotlin/org/phoenixframework/PresenceTest.kt index 4eaee21..2874e1f 100644 --- a/src/test/kotlin/org/phoenixframework/PresenceTest.kt +++ b/src/test/kotlin/org/phoenixframework/PresenceTest.kt @@ -196,7 +196,7 @@ class PresenceTest { @Test internal fun `onJoins new presences and onLeaves left presences`() { val newState = fixState - var state = mutableMapOf( + var state: MutableMap>>> = mutableMapOf( "u4" to mutableMapOf("metas" to listOf(mapOf("id" to 4, "phx_ref" to "4")))) val joined: PresenceDiff = mutableMapOf() @@ -245,9 +245,9 @@ class PresenceTest { @Test internal fun `onJoins only newly added metas`() { - var state = mutableMapOf( + var state: MutableMap>>> = mutableMapOf( "u3" to mutableMapOf("metas" to listOf(mapOf("id" to 3, "phx_ref" to "3")))) - val newState = mutableMapOf( + val newState: MutableMap>>> = mutableMapOf( "u3" to mutableMapOf("metas" to listOf( mapOf("id" to 3, "phx_ref" to "3"), mapOf("id" to 3, "phx_ref" to "3.new") @@ -285,9 +285,9 @@ class PresenceTest { @Test internal fun `onLeaves only newly removed metas`() { - val newState = mutableMapOf( + val newState: MutableMap>>> = mutableMapOf( "u3" to mutableMapOf("metas" to listOf(mapOf("id" to 3, "phx_ref" to "3")))) - var state = mutableMapOf( + var state: MutableMap>>> = mutableMapOf( "u3" to mutableMapOf("metas" to listOf( mapOf("id" to 3, "phx_ref" to "3"), mapOf("id" to 3, "phx_ref" to "3.left") @@ -326,13 +326,13 @@ class PresenceTest { @Test internal fun `syncs both joined and left metas`() { - val newState = mutableMapOf( + val newState: MutableMap>>> = mutableMapOf( "u3" to mutableMapOf("metas" to listOf( mapOf("id" to 3, "phx_ref" to "3"), mapOf("id" to 3, "phx_ref" to "3.new") ))) - var state = mutableMapOf( + var state: MutableMap>>> = mutableMapOf( "u3" to mutableMapOf("metas" to listOf( mapOf("id" to 3, "phx_ref" to "3"), mapOf("id" to 3, "phx_ref" to "3.left") @@ -421,13 +421,13 @@ class PresenceTest { @Test internal fun `removes meta while leaving key if other metas exist`() { - var state = mutableMapOf( + var state: MutableMap>>> = mutableMapOf( "u1" to mutableMapOf("metas" to listOf( mapOf("id" to 1, "phx_ref" to "1"), mapOf("id" to 1, "phx_ref" to "1.2") ))) - val leaves = mutableMapOf( + val leaves: MutableMap>>> = mutableMapOf( "u1" to mutableMapOf("metas" to listOf( mapOf("id" to 1, "phx_ref" to "1") ))) diff --git a/src/test/kotlin/org/phoenixframework/SocketTest.kt b/src/test/kotlin/org/phoenixframework/SocketTest.kt index 5fc512f..6bf44a4 100644 --- a/src/test/kotlin/org/phoenixframework/SocketTest.kt +++ b/src/test/kotlin/org/phoenixframework/SocketTest.kt @@ -48,7 +48,7 @@ class SocketTest { internal fun `sets defaults`() { val socket = Socket("wss://localhost:4000/socket") - assertThat(socket.paramsClosure.invoke()).isNull() + assertThat(socket.paramsClosure.invoke()).isEmpty() assertThat(socket.channels).isEmpty() assertThat(socket.sendBuffer).isEmpty() assertThat(socket.ref).isEqualTo(0) diff --git a/src/test/kotlin/org/phoenixframework/WebSocketTransportTest.kt b/src/test/kotlin/org/phoenixframework/WebSocketTransportTest.kt index 381b5ed..b9a7846 100644 --- a/src/test/kotlin/org/phoenixframework/WebSocketTransportTest.kt +++ b/src/test/kotlin/org/phoenixframework/WebSocketTransportTest.kt @@ -136,6 +136,7 @@ class WebSocketTransportTest { transport.readyState = Transport.ReadyState.OPEN transport.onClosing(mockWebSocket, 10, "reason") + verify(mockWebSocket).close(10, "reason") assertThat(transport.readyState).isEqualTo(Transport.ReadyState.CLOSING) }